From 7572cf504cb78c988eea8925c068c7551bdd0634 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 13:07:42 +0200 Subject: [PATCH 01/11] Upgrade to .NET 11 and related packages, update README --- .github/workflows/core_build_publish.yml | 2 +- Directory.Build.props | 2 +- Directory.Packages.props | 31 ++++++++++--------- README.md | 7 +++-- global.json | 4 +-- .../LeanCode.CQRS.Annotations.csproj | 2 +- .../PushNotificationTokenStore.cs | 2 +- .../LeanCode.ForceUpdate.Contracts.csproj | 2 +- test-bed/LeanCode.TestBed.csproj | 2 +- 9 files changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/workflows/core_build_publish.yml b/.github/workflows/core_build_publish.yml index b512bfa1..2edc12d2 100644 --- a/.github/workflows/core_build_publish.yml +++ b/.github/workflows/core_build_publish.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: env: - DOTNET_VERSION: 10.0.100 + DOTNET_VERSION: 11.0.100-rc.1.26425.128 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1 diff --git a/Directory.Build.props b/Directory.Build.props index d7d30d30..2fe03827 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ preview library - net10.0 + net11.0 portable true enable diff --git a/Directory.Packages.props b/Directory.Packages.props index a9117854..a9ce1b96 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,17 +2,18 @@ true - 10.0.0 - 10.0.0 - 10.0.0 + 11.0.0-rc.1.26425.128 + 11.0.0-rc.1.26425.128 + 11.0.0-rc.1.26425.128 - 8.5.7 + + 8.5.10 5.0.0 - 10.0.0 + 11.0.0-rc.1.26425.128 @@ -46,7 +47,7 @@ - + @@ -80,7 +81,7 @@ - + @@ -97,24 +98,24 @@ - + - - + + - - - + + + - - + + diff --git a/README.md b/README.md index 073e5642..ef906507 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![Corelibrary Build & Publish](https://github.com/leancodepl/corelibrary/workflows/Corelibrary%20Build%20&%20Publish/badge.svg) [![Nuget](https://img.shields.io/nuget/vpre/LeanCode.Components)](https://www.nuget.org/packages/LeanCode.Components) [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Fleancode%2Fpublic%2Fshield%2FLeanCode.Components%2Flatest)](https://feedz.io/org/leancode/repository/public/packages/LeanCode.Components) -[![codecov](https://codecov.io/gh/leancodepl/corelibrary/branch/v10.0-preview/graph/badge.svg?token=ROFNA3WTTD)](https://codecov.io/gh/leancodepl/corelibrary) +[![codecov](https://codecov.io/gh/leancodepl/corelibrary/branch/v11.0-preview/graph/badge.svg?token=ROFNA3WTTD)](https://codecov.io/gh/leancodepl/corelibrary) [![License](https://img.shields.io/badge/License-Apache_2.0-green.svg)](https://www.apache.org/licenses/LICENSE-2.0) The LeanCode Core Library is a set of helper libraries developed at [our company](https://leancode.co/) that aids our day-to-day development. Not only does it serve as a facilitator in our day-to-day coding activities, but it also encapsulates comprehensive guidelines, gathers our collective knowledge on application architecture and development best practices. @@ -50,7 +50,7 @@ All of the libraries that are part of the CoreLib are versioned together and req ## Supported versions -Here is the list of available major versions of the library (as of 2024-08-16): +Here is the list of available major versions of the library (as of 2026-09-14): | CoreLib | .NET Core | Under development | Stable | Notes | |---------|-----------|-------------------|------------|-------------------| @@ -65,7 +65,8 @@ Here is the list of available major versions of the library (as of 2024-08-16): | v8.0 | 8.0 | | | Unmaintained | | v8.1 | 8.0 | | ✔ | | | v9.0 | 9.0 | | ✔ | | -| v10.0 | 10.0 | ✔ | | | +| v10.0 | 10.0 | | ✔ | | +| v11.0 | 11.0 | ✔ | | Release candidate | ## Building & Testing diff --git a/global.json b/global.json index 6f5a3e61..4dc8ca55 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "10.0.100", - "allowPrerelease": false, + "version": "11.0.100-rc.1.26425.128", + "allowPrerelease": true, "rollForward": "latestMinor" }, "test": { diff --git a/src/CQRS/LeanCode.CQRS.Annotations/LeanCode.CQRS.Annotations.csproj b/src/CQRS/LeanCode.CQRS.Annotations/LeanCode.CQRS.Annotations.csproj index 96871dba..60ea24f8 100644 --- a/src/CQRS/LeanCode.CQRS.Annotations/LeanCode.CQRS.Annotations.csproj +++ b/src/CQRS/LeanCode.CQRS.Annotations/LeanCode.CQRS.Annotations.csproj @@ -1,7 +1,7 @@  - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 diff --git a/src/Infrastructure/LeanCode.Firebase.FCM/PushNotificationTokenStore.cs b/src/Infrastructure/LeanCode.Firebase.FCM/PushNotificationTokenStore.cs index cd68196f..90a60af5 100644 --- a/src/Infrastructure/LeanCode.Firebase.FCM/PushNotificationTokenStore.cs +++ b/src/Infrastructure/LeanCode.Firebase.FCM/PushNotificationTokenStore.cs @@ -72,7 +72,7 @@ public async Task AddUserTokenAsync(TUserId userId, string newToken, Cancellatio var dateCreatedColumn = GetTokensColumnName(nameof(PushNotificationTokenEntity.DateCreated)); // perform an upsert using SQL merge, ensuring that given token is assigned to our user and no-one else - await dbContext.Database.ExecuteSqlInterpolatedAsync( + await dbContext.Database.ExecuteSqlAsync( FormattableStringFactory.Create( $$""" MERGE INTO {{tokensTable}} "pt" diff --git a/src/Infrastructure/LeanCode.ForceUpdate.Contracts/LeanCode.ForceUpdate.Contracts.csproj b/src/Infrastructure/LeanCode.ForceUpdate.Contracts/LeanCode.ForceUpdate.Contracts.csproj index 3023b200..0cbcf095 100644 --- a/src/Infrastructure/LeanCode.ForceUpdate.Contracts/LeanCode.ForceUpdate.Contracts.csproj +++ b/src/Infrastructure/LeanCode.ForceUpdate.Contracts/LeanCode.ForceUpdate.Contracts.csproj @@ -2,7 +2,7 @@ annotations - net8.0;net9.0;net10.0 + net8.0;net9.0;net10.0;net11.0 diff --git a/test-bed/LeanCode.TestBed.csproj b/test-bed/LeanCode.TestBed.csproj index 33d2f5ef..720fcb25 100644 --- a/test-bed/LeanCode.TestBed.csproj +++ b/test-bed/LeanCode.TestBed.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable enable false From 4555292a027e43e2716efd94c629ab7ee7518669 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 14:24:18 +0200 Subject: [PATCH 02/11] Bump more packages and adjust code as needed --- .config/dotnet-tools.json | 6 +-- Directory.Packages.props | 54 +++++++++---------- .../DefaultLeanCodeCredential.cs | 6 ++- .../LeanCode.Firebase.FCM/FCMClient.cs | 22 ++++---- .../FirebaseConfiguration.cs | 2 +- test/Directory.Build.props | 2 +- .../AzureStorageAuditLogIntegrationTests.cs | 9 +++- .../Properties.cs | 4 +- .../FCMClientTests.cs | 2 +- 9 files changed, 58 insertions(+), 49 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c9a67346..6a5ca6d3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,18 +3,18 @@ "isRoot": true, "tools": { "csharpier": { - "version": "1.2.5", + "version": "1.3.0", "commands": [ "csharpier" ], "rollForward": false }, "dotnet-coverage": { - "version": "18.3.1", + "version": "18.11.2", "commands": [ "dotnet-coverage" ], "rollForward": false } } -} \ No newline at end of file +} diff --git a/Directory.Packages.props b/Directory.Packages.props index a9ce1b96..bcbee9cc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - 5.0.0 + 5.9.0 11.0.0-rc.1.26425.128 @@ -22,41 +22,41 @@ - + - - + + - - - - - + + + + + - + - + - + - - + + - + - - - - + + + + @@ -73,7 +73,7 @@ - + @@ -102,7 +102,7 @@ - + @@ -122,12 +122,12 @@ - - - - - - + + + + + + diff --git a/src/Infrastructure/LeanCode.AzureIdentity/DefaultLeanCodeCredential.cs b/src/Infrastructure/LeanCode.AzureIdentity/DefaultLeanCodeCredential.cs index a019cc93..db9bb9d7 100644 --- a/src/Infrastructure/LeanCode.AzureIdentity/DefaultLeanCodeCredential.cs +++ b/src/Infrastructure/LeanCode.AzureIdentity/DefaultLeanCodeCredential.cs @@ -49,7 +49,11 @@ public static TokenCredential Create(AzureCredentialConfiguration config) if (config.UseManagedIdentity) { - return new ManagedIdentityCredential(config.ClientId); + var managedIdentityId = string.IsNullOrEmpty(config.ClientId) + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.FromUserAssignedClientId(config.ClientId); + + return new ManagedIdentityCredential(managedIdentityId); } else if (config.UseAzureCLI) { diff --git a/src/Infrastructure/LeanCode.Firebase.FCM/FCMClient.cs b/src/Infrastructure/LeanCode.Firebase.FCM/FCMClient.cs index a44bbc69..b0d8110a 100644 --- a/src/Infrastructure/LeanCode.Firebase.FCM/FCMClient.cs +++ b/src/Infrastructure/LeanCode.Firebase.FCM/FCMClient.cs @@ -62,9 +62,9 @@ public virtual async Task SendToUserAsync( CancellationToken cancellationToken = default ) { - message.Tokens = await tokenStore.GetTokensAsync(userId, cancellationToken); + message.Fids = await tokenStore.GetTokensAsync(userId, cancellationToken); - if (message.Tokens.Count == 0) + if (message.Fids.Count == 0) { logger.Information("Cannot send push to user {UserId} - no tokens", userId); } @@ -73,10 +73,10 @@ public virtual async Task SendToUserAsync( logger.Debug( "Sending push notification to user {UserId} that targets {Count} devices", userId, - message.Tokens.Count + message.Fids.Count ); var response = await messaging.SendEachForMulticastAsync(message, dryRun, cancellationToken); - await HandleBatchResponseAsync(response, message.Tokens, cancellationToken); + await HandleBatchResponseAsync(response, message.Fids, cancellationToken); } } @@ -88,9 +88,9 @@ public virtual async Task SendToUsersAsync( ) { var tokens = await tokenStore.GetTokensAsync(userIds, cancellationToken); - message.Tokens = tokens.SelectMany(t => t.Value).ToList(); + message.Fids = tokens.SelectMany(t => t.Value).ToList(); - if (message.Tokens.Count == 0) + if (message.Fids.Count == 0) { logger.Information("Cannot send push to users {UserIds} - no tokens", userIds); } @@ -99,10 +99,10 @@ public virtual async Task SendToUsersAsync( logger.Debug( "Sending push notification to user {Count} users, targeting {Count} devices", userIds.Count, - message.Tokens.Count + message.Fids.Count ); var response = await messaging.SendEachForMulticastAsync(message, dryRun, cancellationToken); - await HandleBatchResponseAsync(response, message.Tokens, cancellationToken); + await HandleBatchResponseAsync(response, message.Fids, cancellationToken); } } @@ -116,7 +116,7 @@ public virtual async Task SendAllAsync( logger.Debug("Sending {Count} push messages", messages.Count()); var response = await messaging.SendEachAsync(messages, dryRun, cancellationToken); - await HandleBatchResponseAsync(response, messages.Select(m => m.Token), cancellationToken); + await HandleBatchResponseAsync(response, messages.Select(m => m.Fid), cancellationToken); } public virtual async Task SendMulticastAsync( @@ -125,10 +125,10 @@ public virtual async Task SendMulticastAsync( CancellationToken cancellationToken = default ) { - logger.Debug("Sending multicast push message to {Count} targets", message.Tokens.Count); + logger.Debug("Sending multicast push message to {Count} targets", message.Fids.Count); var response = await messaging.SendEachForMulticastAsync(message, dryRun, cancellationToken); - await HandleBatchResponseAsync(response, message.Tokens, cancellationToken); + await HandleBatchResponseAsync(response, message.Fids, cancellationToken); } private async Task HandleBatchResponseAsync( diff --git a/src/Infrastructure/LeanCode.Firebase/FirebaseConfiguration.cs b/src/Infrastructure/LeanCode.Firebase/FirebaseConfiguration.cs index 4d31f3f0..634573e5 100644 --- a/src/Infrastructure/LeanCode.Firebase/FirebaseConfiguration.cs +++ b/src/Infrastructure/LeanCode.Firebase/FirebaseConfiguration.cs @@ -28,7 +28,7 @@ public static FirebaseApp Prepare(string? cfg, string name) return FirebaseApp.Create( new AppOptions { - Credential = GoogleCredential.FromJson(cfg), + Credential = CredentialFactory.FromJson(cfg).ToGoogleCredential(), ProjectId = JsonNode.Parse(cfg)?["project_id"]?.ToString(), }, name diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 07f23288..847349a7 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -15,7 +15,7 @@ true - $(TestingPlatformCommandLineArguments) --report-ctrf --coverage --ignore-exit-code 8 --results-directory $(MSBuildThisFileDirectory)/../TestResults --coverage-output-format cobertura --coverage-settings $(MSBuildThisFileDirectory)/coverage.xml --report-ctrf-filename $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).json --coverage-output $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).xml + $(TestingPlatformCommandLineArguments) --report-xunit-ctrf --coverage --ignore-exit-code 8 --results-directory $(MSBuildThisFileDirectory)/../TestResults --coverage-output-format cobertura --coverage-settings $(MSBuildThisFileDirectory)/coverage.xml --report-xunit-ctrf-filename $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).json --coverage-output $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).xml diff --git a/test/Infrastructure/LeanCode.Azure.Tests/AzureStorageAuditLogIntegrationTests.cs b/test/Infrastructure/LeanCode.Azure.Tests/AzureStorageAuditLogIntegrationTests.cs index 4a40906d..829ddc6b 100644 --- a/test/Infrastructure/LeanCode.Azure.Tests/AzureStorageAuditLogIntegrationTests.cs +++ b/test/Infrastructure/LeanCode.Azure.Tests/AzureStorageAuditLogIntegrationTests.cs @@ -200,7 +200,14 @@ await storage.StoreEventAsync( var blobsFound = 0; var linesFound = 0; - await foreach (var b in containerClient.GetBlobsAsync(BlobTraits.All, BlobStates.All, $"{type}/{id}")) + await foreach ( + var b in containerClient.GetBlobsAsync( + BlobTraits.All, + BlobStates.All, + $"{type}/{id}", + CancellationToken.None + ) + ) { var blockBlob = containerClient.GetBlobClient(b.Name); using var stream = new MemoryStream(); diff --git a/test/Infrastructure/LeanCode.AzureIdentity.Tests/Properties.cs b/test/Infrastructure/LeanCode.AzureIdentity.Tests/Properties.cs index e03b0946..1176b0c4 100644 --- a/test/Infrastructure/LeanCode.AzureIdentity.Tests/Properties.cs +++ b/test/Infrastructure/LeanCode.AzureIdentity.Tests/Properties.cs @@ -1,4 +1,2 @@ -using Xunit; - // This is necessary, because some tests rely on setting environmental variables, which might be overwritten by tests. -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] +[assembly: Xunit.v3.Parallelization(Mode = Xunit.Sdk.ParallelMode.None)] diff --git a/test/Infrastructure/LeanCode.Firebase.FCM.Tests/FCMClientTests.cs b/test/Infrastructure/LeanCode.Firebase.FCM.Tests/FCMClientTests.cs index 1a955a0c..765747f8 100644 --- a/test/Infrastructure/LeanCode.Firebase.FCM.Tests/FCMClientTests.cs +++ b/test/Infrastructure/LeanCode.Firebase.FCM.Tests/FCMClientTests.cs @@ -73,7 +73,7 @@ public async Task Gets_user_tokens_when_sending_the_message() await client.SendToUserAsync(UserId, message); - var token = Assert.Single(message.Tokens); + var token = Assert.Single(message.Fids); Assert.Equal(Token, token); } } From 9fa84db50d4ad2340b62ef5103e15df24c5c1b72 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 14:33:20 +0200 Subject: [PATCH 03/11] Group some package versions when they share release history --- Directory.Packages.props | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bcbee9cc..23443421 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,10 @@ 8.5.10 + 1.18.0 + 10.2.3 + 4.0.1 + 5.9.0 @@ -110,12 +114,12 @@ - - + + - - + + @@ -125,9 +129,9 @@ - - - + + + From d6416182d62b5cc658e849b926f6d5a75c609417 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 14:59:44 +0200 Subject: [PATCH 04/11] Leave a TODO for my future self --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index 23443421..bfa2b05d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + 5.9.0 11.0.0-rc.1.26425.128 From 83a73da809cfb40e0322b256767eb601085af133 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 15:00:05 +0200 Subject: [PATCH 05/11] Replace IdentityModel with Duende.IdentityModel --- Directory.Packages.props | 2 +- .../ClientCredentialsHandler.cs | 2 +- .../LeanCode.ClientCredentialsHandler.csproj | 2 +- .../LeanCode.IntegrationTestHelpers.csproj | 2 -- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bfa2b05d..11ffcc30 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,7 +47,7 @@ - + diff --git a/src/Infrastructure/LeanCode.ClientCredentialsHandler/ClientCredentialsHandler.cs b/src/Infrastructure/LeanCode.ClientCredentialsHandler/ClientCredentialsHandler.cs index ea5d5f03..5a2670ae 100644 --- a/src/Infrastructure/LeanCode.ClientCredentialsHandler/ClientCredentialsHandler.cs +++ b/src/Infrastructure/LeanCode.ClientCredentialsHandler/ClientCredentialsHandler.cs @@ -1,6 +1,6 @@ using System.Net; using System.Net.Http.Headers; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using LeanCode.Logging; namespace LeanCode.ClientCredentialsHandler; diff --git a/src/Infrastructure/LeanCode.ClientCredentialsHandler/LeanCode.ClientCredentialsHandler.csproj b/src/Infrastructure/LeanCode.ClientCredentialsHandler/LeanCode.ClientCredentialsHandler.csproj index 433f6214..e7f6092e 100644 --- a/src/Infrastructure/LeanCode.ClientCredentialsHandler/LeanCode.ClientCredentialsHandler.csproj +++ b/src/Infrastructure/LeanCode.ClientCredentialsHandler/LeanCode.ClientCredentialsHandler.csproj @@ -1,6 +1,6 @@ - + diff --git a/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj b/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj index 83be8e63..579fa6b2 100644 --- a/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj +++ b/src/Testing/LeanCode.IntegrationTestHelpers/LeanCode.IntegrationTestHelpers.csproj @@ -19,8 +19,6 @@ - - From 04cb75e2083e358ee41a86ac1c78539cbb86d1a2 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 15:04:10 +0200 Subject: [PATCH 06/11] Update integration tests to .NET 11 and Npgsql.EFCore to rc.1 And bump Postgres version used to 18. --- .github/workflows/core_build_publish.yml | 2 +- Directory.Packages.props | 4 ++-- README.md | 4 ++-- cloud-tests-infrastructure/postgres.tf | 2 +- nuget.config | 6 ++++++ test/LeanCode.IntegrationTests/docker/Dockerfile | 4 ++-- test/LeanCode.IntegrationTests/docker/Dockerfile.watch | 6 +++--- test/LeanCode.IntegrationTests/docker/docker-compose.yml | 2 +- 8 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 nuget.config diff --git a/.github/workflows/core_build_publish.yml b/.github/workflows/core_build_publish.yml index 2edc12d2..444b7de5 100644 --- a/.github/workflows/core_build_publish.yml +++ b/.github/workflows/core_build_publish.yml @@ -121,7 +121,7 @@ jobs: ports: - "1433:1433" postgres: - image: postgres:15 + image: postgres:18 env: POSTGRES_PASSWORD: Passw12# ports: diff --git a/Directory.Packages.props b/Directory.Packages.props index 11ffcc30..bef72ef1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ true - 11.0.0-rc.1.26425.128 + 11.0.0-rc.1.26431.118 11.0.0-rc.1.26425.128 11.0.0-rc.1.26425.128 @@ -110,7 +110,7 @@ - + diff --git a/README.md b/README.md index ef906507..d86645bf 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,9 @@ It has a `docker` folder that contains necessary configuration. You can run the ```sh # For running tests with SQL Server -$ DB=sqlserver docker-compose run test +$ DB=sqlserver docker compose run test # For running tests with PostgreSQL -$ DB=postgres docker-compose run test +$ DB=postgres docker compose run test ``` ### Publishing diff --git a/cloud-tests-infrastructure/postgres.tf b/cloud-tests-infrastructure/postgres.tf index 6db5a19f..b9a2694a 100644 --- a/cloud-tests-infrastructure/postgres.tf +++ b/cloud-tests-infrastructure/postgres.tf @@ -3,7 +3,7 @@ resource "azurerm_postgresql_flexible_server" "server" { location = var.location name = var.postgres_server_name - version = "14" + version = "18" sku_name = "B_Standard_B1ms" storage_mb = "32768" diff --git a/nuget.config b/nuget.config new file mode 100644 index 00000000..8d6cc99c --- /dev/null +++ b/nuget.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/LeanCode.IntegrationTests/docker/Dockerfile b/test/LeanCode.IntegrationTests/docker/Dockerfile index 6b7a54a4..7d937019 100644 --- a/test/LeanCode.IntegrationTests/docker/Dockerfile +++ b/test/LeanCode.IntegrationTests/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 +FROM mcr.microsoft.com/dotnet/sdk:11.0 WORKDIR /app/code/test/LeanCode.IntegrationTests -ENTRYPOINT dotnet run -- --explicit on +ENTRYPOINT ["dotnet", "run", "--", "--explicit", "on"] diff --git a/test/LeanCode.IntegrationTests/docker/Dockerfile.watch b/test/LeanCode.IntegrationTests/docker/Dockerfile.watch index c8c3dfed..b3892bea 100644 --- a/test/LeanCode.IntegrationTests/docker/Dockerfile.watch +++ b/test/LeanCode.IntegrationTests/docker/Dockerfile.watch @@ -1,5 +1,5 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 +FROM mcr.microsoft.com/dotnet/sdk:11.0 -# Wait for SQL Server first WORKDIR /app/code/test/LeanCode.IntegrationTests -ENTRYPOINT dotnet restore && dotnet watch test -- --explicit on +ENTRYPOINT dotnet restore && \ + dotnet watch test -- --explicit on diff --git a/test/LeanCode.IntegrationTests/docker/docker-compose.yml b/test/LeanCode.IntegrationTests/docker/docker-compose.yml index 9ad141e0..ae9ab844 100755 --- a/test/LeanCode.IntegrationTests/docker/docker-compose.yml +++ b/test/LeanCode.IntegrationTests/docker/docker-compose.yml @@ -41,7 +41,7 @@ services: - "1433:1433" postgres: - image: postgres:17 + image: postgres:18 environment: POSTGRES_PASSWORD: Passw12# ports: From 56fc2faed39dd356227f436d686441716425d31d Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Tue, 15 Sep 2026 15:07:09 +0200 Subject: [PATCH 07/11] Update Extensions version to satisfy EFCore requirements, add package source mapping --- Directory.Packages.props | 2 +- nuget.config | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bef72ef1..217437c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ 11.0.0-rc.1.26431.118 11.0.0-rc.1.26425.128 - 11.0.0-rc.1.26425.128 + 11.0.0-rc.1.26431.118 8.5.10 diff --git a/nuget.config b/nuget.config index 8d6cc99c..345b4b2f 100644 --- a/nuget.config +++ b/nuget.config @@ -1,6 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 5553f3df573805104b506dc00e925a6f4a9cee69 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 17:28:32 +0200 Subject: [PATCH 08/11] Skip Postgres-only integration tests more automagically --- .github/workflows/core_build_publish.yml | 2 +- .../PostgresOnlyFactAttribute.cs | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/core_build_publish.yml b/.github/workflows/core_build_publish.yml index 444b7de5..786572ff 100644 --- a/.github/workflows/core_build_publish.yml +++ b/.github/workflows/core_build_publish.yml @@ -152,7 +152,7 @@ jobs: CORELIB_TESTS_AZURE_BLOB_STORAGE_CONTAINER_NAME: ${{ secrets.CORELIB_TESTS_AZURE_BLOB_STORAGE_CONTAINER_NAME }} CORELIB_TESTS_AZURE_TABLE_STORAGE_TABLE_NAME: ${{ secrets.CORELIB_TESTS_AZURE_TABLE_STORAGE_TABLE_NAME }} - name: Integration Test - SQL Server - run: dotnet test --no-build --configuration Release -- --explicit on --filter-query '/[(category=integration)&(database!=postgres)]' + run: dotnet test --no-build --configuration Release -- --explicit on --filter-trait 'category=integration' env: SqlServer__ConnectionStringBase: Server=localhost,1433;User Id=sa;Password=Passw12#;Encrypt=false LeanCodeIntegrationTests__Database: sqlserver diff --git a/test/LeanCode.IntegrationTests/PostgresOnlyFactAttribute.cs b/test/LeanCode.IntegrationTests/PostgresOnlyFactAttribute.cs index 4d3da9f2..727aa56e 100644 --- a/test/LeanCode.IntegrationTests/PostgresOnlyFactAttribute.cs +++ b/test/LeanCode.IntegrationTests/PostgresOnlyFactAttribute.cs @@ -3,11 +3,21 @@ namespace LeanCode.IntegrationTests; -public sealed class PostgresOnlyFactAttribute( - [CallerFilePath] string? sourceFilePath = null, - [CallerLineNumber] int sourceLineNumber = 0 -) : IntegrationFactAttribute(sourceFilePath, sourceLineNumber) +public sealed class PostgresOnlyFactAttribute : IntegrationFactAttribute { + public static bool IsPostgres => Environment.GetEnvironmentVariable(TestDatabaseConfig.ConfigEnvName) == "postgres"; + + public PostgresOnlyFactAttribute( + [CallerFilePath] string? sourceFilePath = null, + [CallerLineNumber] int sourceLineNumber = 0 + ) + : base(sourceFilePath, sourceLineNumber) + { + Skip = "Test requires PostgreSQL."; + SkipType = typeof(PostgresOnlyFactAttribute); + SkipUnless = nameof(IsPostgres); + } + public override IReadOnlyCollection> GetTraits() => base.GetTraits().Append(new("database", "postgres")).ToList(); } From 102a91642ae81164527a40745ac167d2e23ca6f5 Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 17:31:37 +0200 Subject: [PATCH 09/11] Work around Npgsql 11 retaining the CLR type in DateTimeOffset.UtcDateTime --- .../TimestampTzExpressionInterceptor.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Domain/LeanCode.DomainModels.EF/TimestampTzExpressionInterceptor.cs b/src/Domain/LeanCode.DomainModels.EF/TimestampTzExpressionInterceptor.cs index e555010c..e1f33892 100644 --- a/src/Domain/LeanCode.DomainModels.EF/TimestampTzExpressionInterceptor.cs +++ b/src/Domain/LeanCode.DomainModels.EF/TimestampTzExpressionInterceptor.cs @@ -9,6 +9,7 @@ public sealed class TimestampTzExpressionRewriter : ExpressionVisitor { private static readonly Type TimestampTzType = typeof(TimestampTz); private static readonly Type TimeZoneInfoType = typeof(TimeZoneInfo); + private static readonly Type DateTimeType = typeof(DateTime); private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset); public static readonly PropertyInfo LocalTimestampWithoutOffsetProperty = @@ -27,6 +28,10 @@ public sealed class TimestampTzExpressionRewriter : ExpressionVisitor DateTimeOffsetType.GetProperty(nameof(DateTimeOffset.UtcDateTime)) ?? throw new MissingMemberException(DateTimeOffsetType.FullName, nameof(DateTimeOffset.UtcDateTime)); + public static readonly MethodInfo ToUniversalTimeMethod = + DateTimeType.GetMethod(nameof(DateTime.ToUniversalTime), Type.EmptyTypes) + ?? throw new MissingMemberException(DateTimeType.FullName, nameof(DateTime.ToUniversalTime)); + public static readonly MethodInfo ConvertDateTimeBySystemTimeZoneIdMethod = TimeZoneInfoType.GetMethod( nameof(TimeZoneInfo.ConvertTimeBySystemTimeZoneId), @@ -44,7 +49,15 @@ protected override Expression VisitMember(MemberExpression node) return Expression.Call( null, ConvertDateTimeBySystemTimeZoneIdMethod, - Expression.Property(Expression.Property(node.Expression, UtcTimestampProperty), UtcDateTimeProperty), + // Npgsql 11 translates UtcDateTime as a no-op which retains DateTimeOffset as the SQL + // expression's CLR type. ToUniversalTime retags it as DateTime, which the outer translator requires. + Expression.Call( + Expression.Property( + Expression.Property(node.Expression, UtcTimestampProperty), + UtcDateTimeProperty + ), + ToUniversalTimeMethod + ), Expression.Property(node.Expression, TimeZoneIdProperty) ); } From e32b33866915b994c52ac995b8a0a7278115abbf Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Mon, 14 Sep 2026 18:29:12 +0200 Subject: [PATCH 10/11] Isolate the artifacts from integration tests executed in Docker And because the output is now isolated, hijack the ContentRoot early enough to prevent the test host from complaining. --- test/Directory.Build.props | 6 +++++- test/LeanCode.IntegrationTests/TestApp.cs | 2 +- test/LeanCode.IntegrationTests/docker/docker-compose.yml | 6 ++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 847349a7..5ebd9a02 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -13,9 +13,13 @@ true true + $(ArtifactsPath)/test-results + $(MSBuildThisFileDirectory)/../TestResults - $(TestingPlatformCommandLineArguments) --report-xunit-ctrf --coverage --ignore-exit-code 8 --results-directory $(MSBuildThisFileDirectory)/../TestResults --coverage-output-format cobertura --coverage-settings $(MSBuildThisFileDirectory)/coverage.xml --report-xunit-ctrf-filename $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).json --coverage-output $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).xml + $(TestingPlatformCommandLineArguments) --report-xunit-ctrf --coverage --ignore-exit-code 8 --results-directory $(LeanCodeTestResultsDirectory) --coverage-output-format cobertura --coverage-settings $(MSBuildThisFileDirectory)/coverage.xml --report-xunit-ctrf-filename $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).json --coverage-output $(MSBuildProjectName)-$([System.DateTime]::Now.ToString("HH-mm-ss")).xml diff --git a/test/LeanCode.IntegrationTests/TestApp.cs b/test/LeanCode.IntegrationTests/TestApp.cs index 87e649e9..1a91b385 100644 --- a/test/LeanCode.IntegrationTests/TestApp.cs +++ b/test/LeanCode.IntegrationTests/TestApp.cs @@ -37,7 +37,7 @@ protected override IEnumerable GetTestAssemblies() protected override void ConfigureWebHost(IWebHostBuilder builder) { base.ConfigureWebHost(builder); - builder.UseSolutionRelativeContentRoot("test/LeanCode.IntegrationTests"); + builder.UseContentRoot(AppContext.BaseDirectory); } protected override IHostBuilder CreateHostBuilder() diff --git a/test/LeanCode.IntegrationTests/docker/docker-compose.yml b/test/LeanCode.IntegrationTests/docker/docker-compose.yml index ae9ab844..04811d40 100755 --- a/test/LeanCode.IntegrationTests/docker/docker-compose.yml +++ b/test/LeanCode.IntegrationTests/docker/docker-compose.yml @@ -9,6 +9,9 @@ services: - ../../..:/app/code - ~/.nuget:/home/dotnet/.nuget environment: + UseArtifactsOutput: "true" + ArtifactsPath: /tmp/corelibrary-artifacts + ASPNETCORE_TEST_CONTENTROOT_LEANCODE_INTEGRATIONTESTS: /app/code/test/LeanCode.IntegrationTests LeanCodeIntegrationTests__Database: ${DB:?} SqlServer__ConnectionStringBase: Server=sqlserver;User Id=sa;Password=Passw12#;Encrypt=false Postgres__ConnectionStringBase: Host=postgres;Username=postgres;Password=Passw12#;SSL Mode=Disable @@ -24,6 +27,9 @@ services: - ../../..:/app/code - ~/.nuget:/home/dotnet/.nuget environment: + UseArtifactsOutput: "true" + ArtifactsPath: /tmp/corelibrary-artifacts + ASPNETCORE_TEST_CONTENTROOT_LEANCODE_INTEGRATIONTESTS: /app/code/test/LeanCode.IntegrationTests LeanCodeIntegrationTests__Database: ${DB:?} SqlServer__ConnectionStringBase: Server=sqlserver;User Id=sa;Password=Passw12#;Encrypt=false Postgres__ConnectionStringBase: Host=postgres;Username=postgres;Password=Passw12#;SSL Mode=Disable From f89807dbb472a3b32fae7c85ada8b277bacb32dc Mon Sep 17 00:00:00 2001 From: Krzysztof Bogacki Date: Tue, 15 Sep 2026 15:53:34 +0200 Subject: [PATCH 11/11] Address some non-codestyle diagnostics reported by analyzers --- .../DbContextOptionsBuilderExtensions.cs | 2 +- .../LeanCode.Dapper/RawQueryValidator.cs | 12 +++++++++--- .../ServiceToServiceAuthenticationHandler.cs | 6 +++++- .../LeanCode.DomainModels.Tests/Ids/GuidTests.cs | 2 +- .../LeanCode.DomainModels.Tests/Ids/IntTests.cs | 2 +- .../LeanCode.DomainModels.Tests/Ids/LongTests.cs | 2 +- .../Ids/PrefixedGuidTests.cs | 2 +- .../Ids/PrefixedStringTests.cs | 2 +- .../Ids/RawStringTests.cs | 2 +- 9 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Domain/LeanCode.DomainModels.EF/DbContextOptionsBuilderExtensions.cs b/src/Domain/LeanCode.DomainModels.EF/DbContextOptionsBuilderExtensions.cs index 753b8c97..071b71c7 100644 --- a/src/Domain/LeanCode.DomainModels.EF/DbContextOptionsBuilderExtensions.cs +++ b/src/Domain/LeanCode.DomainModels.EF/DbContextOptionsBuilderExtensions.cs @@ -35,7 +35,7 @@ public static DbContextOptionsBuilder AddSqlServerTypedIdMappingPlugins(this DbC } } -internal class TypedIdDbContextOptionsExtension : IDbContextOptionsExtension +internal sealed class TypedIdDbContextOptionsExtension : IDbContextOptionsExtension { private readonly ITypedIdStoreTypeProvider storeTypeProvider; diff --git a/src/Infrastructure/LeanCode.Dapper/RawQueryValidator.cs b/src/Infrastructure/LeanCode.Dapper/RawQueryValidator.cs index e8b646e6..cdf526a4 100644 --- a/src/Infrastructure/LeanCode.Dapper/RawQueryValidator.cs +++ b/src/Infrastructure/LeanCode.Dapper/RawQueryValidator.cs @@ -6,7 +6,7 @@ namespace LeanCode.Dapper; // Inspired by QueryValidator.Fody // https://github.com/kamil-mrzyglod/QueryValidator.Fody/blob/master/QueryValidator.Fody/QueryValidator.Fody/ModuleWeaver.cs -public static class RawQueryValidator +public static partial class RawQueryValidator { [System.Diagnostics.CodeAnalysis.SuppressMessage("?", "CA2100", Justification = "It does not. :)")] [System.Diagnostics.CodeAnalysis.SuppressMessage("?", "CA2201", Justification = "Doesn't need to be specific.")] @@ -29,8 +29,8 @@ public static async Task ValidateQueriesAsync(string connectionString, params As var sqlText = (string?)sql.GetValue(null) ?? ""; // https://xkcd.com/208/ - sqlText = Regex.Replace(sqlText, "IN @[a-zA-Z]{0,}", "IN (1)", RegexOptions.Compiled); - sqlText = Regex.Replace(sqlText, "(?i)(?), new TestGuidId()); + Assert.IsType(typeof(IRawTypedId), new TestGuidId(), exactMatch: false); } [Fact] diff --git a/test/Domain/LeanCode.DomainModels.Tests/Ids/IntTests.cs b/test/Domain/LeanCode.DomainModels.Tests/Ids/IntTests.cs index f3f7e9ca..a4504b43 100644 --- a/test/Domain/LeanCode.DomainModels.Tests/Ids/IntTests.cs +++ b/test/Domain/LeanCode.DomainModels.Tests/Ids/IntTests.cs @@ -15,7 +15,7 @@ public class IntIdTests [SuppressMessage("?", "xUnit2007", Justification = "Cannot use `IRawTypedId` as generic parameter.")] public void Generated_class_implements_ITypedId() { - Assert.IsAssignableFrom(typeof(IRawTypedId), new TestIntId()); + Assert.IsType(typeof(IRawTypedId), new TestIntId(), exactMatch: false); } [Fact] diff --git a/test/Domain/LeanCode.DomainModels.Tests/Ids/LongTests.cs b/test/Domain/LeanCode.DomainModels.Tests/Ids/LongTests.cs index 531c8a74..f55e4ab5 100644 --- a/test/Domain/LeanCode.DomainModels.Tests/Ids/LongTests.cs +++ b/test/Domain/LeanCode.DomainModels.Tests/Ids/LongTests.cs @@ -15,7 +15,7 @@ public class LongIdTests [SuppressMessage("?", "xUnit2007", Justification = "Cannot use `IRawTypedId` as generic parameter.")] public void Generated_class_implements_ITypedId() { - Assert.IsAssignableFrom(typeof(IRawTypedId), new TestLongId()); + Assert.IsType(typeof(IRawTypedId), new TestLongId(), exactMatch: false); } [Fact] diff --git a/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedGuidTests.cs b/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedGuidTests.cs index 656e90dc..6038d830 100644 --- a/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedGuidTests.cs +++ b/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedGuidTests.cs @@ -22,7 +22,7 @@ public class PrefixedGuidIdTests [SuppressMessage("?", "xUnit2007", Justification = "Cannot use `IPrefixedTypedId` as generic parameter.")] public void Generated_class_implements_ITypedId() { - Assert.IsAssignableFrom(typeof(IPrefixedTypedId), new TestPrefixedGuidId()); + Assert.IsType(typeof(IPrefixedTypedId), new TestPrefixedGuidId(), exactMatch: false); } [Fact] diff --git a/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedStringTests.cs b/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedStringTests.cs index d2d135c9..cf495d87 100644 --- a/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedStringTests.cs +++ b/test/Domain/LeanCode.DomainModels.Tests/Ids/PrefixedStringTests.cs @@ -21,7 +21,7 @@ public class PrefixedStringIdTests [SuppressMessage("?", "xUnit2007", Justification = "Cannot use `IPrefixedTypedId` as generic parameter.")] public void Generated_class_implements_ITypedId() { - Assert.IsAssignableFrom(typeof(IPrefixedTypedId), new TestPrefixedStringId()); + Assert.IsType(typeof(IPrefixedTypedId), new TestPrefixedStringId(), exactMatch: false); } [Fact] diff --git a/test/Domain/LeanCode.DomainModels.Tests/Ids/RawStringTests.cs b/test/Domain/LeanCode.DomainModels.Tests/Ids/RawStringTests.cs index 60e5f790..ec07464f 100644 --- a/test/Domain/LeanCode.DomainModels.Tests/Ids/RawStringTests.cs +++ b/test/Domain/LeanCode.DomainModels.Tests/Ids/RawStringTests.cs @@ -20,7 +20,7 @@ public class RawStringIdTests [SuppressMessage("?", "xUnit2007", Justification = "Cannot use `IRawStringTypedId` as generic parameter.")] public void Generated_class_implements_ITypedId() { - Assert.IsAssignableFrom(typeof(IRawStringTypedId), new TestRawStringId()); + Assert.IsType(typeof(IRawStringTypedId), new TestRawStringId(), exactMatch: false); } [Fact]