From 8a44d6e0b4f0079cf7ac90a2b86ebd4ad71ed885 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sat, 21 Sep 2024 09:37:10 +0800 Subject: [PATCH 1/5] replace autoquery services to use constructor injection --- MyApp/_pages/autoquery/crud.md | 28 ++++---- MyApp/_pages/autoquery/index.md | 10 ++- MyApp/_pages/autoquery/rdbms.md | 48 ++++++-------- MyApp/_pages/releases/v4_0_56.md | 6 +- MyApp/_pages/releases/v5_09.md | 24 +++---- MyApp/_pages/releases/v5_12.md | 8 +-- MyApp/_pages/releases/v6_11.md | 96 ++++++++++++++-------------- MyApp/_pages/servicestack-ai.md | 90 +++++++++++++------------- MyApp/_pages/vue/custom-autoforms.md | 6 +- 9 files changed, 143 insertions(+), 173 deletions(-) diff --git a/MyApp/_pages/autoquery/crud.md b/MyApp/_pages/autoquery/crud.md index 3797ea33f1..a362e11323 100644 --- a/MyApp/_pages/autoquery/crud.md +++ b/MyApp/_pages/autoquery/crud.md @@ -138,23 +138,19 @@ Just as you can create [Custom AutoQuery Implementations](/autoquery/rdbms.html# you can also override AutoQuery CRUD implementations by creating implementations with AutoQuery CRUD Request DTOs and calling the relevate `IAutoQueryDb` method, e.g: ```csharp -public class MyCrudServices : Service +public class MyCrudServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - - public object Post(CreateRockstar request) => AutoQuery.Create(request, base.Request); - public object Put(UpdateRockstar request) => AutoQuery.Update(request, base.Request); - public object Delete(DeleteRockstar request) => AutoQuery.Delete(request, base.Request); + public object Post(CreateRockstar request) => autoQuery.Create(request, base.Request); + public object Put(UpdateRockstar request) => autoQuery.Update(request, base.Request); + public object Delete(DeleteRockstar request) => autoQuery.Delete(request, base.Request); } // Async -public class MyCrudServices : Service +public class MyCrudServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - - public Task Post(CreateRockstar request) => AutoQuery.CreateAsync(request, base.Request); - public Task Put(UpdateRockstar request) => AutoQuery.UpdateAsync(request, base.Request); - public Task Delete(DeleteRockstar request) => AutoQuery.DeleteAsync(request, base.Request); + public Task Post(CreateRockstar request) => autoQuery.CreateAsync(request, base.Request); + public Task Put(UpdateRockstar request) => autoQuery.UpdateAsync(request, base.Request); + public Task Delete(DeleteRockstar request) => autoQuery.DeleteAsync(request, base.Request); } ``` @@ -490,15 +486,13 @@ have them you'd need to provide custom implementations that can delegate to thei ```csharp [ConnectionInfo(NamedConnection = MyDatabases.Reporting)] -public class MyReportingServices : Service +public class MyReportingServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public Task Any(CreateConnectionInfoRockstar request) => - AutoQuery.CreateAsync(request, Request); + autoQuery.CreateAsync(request, Request); public Task Any(UpdateConnectionInfoRockstar request) => - AutoQuery.UpdateAsync(request, Request); + autoQuery.UpdateAsync(request, Request); } ``` diff --git a/MyApp/_pages/autoquery/index.md b/MyApp/_pages/autoquery/index.md index efc8e547aa..5b403e46e4 100644 --- a/MyApp/_pages/autoquery/index.md +++ b/MyApp/_pages/autoquery/index.md @@ -214,15 +214,13 @@ public class CustomRockstar } // Override with custom implementation -public class MyQueryServices : Service +public class MyQueryServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public async Task Any(QueryRockstarAlbums query) { - using var db = AutoQuery.GetDb(query, base.Request); - var q = AutoQuery.CreateQuery(query, base.Request, db); - return await AutoQuery.ExecuteAsync(query, q, base.Request, db); + using var db = autoQuery.GetDb(query, base.Request); + var q = autoQuery.CreateQuery(query, base.Request, db); + return await autoQuery.ExecuteAsync(query, q, base.Request, db); } } ``` diff --git a/MyApp/_pages/autoquery/rdbms.md b/MyApp/_pages/autoquery/rdbms.md index ec8b7a77ca..f84ceaa83e 100644 --- a/MyApp/_pages/autoquery/rdbms.md +++ b/MyApp/_pages/autoquery/rdbms.md @@ -98,25 +98,23 @@ The behavior of queries can be completely customized by simply providing your ow ```csharp // Override with custom implementation -public class MyQueryServices : Service +public class MyQueryServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - // Sync public object Any(FindMovies query) { - using var db = AutoQuery.GetDb(query, base.Request); - var q = AutoQuery.CreateQuery(query, base.Request, db); - return AutoQuery.Execute(query, q, base.Request, db); + using var db = autoQuery.GetDb(query, base.Request); + var q = autoQuery.CreateQuery(query, base.Request, db); + return autoQuery.Execute(query, q, base.Request, db); } // Async public async Task Any(QueryRockstars query) { - using var db = AutoQuery.GetDb(query, base.Request); - var q = AutoQuery.CreateQuery(query, base.Request, db); - return await AutoQuery.ExecuteAsync(query, q, base.Request, db); - } + using var db = autoQuery.GetDb(query, base.Request); + var q = autoQuery.CreateQuery(query, base.Request, db); + return await autoQuery.ExecuteAsync(query, q, base.Request, db); + } } ``` @@ -1078,14 +1076,12 @@ public class QueryPosts : QueryDb } [CacheResponse(Duration = 600)] -public class PostPublicServices : PostServicesBase +public class PostPublicServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public object Any(QueryPosts request) { - using var db = AutoQuery.GetDb(query, base.Request); - var q = AutoQuery.CreateQuery(query, base.Request, db) //Populated SqlExpression + using var db = autoQuery.GetDb(query, base.Request); + var q = autoQuery.CreateQuery(query, base.Request, db) //Populated SqlExpression q.Where(x => x.Deleted == null); @@ -1197,11 +1193,9 @@ E.g. This implementation applies the `[ConnectionInfo]` behavior to all its Serv ```csharp [ConnectionInfo(NamedConnection = "Reporting")] -public class MyReportingServices : Service +public class MyReportingServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - - public Task Any(CreateReport request) => AutoQuery.CreateAsync(request, base.Request); + public Task Any(CreateReport request) => autoQuery.CreateAsync(request,base.Request); } ``` @@ -1338,15 +1332,13 @@ Where it's also queryable with: We've already covered some of extensibility options with Customizable **QueryDbFields** and **Implicit Conventions**, the most customizable would be to override the default implementation with a custom one, e.g: ```csharp -public class MyQueryServices : Service +public class MyQueryServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - //Override with custom implementation public object Any(FindMovies dto) { - var q = AutoQuery.CreateQuery(dto, Request.GetRequestParams(), base.Request); - return AutoQuery.Execute(dto, q, base.Request); + var q = autoQuery.CreateQuery(dto, Request.GetRequestParams(), base.Request); + return autoQuery.Execute(dto, q, base.Request); } } ``` @@ -1430,19 +1422,17 @@ Plugins.Add(new AutoQueryFeature { It also wont generate implementations for custom AutoBatch implementations, e.g. you can add a custom implementation that does what the generated implementation would've done and execute using the same DB Connection and Transaction with: ```csharp -public class CustomAutoQueryServices : Service +public class CustomAutoQueryServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public object Any(CreateItem[] requests) { - using var db = AutoQuery.GetDb(Request); + using var db = autoQuery.GetDb(Request); using var dbTrans = db.OpenTransaction(); var results = new List(); foreach (var request in requests) { - var response = await AutoQuery.CreateAsync(request, Request, db); + var response = await autoQuery.CreateAsync(request, Request, db); results.Add(response); } diff --git a/MyApp/_pages/releases/v4_0_56.md b/MyApp/_pages/releases/v4_0_56.md index 163da42388..ddf613d8b9 100644 --- a/MyApp/_pages/releases/v4_0_56.md +++ b/MyApp/_pages/releases/v4_0_56.md @@ -725,9 +725,8 @@ public class QueryCustomers : QueryDb public int? OrAgeOlderThan { get; set; } } -public class AutoQueryRDBMSServices : Service +public class AutoQueryRDBMSServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } ... } @@ -739,9 +738,8 @@ public class QueryCustomers : QueryData public int? OrAgeOlderThan { get; set; } } -public class AutoQueryDataServices : Service +public class AutoQueryDataServices(IAutoQueryData autoQuery) : Service { - public IAutoQueryData AutoQuery { get; set; } ... } ``` diff --git a/MyApp/_pages/releases/v5_09.md b/MyApp/_pages/releases/v5_09.md index 36b9a5c211..6ff36e27bd 100644 --- a/MyApp/_pages/releases/v5_09.md +++ b/MyApp/_pages/releases/v5_09.md @@ -1390,15 +1390,13 @@ have them you'd need to provide custom implementations that can delegate to thei ```csharp [ConnectionInfo(NamedConnection = MyDatabases.Reporting)] -public class MyReportingServices : Service +public class MyReportingServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public Task Any(CreateConnectionInfoRockstar request) => - AutoQuery.CreateAsync(request, Request); + autoQuery.CreateAsync(request, Request); public Task Any(UpdateConnectionInfoRockstar request) => - AutoQuery.UpdateAsync(request, Request); + autoQuery.UpdateAsync(request, Request); } ``` @@ -3441,10 +3439,8 @@ E.g. This implementation applies the `[ConnectionInfo]` behavior to all its Serv ```csharp [ConnectionInfo(NamedConnection = "Reporting")] -public class MyReportingServices : Service +public class MyReportingServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public Task Any(CreateReport request) => AutoQuery.CreateAsync(request, base.Request); } ``` @@ -3458,9 +3454,9 @@ There's a minor optimization existing custom AutoQuery implementations can do by // Sync public object Any(QueryRockstars query) { - using var db = AutoQuery.GetDb(query, base.Request); - var q = AutoQuery.CreateQuery(query, base.Request, db); - return await AutoQuery.Execute(query, q, base.Request, db); + using var db = autoQuery.GetDb(query, base.Request); + var q = autoQuery.CreateQuery(query, base.Request, db); + return await autoQuery.Execute(query, q, base.Request, db); } ``` @@ -3470,9 +3466,9 @@ Another optimization if using SQL Server or PostgreSQL RDBMS's is to refactor it // Async public async Task Any(QueryRockstars query) { - using var db = AutoQuery.GetDb(query, base.Request); - var q = AutoQuery.CreateQuery(query, base.Request, db); - return await AutoQuery.ExecuteAsync(query, q, base.Request, db); + using var db = autoQuery.GetDb(query, base.Request); + var q = autoQuery.CreateQuery(query, base.Request, db); + return await autoQuery.ExecuteAsync(query, q, base.Request, db); } ``` diff --git a/MyApp/_pages/releases/v5_12.md b/MyApp/_pages/releases/v5_12.md index 9ba0a7fd54..f44059378c 100644 --- a/MyApp/_pages/releases/v5_12.md +++ b/MyApp/_pages/releases/v5_12.md @@ -2327,19 +2327,17 @@ Plugins.Add(new AutoQueryFeature { It also wont generate implementations for custom AutoBatch implementations, e.g. you can add a custom implementation that does what the generated implementation would've done and execute using the same DB Connection and Transaction with: ```csharp -public class CustomAutoQueryServices : Service +public class CustomAutoQueryServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public object Any(CreateItem[] requests) { - using var db = AutoQuery.GetDb(Request); + using var db = autoQuery.GetDb(Request); using var dbTrans = db.OpenTransaction(); var results = new List(); foreach (var request in requests) { - var response = await AutoQuery.CreateAsync(request, Request, db); + var response = await autoQuery.CreateAsync(request, Request, db); results.Add(response); } diff --git a/MyApp/_pages/releases/v6_11.md b/MyApp/_pages/releases/v6_11.md index 358e361a8e..dbcd93978e 100644 --- a/MyApp/_pages/releases/v6_11.md +++ b/MyApp/_pages/releases/v6_11.md @@ -569,50 +569,50 @@ is used to transcribe the uploaded Web Audio recording for **all its examples** `Recording` RDBMS Table before invoking the Speech-to-Text API that is then updated after its successful or error response: ```csharp -public IAutoQueryDb AutoQuery { get; set; } -public ISpeechToTextFactory SpeechToTextFactory { get; set; } - -public async Task Any(CreateRecording request) +public class MyServices(IAutoQueryDb autoQuery, ISpeechToTextFactory speechToTextFactory) { - var feature = request.Feature.ToLower(); - var recording = (Recording)await AutoQuery.CreateAsync(request, Request); - var speechToText = SpeechToTextFactory.Get(request.Feature); + public async Task Any(CreateRecording request) + { + var feature = request.Feature.ToLower(); + var recording = (Recording)await autoQuery.CreateAsync(request, Request); + var speechToText = speechToTextFactory.Get(request.Feature); - var transcribeStart = DateTime.UtcNow; - await Db.UpdateOnlyAsync(() => new Recording { TranscribeStart=transcribeStart }, - where: x => x.Id == recording.Id); + var transcribeStart = DateTime.UtcNow; + await Db.UpdateOnlyAsync(() => new Recording { TranscribeStart=transcribeStart }, + where: x => x.Id == recording.Id); - ResponseStatus? responseStatus = null; - try - { - var response = await speechToText.TranscribeAsync(request.Path); - var transcribeEnd = DateTime.UtcNow; - await Db.UpdateOnlyAsync(() => new Recording + ResponseStatus? responseStatus = null; + try { - Feature = feature, - Provider = speechToText.GetType().Name, - Transcript = response.Transcript, - TranscriptConfidence = response.Confidence, - TranscriptResponse = response.ApiResponse, - TranscribeEnd = transcribeEnd, - TranscribeDurationMs = (transcribeEnd-transcribeStart).TotalMilliseconds, - Error = response.ResponseStatus.ToJson(), - }, where: x => x.Id == recording.Id); - responseStatus = response.ResponseStatus; - } - catch (Exception e) - { - await Db.UpdateOnlyAsync(() => new Recording { Error = e.ToString() }, - where: x => x.Id == recording.Id); - responseStatus = e.ToResponseStatus(); - } + var response = await speechToText.TranscribeAsync(request.Path); + var transcribeEnd = DateTime.UtcNow; + await Db.UpdateOnlyAsync(() => new Recording + { + Feature = feature, + Provider = speechToText.GetType().Name, + Transcript = response.Transcript, + TranscriptConfidence = response.Confidence, + TranscriptResponse = response.ApiResponse, + TranscribeEnd = transcribeEnd, + TranscribeDurationMs = (transcribeEnd-transcribeStart).TotalMilliseconds, + Error = response.ResponseStatus.ToJson(), + }, where: x => x.Id == recording.Id); + responseStatus = response.ResponseStatus; + } + catch (Exception e) + { + await Db.UpdateOnlyAsync(() => new Recording { Error = e.ToString() }, + where: x => x.Id == recording.Id); + responseStatus = e.ToResponseStatus(); + } - recording = await Db.SingleByIdAsync(recording.Id); + recording = await Db.SingleByIdAsync(recording.Id); - if (responseStatus != null) - throw new HttpError(responseStatus, HttpStatusCode.BadRequest); + if (responseStatus != null) + throw new HttpError(responseStatus, HttpStatusCode.BadRequest); - return recording; + return recording; + } } ``` @@ -809,18 +809,18 @@ that just like `CreateRecording` is a custom AutoQuery CRUD Service that uses Au initial `Chat` record, that's later updated with the GPT Chat API Response: ```csharp -public class GptServices : Service +public class GptServices( + IAutoQueryDb autoQuery, + IPromptProviderFactory promptFactory, + ITypeChat typeChat, + IPromptProvider promptProvider) : Service { //... - public IAutoQueryDb AutoQuery { get; set; } - public IPromptProvider PromptProvider { get; set; } - public ITypeChat TypeChatProvider { get; set; } - public async Task Any(CreateChat request) { var feature = request.Feature.ToLower(); - var promptProvider = PromptFactory.Get(feature); - var chat = (Chat)await AutoQuery.CreateAsync(request, Request); + var promptProvider = promptFactory.Get(feature); + var chat = (Chat)await autoQuery.CreateAsync(request, Request); var chatStart = DateTime.UtcNow; await Db.UpdateOnlyAsync(() => new Chat { ChatStart = chatStart }, @@ -833,7 +833,7 @@ public class GptServices : Service var prompt = await promptProvider.CreatePromptAsync(request.UserMessage); var typeChatRequest = CreateTypeChatRequest(feature, schema, prompt, request.UserMessage); - var response = await TypeChat.TranslateMessageAsync(typeChatRequest); + var response = await typeChat.TranslateMessageAsync(typeChatRequest); var chatEnd = DateTime.UtcNow; await Db.UpdateOnlyAsync(() => new Chat { @@ -1063,10 +1063,8 @@ contains the custom implementation which continues to utilize AutoQuery's **Part as well as removing or adding any Options the user makes to the `Category`: ```csharp -public class CoffeeShopServices : Service +public class CoffeeShopServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public async Task Any(UpdateCategory request) { // Perform all RDBMS Updates within the same Transaction @@ -1077,7 +1075,7 @@ public class CoffeeShopServices : Service // Only call AutoQuery Update if there's something to update if (request.ToObjectDictionary().HasNonDefaultValues(ignoreKeys:ignore)) { - response = (Category) await AutoQuery.PartialUpdateAsync(request, Request, Db); + response = (Category) await autoQuery.PartialUpdateAsync(request, Request, Db); } if (request.RemoveOptionIds?.Count > 0) { diff --git a/MyApp/_pages/servicestack-ai.md b/MyApp/_pages/servicestack-ai.md index 861e4ec2df..9ceb483b43 100644 --- a/MyApp/_pages/servicestack-ai.md +++ b/MyApp/_pages/servicestack-ai.md @@ -565,50 +565,50 @@ is used to transcribe the uploaded Web Audio recording for **all its examples** `Recording` RDBMS Table before invoking the Speech-to-Text API that is then updated after its successful or error response: ```csharp -public IAutoQueryDb AutoQuery { get; set; } -public ISpeechToTextFactory SpeechToTextFactory { get; set; } - -public async Task Any(CreateRecording request) +public class MyServices(IAutoQueryDb autoQuery, ISpeechToTextFactory speechToTextFactory) { - var feature = request.Feature.ToLower(); - var recording = (Recording)await AutoQuery.CreateAsync(request, Request); - var speechToText = SpeechToTextFactory.Get(request.Feature); + public async Task Any(CreateRecording request) + { + var feature = request.Feature.ToLower(); + var recording = (Recording)await autoQuery.CreateAsync(request, Request); + var speechToText = speechToTextFactory.Get(request.Feature); - var transcribeStart = DateTime.UtcNow; - await Db.UpdateOnlyAsync(() => new Recording { TranscribeStart=transcribeStart }, - where: x => x.Id == recording.Id); + var transcribeStart = DateTime.UtcNow; + await Db.UpdateOnlyAsync(() => new Recording { TranscribeStart=transcribeStart }, + where: x => x.Id == recording.Id); - ResponseStatus? responseStatus = null; - try - { - var response = await speechToText.TranscribeAsync(request.Path); - var transcribeEnd = DateTime.UtcNow; - await Db.UpdateOnlyAsync(() => new Recording + ResponseStatus? responseStatus = null; + try { - Feature = feature, - Provider = speechToText.GetType().Name, - Transcript = response.Transcript, - TranscriptConfidence = response.Confidence, - TranscriptResponse = response.ApiResponse, - TranscribeEnd = transcribeEnd, - TranscribeDurationMs = (transcribeEnd-transcribeStart).TotalMilliseconds, - Error = response.ResponseStatus.ToJson(), - }, where: x => x.Id == recording.Id); - responseStatus = response.ResponseStatus; - } - catch (Exception e) - { - await Db.UpdateOnlyAsync(() => new Recording { Error = e.ToString() }, - where: x => x.Id == recording.Id); - responseStatus = e.ToResponseStatus(); - } + var response = await speechToText.TranscribeAsync(request.Path); + var transcribeEnd = DateTime.UtcNow; + await Db.UpdateOnlyAsync(() => new Recording + { + Feature = feature, + Provider = speechToText.GetType().Name, + Transcript = response.Transcript, + TranscriptConfidence = response.Confidence, + TranscriptResponse = response.ApiResponse, + TranscribeEnd = transcribeEnd, + TranscribeDurationMs = (transcribeEnd-transcribeStart).TotalMilliseconds, + Error = response.ResponseStatus.ToJson(), + }, where: x => x.Id == recording.Id); + responseStatus = response.ResponseStatus; + } + catch (Exception e) + { + await Db.UpdateOnlyAsync(() => new Recording { Error = e.ToString() }, + where: x => x.Id == recording.Id); + responseStatus = e.ToResponseStatus(); + } - recording = await Db.SingleByIdAsync(recording.Id); + recording = await Db.SingleByIdAsync(recording.Id); - if (responseStatus != null) - throw new HttpError(responseStatus, HttpStatusCode.BadRequest); + if (responseStatus != null) + throw new HttpError(responseStatus, HttpStatusCode.BadRequest); - return recording; + return recording; + } } ``` @@ -805,18 +805,18 @@ that just like `CreateRecording` is a custom AutoQuery CRUD Service that uses Au initial `Chat` record, that's later updated with the GPT Chat API Response: ```csharp -public class GptServices : Service +public class GptServices( + IAutoQueryDb autoQuery, + IPromptProviderFactory promptFactory, + ITypeChat typeChat, + IPromptProvider promptProvider) : Service { //... - public IAutoQueryDb AutoQuery { get; set; } - public IPromptProvider PromptProvider { get; set; } - public ITypeChat TypeChatProvider { get; set; } - public async Task Any(CreateChat request) { var feature = request.Feature.ToLower(); - var promptProvider = PromptFactory.Get(feature); - var chat = (Chat)await AutoQuery.CreateAsync(request, Request); + var promptProvider = promptFactory.Get(feature); + var chat = (Chat)await autoQuery.CreateAsync(request, Request); var chatStart = DateTime.UtcNow; await Db.UpdateOnlyAsync(() => new Chat { ChatStart = chatStart }, @@ -829,7 +829,7 @@ public class GptServices : Service var prompt = await promptProvider.CreatePromptAsync(request.UserMessage); var typeChatRequest = CreateTypeChatRequest(feature, schema, prompt, request.UserMessage); - var response = await TypeChat.TranslateMessageAsync(typeChatRequest); + var response = await typeChat.TranslateMessageAsync(typeChatRequest); var chatEnd = DateTime.UtcNow; await Db.UpdateOnlyAsync(() => new Chat { diff --git a/MyApp/_pages/vue/custom-autoforms.md b/MyApp/_pages/vue/custom-autoforms.md index 50e8f7cef5..bfe51dba9f 100644 --- a/MyApp/_pages/vue/custom-autoforms.md +++ b/MyApp/_pages/vue/custom-autoforms.md @@ -60,10 +60,8 @@ contains the custom implementation which continues to utilize AutoQuery's **Part as well as removing or adding any Options the user makes to the `Category`: ```csharp -public class CoffeeShopServices : Service +public class CoffeeShopServices(IAutoQueryDb autoQuery) : Service { - public IAutoQueryDb AutoQuery { get; set; } - public async Task Any(UpdateCategory request) { // Perform all RDBMS Updates within the same Transaction @@ -74,7 +72,7 @@ public class CoffeeShopServices : Service // Only call AutoQuery Update if there's something to update if (request.ToObjectDictionary().HasNonDefaultValues(ignoreKeys:ignore)) { - response = (Category) await AutoQuery.PartialUpdateAsync(request, Request, Db); + response = (Category) await autoQuery.PartialUpdateAsync(request, Request, Db); } if (request.RemoveOptionIds?.Count > 0) { From c387bc229da23486636b305e7873dafd020a83b4 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 23 Sep 2024 17:50:37 +0800 Subject: [PATCH 2/5] Add podcast episode AudioPlayer on all v8.x release notes --- MyApp/_pages/releases/v8_00.md | 7 +- MyApp/_pages/releases/v8_01.md | 9 +- MyApp/_pages/releases/v8_02.md | 5 + MyApp/_pages/releases/v8_03.md | 5 + MyApp/_pages/releases/v8_04.md | 5 + MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs | 452 +++++++++++++++++++ MyApp/wwwroot/pages/releases/v8_00.mjs | 2 + MyApp/wwwroot/pages/releases/v8_01.mjs | 2 + MyApp/wwwroot/pages/releases/v8_02.mjs | 2 + MyApp/wwwroot/pages/releases/v8_03.mjs | 22 + MyApp/wwwroot/pages/releases/v8_04.mjs | 12 + 11 files changed, 518 insertions(+), 5 deletions(-) create mode 100644 MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs create mode 100644 MyApp/wwwroot/pages/releases/v8_03.mjs create mode 100644 MyApp/wwwroot/pages/releases/v8_04.mjs diff --git a/MyApp/_pages/releases/v8_00.md b/MyApp/_pages/releases/v8_00.md index 1773364ba3..9f9a1f680a 100644 --- a/MyApp/_pages/releases/v8_00.md +++ b/MyApp/_pages/releases/v8_00.md @@ -2,7 +2,12 @@ title: ServiceStack v8 --- - +
+ +
+ +![](/img/pages/release-notes/v8/net8.webp) ## .NET 8 diff --git a/MyApp/_pages/releases/v8_01.md b/MyApp/_pages/releases/v8_01.md index 37c053f6fc..6d8fc11626 100644 --- a/MyApp/_pages/releases/v8_01.md +++ b/MyApp/_pages/releases/v8_01.md @@ -2,11 +2,12 @@ title: ServiceStack v8.1 --- -
- -
+
+ +
- +![](/img/pages/release-notes/v8.1/aspnet-8.webp) ## Full integration with ASP .NET Core 8 diff --git a/MyApp/_pages/releases/v8_02.md b/MyApp/_pages/releases/v8_02.md index b1003846d6..5d5757a61f 100644 --- a/MyApp/_pages/releases/v8_02.md +++ b/MyApp/_pages/releases/v8_02.md @@ -2,6 +2,11 @@ title: ServiceStack v8.2 --- +
+ +
+ ![](/img/pages/release-notes/v8.2/spa-logos.webp) With ServiceStack [now fully integrated with .NET 8](/endpoint-routing), our focus has shifted diff --git a/MyApp/_pages/releases/v8_03.md b/MyApp/_pages/releases/v8_03.md index 2f6edb12ba..9b7e808b89 100644 --- a/MyApp/_pages/releases/v8_03.md +++ b/MyApp/_pages/releases/v8_03.md @@ -2,6 +2,11 @@ title: ServiceStack v8.3 --- +
+ +
+ ![](/img/pages/release-notes/v8.3/bg-security.webp) We've got a couple of exciting features in this release opening ServiceStack up to new use-cases with the potential diff --git a/MyApp/_pages/releases/v8_04.md b/MyApp/_pages/releases/v8_04.md index 3a61e594c7..99be690837 100644 --- a/MyApp/_pages/releases/v8_04.md +++ b/MyApp/_pages/releases/v8_04.md @@ -2,6 +2,11 @@ title: ServiceStack v8.4 --- +
+ +
+ ![](/img/pages/release-notes/v8.4/bg-sqlite.webp) We're excited to announce **Background Jobs** our effortless solution for queueing and managing diff --git a/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs b/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs new file mode 100644 index 0000000000..f5e88ec1e5 --- /dev/null +++ b/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs @@ -0,0 +1,452 @@ +import { ref, computed, nextTick, shallowRef, onMounted, onUnmounted } from "vue" + +const ForwardIcon = { + template: ` + ` +} +const ForwardButton = { + components: { ForwardIcon }, + template: ` + `, + props: { player:Object, amount:Number }, +} + +const MuteIcon = { + template:` + `, + props: { muted:Boolean } +} +const MuteButton = { + components: { MuteIcon }, + template:` + + `, + props: { player:Object }, +} + +const playbackRates = [ + { + value: 1, + icon: { + template:` + + `, + }, + }, + { + value: 1.5, + icon: { + template: ` + + ` + }, + }, + { + value: 2, + icon: { + template: ` + + ` + }, + }, +] +const PlaybackRateButton = { + template:` + + `, + props: { player:Object }, + setup(props) { + const playbackRate = shallowRef(playbackRates[0]) + function nextPlaybackRate() { + let existingIdx = playbackRates.indexOf(playbackRate.value) + let idx = (existingIdx + 1) % playbackRates.length + let next = playbackRates[idx] + playbackRate.value = next + props.player.playbackRate(next.value) + } + return { playbackRate, nextPlaybackRate } + } +} + +const PlayButton = { + template:` + + `, + props: { player:Object }, +} + +const RewindIcon = { + template:` + `, +} +const RewindButton = { + components: { RewindIcon }, + template:` + `, + props: { player:Object, amount:Number }, +} + +const Slider = { + template:` +
+
+
+
+
+ +
+ `, + emits: ['update:currentTime'], + props: { + currentTime: { + type: Number, + default: 0 + }, + duration: { + type: Number, + default: 0 + } + }, + setup(props, { emit }) { + const sliderRef = ref(null) + const isDragging = ref(false) + const progress = computed(() => (props.duration > 0 ? (props.currentTime / props.duration) * 100 : 0)) + function startDrag() { + isDragging.value = true; + } + function stopDrag() { + isDragging.value = false + } + function handleDrag(event) { + if (isDragging.value) { + updateProgress(event) + } + } + function handleClick(event) { + updateProgress(event) + } + function updateProgress(event) { + if (sliderRef.value) { + const rect = sliderRef.value.getBoundingClientRect() + const offsetX = event.clientX - rect.left + const newProgress = (offsetX / rect.width) * 100 + const newTime = (props.duration * newProgress) / 100 + emit('update:currentTime', newTime) + } + } + + const formatTime = (time) => { + const minutes = Math.floor(time / 60) + const seconds = Math.floor(time % 60) + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` + }; + onMounted(() => { + document.addEventListener('mouseup', stopDrag) + }) + onUnmounted(() => { + document.removeEventListener('mouseup', stopDrag) + }) + return { + sliderRef, + progress, + formatTime, + startDrag, + stopDrag, + handleDrag, + handleClick, + } + } +} + +const AudioPlayer = { + components: { + PlayButton, + MuteButton, + RewindButton, + ForwardButton, + PlaybackRateButton, + Slider, + }, + template:` +
+
+ `, + props: { + bus: Object, + id: String, + title: String, + src: String, + playbackRate: Number, + currentTime: Number, + autoPlay: Boolean, + variant: String, + cls: { + type: String, + default: 'flex items-center gap-6 bg-white/90 dark:bg-black/90 px-4 py-4 shadow shadow-slate-200/80 dark:shadow-slate-700/80 ring-1 ring-slate-900/5 dark:ring-slate-50/5 backdrop-blur-sm md:px-6' + }, + innerCls: { + type: String, + default: 'mb-[env(safe-area-inset-bottom)] flex flex-1 flex-col gap-3 overflow-hidden p-1' + }, + controlsCls: { + type: String, + default: 'flex justify-between gap-6' + }, + startControlsCls: { + type: String, + default: 'flex items-center md:hidden' + }, + midControlsCls: { + type: String, + default: 'flex flex-none items-center gap-4' + }, + endControlsCls: { + type: String, + default: 'flex items-center gap-4' + }, + }, + setup(props) { + const refPlayer = ref() + const muted = ref(false) + const state = ref('paused') + const currentTime = ref() + const duration = ref() + const playbackRate = ref(props.playbackRate || 1) + + const variants = { + cls: { + compact: 'flex items-center bg-white/90 dark:bg-black/90 shadow shadow-slate-200/80 dark:shadow-slate-700/80 ring-1 ring-slate-900/5 dark:ring-slate-50/5 backdrop-blur-sm gap-2 pl-2 pr-4 rounded-full' + }, + innerCls: { + compact: 'mb-[env(safe-area-inset-bottom)] flex flex-1 flex-col gap-1 overflow-hidden p-1' + }, + controlsCls: { + compact: 'flex justify-between gap-2' + }, + startControlsCls: {}, + midControlsCls: { + compact: 'flex flex-none items-center gap-1 mr-1' + }, + endControlsCls: { + compact: 'flex items-center gap-1' + }, + } + const v = { + cls: variants.cls[props.variant] ?? props.cls, + innerCls: variants.innerCls[props.variant] ?? props.innerCls, + controlsCls: variants.controlsCls[props.variant] ?? props.controlsCls, + startControlsCls: variants.startControlsCls[props.variant] ?? props.startControlsCls, + midControlsCls: variants.midControlsCls[props.variant] ?? props.midControlsCls, + endControlsCls: variants.endControlsCls[props.variant] ?? props.endControlsCls, + } + + function onPlay() { + state.value = 'playing' + } + function onPause() { + state.value = 'paused' + } + function onTimeUpdate(time) { + currentTime.value = time + } + function onDurationChange(value) { + duration.value = value + } + + const player = globalThis.player = { + get url() { return props.url }, + get id() { return props.id }, + get title() { return props.title }, + play() { + // console.log('play', state.value, refPlayer.value.src, props.src) + if (refPlayer.value.src !== props.src) { + refPlayer.value.src = props.src + refPlayer.value.load() + refPlayer.value.pause() + refPlayer.value.playbackRate = playbackRate.value + refPlayer.value.currentTime = props.currentTime || 0 + } + refPlayer.value.play() + onPlay() + }, + pause() { + refPlayer.value.pause() + onPause() + }, + toggle() { + if (this.isPlaying) { + this.pause() + } else { + this.play() + } + }, + set muted(muted) { + refPlayer.value.muted = muted.value = muted; + }, + get muted() { + return muted.value + }, + toggleMute() { + refPlayer.value.muted = muted.value = !muted.value + }, + seekBy(secs) { + refPlayer.value.currentTime += secs + }, + /** @param {Number} value */ + set currentTime(value) { + refPlayer.value.currentTime = value + }, + playbackRate(rate) { + playbackRate.value = rate + }, + get isPlaying() { return state.value === 'playing' }, + } + + let sub = props.bus?.subscribe('toggleAudioPlayer', () => player.toggle()) + onMounted(() => { + if (props.autoPlay) { + player.toggle() + } + }) + onUnmounted(() => sub?.unsubscribe()) + // console.log('AudioPlayer.mjs', globalThis.player) + + return { + v, + refPlayer, + muted, + player, + currentTime, + duration, + playbackRate, + onPlay, + onPause, + onTimeUpdate, + onDurationChange, + } + } +} + +export default AudioPlayer \ No newline at end of file diff --git a/MyApp/wwwroot/pages/releases/v8_00.mjs b/MyApp/wwwroot/pages/releases/v8_00.mjs index e4439147d3..5b680e2a79 100644 --- a/MyApp/wwwroot/pages/releases/v8_00.mjs +++ b/MyApp/wwwroot/pages/releases/v8_00.mjs @@ -1,4 +1,5 @@ import Templates, { Index } from "../templates/Templates.mjs" +import AudioPlayer from "../podcasts/AudioPlayer.mjs" const BlazorTemplate = { components: { Templates }, @@ -28,6 +29,7 @@ export default { install(app) { }, components: { + AudioPlayer, BlazorTemplate, BlazorVueTemplate, IdentityAuthTemplates, diff --git a/MyApp/wwwroot/pages/releases/v8_01.mjs b/MyApp/wwwroot/pages/releases/v8_01.mjs index 6c19a77cfe..7b415fb09a 100644 --- a/MyApp/wwwroot/pages/releases/v8_01.mjs +++ b/MyApp/wwwroot/pages/releases/v8_01.mjs @@ -1,4 +1,5 @@ import Templates, { Index } from "../templates/Templates.mjs" +import AudioPlayer from "../podcasts/AudioPlayer.mjs" const BlazorTemplate = { components: { Templates }, @@ -28,6 +29,7 @@ export default { install(app) { }, components: { + AudioPlayer, BlazorTemplate, BlazorWasmTemplate, BlazorVueTemplate, diff --git a/MyApp/wwwroot/pages/releases/v8_02.mjs b/MyApp/wwwroot/pages/releases/v8_02.mjs index ed75a8283a..c289f22663 100644 --- a/MyApp/wwwroot/pages/releases/v8_02.mjs +++ b/MyApp/wwwroot/pages/releases/v8_02.mjs @@ -1,4 +1,5 @@ import Templates, { Index } from "../templates/Templates.mjs" +import AudioPlayer from "../podcasts/AudioPlayer.mjs" const ComposeTemplate = { components: { Templates }, @@ -12,6 +13,7 @@ export default { install(app) { }, components: { + AudioPlayer, ComposeTemplate, }, setup() { diff --git a/MyApp/wwwroot/pages/releases/v8_03.mjs b/MyApp/wwwroot/pages/releases/v8_03.mjs new file mode 100644 index 0000000000..8b8f42f23b --- /dev/null +++ b/MyApp/wwwroot/pages/releases/v8_03.mjs @@ -0,0 +1,22 @@ +import Templates, { Index } from "../templates/Templates.mjs" +import AudioPlayer from "../podcasts/AudioPlayer.mjs" + +const AuthTemplates = { + components: { Templates }, + template:``, + setup() { + return { Index } + } +} + +export default { + install(app) { + }, + components: { + AudioPlayer, + AuthTemplates, + }, + setup() { + return { } + } +} diff --git a/MyApp/wwwroot/pages/releases/v8_04.mjs b/MyApp/wwwroot/pages/releases/v8_04.mjs new file mode 100644 index 0000000000..e024c2f29b --- /dev/null +++ b/MyApp/wwwroot/pages/releases/v8_04.mjs @@ -0,0 +1,12 @@ +import AudioPlayer from "../podcasts/AudioPlayer.mjs" + +export default { + install(app) { + }, + components: { + AudioPlayer, + }, + setup() { + return { } + } +} From 48f0103f44a9f857b70a3e0e0b1c7b4bbba7d85f Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 24 Sep 2024 15:55:49 +0800 Subject: [PATCH 3/5] add beacon-url to podcasts --- MyApp/_pages/releases/v8_00.md | 2 +- MyApp/_pages/releases/v8_01.md | 2 +- MyApp/_pages/releases/v8_02.md | 2 +- MyApp/_pages/releases/v8_03.md | 2 +- MyApp/_pages/releases/v8_04.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/MyApp/_pages/releases/v8_00.md b/MyApp/_pages/releases/v8_00.md index 9f9a1f680a..4735df2eec 100644 --- a/MyApp/_pages/releases/v8_00.md +++ b/MyApp/_pages/releases/v8_00.md @@ -3,7 +3,7 @@ title: ServiceStack v8 ---
-
diff --git a/MyApp/_pages/releases/v8_01.md b/MyApp/_pages/releases/v8_01.md index 6d8fc11626..309716c24a 100644 --- a/MyApp/_pages/releases/v8_01.md +++ b/MyApp/_pages/releases/v8_01.md @@ -3,7 +3,7 @@ title: ServiceStack v8.1 ---
-
diff --git a/MyApp/_pages/releases/v8_02.md b/MyApp/_pages/releases/v8_02.md index 5d5757a61f..aa8880c94f 100644 --- a/MyApp/_pages/releases/v8_02.md +++ b/MyApp/_pages/releases/v8_02.md @@ -3,7 +3,7 @@ title: ServiceStack v8.2 ---
-
diff --git a/MyApp/_pages/releases/v8_03.md b/MyApp/_pages/releases/v8_03.md index 9b7e808b89..e6fd1cf0db 100644 --- a/MyApp/_pages/releases/v8_03.md +++ b/MyApp/_pages/releases/v8_03.md @@ -3,7 +3,7 @@ title: ServiceStack v8.3 ---
-
diff --git a/MyApp/_pages/releases/v8_04.md b/MyApp/_pages/releases/v8_04.md index 99be690837..e2486fbef5 100644 --- a/MyApp/_pages/releases/v8_04.md +++ b/MyApp/_pages/releases/v8_04.md @@ -3,7 +3,7 @@ title: ServiceStack v8.4 ---
-
From 13b1a07cd5beb3cd806b8188b2a86e3276a3d670 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 24 Sep 2024 16:00:49 +0800 Subject: [PATCH 4/5] Add support for beaconUrl --- MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs b/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs index f5e88ec1e5..0aa3b75918 100644 --- a/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs +++ b/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs @@ -302,6 +302,7 @@ const AudioPlayer = { currentTime: Number, autoPlay: Boolean, variant: String, + beaconUrl: String, cls: { type: String, default: 'flex items-center gap-6 bg-white/90 dark:bg-black/90 px-4 py-4 shadow shadow-slate-200/80 dark:shadow-slate-700/80 ring-1 ring-slate-900/5 dark:ring-slate-50/5 backdrop-blur-sm md:px-6' @@ -337,7 +338,7 @@ const AudioPlayer = { const variants = { cls: { - compact: 'flex items-center bg-white/90 dark:bg-black/90 shadow shadow-slate-200/80 dark:shadow-slate-700/80 ring-1 ring-slate-900/5 dark:ring-slate-50/5 backdrop-blur-sm gap-2 pl-2 pr-4 rounded-full' + compact: 'flex items-center bg-white/90 dark:bg-black/90 shadow shadow-slate-200/80 dark:shadow-slate-700/80 ring-1 ring-slate-900/5 dark:ring-slate-50/5 backdrop-blur-sm gap-2 pl-1 pr-4 rounded-full' }, innerCls: { compact: 'mb-[env(safe-area-inset-bottom)] flex flex-1 flex-col gap-1 overflow-hidden p-1' @@ -387,6 +388,9 @@ const AudioPlayer = { refPlayer.value.pause() refPlayer.value.playbackRate = playbackRate.value refPlayer.value.currentTime = props.currentTime || 0 + if (props.beaconUrl) { + navigator.sendBeacon(props.beaconUrl) + } } refPlayer.value.play() onPlay() From 0aa8b64423e2a8e6ab0e73b42898aed21314e049 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 27 Sep 2024 13:10:51 +0800 Subject: [PATCH 5/5] update Callback URLs --- MyApp/_pages/background-jobs.md | 14 +++++++------- MyApp/_pages/releases/v8_04.md | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/MyApp/_pages/background-jobs.md b/MyApp/_pages/background-jobs.md index 917ef00977..b279158a82 100644 --- a/MyApp/_pages/background-jobs.md +++ b/MyApp/_pages/background-jobs.md @@ -635,31 +635,31 @@ public class NotifyCheckUrlsCommand(IHttpClientFactory clientFactory) `ReplyTo` can be any URL which by default will have the result POST'ed back to the URL with a JSON Content-Type. Typically URLs will contain a reference Id so external clients can correlate a callback with the internal process that initiated the job. If the callback API is publicly available you'll -want to use an internal Id that can't be guessed so the callback can't be spoofed, like a Guid, e.g: +want to use an internal Id that can't be guessed (like a Guid) so the callback can't be spoofed, e.g: -`$"https://api.example.com?refId={RefId}"` +`$"https://api.example.com/callback?refId={RefId}"` If needed the callback URL can be customized on how the HTTP Request callback is sent. If the URL contains a space, the text before the space is treated as the HTTP method: -`"PUT https://api.example.com"` +`"PUT https://api.example.com/callback"` If the auth part contains a colon `:` it's treated as Basic Auth: -`"username:password@https://api.example.com"` +`"username:password@https://api.example.com/callback"` If name starts with `http.` sends a HTTP Header -`"http.X-API-Key:myApiKey@https://api.example.com"` +`"http.X-API-Key:myApiKey@https://api.example.com/callback"` Otherwise it's sent as a Bearer Token: -`"myToken123@https://api.example.com"` +`"myToken123@https://api.example.com/callback"` Bearer Token or HTTP Headers starting with `$` is substituted with Environment Variable if exists: -`"$API_TOKEN@https://api.example.com"` +`"$API_TOKEN@https://api.example.com/callback"` When needed headers, passwords and tokens can be URL encoded if they contain any delimiter characters. diff --git a/MyApp/_pages/releases/v8_04.md b/MyApp/_pages/releases/v8_04.md index e2486fbef5..cfbdbfdf45 100644 --- a/MyApp/_pages/releases/v8_04.md +++ b/MyApp/_pages/releases/v8_04.md @@ -779,31 +779,31 @@ public class NotifyCheckUrlsCommand(IHttpClientFactory clientFactory) `ReplyTo` can be any URL which by default will have the result POST'ed back to the URL with a JSON Content-Type. Typically URLs will contain a reference Id so external clients can correlate a callback with the internal process that initiated the job. If the callback API is publicly available you'll -want to use an internal Id that can't be guessed so the callback can't be spoofed, like a Guid, e.g: +want to use an internal Id that can't be guessed (like a Guid) so the callback can't be spoofed, e.g: -`$"https://api.example.com?refId={RefId}"` +`$"https://api.example.com/callback?refId={RefId}"` If needed the callback URL can be customized on how the HTTP Request callback is sent. If the URL contains a space, the text before the space is treated as the HTTP method: -`"PUT https://api.example.com"` +`"PUT https://api.example.com/callback"` If the auth part contains a colon `:` it's treated as Basic Auth: -`"username:password@https://api.example.com"` +`"username:password@https://api.example.com/callback"` If name starts with `http.` sends a HTTP Header -`"http.X-API-Key:myApiKey@https://api.example.com"` +`"http.X-API-Key:myApiKey@https://api.example.com/callback"` Otherwise it's sent as a Bearer Token: -`"myToken123@https://api.example.com"` +`"myToken123@https://api.example.com/callback"` Bearer Token or HTTP Headers starting with `$` is substituted with Environment Variable if exists: -`"$API_TOKEN@https://api.example.com"` +`"$API_TOKEN@https://api.example.com/callback"` When needed headers, passwords and tokens can be URL encoded if they contain any delimiter characters.