diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b2244b3..4a8ef53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Represents the **NuGet** versions. +## v3.27.2 +- *Fixed:* The `IServiceCollection.AddCosmosDb` extension method was registering as a singleton; this has been corrected to register as scoped. The dependent `CosmosClient` should remain a singleton as is [best practice](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/best-practice-dotnet). + ## v3.27.1 - *Fixed:* Updated `Microsoft.Extensions.Caching.Memory` package depenedency to latest (including related); resolve [Microsoft Security Advisory CVE-2024-43483](https://github.com/advisories/GHSA-qj66-m88j-hmgj). - *Fixed:* Fixed the `ExecutionContext.UserIsAuthorized` to have base implementation similar to `UserIsInRole`. diff --git a/Common.targets b/Common.targets index 5be1cf1e..0245c7be 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.27.1 + 3.27.2 preview Avanade Avanade diff --git a/samples/My.Hr/My.Hr.Api/Startup.cs b/samples/My.Hr/My.Hr.Api/Startup.cs index 6cbeb61c..97958c3a 100644 --- a/samples/My.Hr/My.Hr.Api/Startup.cs +++ b/samples/My.Hr/My.Hr.Api/Startup.cs @@ -34,7 +34,7 @@ public void ConfigureServices(IServiceCollection services) .AddAzureServiceBusSender() .AddAzureServiceBusPurger() .AddJsonMergePatch() - .AddWebApi((_, c) => c.UnhandledExceptionAsync = (ex, _, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? WebApiBase.CreateActionResultFromExtendedException(new ConcurrencyException()) : null)) + .AddWebApi((_, webapi) => webapi.UnhandledExceptionAsync = (ex, _, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? webapi.CreateActionResultFromExtendedException(new ConcurrencyException()) : null)) .AddReferenceDataContentWebApi() .AddRequestCache(); diff --git a/samples/My.Hr/My.Hr.Functions/Startup.cs b/samples/My.Hr/My.Hr.Functions/Startup.cs index 8d1b55d5..63f1af96 100644 --- a/samples/My.Hr/My.Hr.Functions/Startup.cs +++ b/samples/My.Hr/My.Hr.Functions/Startup.cs @@ -40,7 +40,7 @@ public override void Configure(IFunctionsHostBuilder builder) .AddEventPublisher() .AddSingleton(sp => new Az.ServiceBusClient(sp.GetRequiredService().ServiceBusConnection__fullyQualifiedNamespace)) .AddAzureServiceBusSender() - .AddWebApi((_, c) => c.UnhandledExceptionAsync = (ex, _, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? WebApiBase.CreateActionResultFromExtendedException(new ConcurrencyException()) : null)) + .AddWebApi((_, webapi) => webapi.UnhandledExceptionAsync = (ex, _, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? webapi.CreateActionResultFromExtendedException(new ConcurrencyException()) : null)) .AddJsonMergePatch() .AddWebApiPublisher() .AddAzureServiceBusSubscriber(); diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs b/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs index b69eddff..e964e56c 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs @@ -63,6 +63,12 @@ public abstract class WebApiBase(ExecutionContext executionContext, SettingsBase /// Searches the for or one of the other to determine the (uses first value found in sequence). public IEnumerable SecondaryCorrelationIdNames { get; set; } = ["x-ms-client-tracking-id"]; + /// + /// Gets or sets the creator function used by . + /// + /// This allows an alternate serialization or handling as required. Defaults to the . + public Func ExtendedExceptionActionResultCreator { get; set; } = DefaultExtendedExceptionActionResultCreator; + /// /// Gets the list of correlation identifier names, being and (inclusive). /// @@ -183,7 +189,9 @@ public static async Task CreateActionResultFromExceptionAsync(Web if (eex.ShouldBeLogged) logger.LogError(exception, "{Error}", exception.Message); - ar = CreateActionResultFromExtendedException(eex); + ar = owner is null + ? DefaultExtendedExceptionActionResultCreator(eex) + : owner.CreateActionResultFromExtendedException(eex); } else { @@ -204,7 +212,14 @@ public static async Task CreateActionResultFromExceptionAsync(Web /// Creates an from an . /// /// The . - public static IActionResult CreateActionResultFromExtendedException(IExtendedException extendedException) + public IActionResult CreateActionResultFromExtendedException(IExtendedException extendedException) => ExtendedExceptionActionResultCreator(extendedException); + + /// + /// The default . + /// + /// The . + /// The resulting . + public static IActionResult DefaultExtendedExceptionActionResultCreator(IExtendedException extendedException) { if (extendedException is ValidationException vex && vex.Messages is not null && vex.Messages.Count > 0) { diff --git a/src/CoreEx.Cosmos/CosmosDb.cs b/src/CoreEx.Cosmos/CosmosDb.cs index 6cce0d36..fb69a2f6 100644 --- a/src/CoreEx.Cosmos/CosmosDb.cs +++ b/src/CoreEx.Cosmos/CosmosDb.cs @@ -24,6 +24,10 @@ namespace CoreEx.Cosmos /// The . /// The . /// Enables the to be overridden; defaults to . + /// It is recommended that the is registered as a scoped service to enable capabilities such as that must be scoped. + /// Use to + /// register the scoped instance. + /// The dependent should however be registered as a singleton as is best practice. public class CosmosDb(Database database, IMapper mapper, CosmosDbInvoker? invoker = null) : ICosmosDb { private static CosmosDbInvoker? _invoker; @@ -181,7 +185,8 @@ public async Task SelectMultiSetWithResultAsync(PartitionKey partitionKe if (multiSetList.Any(x => !x.Container.IsCosmosDbValueModel)) throw new ArgumentException($"All {nameof(IMultiSetArgs)} containers must be of type CosmosDbValueContainer.", nameof(multiSetArgs)); - + + // Build the Cosmos SQL statement. var container = multiSetList[0].Container; var types = new Dictionary([ new KeyValuePair(container.ModelType.Name, multiSetList[0]) ]); var sb = string.IsNullOrEmpty(sql) ? new StringBuilder($"SELECT * FROM c WHERE c.type in (\"{container.ModelType.Name}\"") : null; diff --git a/src/CoreEx.Cosmos/CosmosDbServiceCollectionExtensions.cs b/src/CoreEx.Cosmos/CosmosDbServiceCollectionExtensions.cs index f2a4aa12..e4aaa348 100644 --- a/src/CoreEx.Cosmos/CosmosDbServiceCollectionExtensions.cs +++ b/src/CoreEx.Cosmos/CosmosDbServiceCollectionExtensions.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.DependencyInjection public static class CosmosDbServiceCollectionExtensions { /// - /// Adds an as a singleton service. + /// Adds an as a scoped service. /// /// The . /// The . @@ -23,7 +23,7 @@ public static class CosmosDbServiceCollectionExtensions /// The to support fluent-style method-chaining. public static IServiceCollection AddCosmosDb(this IServiceCollection services, Func create, bool healthCheck = true) where TCosmosDb : class, ICosmosDb { - services.ThrowIfNull(nameof(services)).AddSingleton(sp => create.ThrowIfNull(nameof(create)).Invoke(sp)); + services.ThrowIfNull(nameof(services)).AddScoped(sp => create.ThrowIfNull(nameof(create)).Invoke(sp)); if (healthCheck) services.AddHealthChecks().AddCosmosDbHealthCheck(); @@ -31,7 +31,7 @@ public static IServiceCollection AddCosmosDb(this IServiceCollection } /// - /// Adds an as a singleton service including a corresponding health check. + /// Adds an as a scoped service including a corresponding health check. /// /// The . /// The . @@ -40,7 +40,7 @@ public static IServiceCollection AddCosmosDb(this IServiceCollection /// The to support fluent-style method-chaining. public static IServiceCollection AddCosmosDb(this IServiceCollection services, Func create, string? healthCheckName) where TCosmosDb : class, ICosmosDb { - services.ThrowIfNull(nameof(services)).AddSingleton(sp => create.ThrowIfNull(nameof(create)).Invoke(sp)); + services.ThrowIfNull(nameof(services)).AddScoped(sp => create.ThrowIfNull(nameof(create)).Invoke(sp)); services.AddHealthChecks().AddCosmosDbHealthCheck(healthCheckName); return services; } diff --git a/src/CoreEx/Events/EventDataFormatter.cs b/src/CoreEx/Events/EventDataFormatter.cs index 9cbc6e5b..2a97a7fc 100644 --- a/src/CoreEx/Events/EventDataFormatter.cs +++ b/src/CoreEx/Events/EventDataFormatter.cs @@ -152,7 +152,7 @@ public virtual void Format(EventData @event) var value = @event.Value; @event.Id ??= Guid.NewGuid().ToString(); - @event.Timestamp ??= new DateTimeOffset(ExecutionContext.SystemTime.UtcNow); + @event.Timestamp ??= new DateTimeOffset(ExecutionContext.HasCurrent ? ExecutionContext.Current.Timestamp : ExecutionContext.SystemTime.UtcNow); if (PropertySelection.HasFlag(EventDataProperty.Key)) { diff --git a/src/CoreEx/ExecutionContext.cs b/src/CoreEx/ExecutionContext.cs index 6659e02e..74aedca4 100644 --- a/src/CoreEx/ExecutionContext.cs +++ b/src/CoreEx/ExecutionContext.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Threading; namespace CoreEx