Skip to content

Commit 7e45c7e

Browse files
authored
v3.12.0 (#91)
* PostgreSQL. Minor fixes. Internal cleanup. * Run Code Analysis * Text.SentenceCase * Readme doco update. * JsonDataReader DataOnly/TimeOnly SentenceCase fix * README tweak.
1 parent e683b4c commit 7e45c7e

File tree

174 files changed

+1016
-1275
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

174 files changed

+1016
-1275
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
Represents the **NuGet** versions.
44

5+
## v3.12.0
6+
- *Enhancement*: Added new `CoreEx.Database.Postgres` project/package to support [PostgreSQL](https://www.postgresql.org/) database capabilities. Primarily encapsulates the open-source [`Npqsql`](https://www.npgsql.org/) .NET ADO database provider for PostgreSQL.
7+
- Added `EncodedStringToUInt32Converter` to support PostgreSQL `xmin` column encoding as the row version/etag.
8+
- *Enhancement*: Migrated sentence case logic from inside `PropertyExpression` into `CoreEx.Text.SentenceCase` to improve discoverablity and reuse opportunities.
9+
- *Fixed:* The `IServiceCollection.AddAzureServiceBusClient` extension method as been removed; the `ServiceBusClient` will need to be instantiated prior to usage. Standard approach is for consumers to create client instances independently.
10+
- *Fixed*: The `WorkOrchestrator.GetAsync<T>()` and `WorkOrchestrator.GetAsync(string type, ..)` methods were not automatically cancelling where expired.
11+
- *Fixed*: The `InvokerArgs` activity tracing updated to correctly capture the `Exception.Message` where an `Exception` has been thrown.
12+
- *Internal*:
13+
- All `throw new ArgumentNullException` checking migrated to the `xxx.ThrowIfNull` extension method equivalent.
14+
- All _Run Code Analysis_ issues resolved.
15+
516
## v3.11.0
617
- *Enhancement*: The `ITypedToResult` updated to correctly implement `IToResult` as the simple `ToResult` where required.
718
- *Enhancement*: Added `Result.AsTask()` and `Result<T>.AsTask` to simplify the conversion to a completed `Task<Result>` or `Task<Result<T>>` where applicable.

Common.targets

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>3.11.0</Version>
3+
<Version>3.12.0</Version>
44
<LangVersion>preview</LangVersion>
55
<Authors>Avanade</Authors>
66
<Company>Avanade</Company>

CoreEx.sln

+9-2
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.UnitTesting", "src\C
8383
EndProject
8484
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.UnitTesting.NUnit", "src\CoreEx.UnitTesting.NUnit\CoreEx.UnitTesting.NUnit.csproj", "{91910971-4B1A-4791-9BB4-65FAB3ED3C76}"
8585
EndProject
86-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreEx.TestFunctionIso", "tests\CoreEx.TestFunctionIso\CoreEx.TestFunctionIso.csproj", "{6F7B4F1E-3C3A-4CD7-A9BF-973A5053C1C8}"
86+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.TestFunctionIso", "tests\CoreEx.TestFunctionIso\CoreEx.TestFunctionIso.csproj", "{6F7B4F1E-3C3A-4CD7-A9BF-973A5053C1C8}"
8787
EndProject
88-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreEx.Test2", "tests\CoreEx.Test2\CoreEx.Test2.csproj", "{910B5894-46BC-4427-95D6-2804F06458E3}"
88+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreEx.Test2", "tests\CoreEx.Test2\CoreEx.Test2.csproj", "{910B5894-46BC-4427-95D6-2804F06458E3}"
89+
EndProject
90+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreEx.Database.Postgres", "src\CoreEx.Database.Postgres\CoreEx.Database.Postgres.csproj", "{C042AC2A-415D-432E-83FA-B911FD9ED378}"
8991
EndProject
9092
Global
9193
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -209,6 +211,10 @@ Global
209211
{910B5894-46BC-4427-95D6-2804F06458E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
210212
{910B5894-46BC-4427-95D6-2804F06458E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
211213
{910B5894-46BC-4427-95D6-2804F06458E3}.Release|Any CPU.Build.0 = Release|Any CPU
214+
{C042AC2A-415D-432E-83FA-B911FD9ED378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
215+
{C042AC2A-415D-432E-83FA-B911FD9ED378}.Debug|Any CPU.Build.0 = Debug|Any CPU
216+
{C042AC2A-415D-432E-83FA-B911FD9ED378}.Release|Any CPU.ActiveCfg = Release|Any CPU
217+
{C042AC2A-415D-432E-83FA-B911FD9ED378}.Release|Any CPU.Build.0 = Release|Any CPU
212218
EndGlobalSection
213219
GlobalSection(SolutionProperties) = preSolution
214220
HideSolutionNode = FALSE
@@ -245,6 +251,7 @@ Global
245251
{91910971-4B1A-4791-9BB4-65FAB3ED3C76} = {D2C61D4A-2A6D-4284-BF9D-09F51BA735B8}
246252
{6F7B4F1E-3C3A-4CD7-A9BF-973A5053C1C8} = {3145DCB9-98FB-4519-BCC0-75A22A252EDC}
247253
{910B5894-46BC-4427-95D6-2804F06458E3} = {3145DCB9-98FB-4519-BCC0-75A22A252EDC}
254+
{C042AC2A-415D-432E-83FA-B911FD9ED378} = {4B6BC31E-93B1-42B0-AE09-AD85AC4DB657}
248255
EndGlobalSection
249256
GlobalSection(ExtensibilityGlobals) = postSolution
250257
SolutionGuid = {8B4566D2-9B22-4E27-9654-402BDBA6C744}

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ Package | Status | Source & documentation
2626
`CoreEx.Azure` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Azure.svg)](https://badge.fury.io/nu/CoreEx.Azure) | [Link](./src/CoreEx.Azure)
2727
`CoreEx.Cosmos` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Cosmos.svg)](https://badge.fury.io/nu/CoreEx.Cosmos) | [Link](./src/CoreEx.Cosmos)
2828
`CoreEx.Database` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Database.svg)](https://badge.fury.io/nu/CoreEx.Database) | [Link](./src/CoreEx.Database)
29-
`CoreEx.Database.SqlServer` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Database.SqlServer.svg)](https://badge.fury.io/nu/CoreEx.Database.SqlServer) | [Link](./src/CoreEx.Database.SqlServer)
3029
`CoreEx.Database.MySql` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Database.MySql.svg)](https://badge.fury.io/nu/CoreEx.Database.MySql) | [Link](./src/CoreEx.Database.MySql)
30+
`CoreEx.Database.Postgres` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Database.Postgres.svg)](https://badge.fury.io/nu/CoreEx.Database.Postgres) | [Link](./src/CoreEx.Database.Postgres)
31+
`CoreEx.Database.SqlServer` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Database.SqlServer.svg)](https://badge.fury.io/nu/CoreEx.Database.SqlServer) | [Link](./src/CoreEx.Database.SqlServer)
3132
`CoreEx.EntityFrameworkCore` | [![NuGet version](https://badge.fury.io/nu/CoreEx.EntityFrameworkCore.svg)](https://badge.fury.io/nu/CoreEx.EntityFrameworkCore) | [Link](./src/CoreEx.EntityFrameworkCore)
3233
`CoreEx.FluentValidation` | [![NuGet version](https://badge.fury.io/nu/CoreEx.FluentValidation.svg)](https://badge.fury.io/nu/CoreEx.FluentValidation) | [Link](./src/CoreEx.FluentValidation)
3334
`CoreEx.Newtonsoft` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Newtonsoft.svg)](https://badge.fury.io/nu/CoreEx.Newtonsoft) | [Link](./src/CoreEx.Newtonsoft)
@@ -59,9 +60,9 @@ These other _Avanade_ repositories leverage _CoreEx_:
5960

6061
Repo | Description
6162
-|-
62-
[Beef](https://github.com/Avanade/beef) | Code-generation capabilities to support the industrialization of API development leveraging `CoreEx` as the primary runtime framework (_Beef_ version `v5+`).
63-
[DbEx](https://github.com/Avanade/dbex) | Provides database extensions for DbUp-inspired database migrations.
64-
[NTangle](https://github.com/Avanade/ntangle) | Change Data Capture (CDC) code generation tool and runtime.
63+
[*Beef*](https://github.com/Avanade/beef) | Code-generation capabilities to support the industrialization of API development leveraging `CoreEx` as the primary runtime framework (_Beef_ version `v5+`).
64+
[*DbEx*](https://github.com/Avanade/dbex) | Provides database extensions for DbUp-inspired database migrations.
65+
[*NTangle*](https://github.com/Avanade/ntangle) | Change Data Capture (CDC) code generation tool and runtime.
6566

6667
<br/>
6768

nuget-publish.ps1

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ param(
5656
"src\CoreEx.Database",
5757
"src\CoreEx.Database.SqlServer",
5858
"src\CoreEx.Database.MySql",
59+
"src\CoreEx.Database.Postgres",
5960
"src\CoreEx.EntityFrameworkCore",
6061
"src\CoreEx.Cosmos",
6162
"src\CoreEx.OData",

samples/My.Hr/My.Hr.Api/Startup.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
using Microsoft.EntityFrameworkCore;
55
using Microsoft.Extensions.Diagnostics.HealthChecks;
66
using Azure.Monitor.OpenTelemetry.AspNetCore;
7-
using OpenTelemetry.Instrumentation.AspNetCore;
87
using OpenTelemetry.Trace;
8+
using Az = Azure.Messaging.ServiceBus;
99

1010
namespace My.Hr.Api;
1111

@@ -28,9 +28,9 @@ public void ConfigureServices(IServiceCollection services)
2828
.AddEventDataSerializer()
2929
.AddEventDataFormatter()
3030
.AddEventPublisher()
31+
.AddSingleton(sp => new Az.ServiceBusClient(sp.GetRequiredService<HrSettings>().ServiceBusConnection__fullyQualifiedNamespace))
3132
.AddAzureServiceBusSender()
3233
.AddAzureServiceBusPurger()
33-
.AddAzureServiceBusClient(connectionName: nameof(HrSettings.ServiceBusConnection), configure: (o, sp) => o.RetryOptions.MaxRetries = 3)
3434
.AddJsonMergePatch()
3535
.AddWebApi((_, c) => c.UnhandledExceptionAsync = (ex, _, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? WebApiBase.CreateActionResultFromExtendedException(new ConcurrencyException()) : null))
3636
.AddReferenceDataContentWebApi()

samples/My.Hr/My.Hr.Functions/Startup.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
using CoreEx.Database;
77
using CoreEx.DataBase.HealthChecks;
88
using CoreEx.HealthChecks;
9-
using CoreEx.RefData;
109
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
1110
using Microsoft.EntityFrameworkCore;
12-
using Microsoft.Extensions.Caching.Memory;
1311
using Microsoft.Extensions.DependencyInjection;
1412
using Microsoft.Extensions.Diagnostics.HealthChecks;
1513
using My.Hr.Business;
1614
using My.Hr.Business.Data;
1715
using My.Hr.Business.External;
1816
using My.Hr.Business.Services;
17+
using Az = Azure.Messaging.ServiceBus;
1918

2019
[assembly: FunctionsStartup(typeof(My.Hr.Functions.Startup))]
2120

@@ -40,12 +39,12 @@ public override void Configure(IFunctionsHostBuilder builder)
4039
.AddEventDataSerializer()
4140
.AddEventDataFormatter()
4241
.AddEventPublisher()
42+
.AddSingleton(sp => new Az.ServiceBusClient(sp.GetRequiredService<HrSettings>().ServiceBusConnection__fullyQualifiedNamespace))
4343
.AddAzureServiceBusSender()
4444
.AddWebApi((_, c) => c.UnhandledExceptionAsync = (ex, _, _) => Task.FromResult(ex is DbUpdateConcurrencyException efex ? WebApiBase.CreateActionResultFromExtendedException(new ConcurrencyException()) : null))
4545
.AddJsonMergePatch()
4646
.AddWebApiPublisher()
47-
.AddAzureServiceBusSubscriber()
48-
.AddAzureServiceBusClient(connectionName: nameof(HrSettings.ServiceBusConnection));
47+
.AddAzureServiceBusSubscriber();
4948

5049
// Register the health checks.
5150
builder.Services

samples/My.Hr/My.Hr.UnitTest/appsettings.unittest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"DOTNET_ENVIRONMENT": "Development",
33
"VerificationResultsQueueName": "verificationResults",
44
"VerificationQueueName": "pendingVerifications",
5-
"ServiceBusConnection__fullyQualifiedNamespace": "topsecret",
5+
"ServiceBusConnection__fullyQualifiedNamespace": "Endpoint=sb://top-secret.servicebus.windows.net/;SharedAccessKeyName=top-secret;SharedAccessKey=top-encrypted-secret;",
66
"AgifyApiEndpointUri": "https://api.agify.mock.io",
77
"NationalizeApiClientApiEndpointUri": "https://api.nationalize.mock.io",
88
"GenderizeApiClientApiEndpointUri": "https://api.genderize.mock.io",

src/CoreEx.AspNetCore/Abstractions/ReferenceDataOrchestratorExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static Task<ReferenceDataMultiDictionary> GetNamedAsync(this ReferenceDat
5656
/// <summary>
5757
/// Perform a further split of the string values.
5858
/// </summary>
59-
private static IEnumerable<string> SplitStringValues(IEnumerable<string> values)
59+
private static List<string> SplitStringValues(IEnumerable<string> values)
6060
{
6161
var list = new List<string>();
6262
foreach (var value in values)

src/CoreEx.AspNetCore/HealthChecks/HealthService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async Task<IActionResult> RunAsync()
4242
/// <summary>
4343
/// Builds the health report response.
4444
/// </summary>
45-
private IActionResult BuildResponse(HealthReport healthReport)
45+
private ContentResult BuildResponse(HealthReport healthReport)
4646
{
4747
var code = healthReport.Status == HealthStatus.Healthy ? HttpStatusCode.OK : HttpStatusCode.ServiceUnavailable;
4848

src/CoreEx.AspNetCore/WebApis/ValueContentResult.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using CoreEx.AspNetCore.Http;
55
using CoreEx.Entities;
66
using CoreEx.Json;
7-
using CoreEx.Results;
87
using Microsoft.AspNetCore.Http;
98
using Microsoft.AspNetCore.Mvc;
109
using Microsoft.Net.Http.Headers;
@@ -159,12 +158,12 @@ public static bool TryCreateValueContentResult<T>(T value, HttpStatusCode status
159158
bool hasETag = TryGetETag(val, out var etag);
160159

161160
Action<IJsonPreFilterInspector>? inspector;
162-
if (requestOptions.IncludeFields != null && requestOptions.IncludeFields.Any())
161+
if (requestOptions.IncludeFields != null && requestOptions.IncludeFields.Length > 0)
163162
{
164163
inspector = hasETag ? null : fi => etag = GenerateETag(requestOptions, val, fi.ToJsonString(), jsonSerializer);
165164
jsonSerializer.TryApplyFilter(val, requestOptions.IncludeFields, out json, JsonPropertyFilter.Include, preFilterInspector: inspector);
166165
}
167-
else if (requestOptions.ExcludeFields != null && requestOptions.ExcludeFields.Any())
166+
else if (requestOptions.ExcludeFields != null && requestOptions.ExcludeFields.Length > 0)
168167
{
169168
inspector = hasETag ? null : fi => etag = GenerateETag(requestOptions, val, fi.ToJsonString(), jsonSerializer);
170169
jsonSerializer.TryApplyFilter(val, requestOptions.ExcludeFields, out json, JsonPropertyFilter.Exclude, preFilterInspector: inspector);

src/CoreEx.AspNetCore/WebApis/WebApi.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ private async Task<IActionResult> PutInternalAsync<TValue>(HttpRequest request,
695695

696696
// Get the current value before we perform the update; also performing a concurrency match.
697697
var cvalue = await get(wap, ct).ConfigureAwait(false);
698-
var ex = cvalue == null ? new NotFoundException() : ConcurrencyETagMatching(wap, cvalue, wapv!.Value, simulatedConcurrency);
698+
var ex = cvalue == null ? (Exception)new NotFoundException() : ConcurrencyETagMatching(wap, cvalue, wapv!.Value, simulatedConcurrency);
699699
if (ex is not null)
700700
return await CreateActionResultFromExceptionAsync(this, request.HttpContext, ex, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false);
701701

@@ -708,7 +708,7 @@ private async Task<IActionResult> PutInternalAsync<TValue>(HttpRequest request,
708708
/// <summary>
709709
/// Where etags are supported or automatic concurrency then we need to make sure one was provided up-front and match.
710710
/// </summary>
711-
private Exception? ConcurrencyETagMatching<TValue>(WebApiParam wap, TValue getValue, TValue putValue, bool autoConcurrency)
711+
private ConcurrencyException? ConcurrencyETagMatching<TValue>(WebApiParam wap, TValue getValue, TValue putValue, bool autoConcurrency)
712712
{
713713
var et = putValue as IETag;
714714
if (et != null || autoConcurrency)
@@ -844,7 +844,7 @@ public async Task<IActionResult> PatchAsync<TValue>(HttpRequest request, Func<We
844844
{
845845
// Get the current value and perform a concurrency match before we perform the merge.
846846
var value = await get(wap, ct2).ConfigureAwait(false);
847-
var ex = value is null ? new NotFoundException() : ConcurrencyETagMatching(wap, value, jpv, simulatedConcurrency);
847+
var ex = value is null ? (Exception)new NotFoundException() : ConcurrencyETagMatching(wap, value, jpv, simulatedConcurrency);
848848
if (ex is not null)
849849
throw ex;
850850

src/CoreEx.AspNetCore/WebApis/WebApiBase.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ public static IActionResult CreateActionResultFromExtendedException(IExtendedExc
237237
/// <summary>
238238
/// Creates an <see cref="IActionResult"/> from an unexpected <paramref name="exception"/>.
239239
/// </summary>
240-
private static IActionResult CreateActionResultForUnexpectedResult(Exception exception, bool includeExceptionInResult) => includeExceptionInResult
240+
private static ContentResult CreateActionResultForUnexpectedResult(Exception exception, bool includeExceptionInResult) => includeExceptionInResult
241241
? new ContentResult { StatusCode = (int)HttpStatusCode.InternalServerError, ContentType = MediaTypeNames.Text.Plain, Content = $"An unexpected internal server error has occurred. CorrelationId={ExecutionContext.Current.CorrelationId} Exception={exception}" }
242242
: new ContentResult { StatusCode = (int)HttpStatusCode.InternalServerError, ContentType = MediaTypeNames.Text.Plain, Content = $"An unexpected internal server error has occurred. CorrelationId={ExecutionContext.Current.CorrelationId}" };
243243
}

src/CoreEx.AutoMapper/AutoMapperConverterWrapper.cs

+3-8
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,18 @@ namespace CoreEx.Mapping.Converters
1111
/// <typeparam name="TDestination">The destination <see cref="Type"/>.</typeparam>
1212
/// <typeparam name="TConverter">The <see cref="IConverter{TSource, TDestination}"/> <see cref="Type"/>.</typeparam>
1313
/// <typeparam name="TSelf">The declaring <see cref="Type"/> itself to enable <see cref="Default"/>.</typeparam>
14-
public abstract class AutoMapperConverterWrapper<TSource, TDestination, TConverter, TSelf>
14+
/// <param name="create">The optional function to create the <typeparamref name="TConverter"/> instance.</param>
15+
public abstract class AutoMapperConverterWrapper<TSource, TDestination, TConverter, TSelf>(Func<TConverter>? create = null)
1516
where TConverter : IConverter<TSource, TDestination>, new()
1617
where TSelf : AutoMapperConverterWrapper<TSource, TDestination, TConverter, TSelf>, new()
1718
{
18-
private readonly Func<TConverter> _create;
19+
private readonly Func<TConverter> _create = create ?? (() => new TConverter());
1920

2021
/// <summary>
2122
/// Gets or sets the default (singleton) instance.
2223
/// </summary>
2324
public static TSelf Default { get; set; } = new();
2425

25-
/// <summary>
26-
/// Initializes a new instance of the <see cref="AutoMapperConverterWrapper{TSource, TDestination, TConverter, TSelf}"/> class.
27-
/// </summary>
28-
/// <param name="create">The optional function to create the <typeparamref name="TConverter"/> instance.</param>
29-
public AutoMapperConverterWrapper(Func<TConverter>? create = null) => _create = create ?? (() => new TConverter());
30-
3126
/// <summary>
3227
/// Gets the source to destination <see cref="AutoMapper.IValueConverter{TSource, TDestination}"/>.
3328
/// </summary>

0 commit comments

Comments
 (0)