From 716cc6091540acfc8d90bcf017461be7578a8861 Mon Sep 17 00:00:00 2001 From: Eric Sibly Date: Sat, 7 Dec 2024 14:46:07 -0800 Subject: [PATCH] v3.30.1 - *Fixed:* Added support for `SettingsBase.DateTimeTransform`, `StringTransform`, `StringTrim` and `StringCase` to allow specification via configuration. - *Fixed:* Added support for `CoreEx:` hierarchy (optional) for all _CoreEx_ settings to enable a more structured and explicit configuration. --- CHANGELOG.md | 4 ++ Common.targets | 2 +- samples/My.Hr/My.Hr.Api/appsettings.json | 18 ++--- .../My.Hr.Database/My.Hr.Database.csproj | 2 +- .../My.Hr.UnitTest/EmployeeFunctionTest.cs | 3 + .../My.Hr.UnitTest/My.Hr.UnitTest.csproj | 2 +- .../ServiceBusOrchestratedSubscriber.cs | 8 +-- .../ServiceBus/ServiceBusPurger.cs | 4 +- .../ServiceBus/ServiceBusSender.cs | 2 +- .../ServiceBus/ServiceBusSubscriber.cs | 8 +-- .../Storage/TableWorkStatePersistence.cs | 14 +--- src/CoreEx.Data/CoreEx.Data.csproj | 2 +- .../Querying/QueryFilterFieldConfigBase.cs | 4 ++ .../DatabaseServiceCollectionExtensions.cs | 2 +- .../Outbox/EventOutboxHostedService.cs | 4 +- .../Outbox/EventOutboxService.cs | 4 +- .../Mapping/DatabaseMapperT.cs | 17 +++-- .../Mapping/DataverseMapperT.cs | 13 ++-- .../Json/ContractResolver.cs | 4 +- src/CoreEx.OData/Mapping/ODataMapperT.cs | 13 ++-- src/CoreEx.OData/README.md | 2 +- src/CoreEx.Solace/PubSub/PubSubSender.cs | 2 +- .../CoreEx.UnitTesting.Azure.Functions.csproj | 2 +- ...CoreEx.UnitTesting.Azure.ServiceBus.csproj | 2 +- .../CoreEx.UnitTesting.csproj | 2 +- .../Rules/CompareRuleBase.cs | 11 +-- src/CoreEx.Validation/Rules/EnumRule.cs | 4 ++ .../ValidationServiceCollectionExtensions.cs | 4 +- src/CoreEx/Abstractions/Internal.cs | 10 ++- .../Abstractions/Reflection/TypeReflector.cs | 2 +- src/CoreEx/Configuration/SettingsBase.cs | 68 +++++++++++++++---- src/CoreEx/CoreEx.csproj | 2 +- src/CoreEx/Entities/Cleaner.cs | 18 ++--- src/CoreEx/Entities/Extended/EntityCore.cs | 4 ++ src/CoreEx/Events/CloudEventSerializerBase.cs | 11 +-- src/CoreEx/Events/EventDataSerializerBase.cs | 19 ++---- src/CoreEx/Events/EventPublisher.cs | 2 +- src/CoreEx/Events/EventSubscriberBase.cs | 34 +++------- src/CoreEx/Events/InMemoryPublisher.cs | 4 +- src/CoreEx/Events/InMemorySender.cs | 2 +- .../EventSubscriberOrchestrator.cs | 2 +- src/CoreEx/ExecutionContext.cs | 4 ++ src/CoreEx/Hosting/FileLockSynchronizer.cs | 2 +- src/CoreEx/Hosting/TimerHostedServiceBase.cs | 6 +- .../Hosting/Work/FileWorkStatePersistence.cs | 2 +- .../Work/InMemoryWorkStatePersistence.cs | 2 +- .../Hosting/Work/WorkStateOrchestrator.cs | 2 +- src/CoreEx/Hosting/Work/WorkStatus.cs | 6 +- src/CoreEx/Http/HttpArg.cs | 4 ++ src/CoreEx/Http/TypedHttpClientBase.cs | 5 +- src/CoreEx/Invokers/InvokeArgs.cs | 4 +- .../Json/Compare/JsonElementComparer.cs | 6 +- .../Json/Compare/JsonElementComparerResult.cs | 2 +- src/CoreEx/Json/Mapping/JsonObjectMapperT.cs | 13 ++-- .../Caching/SettingsBasedCacheEntry.cs | 4 +- src/CoreEx/RefData/ReferenceDataCodeList.cs | 4 +- src/CoreEx/RefData/ReferenceDataCollection.cs | 4 ++ .../RefData/ReferenceDataOrchestrator.cs | 4 ++ src/CoreEx/ValidationException.cs | 2 +- .../CoreEx.Cosmos.Test.csproj | 2 +- tests/CoreEx.Test/CoreEx.Test.csproj | 2 +- .../Framework/Entities/CleanerTest.cs | 65 +++++++++++++++++- .../Framework/WebApis/WebApiPublisherTest.cs | 2 +- .../HttpTriggerPublishFunctionTest.cs | 8 +-- tests/CoreEx.Test2/CoreEx.Test2.csproj | 2 +- .../Validation/ValidationExtensionsTest.cs | 2 +- .../TestFunctionIso/ServiceBusTest.cs | 4 +- .../Functions/HttpTriggerFunction.cs | 12 +--- 68 files changed, 310 insertions(+), 202 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b18820..6a343378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Represents the **NuGet** versions. +## v3.30.1 +- *Fixed:* Added support for `SettingsBase.DateTimeTransform`, `StringTransform`, `StringTrim` and `StringCase` to allow specification via configuration. +- *Fixed:* Added support for `CoreEx:` hierarchy (optional) for all _CoreEx_ settings to enable a more structured and explicit configuration. + ## v3.30.0 - *Enhancement:* Integrated `UnitTestEx` version `5.0.0` to enable the latest capabilities and improvements. - `CoreEx.UnitTesting.NUnit` given changes is no longer required and has been deprecated, the `UnitTestEx.NUnit` (or other) must be explicitly referenced as per testing framework being used. diff --git a/Common.targets b/Common.targets index f3ea876f..c0f9c6aa 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.30.0 + 3.30.1 preview Avanade Avanade diff --git a/samples/My.Hr/My.Hr.Api/appsettings.json b/samples/My.Hr/My.Hr.Api/appsettings.json index 8b858ae5..c8b9c000 100644 --- a/samples/My.Hr/My.Hr.Api/appsettings.json +++ b/samples/My.Hr/My.Hr.Api/appsettings.json @@ -5,14 +5,16 @@ "Microsoft.AspNetCore": "Warning" } }, - "PagingDefaultTake": 25, - "PagingMaxTake": 500, - "RefDataCache": { - "AbsoluteExpirationRelativeToNow": "01:45:00", - "SlidingExpiration": "00:15:00", - "Gender": { - "AbsoluteExpirationRelativeToNow": "03:00:00", - "SlidingExpiration": "00:45:00" + "CoreEx": { + "PagingDefaultTake": 25, + "PagingMaxTake": 500, + "RefDataCache": { + "AbsoluteExpirationRelativeToNow": "01:45:00", + "SlidingExpiration": "00:15:00", + "Gender": { + "AbsoluteExpirationRelativeToNow": "03:00:00", + "SlidingExpiration": "00:45:00" + } } }, "ServiceBusConnection": { diff --git a/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj b/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj index aee41a46..9acbcaeb 100644 --- a/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj +++ b/samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj @@ -22,7 +22,7 @@ - + diff --git a/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs b/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs index d76af59e..e06b5d65 100644 --- a/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs +++ b/samples/My.Hr/My.Hr.UnitTest/EmployeeFunctionTest.cs @@ -86,6 +86,7 @@ public void A130_Get_IncludeFields() using var test = FunctionTester.Create(); var e = test.HttpTrigger() + .WithRouteCheck(UnitTestEx.Azure.Functions.RouteCheckOption.PathAndQueryStartsWith) .Run(f => f.GetAsync(test.CreateHttpRequest(HttpMethod.Get, $"api/employees/{1.ToGuid()}", new CoreEx.Http.HttpRequestOptions().Include("FirstName", "LastName")), 1.ToGuid())) .AssertOK() .AssertJson("{\"firstName\":\"Wendy\",\"lastName\":\"Jones\"}"); @@ -114,6 +115,7 @@ public void B110_GetAll_Paging() using var test = FunctionTester.Create(); var v = test.HttpTrigger() + .WithRouteCheck(UnitTestEx.Azure.Functions.RouteCheckOption.PathAndQueryStartsWith) .Run(f => f.GetAllAsync(test.CreateHttpRequest(HttpMethod.Get, "api/employees", CoreEx.Http.HttpRequestOptions.Create(PagingArgs.CreateSkipAndTake(1, 2, true))))) .AssertOK() .GetValue(); @@ -134,6 +136,7 @@ public void B120_GetAll_PagingAndIncludeFields() using var test = FunctionTester.Create(); var v = test.HttpTrigger() + .WithRouteCheck(UnitTestEx.Azure.Functions.RouteCheckOption.PathAndQueryStartsWith) .Run(f => f.GetAllAsync(test.CreateHttpRequest(HttpMethod.Get, "api/employees", CoreEx.Http.HttpRequestOptions.Create(PagingArgs.CreateSkipAndTake(1, 2, false)).Include("lastname")))) .AssertOK() .AssertJson("[ { \"lastName\": \"Jones\" }, { \"lastName\": \"Smith\" } ]") diff --git a/samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj b/samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj index f31defe7..ac0822df 100644 --- a/samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj +++ b/samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj @@ -31,7 +31,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/CoreEx.Azure/ServiceBus/ServiceBusOrchestratedSubscriber.cs b/src/CoreEx.Azure/ServiceBus/ServiceBusOrchestratedSubscriber.cs index 208e6610..9a0797c5 100644 --- a/src/CoreEx.Azure/ServiceBus/ServiceBusOrchestratedSubscriber.cs +++ b/src/CoreEx.Azure/ServiceBus/ServiceBusOrchestratedSubscriber.cs @@ -39,10 +39,10 @@ public ServiceBusOrchestratedSubscriber(EventSubscriberOrchestrator orchestrator { Orchestrator = orchestrator.ThrowIfNull(nameof(orchestrator)); ServiceBusSubscriberInvoker = serviceBusSubscriberInvoker ?? (ServiceBusSubscriber._invoker ??= new ServiceBusSubscriberInvoker()); - AbandonOnTransient = settings.GetValue($"{GetType().Name}__{nameof(AbandonOnTransient)}", false); - MaxDeliveryCount = settings.GetValue($"{GetType().Name}__{nameof(MaxDeliveryCount)}"); - RetryDelay = settings.GetValue($"{GetType().Name}__{nameof(RetryDelay)}"); - MaxRetryDelay = settings.GetValue($"{GetType().Name}__{nameof(MaxRetryDelay)}"); + AbandonOnTransient = settings.GetCoreExValue($"{GetType().Name}:{nameof(AbandonOnTransient)}", false); + MaxDeliveryCount = settings.GetCoreExValue($"{GetType().Name}:{nameof(MaxDeliveryCount)}"); + RetryDelay = settings.GetCoreExValue($"{GetType().Name}:{nameof(RetryDelay)}"); + MaxRetryDelay = settings.GetCoreExValue($"{GetType().Name}:{nameof(MaxRetryDelay)}"); } /// diff --git a/src/CoreEx.Azure/ServiceBus/ServiceBusPurger.cs b/src/CoreEx.Azure/ServiceBus/ServiceBusPurger.cs index 65146f98..d4713b05 100644 --- a/src/CoreEx.Azure/ServiceBus/ServiceBusPurger.cs +++ b/src/CoreEx.Azure/ServiceBus/ServiceBusPurger.cs @@ -45,8 +45,8 @@ private async Task PurgeAsync(string queueOrTopicName, string? subscriptionName, queueOrTopicName.ThrowIfNullOrEmpty(nameof(queueOrTopicName)); // Get queue name and subscription name by checking settings override. - var qn = Settings.GetValue($"Publisher_ServiceBusQueueName_{queueOrTopicName}", defaultValue: queueOrTopicName); - var sn = string.IsNullOrEmpty(subscriptionName) ? null : Settings.GetValue($"Publisher_ServiceBusSubscriptionName_{subscriptionName}", defaultValue: subscriptionName); + var qn = Settings.GetCoreExValue($"Publisher_ServiceBusQueueName_{queueOrTopicName}", defaultValue: queueOrTopicName); + var sn = string.IsNullOrEmpty(subscriptionName) ? null : Settings.GetCoreExValue($"Publisher_ServiceBusSubscriptionName_{subscriptionName}", defaultValue: subscriptionName); // Receive from Dead letter var o = new ServiceBusReceiverOptions { SubQueue = subQueue, PrefetchCount = 500, ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete }; diff --git a/src/CoreEx.Azure/ServiceBus/ServiceBusSender.cs b/src/CoreEx.Azure/ServiceBus/ServiceBusSender.cs index c19752f8..db9e3ead 100644 --- a/src/CoreEx.Azure/ServiceBus/ServiceBusSender.cs +++ b/src/CoreEx.Azure/ServiceBus/ServiceBusSender.cs @@ -96,7 +96,7 @@ public Task SendAsync(IEnumerable events, CancellationToken cance { var n = qitem.Key == _unspecifiedQueueOrTopicName ? null : qitem.Key; var key = $"{GetType().Name}_QueueOrTopicName{(n is null ? "" : $"_{n}")}"; - var qn = Settings.GetValue($"{GetType().Name}:QueueOrTopicName{(n is null ? "" : $"_{n}")}", defaultValue: n) ?? throw new EventSendException(PrependStats($"'{key}' configuration setting must have a non-null value.", totalCount, unsentEvents.Count), unsentEvents); + var qn = Settings.GetCoreExValue($"{GetType().Name}:QueueOrTopicName{(n is null ? "" : $"_{n}")}", defaultValue: n) ?? throw new EventSendException(PrependStats($"'{key}' configuration setting must have a non-null value.", totalCount, unsentEvents.Count), unsentEvents); var queue = qitem.Value; var sentIds = new List(); diff --git a/src/CoreEx.Azure/ServiceBus/ServiceBusSubscriber.cs b/src/CoreEx.Azure/ServiceBus/ServiceBusSubscriber.cs index c097cee5..f85c26e7 100644 --- a/src/CoreEx.Azure/ServiceBus/ServiceBusSubscriber.cs +++ b/src/CoreEx.Azure/ServiceBus/ServiceBusSubscriber.cs @@ -51,10 +51,10 @@ public ServiceBusSubscriber(ExecutionContext executionContext, SettingsBase sett : base(eventDataConverter ?? new ServiceBusReceivedMessageEventDataConverter(eventSerializer ?? new CoreEx.Text.Json.EventDataSerializer()), executionContext, settings, logger, eventSubscriberInvoker) { ServiceBusSubscriberInvoker = serviceBusSubscriberInvoker ?? (_invoker ??= new ServiceBusSubscriberInvoker()); - AbandonOnTransient = settings.GetValue($"{GetType().Name}__{nameof(AbandonOnTransient)}", false); - MaxDeliveryCount = settings.GetValue($"{GetType().Name}__{nameof(MaxDeliveryCount)}"); - RetryDelay = settings.GetValue($"{GetType().Name}__{nameof(RetryDelay)}"); - MaxRetryDelay = settings.GetValue($"{GetType().Name}__{nameof(MaxRetryDelay)}"); + AbandonOnTransient = settings.GetCoreExValue($"{GetType().Name}:{nameof(AbandonOnTransient)}", false); + MaxDeliveryCount = settings.GetCoreExValue($"{GetType().Name}:{nameof(MaxDeliveryCount)}"); + RetryDelay = settings.GetCoreExValue($"{GetType().Name}:{nameof(RetryDelay)}"); + MaxRetryDelay = settings.GetCoreExValue($"{GetType().Name}:{nameof(MaxRetryDelay)}"); } /// diff --git a/src/CoreEx.Azure/Storage/TableWorkStatePersistence.cs b/src/CoreEx.Azure/Storage/TableWorkStatePersistence.cs index 21d0c852..9c42c138 100644 --- a/src/CoreEx.Azure/Storage/TableWorkStatePersistence.cs +++ b/src/CoreEx.Azure/Storage/TableWorkStatePersistence.cs @@ -3,7 +3,6 @@ using Azure; using Azure.Data.Tables; using CoreEx.Hosting.Work; -using CoreEx.Json; using System; using System.IO; using System.Linq; @@ -18,11 +17,8 @@ namespace CoreEx.Azure.Storage /// The maximum size currently supported is 960,000 bytes. public class TableWorkStatePersistence : IWorkStatePersistence { - private static readonly string[] _columns = [nameof(WorkState.TypeName), nameof(WorkState.CorrelationId), nameof(WorkState.Status), nameof(WorkState.Created), nameof(WorkState.Expiry), nameof(WorkState.Started), nameof(WorkState.Indeterminate), nameof(WorkState.Finished), nameof(WorkState.Reason)]; - private readonly TableClient _workStateTableClient; private readonly TableClient _workDataTableClient; - private readonly IJsonSerializer _jsonSerializer; private readonly SemaphoreSlim _semaphore = new(1, 1); private volatile bool _firstTime = true; @@ -32,17 +28,15 @@ public class TableWorkStatePersistence : IWorkStatePersistence /// The . /// The work state table name. /// The work data table name. - /// The . Defaults to . - public TableWorkStatePersistence(TableServiceClient tableServiceClient, string workStateTableName = "workstate", string workDataTableName = "workdata", IJsonSerializer? jsonSerializer = null) - : this(tableServiceClient.ThrowIfNull(nameof(tableServiceClient)).GetTableClient(workStateTableName), tableServiceClient.GetTableClient(workDataTableName), jsonSerializer) { } + public TableWorkStatePersistence(TableServiceClient tableServiceClient, string workStateTableName = "workstate", string workDataTableName = "workdata") + : this(tableServiceClient.ThrowIfNull(nameof(tableServiceClient)).GetTableClient(workStateTableName), tableServiceClient.GetTableClient(workDataTableName)) { } /// /// Initializes a new instance of the class. /// /// The work state . /// The work data . - /// The . Defaults to . - public TableWorkStatePersistence(TableClient workStateTableClient, TableClient workDataTableClient, IJsonSerializer? jsonSerializer = null) + public TableWorkStatePersistence(TableClient workStateTableClient, TableClient workDataTableClient) { if (workStateTableClient.ThrowIfNull(nameof(workStateTableClient)).Name == workDataTableClient.ThrowIfNull(nameof(workDataTableClient)).Name) throw new ArgumentException("The work state and data table names must be different.", nameof(workDataTableClient)); @@ -52,8 +46,6 @@ public TableWorkStatePersistence(TableClient workStateTableClient, TableClient w _workDataTableClient.CreateIfNotExists(); _workStateTableClient.CreateIfNotExists(); - - _jsonSerializer = jsonSerializer ?? JsonSerializer.Default; } private class WorkStateEntity() : WorkState, ITableEntity diff --git a/src/CoreEx.Data/CoreEx.Data.csproj b/src/CoreEx.Data/CoreEx.Data.csproj index 19f8e4a7..36ab17ca 100644 --- a/src/CoreEx.Data/CoreEx.Data.csproj +++ b/src/CoreEx.Data/CoreEx.Data.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/CoreEx.Data/Querying/QueryFilterFieldConfigBase.cs b/src/CoreEx.Data/Querying/QueryFilterFieldConfigBase.cs index 98b143fc..b310d7b0 100644 --- a/src/CoreEx.Data/Querying/QueryFilterFieldConfigBase.cs +++ b/src/CoreEx.Data/Querying/QueryFilterFieldConfigBase.cs @@ -219,7 +219,11 @@ public virtual StringBuilder AppendToString(StringBuilder stringBuilder) protected StringBuilder AppendOperatorsToString(StringBuilder stringBuilder) { var first = true; +#if NET6_0_OR_GREATER + foreach (var e in Enum.GetValues()) +#else foreach (var e in Enum.GetValues(typeof(QueryFilterOperator))) +#endif { if (Operators.HasFlag((QueryFilterOperator)e)) { diff --git a/src/CoreEx.Database.SqlServer/DatabaseServiceCollectionExtensions.cs b/src/CoreEx.Database.SqlServer/DatabaseServiceCollectionExtensions.cs index d8e12852..15851e53 100644 --- a/src/CoreEx.Database.SqlServer/DatabaseServiceCollectionExtensions.cs +++ b/src/CoreEx.Database.SqlServer/DatabaseServiceCollectionExtensions.cs @@ -28,7 +28,7 @@ public static class DatabaseServiceCollectionExtensions /// To turn off the execution of the (s) at runtime set the 'EventOutboxHostedService:Enabled' configuration setting to false. public static IServiceCollection AddSqlServerEventOutboxHostedService(this IServiceCollection services, Func eventOutboxDequeueFactory, string? partitionKey = null, string? destination = null, bool healthCheck = true) { - var exe = services.BuildServiceProvider().GetRequiredService().GetValue("EventOutboxHostedService__Enabled"); + var exe = services.BuildServiceProvider().GetRequiredService().GetCoreExValue("EventOutboxHostedService:Enabled"); if (!exe.HasValue || exe.Value) { // Add the health check. diff --git a/src/CoreEx.Database.SqlServer/Outbox/EventOutboxHostedService.cs b/src/CoreEx.Database.SqlServer/Outbox/EventOutboxHostedService.cs index 12bcd310..f78c6a72 100644 --- a/src/CoreEx.Database.SqlServer/Outbox/EventOutboxHostedService.cs +++ b/src/CoreEx.Database.SqlServer/Outbox/EventOutboxHostedService.cs @@ -110,7 +110,7 @@ public EventOutboxHostedService(IServiceProvider serviceProvider, ILoggerWill default to configuration, a) : , then b) , where specified; otherwise, . public override TimeSpan Interval { - get => _interval ?? Settings.GetValue($"{ServiceName}:{IntervalName}".Replace(".", "_")) ?? Settings.GetValue(IntervalName.Replace(".", "_")) ?? DefaultInterval; + get => _interval ?? Settings.GetCoreExValue($"{ServiceName}:{IntervalName}".Replace(".", "_")) ?? Settings.GetCoreExValue(IntervalName.Replace(".", "_")) ?? DefaultInterval; set => _interval = value; } @@ -120,7 +120,7 @@ public override TimeSpan Interval /// Will default to configuration, a) : , then b) , where specified; otherwise, 10. public int MaxDequeueSize { - get => _maxDequeueSize ?? Settings.GetValue($"{ServiceName}:{MaxDequeueSizeName}".Replace(".", "_")) ?? Settings.GetValue(MaxDequeueSizeName.Replace(".", "_")) ?? 10; + get => _maxDequeueSize ?? Settings.GetCoreExValue($"{ServiceName}:{MaxDequeueSizeName}".Replace(".", "_")) ?? Settings.GetCoreExValue(MaxDequeueSizeName.Replace(".", "_")) ?? 10; set => _maxDequeueSize = value; } diff --git a/src/CoreEx.Database.SqlServer/Outbox/EventOutboxService.cs b/src/CoreEx.Database.SqlServer/Outbox/EventOutboxService.cs index 82922dca..9cb9083a 100644 --- a/src/CoreEx.Database.SqlServer/Outbox/EventOutboxService.cs +++ b/src/CoreEx.Database.SqlServer/Outbox/EventOutboxService.cs @@ -49,7 +49,7 @@ public class EventOutboxService(IServiceProvider serviceProvider, ILoggerWill default to configuration, a) : , then b) , where specified; otherwise, . public override int MaxIterations { - get => _maxIterations ?? Settings.GetValue($"{ServiceName}:{MaxIterationsName}".Replace(".", "_")) ?? Settings.GetValue(MaxIterationsName.Replace(".", "_")) ?? DefaultMaxIterations; + get => _maxIterations ?? Settings.GetCoreExValue($"{ServiceName}:{MaxIterationsName}".Replace(".", "_")) ?? Settings.GetCoreExValue(MaxIterationsName.Replace(".", "_")) ?? DefaultMaxIterations; set => _maxIterations = value; } @@ -59,7 +59,7 @@ public override int MaxIterations /// Will default to configuration, a) : , then b) , where specified; otherwise, 10. public int MaxDequeueSize { - get => _maxDequeueSize ?? Settings.GetValue($"{ServiceName}:{MaxDequeueSizeName}".Replace(".", "_")) ?? Settings.GetValue(MaxDequeueSizeName.Replace(".", "_")) ?? 10; + get => _maxDequeueSize ?? Settings.GetCoreExValue($"{ServiceName}:{MaxDequeueSizeName}".Replace(".", "_")) ?? Settings.GetCoreExValue(MaxDequeueSizeName.Replace(".", "_")) ?? 10; set => _maxDequeueSize = value; } diff --git a/src/CoreEx.Database/Mapping/DatabaseMapperT.cs b/src/CoreEx.Database/Mapping/DatabaseMapperT.cs index d4f90692..853e192c 100644 --- a/src/CoreEx.Database/Mapping/DatabaseMapperT.cs +++ b/src/CoreEx.Database/Mapping/DatabaseMapperT.cs @@ -122,19 +122,18 @@ public PropertyColumnMapper Property( /// /// Validates and adds a new IPropertyColumnMapper. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "They are the arguments from the calling method.")] - private void AddMapping(IPropertyColumnMapper pcm) + private void AddMapping(PropertyColumnMapper propertyColumnMapper) { - if (_mappings.Any(x => x.PropertyName == pcm.PropertyName)) - throw new ArgumentException($"Source property '{pcm.PropertyName}' must not be specified more than once.", "propertyExpression"); + if (_mappings.Any(x => x.PropertyName == propertyColumnMapper.PropertyName)) + throw new ArgumentException($"Source property '{propertyColumnMapper.PropertyName}' must not be specified more than once.", nameof(propertyColumnMapper)); - if (_mappings.Any(x => x.ColumnName == pcm.ColumnName)) - throw new ArgumentException($"Column '{pcm.ColumnName}' must not be specified more than once.", "columnName"); + if (_mappings.Any(x => x.ColumnName == propertyColumnMapper.ColumnName)) + throw new ArgumentException($"Column '{propertyColumnMapper.ColumnName}' must not be specified more than once.", nameof(propertyColumnMapper)); - if (_mappings.Any(x => x.ParameterName == pcm.ParameterName)) - throw new ArgumentException($"Parameter '{pcm.ParameterName}' must not be specified more than once.", "parameterName"); + if (_mappings.Any(x => x.ParameterName == propertyColumnMapper.ParameterName)) + throw new ArgumentException($"Parameter '{propertyColumnMapper.ParameterName}' must not be specified more than once.", nameof(propertyColumnMapper)); - _mappings.Add(pcm); + _mappings.Add(propertyColumnMapper); } /// diff --git a/src/CoreEx.Dataverse/Mapping/DataverseMapperT.cs b/src/CoreEx.Dataverse/Mapping/DataverseMapperT.cs index 1bec0b92..3032b8e1 100644 --- a/src/CoreEx.Dataverse/Mapping/DataverseMapperT.cs +++ b/src/CoreEx.Dataverse/Mapping/DataverseMapperT.cs @@ -133,16 +133,15 @@ public PropertyColumnMapper Property( /// /// Validates and adds a new IPropertyColumnMapper. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "They are the arguments from the calling method.")] - private void AddMapping(IPropertyColumnMapper pcm) + private void AddMapping(PropertyColumnMapper propertyColumnMapper) { - if (_mappings.Any(x => x.PropertyName == pcm.PropertyName)) - throw new ArgumentException($"Source property '{pcm.PropertyName}' must not be specified more than once.", "propertyExpression"); + if (_mappings.Any(x => x.PropertyName == propertyColumnMapper.PropertyName)) + throw new ArgumentException($"Source property '{propertyColumnMapper.PropertyName}' must not be specified more than once.", nameof(propertyColumnMapper)); - if (_mappings.Any(x => x.ColumnName == pcm.ColumnName)) - throw new ArgumentException($"Column '{pcm.ColumnName}' must not be specified more than once.", "columnName"); + if (_mappings.Any(x => x.ColumnName == propertyColumnMapper.ColumnName)) + throw new ArgumentException($"Column '{propertyColumnMapper.ColumnName}' must not be specified more than once.", nameof(propertyColumnMapper)); - _mappings.Add(pcm); + _mappings.Add(propertyColumnMapper); } /// diff --git a/src/CoreEx.Newtonsoft/Json/ContractResolver.cs b/src/CoreEx.Newtonsoft/Json/ContractResolver.cs index e11ca535..202dd1ca 100644 --- a/src/CoreEx.Newtonsoft/Json/ContractResolver.cs +++ b/src/CoreEx.Newtonsoft/Json/ContractResolver.cs @@ -31,8 +31,8 @@ public class ContractResolver : CamelCasePropertyNamesContractResolver /// static ContractResolver() { - _default.AddType(typeof(EntityCore)) - .AddType(typeof(EntityBase)) + _default.AddType() + .AddType() .AddType(typeof(ReferenceDataBaseEx<,>)) .AddType(typeof(ReferenceDataBase<>)) .AddType() diff --git a/src/CoreEx.OData/Mapping/ODataMapperT.cs b/src/CoreEx.OData/Mapping/ODataMapperT.cs index 80fe80c5..7338f29c 100644 --- a/src/CoreEx.OData/Mapping/ODataMapperT.cs +++ b/src/CoreEx.OData/Mapping/ODataMapperT.cs @@ -142,16 +142,15 @@ public PropertyColumnMapper Map(Expre /// /// Validates and adds a new IPropertyColumnMapper. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "They are the arguments from the calling method.")] - private void AddMapping(IPropertyColumnMapper pcm) + private void AddMapping(PropertyColumnMapper propertyColumnMapper) { - if (_mappings.Any(x => x.PropertyName == pcm.PropertyName)) - throw new ArgumentException($"Source property '{pcm.PropertyName}' must not be specified more than once.", "propertyExpression"); + if (_mappings.Any(x => x.PropertyName == propertyColumnMapper.PropertyName)) + throw new ArgumentException($"Source property '{propertyColumnMapper.PropertyName}' must not be specified more than once.", nameof(propertyColumnMapper)); - if (_mappings.Any(x => x.ColumnName == pcm.ColumnName)) - throw new ArgumentException($"Column '{pcm.ColumnName}' must not be specified more than once.", "columnName"); + if (_mappings.Any(x => x.ColumnName == propertyColumnMapper.ColumnName)) + throw new ArgumentException($"Column '{propertyColumnMapper.ColumnName}' must not be specified more than once.", nameof(propertyColumnMapper)); - _mappings.Add(pcm); + _mappings.Add(propertyColumnMapper); } /// diff --git a/src/CoreEx.OData/README.md b/src/CoreEx.OData/README.md index 471a4431..d9c3013a 100644 --- a/src/CoreEx.OData/README.md +++ b/src/CoreEx.OData/README.md @@ -136,7 +136,7 @@ public class DemoSettings : SettingsBase /// /// Gets the Dataverse connection string. /// - public string DataverseConnectionString => GetRequiredValue("ConnectionStrings__Dataverse"); + public string DataverseConnectionString => GetRequiredValue("ConnectionStrings:Dataverse"); /// /// Gets the from the . diff --git a/src/CoreEx.Solace/PubSub/PubSubSender.cs b/src/CoreEx.Solace/PubSub/PubSubSender.cs index 1bd030aa..a9d78ddf 100644 --- a/src/CoreEx.Solace/PubSub/PubSubSender.cs +++ b/src/CoreEx.Solace/PubSub/PubSubSender.cs @@ -39,7 +39,7 @@ public PubSubSender(IContext solaceContext, SessionProperties sessionProperties, Logger = logger.ThrowIfNull(nameof(logger)); Invoker = invoker ?? (_invoker ??= new PubSubSenderInvoker()); Converter = converter ?? new EventSendDataToPubSubConverter(); - DefaultQueueOrTopicName = Settings.GetValue($"{GetType().Name}:QueueOrTopicName", defaultValue: _unspecifiedQueueOrTopicName); + DefaultQueueOrTopicName = Settings.GetCoreExValue($"{GetType().Name}:QueueOrTopicName", defaultValue: _unspecifiedQueueOrTopicName); } /// diff --git a/src/CoreEx.UnitTesting.Azure.Functions/CoreEx.UnitTesting.Azure.Functions.csproj b/src/CoreEx.UnitTesting.Azure.Functions/CoreEx.UnitTesting.Azure.Functions.csproj index 8d232d75..97ca2d5a 100644 --- a/src/CoreEx.UnitTesting.Azure.Functions/CoreEx.UnitTesting.Azure.Functions.csproj +++ b/src/CoreEx.UnitTesting.Azure.Functions/CoreEx.UnitTesting.Azure.Functions.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/CoreEx.UnitTesting.Azure.ServiceBus/CoreEx.UnitTesting.Azure.ServiceBus.csproj b/src/CoreEx.UnitTesting.Azure.ServiceBus/CoreEx.UnitTesting.Azure.ServiceBus.csproj index de87f408..f2033110 100644 --- a/src/CoreEx.UnitTesting.Azure.ServiceBus/CoreEx.UnitTesting.Azure.ServiceBus.csproj +++ b/src/CoreEx.UnitTesting.Azure.ServiceBus/CoreEx.UnitTesting.Azure.ServiceBus.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/CoreEx.UnitTesting/CoreEx.UnitTesting.csproj b/src/CoreEx.UnitTesting/CoreEx.UnitTesting.csproj index 8d44ddcd..19bca45a 100644 --- a/src/CoreEx.UnitTesting/CoreEx.UnitTesting.csproj +++ b/src/CoreEx.UnitTesting/CoreEx.UnitTesting.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/CoreEx.Validation/Rules/CompareRuleBase.cs b/src/CoreEx.Validation/Rules/CompareRuleBase.cs index e41a3c22..36ffb522 100644 --- a/src/CoreEx.Validation/Rules/CompareRuleBase.cs +++ b/src/CoreEx.Validation/Rules/CompareRuleBase.cs @@ -11,18 +11,13 @@ namespace CoreEx.Validation.Rules /// /// The entity . /// The property . - public abstract class CompareRuleBase : ValueRuleBase where TEntity : class + /// The . + public abstract class CompareRuleBase(CompareOperator compareOperator) : ValueRuleBase where TEntity : class { - /// - /// Initializes a new instance of the class. - /// - /// The . - protected CompareRuleBase(CompareOperator compareOperator) => Operator = compareOperator; - /// /// Gets the . /// - public CompareOperator Operator { get; private set; } + public CompareOperator Operator { get; private set; } = compareOperator; /// /// Gets or sets the comparer. diff --git a/src/CoreEx.Validation/Rules/EnumRule.cs b/src/CoreEx.Validation/Rules/EnumRule.cs index a407ceee..44de7a81 100644 --- a/src/CoreEx.Validation/Rules/EnumRule.cs +++ b/src/CoreEx.Validation/Rules/EnumRule.cs @@ -22,7 +22,11 @@ public class EnumRule : ValueRuleBase wh protected override Task ValidateAsync(PropertyContext context, CancellationToken cancellationToken = default) { // Make sure the enum is defined. +#if NET6_0_OR_GREATER + if (!Enum.IsDefined(context.Value)) +#else if (!Enum.IsDefined(typeof(TProperty), context.Value)) +#endif context.CreateErrorMessage(ErrorText ?? ValidatorStrings.InvalidFormat); return Task.CompletedTask; diff --git a/src/CoreEx.Validation/ValidationServiceCollectionExtensions.cs b/src/CoreEx.Validation/ValidationServiceCollectionExtensions.cs index 6b77d829..4a0a8375 100644 --- a/src/CoreEx.Validation/ValidationServiceCollectionExtensions.cs +++ b/src/CoreEx.Validation/ValidationServiceCollectionExtensions.cs @@ -107,9 +107,9 @@ public static IServiceCollection AddValidators(this IServiceCollection services, select new { valueType, type }) { if (alsoRegisterInterfaces) - av.MakeGenericMethod(match.valueType, match.type).Invoke(null, new object[] { services }); + av.MakeGenericMethod(match.valueType, match.type).Invoke(null, [services]); else - av.MakeGenericMethod(match.type).Invoke(null, new object[] { services }); + av.MakeGenericMethod(match.type).Invoke(null, [services]); } } diff --git a/src/CoreEx/Abstractions/Internal.cs b/src/CoreEx/Abstractions/Internal.cs index a01ae4aa..d4740226 100644 --- a/src/CoreEx/Abstractions/Internal.cs +++ b/src/CoreEx/Abstractions/Internal.cs @@ -1,6 +1,14 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx using Microsoft.Extensions.Caching.Memory; +using System.Runtime.CompilerServices; + +[assembly: + InternalsVisibleTo("CoreEx.AspNetCore, PublicKey=00240000048000009400000006020000002400005253413100040000010001007dee530af6d801902d40685e9cd0a3d8991ddbf545be3ef6147c9f79bacd7464d92fbd94fee34e885c37e3dff4ea15a4f9978f1f614798e0f48e3a3d5bf15e8b2fba9c19b6966838f97444bc247bc101454946d70ac93207cf2c611956aed59c316f81f1bf8c8486f8f0b3f9adf93c2f07e06a86745f6dc4b819c2bc2f3fdad5"), + InternalsVisibleTo("CoreEx.Azure, PublicKey=00240000048000009400000006020000002400005253413100040000010001007dee530af6d801902d40685e9cd0a3d8991ddbf545be3ef6147c9f79bacd7464d92fbd94fee34e885c37e3dff4ea15a4f9978f1f614798e0f48e3a3d5bf15e8b2fba9c19b6966838f97444bc247bc101454946d70ac93207cf2c611956aed59c316f81f1bf8c8486f8f0b3f9adf93c2f07e06a86745f6dc4b819c2bc2f3fdad5"), + InternalsVisibleTo("CoreEx.Database.SqlServer, PublicKey=00240000048000009400000006020000002400005253413100040000010001007dee530af6d801902d40685e9cd0a3d8991ddbf545be3ef6147c9f79bacd7464d92fbd94fee34e885c37e3dff4ea15a4f9978f1f614798e0f48e3a3d5bf15e8b2fba9c19b6966838f97444bc247bc101454946d70ac93207cf2c611956aed59c316f81f1bf8c8486f8f0b3f9adf93c2f07e06a86745f6dc4b819c2bc2f3fdad5"), + InternalsVisibleTo("CoreEx.Solace, PublicKey=00240000048000009400000006020000002400005253413100040000010001007dee530af6d801902d40685e9cd0a3d8991ddbf545be3ef6147c9f79bacd7464d92fbd94fee34e885c37e3dff4ea15a4f9978f1f614798e0f48e3a3d5bf15e8b2fba9c19b6966838f97444bc247bc101454946d70ac93207cf2c611956aed59c316f81f1bf8c8486f8f0b3f9adf93c2f07e06a86745f6dc4b819c2bc2f3fdad5"), +] namespace CoreEx.Abstractions { @@ -15,7 +23,7 @@ public static class Internal /// /// Gets the CoreEx fallback . /// - public static IMemoryCache MemoryCache => ExecutionContext.GetService() ?? (_fallbackCache ??= new MemoryCache(new MemoryCacheOptions())); + internal static IMemoryCache MemoryCache => ExecutionContext.GetService() ?? (_fallbackCache ??= new MemoryCache(new MemoryCacheOptions())); /// /// Represents a cache for internal capabilities. diff --git a/src/CoreEx/Abstractions/Reflection/TypeReflector.cs b/src/CoreEx/Abstractions/Reflection/TypeReflector.cs index 96969790..e86ecdfe 100644 --- a/src/CoreEx/Abstractions/Reflection/TypeReflector.cs +++ b/src/CoreEx/Abstractions/Reflection/TypeReflector.cs @@ -70,7 +70,7 @@ public static ITypeReflector GetReflector(TypeReflectorArgs? args, Type type) => (args ??= TypeReflectorArgs.Default).Cache.GetOrCreate(type.ThrowIfNull(nameof(args)), ce => { var ec = typeof(TypeReflector<>).MakeGenericType(type).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, [typeof(TypeReflectorArgs)], null)!; - var tr = (ITypeReflector)ec.Invoke(new object[] { args }); + var tr = (ITypeReflector)ec.Invoke([args]); args.TypeBuilder?.Invoke(tr); return ConfigureCacheEntry(ce, tr); })!; diff --git a/src/CoreEx/Configuration/SettingsBase.cs b/src/CoreEx/Configuration/SettingsBase.cs index 1f618f87..00a04511 100644 --- a/src/CoreEx/Configuration/SettingsBase.cs +++ b/src/CoreEx/Configuration/SettingsBase.cs @@ -1,5 +1,6 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx +using CoreEx.Entities; using CoreEx.Hosting.Work; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; @@ -16,6 +17,10 @@ public abstract class SettingsBase { private readonly List _prefixes = []; private bool? _validationUseJsonNames; + private DateTimeTransform? _dateTimeTransform; + private StringTransform? _stringTransform; + private StringTrim? _stringTrim; + private StringCase? _stringCase; /// /// Initializes a new instance of the class. @@ -76,7 +81,7 @@ public T GetValue([CallerMemberName] string key = "", T defaultValue = defaul private bool TryGetValue(string key, out T value) { // Try the key as specified. - if (Configuration!.GetSection(key)?.Value != null) + if (Configuration is not null && Configuration.GetSection(key)?.Value != null) { value = Configuration.GetValue(key)!; return true; @@ -116,17 +121,27 @@ public T GetRequiredValue([CallerMemberName] string key = "") return TryGetValue(ckey, out kv) ? kv : throw new ArgumentException($"Configuration key '{key}' has not been configured and the value is required.", nameof(key)); } + /// + /// Gets the value using the specified excluding any prefix (key is inferred where not specified using ). + /// + /// The value . + /// The key excluding any prefix (key is inferred where not specified using ). + /// The default fallback value used where no non-default value is found. + /// The corresponding setting value. + /// This is considered a standard setting and will be checked within the CoreEx: nested strructure first to enable clear separation. + internal T GetCoreExValue([CallerMemberName] string key = "", T defaultValue = default!) => TryGetValue($"CoreEx:{key.ThrowIfNullOrEmpty(nameof(key))}", out var value) ? value : GetValue(key, defaultValue); + /// /// Indicates whether to the include the underlying content in the externally returned result. /// /// Defaults to false. - public bool IncludeExceptionInResult => GetValue(nameof(IncludeExceptionInResult), false); + public bool IncludeExceptionInResult => GetCoreExValue(nameof(IncludeExceptionInResult), false); /// /// Gets the default maximum event publish collection size. /// /// Defaults to 100. - public int MaxPublishCollSize => GetValue(nameof(MaxPublishCollSize), 100); + public int MaxPublishCollSize => GetCoreExValue(nameof(MaxPublishCollSize), 100); /// /// Gets the from the environment variables. @@ -137,42 +152,71 @@ public T GetRequiredValue([CallerMemberName] string key = "") /// Indicates whether to include any extra Health Check data that might be considered sensitive. /// /// Defaults to false. - public bool IncludeSensitiveHealthCheckData => GetValue(nameof(IncludeSensitiveHealthCheckData), false); + public bool IncludeSensitiveHealthCheckData => GetCoreExValue(nameof(IncludeSensitiveHealthCheckData), false); /// /// Gets the ; i.e. page size. /// /// Defaults to 100. - public long PagingDefaultTake => GetValue(nameof(PagingDefaultTake), 100); + public long PagingDefaultTake => GetCoreExValue(nameof(PagingDefaultTake), 100); /// /// Gets the ; i.e. absolute maximum page size. /// /// Defaults to 1000. - public long PagingMaxTake => GetValue(nameof(PagingMaxTake), 1000); + public long PagingMaxTake => GetCoreExValue(nameof(PagingMaxTake), 1000); /// /// Gets the default . /// /// Defaults to 2 hours. - public TimeSpan? RefDataCacheAbsoluteExpirationRelativeToNow => GetValue($"RefDataCache__{nameof(ICacheEntry.AbsoluteExpirationRelativeToNow)}", TimeSpan.FromHours(2)); + public TimeSpan? RefDataCacheAbsoluteExpirationRelativeToNow => GetCoreExValue($"RefDataCache:{nameof(ICacheEntry.AbsoluteExpirationRelativeToNow)}", TimeSpan.FromHours(2)); /// /// Gets the default . /// /// Defaults to 30 minutes. - public TimeSpan? RefDataCacheSlidingExpiration => GetValue($"RefDataCache__{nameof(ICacheEntry.SlidingExpiration)}", TimeSpan.FromMinutes(30)); + public TimeSpan? RefDataCacheSlidingExpiration => GetCoreExValue($"RefDataCache:{nameof(ICacheEntry.SlidingExpiration)}", TimeSpan.FromMinutes(30)); /// /// Indicates whether the validation (CoreEx.Validation) should use JSON names. /// - /// Defaults to true. - public bool ValidationUseJsonNames => _validationUseJsonNames ??= GetValue(nameof(ValidationUseJsonNames), true); + /// Defaults to true. + /// The value is cached on first use and is then considered immutable as a change in behaviour may have unintended consequences. + public bool ValidationUseJsonNames => _validationUseJsonNames ??= GetCoreExValue(nameof(ValidationUseJsonNames), true); /// - /// Gets or sets the . + /// Gets the . /// /// Defaults to 1 hour. - public TimeSpan WorkerExpiryTimeSpan => GetValue(nameof(WorkerExpiryTimeSpan), TimeSpan.FromHours(1)); + public TimeSpan WorkerExpiryTimeSpan => GetCoreExValue(nameof(WorkerExpiryTimeSpan), TimeSpan.FromHours(1)); + + /// + /// Gets the . + /// + /// Defaults to . + /// The value is cached on first use and is then considered immutable as a change in behaviour may have unintended consequences. + public DateTimeTransform DateTimeTransform => _dateTimeTransform ??= GetCoreExValue($"{nameof(Cleaner)}:{nameof(DateTimeTransform)}", Cleaner.DefaultDateTimeTransform); + + /// + /// Gets the . + /// + /// Defaults to . + /// The value is cached on first use and is then considered immutable as a change in behaviour may have unintended consequences. + public StringTransform StringTransform => _stringTransform ??= GetCoreExValue($"{nameof(Cleaner)}:{nameof(StringTransform)}", Cleaner.DefaultStringTransform); + + /// + /// Gets the . + /// + /// Defaults to . + /// The value is cached on first use and is then considered immutable as a change in behaviour may have unintended consequences. + public StringCase StringCase => _stringCase ??= GetCoreExValue($"{nameof(Cleaner)}:{nameof(StringCase)}", Cleaner.DefaultStringCase); + + /// + /// Gets the . + /// + /// Defaults to . + /// The value is cached on first use and is then considered immutable as a change in behaviour may have unintended consequences. + public StringTrim StringTrim => _stringTrim ??= GetCoreExValue($"{nameof(Cleaner)}:{nameof(StringTrim)}", Cleaner.DefaultStringTrim); } } \ No newline at end of file diff --git a/src/CoreEx/CoreEx.csproj b/src/CoreEx/CoreEx.csproj index c64279a4..594fdbd9 100644 --- a/src/CoreEx/CoreEx.csproj +++ b/src/CoreEx/CoreEx.csproj @@ -67,7 +67,7 @@ - + diff --git a/src/CoreEx/Entities/Cleaner.cs b/src/CoreEx/Entities/Cleaner.cs index 43054d06..1b9e7e94 100644 --- a/src/CoreEx/Entities/Cleaner.cs +++ b/src/CoreEx/Entities/Cleaner.cs @@ -1,5 +1,6 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx +using CoreEx.Configuration; using CoreEx.Globalization; using System; using System.Collections.Generic; @@ -72,14 +73,8 @@ public static StringCase DefaultStringCase /// The cleaned value. public static string? Clean(string? value, StringTrim trim = StringTrim.UseDefault, StringTransform transform = StringTransform.UseDefault, StringCase casing = StringCase.UseDefault) { - if (trim == StringTrim.UseDefault) - trim = DefaultStringTrim; - if (transform == StringTransform.UseDefault) - transform = DefaultStringTransform; - - if (casing == StringCase.UseDefault) - casing = DefaultStringCase; + transform = ExecutionContext.GetService()?.StringTransform ?? DefaultStringTransform; // Handle a null string. if (value == null) @@ -90,6 +85,9 @@ public static StringCase DefaultStringCase return value; } + if (trim == StringTrim.UseDefault) + trim = ExecutionContext.GetService()?.StringTrim ?? DefaultStringTrim; + // Trim the string. var tmp = trim switch { @@ -111,6 +109,9 @@ public static StringCase DefaultStringCase return tmp; // Apply casing to the string. + if (casing == StringCase.UseDefault) + casing = ExecutionContext.GetService()?.StringCase ?? DefaultStringCase; + return casing switch { StringCase.Lower => CultureInfo.CurrentCulture.TextInfo.ToCasing(tmp, TextInfoCasing.Lower), @@ -134,10 +135,11 @@ public static StringCase DefaultStringCase /// The value to clean. /// The to be applied. /// The cleaned value. + /// Will attempt to use as a default where possible. public static DateTime Clean(DateTime value, DateTimeTransform transform) { if (transform == DateTimeTransform.UseDefault) - transform = DefaultDateTimeTransform; + transform = ExecutionContext.GetService()?.DateTimeTransform ?? DefaultDateTimeTransform; switch (transform) { diff --git a/src/CoreEx/Entities/Extended/EntityCore.cs b/src/CoreEx/Entities/Extended/EntityCore.cs index a861ea1c..e0ed66db 100644 --- a/src/CoreEx/Entities/Extended/EntityCore.cs +++ b/src/CoreEx/Entities/Extended/EntityCore.cs @@ -15,7 +15,11 @@ namespace CoreEx.Entities.Extended [System.Diagnostics.DebuggerStepThrough] public abstract class EntityCore : INotifyPropertyChanged, IChangeTracking, IReadOnly { +#if NET9_0_OR_GREATER + private readonly System.Threading.Lock _lock = new(); +#else private readonly object _lock = new(); +#endif private Dictionary? _propertyEventHandlers; /// diff --git a/src/CoreEx/Events/CloudEventSerializerBase.cs b/src/CoreEx/Events/CloudEventSerializerBase.cs index 9e085003..def781ec 100644 --- a/src/CoreEx/Events/CloudEventSerializerBase.cs +++ b/src/CoreEx/Events/CloudEventSerializerBase.cs @@ -15,7 +15,8 @@ namespace CoreEx.Events /// /// Provides the base capabilities. /// - public abstract class CloudEventSerializerBase : IEventSerializer + /// The . + public abstract class CloudEventSerializerBase(EventDataFormatter? eventDataFormatter) : IEventSerializer { private const string SubjectName = "subject"; private const string ActionName = "action"; @@ -32,14 +33,8 @@ public abstract class CloudEventSerializerBase : IEventSerializer /// an attribute name must consist of lowercase letters and digits only; any that contain other characters will be ignored. public static string[] ReservedNames { get; } = ["id", "time", "type", "source", SubjectName, ActionName, CorrelationIdName, TenantIdName, ETagName, PartitionKeyName, KeyName]; - /// - /// Initializes a new instance of the class. - /// - /// The . - protected CloudEventSerializerBase(EventDataFormatter? eventDataFormatter) => EventDataFormatter = eventDataFormatter ?? new EventDataFormatter(); - /// - public EventDataFormatter EventDataFormatter { get; } + public EventDataFormatter EventDataFormatter { get; } = eventDataFormatter ?? new EventDataFormatter(); /// public IAttachmentStorage? AttachmentStorage { get; set; } diff --git a/src/CoreEx/Events/EventDataSerializerBase.cs b/src/CoreEx/Events/EventDataSerializerBase.cs index 21b7fbc5..88f06a46 100644 --- a/src/CoreEx/Events/EventDataSerializerBase.cs +++ b/src/CoreEx/Events/EventDataSerializerBase.cs @@ -12,26 +12,17 @@ namespace CoreEx.Events /// Provides the base capabilities. /// /// The indicates whether the is serialized only (default); or alternatively, the complete . - public abstract class EventDataSerializerBase : IEventSerializer + /// The . + /// The . + public abstract class EventDataSerializerBase(IJsonSerializer jsonSerializer, EventDataFormatter? eventDataFormatter) : IEventSerializer { - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - protected EventDataSerializerBase(IJsonSerializer jsonSerializer, EventDataFormatter? eventDataFormatter) - { - JsonSerializer = jsonSerializer.ThrowIfNull(nameof(jsonSerializer)); - EventDataFormatter = eventDataFormatter ?? new EventDataFormatter(); - } - /// /// Gets the . /// - public IJsonSerializer JsonSerializer { get; } + public IJsonSerializer JsonSerializer { get; } = jsonSerializer.ThrowIfNull(nameof(jsonSerializer)); /// - public EventDataFormatter EventDataFormatter { get; } + public EventDataFormatter EventDataFormatter { get; } = eventDataFormatter ?? new EventDataFormatter(); /// public IAttachmentStorage? AttachmentStorage { get; set; } diff --git a/src/CoreEx/Events/EventPublisher.cs b/src/CoreEx/Events/EventPublisher.cs index a5f1309e..6a4ac0cb 100644 --- a/src/CoreEx/Events/EventPublisher.cs +++ b/src/CoreEx/Events/EventPublisher.cs @@ -83,7 +83,7 @@ public Task SendAsync(CancellationToken cancellationToken = default) => EventPub list.Add(esd); } - await EventSender.SendAsync(list.ToArray(), cancellationToken).ConfigureAwait(false); + await EventSender.SendAsync([.. list], cancellationToken).ConfigureAwait(false); }, cancellationToken); /// diff --git a/src/CoreEx/Events/EventSubscriberBase.cs b/src/CoreEx/Events/EventSubscriberBase.cs index db0278a1..7ebd6e9f 100644 --- a/src/CoreEx/Events/EventSubscriberBase.cs +++ b/src/CoreEx/Events/EventSubscriberBase.cs @@ -15,7 +15,12 @@ namespace CoreEx.Events /// /// Provides the base event subscriber capabilities. /// - public abstract class EventSubscriberBase : IErrorHandling + /// The . + /// The . + /// The . + /// The . + /// The . + public abstract class EventSubscriberBase(IEventDataConverter eventDataConverter, ExecutionContext executionContext, SettingsBase settings, ILogger logger, EventSubscriberInvoker? eventSubscriberInvoker = null) : IErrorHandling { private static EventSubscriberInvoker? _invoker; private ErrorHandler? _errorHandler; @@ -35,47 +40,30 @@ public abstract class EventSubscriberBase : IErrorHandling /// public static readonly LText NullEventErrorText = new($"{typeof(BusinessException).FullName}.{nameof(NullEventErrorText)}", $"{MessageErrorText} Event deserialized as null."); - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - /// The . - /// The . - /// The . - protected EventSubscriberBase(IEventDataConverter eventDataConverter, ExecutionContext executionContext, SettingsBase settings, ILogger logger, EventSubscriberInvoker? eventSubscriberInvoker = null) - { - EventDataConverter = eventDataConverter.ThrowIfNull(nameof(eventDataConverter)); - ExecutionContext = executionContext.ThrowIfNull(nameof(executionContext)); - Settings = settings.ThrowIfNull(nameof(settings)); - Logger = logger.ThrowIfNull(nameof(logger)); - EventSubscriberInvoker = eventSubscriberInvoker ?? (_invoker ??= new EventSubscriberInvoker()); - } - /// /// Gets the . /// - public IEventDataConverter EventDataConverter { get; } + public IEventDataConverter EventDataConverter { get; } = eventDataConverter.ThrowIfNull(nameof(eventDataConverter)); /// /// Gets the . /// - public ExecutionContext ExecutionContext { get; } + public ExecutionContext ExecutionContext { get; } = executionContext.ThrowIfNull(nameof(executionContext)); /// /// Gets the . /// - public SettingsBase Settings { get; } + public SettingsBase Settings { get; } = settings.ThrowIfNull(nameof(settings)); /// /// Gets the . /// - public ILogger Logger { get; } + public ILogger Logger { get; } = logger.ThrowIfNull(nameof(logger)); /// /// Gets the . /// - public EventSubscriberInvoker EventSubscriberInvoker { get; } + public EventSubscriberInvoker EventSubscriberInvoker { get; } = eventSubscriberInvoker ?? (_invoker ??= new EventSubscriberInvoker()); /// /// Gets or sets the where an occurs during / /. diff --git a/src/CoreEx/Events/InMemoryPublisher.cs b/src/CoreEx/Events/InMemoryPublisher.cs index 2920c11c..4813a464 100644 --- a/src/CoreEx/Events/InMemoryPublisher.cs +++ b/src/CoreEx/Events/InMemoryPublisher.cs @@ -53,14 +53,14 @@ protected override Task OnEventSendAsync(string? name, EventData eventData, Even /// /// An array of names. /// Where (with no name) is used the underlying destination name will be null. - public string?[] GetNames() => _dict.Keys.ToArray(); + public string?[] GetNames() => [.. _dict.Keys]; /// /// Gets the events sent (in order) to the named destination. /// /// The destination name. /// The corresponding events. - public EventData[] GetEvents(string? name = null) => _dict.TryGetValue(name ?? NullName, out var queue) ? [.. queue] : Array.Empty(); + public EventData[] GetEvents(string? name = null) => _dict.TryGetValue(name ?? NullName, out var queue) ? [.. queue] : []; /// /// Resets (clears) the in-memory state. diff --git a/src/CoreEx/Events/InMemorySender.cs b/src/CoreEx/Events/InMemorySender.cs index bf395830..305e2630 100644 --- a/src/CoreEx/Events/InMemorySender.cs +++ b/src/CoreEx/Events/InMemorySender.cs @@ -27,7 +27,7 @@ public Task SendAsync(IEnumerable events, CancellationToken cance /// /// Gets the events sent (in order). /// - public EventSendData[] GetEvents() => _queue.ToArray(); + public EventSendData[] GetEvents() => [.. _queue]; /// /// Resets (clears) the in-memory state. diff --git a/src/CoreEx/Events/Subscribing/EventSubscriberOrchestrator.cs b/src/CoreEx/Events/Subscribing/EventSubscriberOrchestrator.cs index 81aaa495..50e57415 100644 --- a/src/CoreEx/Events/Subscribing/EventSubscriberOrchestrator.cs +++ b/src/CoreEx/Events/Subscribing/EventSubscriberOrchestrator.cs @@ -181,7 +181,7 @@ private bool TryMatchSubscriberInternal(EventData @event, EventSubscriberArgs ar if (subscriber != null) return false; - if (att.ExtendedMatchMethodInfo is not null && !(bool)att.ExtendedMatchMethodInfo.Invoke(null, new object[] { @event, args })!) + if (att.ExtendedMatchMethodInfo is not null && !(bool)att.ExtendedMatchMethodInfo.Invoke(null, [@event, args])!) return false; subscriber = (IEventSubscriber)(ServiceProvider?.GetService(item.SubscriberType) ?? ExecutionContext.GetRequiredService(item.SubscriberType)); diff --git a/src/CoreEx/ExecutionContext.cs b/src/CoreEx/ExecutionContext.cs index 63f943c5..ac0a7cb0 100644 --- a/src/CoreEx/ExecutionContext.cs +++ b/src/CoreEx/ExecutionContext.cs @@ -30,7 +30,11 @@ public class ExecutionContext : ITenantId, IDisposable private HashSet? _permissions; private bool _isCopied; private bool _disposed; +#if NET9_0_OR_GREATER + private readonly System.Threading.Lock _lock = new(); +#else private readonly object _lock = new(); +#endif /// /// Gets or sets the function to create a default instance. diff --git a/src/CoreEx/Hosting/FileLockSynchronizer.cs b/src/CoreEx/Hosting/FileLockSynchronizer.cs index 6bfef6f9..05eb0129 100644 --- a/src/CoreEx/Hosting/FileLockSynchronizer.cs +++ b/src/CoreEx/Hosting/FileLockSynchronizer.cs @@ -21,7 +21,7 @@ public class FileLockSynchronizer(SettingsBase settings) : IServiceSynchronizer /// public const string ConfigKey = "FileLockSynchronizerPath"; - private readonly string _path = settings.ThrowIfNull(nameof(settings)).GetValue(ConfigKey) ?? throw new ArgumentException($"Configuration setting '{ConfigKey}' either does not exist or has no value.", nameof(settings)); + private readonly string _path = settings.ThrowIfNull(nameof(settings)).GetCoreExValue(ConfigKey) ?? throw new ArgumentException($"Configuration setting '{ConfigKey}' either does not exist or has no value.", nameof(settings)); private readonly ConcurrentDictionary _dict = new(); private bool _disposed; diff --git a/src/CoreEx/Hosting/TimerHostedServiceBase.cs b/src/CoreEx/Hosting/TimerHostedServiceBase.cs index 323bc809..33848147 100644 --- a/src/CoreEx/Hosting/TimerHostedServiceBase.cs +++ b/src/CoreEx/Hosting/TimerHostedServiceBase.cs @@ -25,8 +25,12 @@ public abstract class TimerHostedServiceBase : IHostedService, IDisposable { private static readonly Random _random = new(); - private readonly TimerHostedServiceHealthCheck? _healthCheck; +#if NET9_0_OR_GREATER + private readonly System.Threading.Lock _lock = new(); +#else private readonly object _lock = new(); +#endif + private readonly TimerHostedServiceHealthCheck? _healthCheck; private TimerHostedServiceStatus _status = TimerHostedServiceStatus.Initialized; private string? _name; private CancellationTokenSource? _cts; diff --git a/src/CoreEx/Hosting/Work/FileWorkStatePersistence.cs b/src/CoreEx/Hosting/Work/FileWorkStatePersistence.cs index 62f35c32..fb733fca 100644 --- a/src/CoreEx/Hosting/Work/FileWorkStatePersistence.cs +++ b/src/CoreEx/Hosting/Work/FileWorkStatePersistence.cs @@ -32,7 +32,7 @@ public class FileWorkStatePersistence : IWorkStatePersistence /// The . Defaults to . public FileWorkStatePersistence(SettingsBase settings, IJsonSerializer? jsonSerializer = null) { - _path = settings.ThrowIfNull(nameof(settings)).GetValue(ConfigKey); + _path = settings.ThrowIfNull(nameof(settings)).GetCoreExValue(ConfigKey); if (string.IsNullOrEmpty(_path)) throw new ArgumentException($"Configuration setting '{ConfigKey}' either does not exist or has no value.", nameof(settings)); diff --git a/src/CoreEx/Hosting/Work/InMemoryWorkStatePersistence.cs b/src/CoreEx/Hosting/Work/InMemoryWorkStatePersistence.cs index 807fc26a..c8397c45 100644 --- a/src/CoreEx/Hosting/Work/InMemoryWorkStatePersistence.cs +++ b/src/CoreEx/Hosting/Work/InMemoryWorkStatePersistence.cs @@ -22,7 +22,7 @@ public class InMemoryWorkStatePersistence(ILogger? /// /// Gets all the entries. /// - public WorkState[] GetWorkStates() => _workStates.Values.ToArray(); + public WorkState[] GetWorkStates() => [.. _workStates.Values]; /// public Task GetAsync(string id, CancellationToken cancellationToken) diff --git a/src/CoreEx/Hosting/Work/WorkStateOrchestrator.cs b/src/CoreEx/Hosting/Work/WorkStateOrchestrator.cs index c1f4e2f3..9eb1ee64 100644 --- a/src/CoreEx/Hosting/Work/WorkStateOrchestrator.cs +++ b/src/CoreEx/Hosting/Work/WorkStateOrchestrator.cs @@ -261,7 +261,7 @@ public async Task> CancelAsync(string id, string reason, Cance if (WorkStatus.Finished.HasFlag(ws.Status)) return Result.Fail($"A cancellation can not be performed when the current status is {ws.Status}."); - ws.Status = WorkStatus.Cancelled; + ws.Status = WorkStatus.Canceled; ws.Finished = DateTimeOffset.UtcNow; ws.Reason = reason.ThrowIfNullOrEmpty(nameof(reason)); diff --git a/src/CoreEx/Hosting/Work/WorkStatus.cs b/src/CoreEx/Hosting/Work/WorkStatus.cs index febce1f6..6300378b 100644 --- a/src/CoreEx/Hosting/Work/WorkStatus.cs +++ b/src/CoreEx/Hosting/Work/WorkStatus.cs @@ -41,7 +41,7 @@ public enum WorkStatus /// /// Indicates that the underlying work has been cancelled. /// - Cancelled = 128, + Canceled = 128, /// /// Indicates that the underlying work is in progress; either started or indeterminate. @@ -56,11 +56,11 @@ public enum WorkStatus /// /// Indicates the the underlying work has been terminated; either expired, failed or cancelled. /// - Terminated = Expired | Failed | Cancelled, + Terminated = Expired | Failed | Canceled, /// /// Indicates that the underlying work has been completed, expired, failed or cancelled. /// - Finished = Completed | Expired | Failed | Cancelled + Finished = Completed | Expired | Failed | Canceled } } \ No newline at end of file diff --git a/src/CoreEx/Http/HttpArg.cs b/src/CoreEx/Http/HttpArg.cs index 879daaf8..a5c377f2 100644 --- a/src/CoreEx/Http/HttpArg.cs +++ b/src/CoreEx/Http/HttpArg.cs @@ -56,6 +56,10 @@ public class HttpArg(string name, T value, HttpArgType argType = HttpArgType. str = rd?.Code; else if (Value is DateTime dt) str = dt.ToString("o", CultureInfo.InvariantCulture); + else if (Value is DateTimeOffset dto) + str = dto.ToString("o", CultureInfo.InvariantCulture); + else if (Value is bool b) + str = b.ToString().ToLowerInvariant(); else if (Value is IFormattable fmt) str = fmt.ToString(null, CultureInfo.InvariantCulture); else diff --git a/src/CoreEx/Http/TypedHttpClientBase.cs b/src/CoreEx/Http/TypedHttpClientBase.cs index fa923a33..a0704c8f 100644 --- a/src/CoreEx/Http/TypedHttpClientBase.cs +++ b/src/CoreEx/Http/TypedHttpClientBase.cs @@ -123,7 +123,7 @@ private async Task CreateRequestInternalAsync(HttpMethod met var qs = HttpUtility.ParseQueryString(ub.Query); // Extend the query string from the IHttpArgs. - foreach (var arg in (args ??= Array.Empty()).Where(x => x != null)) + foreach (var arg in (args ??= []).Where(x => x != null)) { arg.AddToQueryString(qs, JsonSerializer); } @@ -235,7 +235,6 @@ private static int FindBraceIndex(string format, char brace, int startIndex, int return braceIndex; } - /// /// Deserialize the JSON into of . /// @@ -279,7 +278,7 @@ public static (bool result, string error) IsTransient(HttpResponseMessage? respo return (true, $"Http Request Exception occurred: {exception.Message}"); if (exception is TaskCanceledException) - return (true, "Task was cancelled."); + return (true, "Task was canceled."); } if (response == null) diff --git a/src/CoreEx/Invokers/InvokeArgs.cs b/src/CoreEx/Invokers/InvokeArgs.cs index 449211b4..c93a2c7f 100644 --- a/src/CoreEx/Invokers/InvokeArgs.cs +++ b/src/CoreEx/Invokers/InvokeArgs.cs @@ -42,7 +42,7 @@ private static bool IsTracingEnabled(Type invokerType) if (settings.Configuration is null) return true; - return settings.GetValue($"Invokers:{invokerType.FullName}:TracingEnabled") ?? settings.GetValue("Invokers:Default:TracingEnabled") ?? true; + return settings.GetCoreExValue($"Invokers:{invokerType.FullName}:TracingEnabled") ?? settings.GetCoreExValue("Invokers:Default:TracingEnabled") ?? true; } /// @@ -54,7 +54,7 @@ private static bool IsLoggingEnabled(Type invokerType) if (settings.Configuration is null) return true; - return settings.GetValue($"Invokers:{invokerType.FullName}:LoggingEnabled") ?? settings.GetValue("Invokers:Default:LoggingEnabled") ?? true; + return settings.GetCoreExValue($"Invokers:{invokerType.FullName}:LoggingEnabled") ?? settings.GetCoreExValue("Invokers:Default:LoggingEnabled") ?? true; } /// diff --git a/src/CoreEx/Json/Compare/JsonElementComparer.cs b/src/CoreEx/Json/Compare/JsonElementComparer.cs index bb0decf9..cb94cb50 100644 --- a/src/CoreEx/Json/Compare/JsonElementComparer.cs +++ b/src/CoreEx/Json/Compare/JsonElementComparer.cs @@ -377,8 +377,8 @@ private static void ComputeHashCode(JsonElement json, ref HashCode hash) /// private sealed class CompareState { - private readonly Stack _unqualifiedPaths = new(new[] { "$" }); - private readonly Stack _paths = new(new[] { "$" }); + private readonly Stack _unqualifiedPaths = new(["$"]); + private readonly Stack _paths = new(["$"]); /// /// Initializes a new instance of the class. @@ -391,7 +391,7 @@ public CompareState(JsonElementComparerResult result, IEqualityComparer? Result = result; PathComparer = pathComparer ?? StringComparer.InvariantCultureIgnoreCase; var maxDepth = 0; - PathsToIgnore = new(Text.Json.JsonFilterer.CreateDictionary(pathsToIgnore, JsonPropertyFilter.Exclude, StringComparison.Ordinal, ref maxDepth, true).Keys); + PathsToIgnore = new(JsonFilterer.CreateDictionary(pathsToIgnore, JsonPropertyFilter.Exclude, StringComparison.Ordinal, ref maxDepth, true).Keys); } /// diff --git a/src/CoreEx/Json/Compare/JsonElementComparerResult.cs b/src/CoreEx/Json/Compare/JsonElementComparerResult.cs index ec3d63f6..c7029a9f 100644 --- a/src/CoreEx/Json/Compare/JsonElementComparerResult.cs +++ b/src/CoreEx/Json/Compare/JsonElementComparerResult.cs @@ -77,7 +77,7 @@ internal JsonElementComparerResult(JsonElement left, JsonElement right, int maxD /// Gets the array. /// /// The differences found up to the specified. - public JsonElementDifference[] GetDifferences() => _differences is null ? [] : _differences.ToArray(); + public JsonElementDifference[] GetDifferences() => _differences is null ? [] : [.. _differences]; /// /// Adds a . diff --git a/src/CoreEx/Json/Mapping/JsonObjectMapperT.cs b/src/CoreEx/Json/Mapping/JsonObjectMapperT.cs index eb913fa6..449d2fb3 100644 --- a/src/CoreEx/Json/Mapping/JsonObjectMapperT.cs +++ b/src/CoreEx/Json/Mapping/JsonObjectMapperT.cs @@ -138,16 +138,15 @@ public PropertyJsonMapper Property(Ex /// /// Validates and adds a new IPropertyJsonMapper. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "They are the arguments from the calling method.")] - private void AddMapping(IPropertyJsonMapper pcm) + private void AddMapping(PropertyJsonMapper propertyJsonMapper) { - if (_mappings.Any(x => x.PropertyName == pcm.PropertyName)) - throw new ArgumentException($"Source property '{pcm.PropertyName}' must not be specified more than once.", "propertyExpression"); + if (_mappings.Any(x => x.PropertyName == propertyJsonMapper.PropertyName)) + throw new ArgumentException($"Source property '{propertyJsonMapper.PropertyName}' must not be specified more than once.", nameof(propertyJsonMapper)); - if (_mappings.Any(x => x.JsonName == pcm.JsonName)) - throw new ArgumentException($"Column '{pcm.JsonName}' must not be specified more than once.", "jsonName"); + if (_mappings.Any(x => x.JsonName == propertyJsonMapper.JsonName)) + throw new ArgumentException($"Column '{propertyJsonMapper.JsonName}' must not be specified more than once.", nameof(propertyJsonMapper)); - _mappings.Add(pcm); + _mappings.Add(propertyJsonMapper); } /// diff --git a/src/CoreEx/RefData/Caching/SettingsBasedCacheEntry.cs b/src/CoreEx/RefData/Caching/SettingsBasedCacheEntry.cs index e6dd2da0..339559a8 100644 --- a/src/CoreEx/RefData/Caching/SettingsBasedCacheEntry.cs +++ b/src/CoreEx/RefData/Caching/SettingsBasedCacheEntry.cs @@ -36,8 +36,8 @@ public class SettingsBasedCacheEntry(SettingsBase? settings) : ICacheEntryConfig /// This should be overridden where more advanced behaviour is required. public virtual void CreateCacheEntry(Type type, ICacheEntry entry) { - entry.AbsoluteExpirationRelativeToNow = Settings?.GetValue($"RefDataCache__{type.Name}__{nameof(ICacheEntry.AbsoluteExpirationRelativeToNow)}", Settings.RefDataCacheAbsoluteExpirationRelativeToNow) ?? TimeSpan.FromHours(2); - entry.SlidingExpiration = Settings?.GetValue($"RefDataCache__{type.Name}__{nameof(ICacheEntry.SlidingExpiration)}", Settings.RefDataCacheSlidingExpiration) ?? TimeSpan.FromMinutes(30); + entry.AbsoluteExpirationRelativeToNow = Settings?.GetCoreExValue($"RefDataCache:{type.Name}:{nameof(ICacheEntry.AbsoluteExpirationRelativeToNow)}", Settings.RefDataCacheAbsoluteExpirationRelativeToNow) ?? TimeSpan.FromHours(2); + entry.SlidingExpiration = Settings?.GetCoreExValue($"RefDataCache:{type.Name}:{nameof(ICacheEntry.SlidingExpiration)}", Settings.RefDataCacheSlidingExpiration) ?? TimeSpan.FromMinutes(30); } } } \ No newline at end of file diff --git a/src/CoreEx/RefData/ReferenceDataCodeList.cs b/src/CoreEx/RefData/ReferenceDataCodeList.cs index 5d9a6cc4..64c8308b 100644 --- a/src/CoreEx/RefData/ReferenceDataCodeList.cs +++ b/src/CoreEx/RefData/ReferenceDataCodeList.cs @@ -32,7 +32,7 @@ namespace CoreEx.RefData /// Initializes a new instance of the class with a list of items. /// /// The list of items. - public ReferenceDataCodeList(IEnumerable items) => _codes = new((items ?? Array.Empty()).Select(x => x.Code)); + public ReferenceDataCodeList(IEnumerable items) => _codes = new((items ?? []).Select(x => x.Code)); /// /// Initializes a new instance of the class with a array. @@ -53,7 +53,7 @@ namespace CoreEx.RefData /// Creates a new list from the underlying contents. /// /// A new list - public List ToRefDataList() => this.ToList(); + public List ToRefDataList() => [.. this]; /// /// Creates a new list from the underlying contents. diff --git a/src/CoreEx/RefData/ReferenceDataCollection.cs b/src/CoreEx/RefData/ReferenceDataCollection.cs index 2c5e5a72..471b0e2c 100644 --- a/src/CoreEx/RefData/ReferenceDataCollection.cs +++ b/src/CoreEx/RefData/ReferenceDataCollection.cs @@ -16,7 +16,11 @@ namespace CoreEx.RefData /// The . public class ReferenceDataCollection : IReferenceDataCollection, ICollection where TId : IComparable, IEquatable where TRef : class, IReferenceData { +#if NET9_0_OR_GREATER + private readonly System.Threading.Lock _lock = new(); +#else private readonly object _lock = new(); +#endif private readonly ConcurrentDictionary _rdcId = new(); private readonly ConcurrentDictionary _rdcCode; private Dictionary<(string, object?), TRef>? _mappingsDict; diff --git a/src/CoreEx/RefData/ReferenceDataOrchestrator.cs b/src/CoreEx/RefData/ReferenceDataOrchestrator.cs index 2f2d4e3f..4fc51340 100644 --- a/src/CoreEx/RefData/ReferenceDataOrchestrator.cs +++ b/src/CoreEx/RefData/ReferenceDataOrchestrator.cs @@ -38,7 +38,11 @@ public class ReferenceDataOrchestrator private static readonly AsyncLocal _asyncLocal = new(); +#if NET9_0_OR_GREATER + private readonly System.Threading.Lock _lock = new(); +#else private readonly object _lock = new(); +#endif private readonly ConcurrentDictionary _typeToProvider = new(); private readonly ConcurrentDictionary _nameToType = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _semaphores = new(); diff --git a/src/CoreEx/ValidationException.cs b/src/CoreEx/ValidationException.cs index 750c39c9..a3ad31f8 100644 --- a/src/CoreEx/ValidationException.cs +++ b/src/CoreEx/ValidationException.cs @@ -58,7 +58,7 @@ public ValidationException() : this(null!) { } /// /// The . /// The error message. - public ValidationException(MessageItem item, string? message = null) : this(new MessageItem[] { item }, message) { } + public ValidationException(MessageItem item, string? message = null) : this([item], message) { } /// /// Gets the underlying messages. diff --git a/tests/CoreEx.Cosmos.Test/CoreEx.Cosmos.Test.csproj b/tests/CoreEx.Cosmos.Test/CoreEx.Cosmos.Test.csproj index 75c92137..af4619b5 100644 --- a/tests/CoreEx.Cosmos.Test/CoreEx.Cosmos.Test.csproj +++ b/tests/CoreEx.Cosmos.Test/CoreEx.Cosmos.Test.csproj @@ -34,7 +34,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/tests/CoreEx.Test/CoreEx.Test.csproj b/tests/CoreEx.Test/CoreEx.Test.csproj index 1a5bb763..d93a566a 100644 --- a/tests/CoreEx.Test/CoreEx.Test.csproj +++ b/tests/CoreEx.Test/CoreEx.Test.csproj @@ -23,7 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/tests/CoreEx.Test/Framework/Entities/CleanerTest.cs b/tests/CoreEx.Test/Framework/Entities/CleanerTest.cs index fcec86e1..6a7f74c2 100644 --- a/tests/CoreEx.Test/Framework/Entities/CleanerTest.cs +++ b/tests/CoreEx.Test/Framework/Entities/CleanerTest.cs @@ -1,6 +1,10 @@ -using CoreEx.Entities; +using CoreEx.Configuration; +using CoreEx.Entities; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using System; +using System.Collections.Generic; namespace CoreEx.Test.Framework.Entities { @@ -41,6 +45,65 @@ public void NullableDateTimeCleaning() Assert.That(dtc, Is.Null); } + [Test] + public void DateTimeTransformFromSettings() + { + Cleaner.DefaultDateTimeTransform = DateTimeTransform.DateTimeUtc; + + ConfigurationBuilder builder = new(); + Dictionary testSettings = new() + { + {"CoreEx:Cleaner:DateTimeTransform", "DateTimeLocal"} + }; + builder.AddInMemoryCollection(testSettings); + + IServiceProvider serviceProvider = new ServiceCollection() + .AddSingleton(builder.Build()) + .AddDefaultSettings() + .BuildServiceProvider(); + + using var scope = serviceProvider.CreateScope(); + using var ec = ExecutionContext.CreateNew(); + ec.ServiceProvider = scope.ServiceProvider; + + DateTime? dt = DateTime.UtcNow; + DateTime? dtc = Cleaner.Clean(dt); + Assert.Multiple(() => + { + Assert.That(dtc!.Value.Kind, Is.EqualTo(DateTimeKind.Local)); + Assert.That(Cleaner.DefaultDateTimeTransform, Is.EqualTo(DateTimeTransform.DateTimeUtc)); + }); + } + + [Test] + public void DateTimeTransformFromSettings_Load() + { + Cleaner.DefaultDateTimeTransform = DateTimeTransform.DateTimeUtc; + + ConfigurationBuilder builder = new(); + Dictionary testSettings = new() + { + {"Cleaner:DateTimeTransform", "DateTimeLocal"} + }; + builder.AddInMemoryCollection(testSettings); + + IServiceProvider serviceProvider = new ServiceCollection() + .AddSingleton(builder.Build()) + .AddDefaultSettings() + .BuildServiceProvider(); + + using var scope = serviceProvider.CreateScope(); + using var ec = ExecutionContext.CreateNew(); + ec.ServiceProvider = scope.ServiceProvider; + + DateTime? dtc = DateTime.UtcNow; + for (int i = 0; i < 10000; i++) + { + dtc = Cleaner.Clean(DateTime.UtcNow); + Assert.That(dtc!.Value.Kind, Is.EqualTo(DateTimeKind.Local), "Iteration" + i); + } + } + [Test] public void StringTransformCleaning() { diff --git a/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs b/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs index 635af5f1..4311b20e 100644 --- a/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs +++ b/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs @@ -544,7 +544,7 @@ public void CancelWorkStatus_Success() Assert.That(ws, Is.Not.Null); Assert.Multiple(() => { - Assert.That(ws!.Status, Is.EqualTo(WorkStatus.Cancelled)); + Assert.That(ws!.Status, Is.EqualTo(WorkStatus.Canceled)); Assert.That(ws.Reason, Is.EqualTo("No reason was specified.")); }); } diff --git a/tests/CoreEx.Test/TestFunction/HttpTriggerPublishFunctionTest.cs b/tests/CoreEx.Test/TestFunction/HttpTriggerPublishFunctionTest.cs index 5d51604d..c7f1cd23 100644 --- a/tests/CoreEx.Test/TestFunction/HttpTriggerPublishFunctionTest.cs +++ b/tests/CoreEx.Test/TestFunction/HttpTriggerPublishFunctionTest.cs @@ -97,7 +97,7 @@ public void InvalidValue() test.ReplaceScoped(_ => imp) .HttpTrigger() - .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products", new { id = "A", price = 1.99m }))) + .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products/publish", new { id = "A", price = 1.99m }))) .AssertBadRequest() .AssertErrors(new ApiError("Name", "'Name' must not be empty.")); @@ -113,7 +113,7 @@ public void InvalidValue_Newtonsoft() test.ReplaceScoped(_ => imp) .ConfigureServices(sc => sc.ReplaceScoped()) .HttpTrigger() - .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products", new { id = "A", price = 1.99m }))) + .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products/publish", new { id = "A", price = 1.99m }))) .AssertBadRequest() .AssertErrors(new ApiError("Name", "'Name' must not be empty.")); @@ -128,7 +128,7 @@ public void Success() test.ReplaceScoped(_ => imp) .HttpTrigger() - .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products", new { id = "A", name = "B", price = 1.99m }))) + .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products/publish", new { id = "A", name = "B", price = 1.99m }))) .AssertAccepted(); Assert.That(imp.GetNames(), Has.Length.EqualTo(1)); @@ -147,7 +147,7 @@ public void Success_Newtonsoft() test.ReplaceScoped(_ => imp) .ConfigureServices(sc => sc.ReplaceScoped()) .HttpTrigger() - .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products", new { id = "A", name = "B", price = 1.99m }))) + .Run(f => f.RunAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest/products/publish", new { id = "A", name = "B", price = 1.99m }))) .AssertAccepted(); Assert.That(imp.GetNames(), Has.Length.EqualTo(1)); diff --git a/tests/CoreEx.Test2/CoreEx.Test2.csproj b/tests/CoreEx.Test2/CoreEx.Test2.csproj index 680df577..8f369af1 100644 --- a/tests/CoreEx.Test2/CoreEx.Test2.csproj +++ b/tests/CoreEx.Test2/CoreEx.Test2.csproj @@ -20,7 +20,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/CoreEx.Test2/Framework/Validation/ValidationExtensionsTest.cs b/tests/CoreEx.Test2/Framework/Validation/ValidationExtensionsTest.cs index 4598c9bf..9a5a2b38 100644 --- a/tests/CoreEx.Test2/Framework/Validation/ValidationExtensionsTest.cs +++ b/tests/CoreEx.Test2/Framework/Validation/ValidationExtensionsTest.cs @@ -27,7 +27,7 @@ private static async Task> ValidateAsync(string? email2) return await Result.Go().ValidatesAsync(email2, v => { var v2 = v; - var v3 = v.Email(); + var v3 = v2.Email(); }); } } diff --git a/tests/CoreEx.Test2/TestFunctionIso/ServiceBusTest.cs b/tests/CoreEx.Test2/TestFunctionIso/ServiceBusTest.cs index 62253173..84470217 100644 --- a/tests/CoreEx.Test2/TestFunctionIso/ServiceBusTest.cs +++ b/tests/CoreEx.Test2/TestFunctionIso/ServiceBusTest.cs @@ -42,7 +42,7 @@ public async Task Complete_WorkStatus_Cancelled() await wo.CancelAsync(message.MessageId, "No longer needed."); test.ServiceBusTrigger() - .ExpectLogContains("warn: Unable to process message as corresponding work state status is Cancelled: No longer needed.") + .ExpectLogContains("warn: Unable to process message as corresponding work state status is Canceled: No longer needed.") .Run(f => f.Run(message, actions)) .AssertSuccess(); @@ -50,7 +50,7 @@ public async Task Complete_WorkStatus_Cancelled() var ws = await wo.GetAsync(message.MessageId); Assert.That(ws, Is.Not.Null); - Assert.That(ws!.Status, Is.EqualTo(WorkStatus.Cancelled)); + Assert.That(ws!.Status, Is.EqualTo(WorkStatus.Canceled)); } [Test] diff --git a/tests/CoreEx.TestFunction/Functions/HttpTriggerFunction.cs b/tests/CoreEx.TestFunction/Functions/HttpTriggerFunction.cs index 8c4e8e69..ee3a2040 100644 --- a/tests/CoreEx.TestFunction/Functions/HttpTriggerFunction.cs +++ b/tests/CoreEx.TestFunction/Functions/HttpTriggerFunction.cs @@ -10,16 +10,10 @@ namespace CoreEx.TestFunction.Functions { - public class HttpTriggerFunction + public class HttpTriggerFunction(WebApi webApi, ProductService service) { - private readonly WebApi _webApi; - private readonly ProductService _service; - - public HttpTriggerFunction(WebApi webApi, ProductService service) - { - _webApi = webApi; - _service = service; - } + private readonly WebApi _webApi = webApi; + private readonly ProductService _service = service; [FunctionName("HttpTriggerProductGet")] public Task GetAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "products/{id}")] HttpRequest request, string id)