From febce6c14bd1db333506bda1b6e0a05f9836ce55 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 1 Dec 2025 08:22:12 +0000 Subject: [PATCH 01/30] feat: update target framework on projects to dotnet 10 --- .../Monaco.Template.Backend.Api.csproj | 2 +- .../Monaco.Template.Backend.Application.Tests.csproj | 2 +- .../Monaco.Template.Backend.Application.csproj | 2 +- .../Monaco.Template.Backend.ArchitectureTests.csproj | 2 +- .../Monaco.Template.Backend.Common.Api.Application.csproj | 2 +- .../Monaco.Template.Backend.Common.Api.csproj | 2 +- .../Monaco.Template.Backend.Common.ApiGateway.csproj | 2 +- .../Monaco.Template.Backend.Common.Application.csproj | 2 +- .../Monaco.Template.Backend.Common.BlobStorage.Tests.csproj | 2 +- .../Monaco.Template.Backend.Common.BlobStorage.csproj | 2 +- .../Monaco.Template.Backend.Common.Domain.Tests.csproj | 2 +- .../Monaco.Template.Backend.Common.Domain.csproj | 2 +- .../Monaco.Template.Backend.Common.Infrastructure.csproj | 2 +- .../Monaco.Template.Backend.Common.Serilog.csproj | 2 +- .../Monaco.Template.Backend.Common.Tests.csproj | 2 +- .../Monaco.Template.Backend.Domain.Tests.csproj | 2 +- .../Monaco.Template.Backend.Domain.csproj | 2 +- .../Monaco.Template.Backend.IntegrationTests.csproj | 2 +- .../Monaco.Template.Backend.Messages.csproj | 2 +- .../Monaco.Template.Backend.Worker.csproj | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj index 41c418f..a0ba7f1 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable 8ac1d4e3-61ef-452f-a386-ff3ec448fbff diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj index cb89c57..a7b6022 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj index c9a3fe9..5bb0761 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj index 3005b29..358153c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj index b497512..722ee9c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj index 3db4d2c..1a95d00 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj index 8684779..e3b8be5 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable 4c76f225-faad-42ec-801b-9ad3b505b7f5 diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj index fa669d4..395e1d9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj index 27e3045..cd56412 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj index a4f7597..1f73b24 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj index 85f4ba9..84bd1d9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj index 1287484..df5917f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj index f7175a6..4db5036 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj index ee96252..4976643 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj index 770dc5d..78a8bd7 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj index 3a9c523..aa5e67e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj index bf65174..b82d651 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj index b3909af..bce0699 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj index ae3ab6a..204bfc9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj index 90ce0e8..91f1227 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable 8783ba16-eb5c-4c28-ae1d-14de7638b5c1 From 1d0c713d9e46c03eb232e074b9308fa76b45c94e Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 1 Dec 2025 08:22:36 +0000 Subject: [PATCH 02/30] feat: update release pipeline to use dotnet-version 10.x --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09f3f01..ac1e525 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release to NuGet on: release: types: [published] - + jobs: build: runs-on: ubuntu-latest @@ -14,7 +14,7 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.x + dotnet-version: 10.x - name: Install Mono run: | sudo apt-get update From 04f382758353b8bb1b10fbabb4bd37138331d430 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 1 Dec 2025 08:23:42 +0000 Subject: [PATCH 03/30] chore: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4759ca6..20f914b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Each of the different solution templates also provide some basic business compon ### Supported .NET version: -9.0 +10.0 ### Installation From fb3d167b62d56a379b83bedf5bc358147731e160 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 1 Dec 2025 16:39:34 +0000 Subject: [PATCH 04/30] fix: remove FromForm annotation on endpoint for IFormFile parameter --- .../Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs index 7e25b0a..c45c325 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs @@ -19,7 +19,7 @@ public static IEndpointRouteBuilder AddFiles(this IEndpointRouteBuilder builder, files.MapPost("", Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, - [FromForm] IFormFile file, + IFormFile file, HttpContext context) => sender.ExecuteCommandAsync(new CreateFile.Command(file.OpenReadStream(), file.FileName, From 3c06631ae3b5ef4a746ebff755b164f7615ef449 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 1 Dec 2025 22:19:05 +0000 Subject: [PATCH 05/30] chore: update packages and resolve breaking changes --- .../Backend/Solution/Directory.Packages.props | 154 +++++++++--------- .../Features/Company/GetCompanyByIdTests.cs | 2 +- .../Features/Country/GetCountryByIdTests.cs | 2 +- .../MinimalApi/MinimalApiExtensions.cs | 4 - .../Swagger/AuthorizeCheckOperationFilter.cs | 32 ++-- .../Swagger/ConfigureSwaggerExtensions.cs | 4 +- .../Swagger/SwaggerDefaultValues.cs | 16 +- .../DbContextMockExtensions.cs | 12 +- 8 files changed, 107 insertions(+), 119 deletions(-) diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index a6f913d..a8f7dec 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -1,79 +1,79 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs index 45a1a52..f540f85 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/GetCompanyByIdTests.cs @@ -37,7 +37,7 @@ public async Task GetExistingCompanyByIdSucceeds() [Fact(DisplayName = "Get non-existing company by Id fails")] public async Task GetNonExistingCompanyByIdFails() { - _dbContextMock.CreateAndSetupDbSetMock(CompanyFactory.CreateMany()); + _dbContextMock.CreateAndSetupDbSetMock(CompanyFactory.CreateMany().ToList()); var query = new GetCompanyById.Query(Guid.NewGuid()); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs index 58cbd8e..b195741 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Country/GetCountryByIdTests.cs @@ -37,7 +37,7 @@ public async Task GetExistingCountryByIdSucceeds() [Fact(DisplayName = "Get non-existing country by Id fails")] public async Task GetNonExistingCountryByIdFails() { - _dbContextMock.CreateAndSetupDbSetMock(CountryFactory.CreateMany()); + _dbContextMock.CreateAndSetupDbSetMock(CountryFactory.CreateMany().ToList()); var query = new GetCountryById.Query(Guid.NewGuid()); var sut = new GetCountryById.Handler(_dbContextMock.Object); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs index 7ed5bf7..f4dfaf4 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs @@ -42,7 +42,6 @@ public static RouteHandlerBuilder MapGet(this IEndpointRouteBuilder builder, string description) => builder.MapGet(pattern, handler) - .WithOpenApi() .WithName(name) .WithSummary(summary) .WithDescription(description); @@ -66,7 +65,6 @@ public static RouteHandlerBuilder MapPost(this IEndpointRouteBuilder builder, string description) => builder.MapPost(pattern, handler) - .WithOpenApi() .WithName(name) .WithSummary(summary) .WithDescription(description); @@ -90,7 +88,6 @@ public static RouteHandlerBuilder MapPut(this IEndpointRouteBuilder builder, string description) => builder.MapPut(pattern, handler) - .WithOpenApi() .WithName(name) .WithSummary(summary) .WithDescription(description); @@ -114,7 +111,6 @@ public static RouteHandlerBuilder MapDelete(this IEndpointRouteBuilder builder, string description) => builder.MapDelete(pattern, handler) - .WithOpenApi() .WithName(name) .WithSummary(summary) .WithDescription(description); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs index 1489023..104ef0f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; using System.Net; @@ -22,22 +22,18 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) .Any(m => m is IAllowAnonymous)) return; - if (!operation.Responses.ContainsKey(((int)HttpStatusCode.Unauthorized).ToString())) - operation.Responses.Add(((int)HttpStatusCode.Unauthorized).ToString(), - new OpenApiResponse { Description = HttpStatusCode.Unauthorized.ToString() }); - if (!operation.Responses.ContainsKey(((int)HttpStatusCode.Forbidden).ToString())) - operation.Responses.Add(((int)HttpStatusCode.Forbidden).ToString(), - new OpenApiResponse { Description = HttpStatusCode.Forbidden.ToString() }); - - var oAuthScheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Id = "oauth2", - Type = ReferenceType.SecurityScheme - } - }; - - operation.Security = [new OpenApiSecurityRequirement { [oAuthScheme] = new List { _audience } }]; + operation.Responses ??= []; + + var unauthorizedKey = ((int)HttpStatusCode.Unauthorized).ToString(); + if (!operation.Responses.ContainsKey(unauthorizedKey)) + operation.Responses.Add(unauthorizedKey, new OpenApiResponse { Description = HttpStatusCode.Unauthorized.ToString() }); + + var forbiddenKey = ((int)HttpStatusCode.Forbidden).ToString(); + if (!operation.Responses.ContainsKey(forbiddenKey)) + operation.Responses.Add(forbiddenKey, new OpenApiResponse { Description = HttpStatusCode.Forbidden.ToString() }); + + var oAuthScheme = new OpenApiSecuritySchemeReference("oauth2", context.Document); + + operation.Security = [new OpenApiSecurityRequirement { [oAuthScheme] = [_audience] }]; } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs index cfbdc20..97f0114 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Monaco.Template.Backend.Common.Api.Swagger; @@ -77,7 +77,7 @@ public static IServiceCollection ConfigureSwagger(this IServiceCollection servic if (authEndpoint is not null && tokenEndpoint is not null && apiName is not null && scopesList is not null) { - //Add security for authenticated APIs + // Add security for authenticated APIs options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme { diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs index de1befc..f443b9f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Monaco.Template.Backend.Common.Api.Swagger; @@ -15,7 +14,7 @@ namespace Monaco.Template.Backend.Common.Api.Swagger; public class SwaggerDefaultValues : IOperationFilter { /// - /// Applies the filter to the specified operation using the given context. + /// Applies the filter to the specified operation using the given context. /// /// The operation to apply the filter to. /// The current operation filter context. @@ -24,21 +23,18 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) var apiDescription = context.ApiDescription; operation.Deprecated |= apiDescription.IsDeprecated(); - if (operation.Parameters == null) + if (operation.Parameters == null || operation.Parameters.Count == 0) return; // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 foreach (var parameter in operation.Parameters) { - var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); + var description = apiDescription.ParameterDescriptions.FirstOrDefault(p => p.Name == parameter.Name); + if (description is null) + continue; parameter.Description ??= description.ModelMetadata?.Description; - - if (parameter.Schema.Default == null && description.DefaultValue != null) - parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString()); - - parameter.Required |= description.IsRequired; } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs index ad337ad..567e956 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs @@ -11,7 +11,7 @@ public static Mock SetupDbSetMock(this Mock { entity }.AsQueryable().BuildMockDbSet(); + var entityDbSetMock = new List { entity }.BuildMockDbSet(); dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); entityDbSetMock.Setup(x => x.FindAsync(new object[] { entity.Id }, It.IsAny())) .ReturnsAsync(entity); @@ -23,7 +23,7 @@ public static Mock CreateEntityMockAndSetupDbSetMock( where T : Entity { entityMock = new Mock(); - var entityDbSetMock = new List { entityMock.Object }.AsQueryable().BuildMockDbSet(); + var entityDbSetMock = new List { entityMock.Object }.BuildMockDbSet(); dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); entityDbSetMock.Setup(x => x.FindAsync(new object[] { It.IsAny() }, It.IsAny())) .ReturnsAsync(entityMock.Object); @@ -40,7 +40,7 @@ public static Mock CreateAndSetupDbSetMock(this Mock< where TDbContext : DbContext where T : Entity { - entityDbSetMock = new[] { entity }.AsQueryable().BuildMockDbSet(); + entityDbSetMock = new[] { entity }.BuildMockDbSet(); dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); entityDbSetMock.Setup(x => x.FindAsync(new object[] { It.IsAny() }, It.IsAny())) .ReturnsAsync(entity); @@ -53,17 +53,17 @@ public static Mock CreateAndSetupDbSetMock(this Mock< where T : Entity => dbContextMock.CreateAndSetupDbSetMock(entity, out _); - public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, IEnumerable entities, out Mock> entityDbSetMock) + public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, ICollection entities, out Mock> entityDbSetMock) where TDbContext : DbContext where T : Entity { - entityDbSetMock = entities.AsQueryable().BuildMockDbSet(); + entityDbSetMock = entities.BuildMockDbSet(); dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); return dbContextMock; } - public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, IEnumerable entities) + public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, ICollection entities) where TDbContext : DbContext where T : Entity => dbContextMock.CreateAndSetupDbSetMock(entities, out _); From ded88d2a115a171f757460523d44736f230282a9 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 1 Dec 2025 22:26:57 +0000 Subject: [PATCH 06/30] feat: update use of nameof with support for unbound generic types --- .../Queries/QueryPagedBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryPagedBase.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryPagedBase.cs index 970babf..9b6bfc2 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryPagedBase.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryPagedBase.cs @@ -5,14 +5,14 @@ namespace Monaco.Template.Backend.Common.Application.Queries; public abstract record QueryPagedBase(IEnumerable> QueryParams) : QueryBase?>(QueryParams) { - public virtual int Offset => QueryParams.FirstOrDefault(x => x.Key.Equals(nameof(Page.Pager.Offset), StringComparison.InvariantCultureIgnoreCase)) + public virtual int Offset => QueryParams.FirstOrDefault(x => x.Key.Equals(nameof(Page<>.Pager.Offset), StringComparison.InvariantCultureIgnoreCase)) .Value .Select(x => int.TryParse(x, out var y) ? y : 0) .Where(x => x >= 0) .DefaultIfEmpty(0) .FirstOrDefault(); - public virtual int Limit => QueryParams.FirstOrDefault(x => x.Key.Equals(nameof(Page.Pager.Limit), StringComparison.InvariantCultureIgnoreCase)) + public virtual int Limit => QueryParams.FirstOrDefault(x => x.Key.Equals(nameof(Page<>.Pager.Limit), StringComparison.InvariantCultureIgnoreCase)) .Value .Select(x => int.TryParse(x, out var y) ? y : 0) .Where(x => x is > 0 and <= 100) From 10ad0c45068e14102186dea6f7426211298bd272 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Wed, 3 Dec 2025 11:10:51 +0000 Subject: [PATCH 07/30] chore: formatting --- .../Persistence/AppDbContext.cs | 2 +- .../ProductEntityConfiguration.cs | 2 +- .../Services/FileService.cs | 2 +- .../Middleware/JwtClaimsMapperMiddleware.cs | 2 +- .../SerilogContextEnricherMiddleware.cs | 2 +- .../Program.cs | 2 +- .../Commands/Behaviors/BehaviorExtensions.cs | 16 ++++---- .../Commands/CommandResult.cs | 2 +- .../Queries/QueryBase.cs | 2 +- .../Validators/Contracts/INonInjectable.cs | 4 +- .../BlobStorageService.cs | 2 +- .../Model/AggregateRoot.cs | 2 +- .../Context/AuditTrail/AuditEntry.cs | 3 +- .../Context/BaseDbContext.cs | 14 +++---- .../Context/Extensions/FilterExtensions.cs | 12 +++--- .../Context/Extensions/MediatorExtension.cs | 2 +- .../Context/Extensions/PagingExtensions.cs | 2 +- .../Context/Extensions/SelectMapExtensions.cs | 2 +- .../Context/Extensions/SortingExtensions.cs | 37 +++++++++++-------- .../AuditEventTelemetryConverter.cs | 2 +- 20 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/AppDbContext.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/AppDbContext.cs index c6e6126..91c4c2d 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/AppDbContext.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/AppDbContext.cs @@ -25,7 +25,7 @@ public AppDbContext(DbContextOptions options, protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - + modelBuilder.AddTransactionalOutboxEntities(); } #endif diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs index 07bbcce..b070c86 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs @@ -39,7 +39,7 @@ public void Configure(EntityTypeBuilder builder) .WithMany() .OnDelete(DeleteBehavior.ClientCascade)) .HasIndex($"{nameof(Product.Pictures)}Id") - .IsUnique(); //Constraint for single usage of file + .IsUnique(); // Constraint for single usage of file builder.HasIndex(x => x.Title) .IsUnique(false); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs index 5f94e77..577d1d5 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Services/FileService.cs @@ -108,7 +108,7 @@ public async Task DownloadFileAsync(File item, CancellationToke $"{item.Name}{item.Extension}", item.ContentType); } - + public Task DeleteFileAsync(File file, CancellationToken cancellationToken) => file switch { diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/JwtClaimsMapperMiddleware.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/JwtClaimsMapperMiddleware.cs index 9b35ce4..3e2b9ad 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/JwtClaimsMapperMiddleware.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/JwtClaimsMapperMiddleware.cs @@ -17,7 +17,7 @@ public class JwtClaimsMapperMiddleware : IMiddleware private const string ScopeClaimType = "scope"; private const string NameClaimType = "name"; private const string RoleClaimType = "role"; - + public Task InvokeAsync(HttpContext context, RequestDelegate next) { if (context.GetEndpoint()?.Metadata.Any(x => x is JwtMapClaimsAttribute) ?? false) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/SerilogContextEnricherMiddleware.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/SerilogContextEnricherMiddleware.cs index 1b64d57..0ece545 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/SerilogContextEnricherMiddleware.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/SerilogContextEnricherMiddleware.cs @@ -8,7 +8,7 @@ public class SerilogContextEnricherMiddleware : IMiddleware { private const string UserIdType = "sub"; private const string UserNameType = "preferred_username"; - + public Task InvokeAsync(HttpContext context, RequestDelegate next) { var user = context.User; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Program.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Program.cs index 8601716..7bb89f6 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Program.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Program.cs @@ -11,7 +11,7 @@ // Add services to the container. var configuration = builder.Configuration; builder.Services - .AddAuthorization(cfg => Scopes.List.ForEach(s => cfg.AddPolicy(s, p => p.RequireScope(s)))) //Register all listed scopes as policies requiring the existance of such scope in User claims + .AddAuthorization(cfg => Scopes.List.ForEach(s => cfg.AddPolicy(s, p => p.RequireScope(s)))) // Register all listed scopes as policies requiring the existence of such scope in User claims .AddJwtBearerAuthentication(configuration["SSO:Authority"]!, configuration["SSO:Audience"]!, bool.Parse(configuration["SSO:RequireHttpsMetadata"] ?? "false")); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs index 36dc45c..abbc0b4 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs @@ -25,17 +25,17 @@ public static class BehaviorExtensions /// The updated with the registered validation behaviors. public static IServiceCollection RegisterCommandValidationBehaviors(this IServiceCollection services, Assembly assembly) { - //Gets the CommandBase derived classes + // Gets the CommandBase derived classes var commandBaseTypes = GetCommandBaseDerivedTypes(assembly); - //And adds the corresponding scoped behaviors for all the detected commands (for both existance and validation checks) + // And adds the corresponding scoped behaviors for all the detected commands (for both existence and validation checks) commandBaseTypes.ForEach(t => services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), typeof(CommandValidationExistsBehavior<>).MakeGenericType(t)) .AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), typeof(CommandValidationBehavior<>).MakeGenericType(t))); - //Gets the CommandBases derived classes + // Gets the CommandBases derived classes var commandBaseResultTypes = GetCommandBaseOfResultDerivedTypes(assembly); - //And adds the corresponding scoped behavior for all the detected commands (only for validation checks) + // And adds the corresponding scoped behavior for all the detected commands (only for validation checks) commandBaseResultTypes.ForEach(t => { var tResult = t.BaseType!.GenericTypeArguments.First(); @@ -58,15 +58,15 @@ public static IServiceCollection RegisterCommandValidationBehaviors(this IServic /// The updated with the registered behaviors. public static IServiceCollection RegisterCommandConcurrencyExceptionBehaviors(this IServiceCollection services, Assembly assembly) { - //Gets the CommandBase derived classes + // Gets the CommandBase derived classes var commandBaseTypes = GetCommandBaseDerivedTypes(assembly); - //And adds the corresponding scoped behaviors for all the detected commands + // And adds the corresponding scoped behaviors for all the detected commands commandBaseTypes.ForEach(t => services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), typeof(ConcurrencyExceptionBehavior<>).MakeGenericType(t))); - //Gets the CommandBases derived classes + // Gets the CommandBases derived classes var commandBaseResultTypes = GetCommandBaseOfResultDerivedTypes(assembly); - //And adds the corresponding scoped behavior for all the detected commands + // And adds the corresponding scoped behavior for all the detected commands commandBaseResultTypes.ForEach(t => { var tResult = t.BaseType!.GenericTypeArguments.First(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/CommandResult.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/CommandResult.cs index 4a78038..4376e59 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/CommandResult.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/CommandResult.cs @@ -63,7 +63,7 @@ public static CommandResult Success() => /// A with a "Not Found" status and an empty list of validation errors/>. public static CommandResult NotFound() => new(new(), true); - + /// /// Creates a instance representing a failed validation. /// diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs index 07ba36e..cef625f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/QueryBase.cs @@ -15,7 +15,7 @@ namespace Monaco.Template.Backend.Common.Application.Queries; public abstract record QueryBase(IEnumerable> QueryParams) : IRequest { private const string ExpandParam = "expand"; - + public virtual IEnumerable> QueryParams { get; } = QueryParams; public virtual string?[] Sort => [.. QueryParams.FirstOrDefault(x => x.Key == "sort").Value]; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Contracts/INonInjectable.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Contracts/INonInjectable.cs index 4c3901a..cd5906f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Contracts/INonInjectable.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Contracts/INonInjectable.cs @@ -1,5 +1,3 @@ namespace Monaco.Template.Backend.Common.Application.Validators.Contracts; -public interface INonInjectable -{ -} \ No newline at end of file +public interface INonInjectable; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/BlobStorageService.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/BlobStorageService.cs index ae2f4ec..57daa35 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/BlobStorageService.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/BlobStorageService.cs @@ -23,7 +23,7 @@ public async Task UploadFileAsync(Stream stream, string fileName, string c await blobClient.UploadAsync(stream, null, metadata, cancellationToken: cancellationToken); return id; } - + public async Task DownloadAsync(Guid fileName, string path = "", CancellationToken cancellationToken = default) { var blobName = GetBlobName(fileName.ToString(), path); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs index 0b3e93a..5e3cf67 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs @@ -19,7 +19,7 @@ protected AggregateRoot(Guid id) : base(id) private readonly List _domainEvents = []; /// - /// List that holds Domain Events for this entity instance + /// List that holds Domain Events for this entity instance /// public IReadOnlyList DomainEvents => _domainEvents; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/AuditTrail/AuditEntry.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/AuditTrail/AuditEntry.cs index bf5ed37..504bed2 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/AuditTrail/AuditEntry.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/AuditTrail/AuditEntry.cs @@ -39,8 +39,7 @@ public AuditEntry(EntityEntry entityEntry) .FindPrimaryKey()? .Properties .ToDictionary(p => p.Name, - p => _entityEntry.Property(p.Name).CurrentValue) ?? - new Dictionary(); + p => _entityEntry.Property(p.Name).CurrentValue) ?? []; private readonly Dictionary _propertiesValues; public IReadOnlyDictionary PropertiesValues => _propertiesValues; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/BaseDbContext.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/BaseDbContext.cs index 62c6eee..6531936 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/BaseDbContext.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/BaseDbContext.cs @@ -34,10 +34,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Ignore(); - //This will apply all configurations that inherit from IEntityTypeConfiguration and have a parameterless constructor + // This will apply all configurations that inherit from IEntityTypeConfiguration and have a parameterless constructor modelBuilder.ApplyConfigurationsFromAssembly(GetConfigurationsAssembly()); - //For the ones deriving from EntityTypeConfigurationBase, we process scan and apply them as follows: + // For the ones deriving from EntityTypeConfigurationBase, we process scan and apply them as follows: var derivedConfigsToRegister = GetConfigurationsAssembly().GetTypes() .Where(t => (t.BaseType?.IsGenericType ?? false) && t.BaseType?.GetGenericTypeDefinition() == typeof(EntityTypeConfigurationBase<>)) @@ -47,19 +47,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public virtual async Task SaveEntitiesAsync(CancellationToken cancellationToken) { - // Dispatch Domain Events collection. + // Dispatch Domain Events collection. // Choices: - // A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including + // A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including // side effects from the domain event handlers which are using the same DbContext with "InstancePerLifetimeScope" or "scoped" lifetime - // B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions. - // You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers. + // B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions. + // You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers. await Publisher.DispatchDomainEventsAsync(this); ResetReferentialEntitiesState(); var entries = GetEntriesForAudit(); - // After executing this line all the changes (from the Command Handler and Domain Event Handlers) + // After executing this line all the changes (from the Command Handler and Domain Event Handlers) // performed through the DbContext will be committed await base.SaveChangesAsync(cancellationToken); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs index 04851c2..7dc08a3 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs @@ -24,14 +24,14 @@ public static IQueryable ApplyFilter(this IQueryable source, { var (filterMapLower, filterList, predicate) = GetData(queryString, filterMap, defaultCondition); - foreach (var (key, values) in filterList) //and while looping through the list of valid ones to use + foreach (var (key, values) in filterList) // and while looping through the list of valid ones to use { - //generate the expression equivalent to that querystring with the mapping corresponding to the DB - var predicateKey = PredicateBuilder.New(false); //Declare a PredicateBuilder for the current key values + // generate the expression equivalent to that querystring with the mapping corresponding to the DB + var predicateKey = PredicateBuilder.New(false); // Declare a PredicateBuilder for the current key values predicateKey = values.Where(value => ValidateDataType(value, GetBodyExpression(filterMapLower[key]).Type)) - .Select(value => GetOperationExpression(key, filterMapLower[key], value)) //then generate the expression for each value - .Aggregate(predicateKey, (current, expr) => current.Or(expr)); //and chain them all with an OR operator - predicate = allConditions ? predicate.And(predicateKey) : predicate.Or(predicateKey); //then add the resulting expression to the more general predicate + .Select(value => GetOperationExpression(key, filterMapLower[key], value)) // then generate the expression for each value + .Aggregate(predicateKey, (current, expr) => current.Or(expr)); // and chain them all with an OR operator + predicate = allConditions ? predicate.And(predicateKey) : predicate.Or(predicateKey); // then add the resulting expression to the more general predicate } return source.Where(predicate); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs index 52869c7..08d340a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs @@ -24,7 +24,7 @@ public static async Task DispatchDomainEventsAsync(this IPublisher publisher, Db foreach (var domainEvent in domainEvents) await publisher.Publish(domainEvent); - //If event handlers produced more domain events, keep processing them until there's no more + // If event handlers produced more domain events, keep processing them until there's no more if (ctx.ChangeTracker .Entries() .Any(x => x.Entity diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs index dc72f74..d9a412c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs @@ -25,7 +25,7 @@ public static async Task> ToPageAsync(this IQueryable< limit, results.FirstOrDefault()?.TotalCount ?? 0); } - + public static async Task> ToPageAsync(this IQueryable query, int offset, int limit, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs index bf1596a..45bf31e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs @@ -45,7 +45,7 @@ await source.Select(selector) await source.Where(predicate) .Select(selector) .SingleOrDefaultAsync(cancellationToken); - + public static async Task FirstOrDefaultAsync(this IQueryable source, Expression> predicate, Expression> selector, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs index 628d189..23666c1 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs @@ -14,8 +14,9 @@ public static IQueryable ApplySort(this IQueryable source, var (sortMapLower, lstSort) = GetData(sortFields, defaultSortField, sortMap); var query = source.AsQueryable(); - foreach (var (key, value) in lstSort) //Loop through the fields and apply the sorting + foreach (var (key, value) in lstSort) // Loop through the fields and apply the sorting query = query.GetOrderedQuery(sortMapLower[key], value, key == lstSort.Keys.First()); + return query; } @@ -29,17 +30,18 @@ public static IEnumerable ApplySort(this IEnumerable source, var (sortMapLower, lstSort) = GetData(sortFields, defaultSortField, sortMap); var query = source.AsEnumerable(); - foreach (var (key, value) in lstSort) //Loop through the fields and apply the sorting + foreach (var (key, value) in lstSort) // Loop through the fields and apply the sorting query = query.GetOrderedQuery(sortMapLower[key], value, key == lstSort.Keys.First()); + return query; } public static (Dictionary>> sortMapLower, Dictionary lstSort) GetData(IEnumerable sortFields, string defaultSortField, Dictionary>> sortMap) { - //convert a Dictionary with Keys into lowercase to ease searching + // convert a Dictionary with Keys into lowercase to ease searching var sortMapLower = sortMap.ToDictionary(x => x.Key.ToLower(), x => x.Value); - //convert the list of fields to sort into a dictionary field/direction and filter out the non-existing ones + // convert the list of fields to sort into a dictionary field/direction and filter out the non-existing ones var lstSort = ProcessSortParam(sortFields, sortMapLower); if (lstSort.Count == 0) //if there's none remaining, load the default ones lstSort = ProcessSortParam([defaultSortField], sortMapLower); @@ -51,13 +53,14 @@ private static IOrderedQueryable GetOrderedQuery(this IQueryable source { var bodyExpression = (MemberExpression)(expression.Body.NodeType == ExpressionType.Convert ? ((UnaryExpression)expression.Body).Operand : expression.Body); var sortLambda = Expression.Lambda(bodyExpression, expression.Parameters); + Expression>> sortMethod = firstSort - ? ascending - ? () => source.OrderBy(k => null!) - : () => source.OrderByDescending(k => null!) - : ascending - ? () => ((IOrderedQueryable)source).ThenBy(k => null!) - : () => ((IOrderedQueryable)source).ThenByDescending(k => null!); + ? ascending + ? () => source.OrderBy(k => null!) + : () => source.OrderByDescending(k => null!) + : ascending + ? () => ((IOrderedQueryable)source).ThenBy(k => null!) + : () => ((IOrderedQueryable)source).ThenByDescending(k => null!); var methodCallExpression = (MethodCallExpression)sortMethod.Body; var method = methodCallExpression.Method.GetGenericMethodDefinition(); @@ -69,13 +72,15 @@ private static IOrderedEnumerable GetOrderedQuery(this IEnumerable sour { var bodyExpression = (MemberExpression)(expression.Body.NodeType == ExpressionType.Convert ? ((UnaryExpression)expression.Body).Operand : expression.Body); var sortLambda = Expression.Lambda(bodyExpression, expression.Parameters); + Expression>> sortMethod = firstSort - ? ascending - ? () => source.OrderBy(k => null!) - : () => source.OrderByDescending(k => null!) - : ascending - ? () => ((IOrderedEnumerable)source).ThenBy(k => null!) - : () => ((IOrderedEnumerable)source).ThenByDescending(k => null!); + ? ascending + ? () => source.OrderBy(k => null!) + : () => source.OrderByDescending(k => null!) + : ascending + ? () => ((IOrderedEnumerable)source).ThenBy(k => null!) + : () => ((IOrderedEnumerable)source).ThenByDescending(k => null!); + if (sortMethod.Body is not MethodCallExpression methodCallExpression) throw new Exception("oops"); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/ApplicationInsights/TelemetryConverters/AuditEventTelemetryConverter.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/ApplicationInsights/TelemetryConverters/AuditEventTelemetryConverter.cs index 6e07ea4..3d36a89 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/ApplicationInsights/TelemetryConverters/AuditEventTelemetryConverter.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/ApplicationInsights/TelemetryConverters/AuditEventTelemetryConverter.cs @@ -16,7 +16,7 @@ public override IEnumerable Convert(LogEvent logEvent, IFormatProvid { ArgumentNullException.ThrowIfNull(logEvent, nameof(logEvent)); - //For complying with S4456: + // For complying with S4456: return GetTelemetries(logEvent, formatProvider); } From feb2b7f42f9b5d78d8a5bfafcb311d66d3b334fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Sun, 7 Dec 2025 12:54:25 +0100 Subject: [PATCH 08/30] Applying C# 14 new features to code, such as extension blocks and other small refactors to update the current code with the most recent language features. --- .../DTOs/Extensions/CompanyExtensions.cs | 41 +- .../DTOs/Extensions/ProductExtensions.cs | 33 +- .../Extensions/EndpointsExtensions.cs | 30 +- .../ServiceCollectionExtensions.cs | 73 ++-- .../Company/Extensions/CompanyExtensions.cs | 79 ++-- .../Country/Extensions/CountryExtensions.cs | 13 +- .../File/Extensions/FileExtensions.cs | 33 +- .../Product/Extensions/ProductExtensions.cs | 103 ++--- .../ResiliencePipelinesExtensions.cs | 9 +- .../Extensions/ArchUnitExtensions.cs | 38 +- .../MediatorExtensions.cs | 278 ++++++------- .../Auth/AuthExtensions.cs | 70 ++-- .../MinimalApi/MinimalApiExtensions.cs | 172 ++++---- .../Commands/Behaviors/BehaviorExtensions.cs | 132 +++--- .../Behaviors/CommandValidationBehavior.cs | 8 +- .../Behaviors/ConcurrencyExceptionBehavior.cs | 8 +- .../Extensions/ClaimsPrincipalExtensions.cs | 73 ++-- .../Queries/Extensions/QueryExtensions.cs | 388 +++++++++--------- .../ResiliencePipelinesExtensions.cs | 25 +- .../Extensions/ValidatorsExtensions.cs | 195 +++++---- .../Extensions/ServiceCollectionExtensions.cs | 13 +- .../Model/Enumeration.cs | 3 +- .../Context/Extensions/FilterExtensions.cs | 102 ++--- .../Context/Extensions/MediatorExtension.cs | 43 +- .../Extensions/OperationsExtensions.cs | 60 ++- .../Context/Extensions/PagingExtensions.cs | 107 ++--- .../Context/Extensions/SelectMapExtensions.cs | 104 +++-- .../Context/Extensions/SortingExtensions.cs | 118 +++--- .../Extensions/EntityTypeBuilderExtensions.cs | 65 +-- .../SerilogExtensions.cs | 9 +- .../DbContextMockExtensions.cs | 105 +++-- .../Factories/FixtureExtensions.cs | 35 +- 32 files changed, 1308 insertions(+), 1257 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/CompanyExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/CompanyExtensions.cs index 399ef9d..a5e9da1 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/CompanyExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/CompanyExtensions.cs @@ -4,24 +4,27 @@ namespace Monaco.Template.Backend.Api.DTOs.Extensions; internal static class CompanyExtensions { - public static CreateCompany.Command MapCreateCommand(this CompanyCreateEditDto value) => - new(value.Name!, - value.Email!, - value.WebSiteUrl!, - value.Street, - value.City, - value.County, - value.PostCode, - value.CountryId); + extension(CompanyCreateEditDto value) + { + public CreateCompany.Command MapCreateCommand() => + new(value.Name!, + value.Email!, + value.WebSiteUrl!, + value.Street, + value.City, + value.County, + value.PostCode, + value.CountryId); - public static EditCompany.Command MapEditCommand(this CompanyCreateEditDto value, Guid id) => - new(id, - value.Name!, - value.Email!, - value.WebSiteUrl!, - value.Street, - value.City, - value.County, - value.PostCode, - value.CountryId); + public EditCompany.Command MapEditCommand(Guid id) => + new(id, + value.Name!, + value.Email!, + value.WebSiteUrl!, + value.Street, + value.City, + value.County, + value.PostCode, + value.CountryId); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs index 5d10d0d..ae1cf0a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/DTOs/Extensions/ProductExtensions.cs @@ -4,20 +4,23 @@ namespace Monaco.Template.Backend.Api.DTOs.Extensions; internal static class ProductExtensions { - public static CreateProduct.Command Map(this ProductCreateEditDto value) => - new(value.Title, - value.Description, - value.Price, - value.CompanyId, - value.Pictures, - value.DefaultPictureId); + extension(ProductCreateEditDto value) + { + public CreateProduct.Command Map() => + new(value.Title, + value.Description, + value.Price, + value.CompanyId, + value.Pictures, + value.DefaultPictureId); - public static EditProduct.Command Map(this ProductCreateEditDto value, Guid id) => - new(id, - value.Title, - value.Description, - value.Price, - value.CompanyId, - value.Pictures, - value.DefaultPictureId); + public EditProduct.Command Map(Guid id) => + new(id, + value.Title, + value.Description, + value.Price, + value.CompanyId, + value.Pictures, + value.DefaultPictureId); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Extensions/EndpointsExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Extensions/EndpointsExtensions.cs index 661c924..5464605 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Extensions/EndpointsExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Extensions/EndpointsExtensions.cs @@ -4,25 +4,27 @@ namespace Monaco.Template.Backend.Api.Endpoints.Extensions; internal static class EndpointsExtensions { - /// - /// Registers all Minimal API endpoints - /// - /// - /// - public static IEndpointRouteBuilder RegisterEndpoints(this IEndpointRouteBuilder builder) + extension(IEndpointRouteBuilder builder) { - var versionSet = builder.NewApiVersionSet() - .HasApiVersion(new ApiVersion(1)) - .Build(); + /// + /// Registers all Minimal API endpoints + /// + /// + public IEndpointRouteBuilder RegisterEndpoints() + { + var versionSet = builder.NewApiVersionSet() + .HasApiVersion(new ApiVersion(1)) + .Build(); - return builder.AddCompanies(versionSet) + return builder.AddCompanies(versionSet) #if (filesSupport) - .AddCountries(versionSet) - .AddFiles(versionSet) - .AddProducts(versionSet); + .AddCountries(versionSet) + .AddFiles(versionSet) + .AddProducts(versionSet); #else - .AddCountries(versionSet); + .AddCountries(versionSet); #endif + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs index 781e902..e05e822 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs @@ -19,43 +19,44 @@ namespace Monaco.Template.Backend.Application.DependencyInjection; public static class ServiceCollectionExtensions { - /// - /// Registers and configures all the services and dependencies of the Application - /// - /// - /// - /// - public static IServiceCollection ConfigureApplication(this IServiceCollection services, - Action options) + extension(IServiceCollection services) { - var optionsValue = new ApplicationOptions(); - options.Invoke(optionsValue); - services.AddResiliencePipelines() - .AddMediatR(config => config.RegisterServicesFromAssemblies(GetApplicationAssembly())) - .RegisterCommandConcurrencyExceptionBehaviors(GetApplicationAssembly()) - .RegisterCommandValidationBehaviors(GetApplicationAssembly()) - .AddValidatorsFromAssembly(GetApplicationAssembly(), - filter: filter => !filter.ValidatorType - .GetInterfaces() - .Contains(typeof(INonInjectable)) && - !filter.ValidatorType.IsAbstract, - includeInternalTypes: true) - .AddDbContext(opts => opts.UseSqlServer(optionsValue.EntityFramework.ConnectionString, - sqlOptions => sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(3), null)) - .UseLazyLoadingProxies() - .EnableSensitiveDataLogging(optionsValue.EntityFramework.EnableEfSensitiveLogging)) - .AddScoped(provider => provider.GetRequiredService()); - #if (filesSupport) - services.RegisterBlobStorageService(opts => - { - opts.ConnectionString = optionsValue.BlobStorage.ConnectionString; - opts.ContainerName = optionsValue.BlobStorage.ContainerName; - }) - .AddScoped(); - #endif + /// + /// Registers and configures all the services and dependencies of the Application + /// + /// + /// + public IServiceCollection ConfigureApplication(Action options) + { + var optionsValue = new ApplicationOptions(); + options.Invoke(optionsValue); + services.AddResiliencePipelines() + .AddMediatR(config => config.RegisterServicesFromAssemblies(GetApplicationAssembly())) + .RegisterCommandConcurrencyExceptionBehaviors(GetApplicationAssembly()) + .RegisterCommandValidationBehaviors(GetApplicationAssembly()) + .AddValidatorsFromAssembly(GetApplicationAssembly(), + filter: filter => !filter.ValidatorType + .GetInterfaces() + .Contains(typeof(INonInjectable)) && + !filter.ValidatorType.IsAbstract, + includeInternalTypes: true) + .AddDbContext(opts => opts.UseSqlServer(optionsValue.EntityFramework.ConnectionString, + sqlOptions => sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(3), null)) + .UseLazyLoadingProxies() + .EnableSensitiveDataLogging(optionsValue.EntityFramework.EnableEfSensitiveLogging)) + .AddScoped(provider => provider.GetRequiredService()); +#if (filesSupport) + services.RegisterBlobStorageService(opts => + { + opts.ConnectionString = optionsValue.BlobStorage.ConnectionString; + opts.ContainerName = optionsValue.BlobStorage.ContainerName; + }) + .AddScoped(); +#endif - return services; - } + return services; + } - private static Assembly GetApplicationAssembly() => Assembly.GetAssembly(typeof(ServiceCollectionExtensions))!; + private static Assembly GetApplicationAssembly() => Assembly.GetAssembly(typeof(ServiceCollectionExtensions))!; + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/Extensions/CompanyExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/Extensions/CompanyExtensions.cs index b9592cb..f9b02cb 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/Extensions/CompanyExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/Extensions/CompanyExtensions.cs @@ -7,43 +7,52 @@ namespace Monaco.Template.Backend.Application.Features.Company.Extensions; internal static class CompanyExtensions { - public static CompanyDto? Map(this Domain.Model.Entities.Company? value, bool expandCountry = false) => - value is null - ? null - : new(value.Id, - value.Name, - value.Email, - value.WebSiteUrl, - value.Address?.Street, - value.Address?.City, - value.Address?.County, - value.Address?.PostCode, - value.Address?.CountryId, - expandCountry ? value.Address?.Country.Map() : null); + extension(Domain.Model.Entities.Company? value) + { + public CompanyDto? Map(bool expandCountry = false) => + value is null + ? null + : new(value.Id, + value.Name, + value.Email, + value.WebSiteUrl, + value.Address?.Street, + value.Address?.City, + value.Address?.County, + value.Address?.PostCode, + value.Address?.CountryId, + expandCountry ? value.Address?.Country.Map() : null); + } - public static Domain.Model.Entities.Company Map(this CreateCompany.Command value, Domain.Model.Entities.Country? country) => - new(value.Name, - value.Email, - value.WebSiteUrl, - country is not null - ? new(value.Street, - value.City, - value.County, - value.PostCode, - country) - : null); + extension(CreateCompany.Command value) + { + public Domain.Model.Entities.Company Map(Domain.Model.Entities.Country? country) => + new(value.Name, + value.Email, + value.WebSiteUrl, + country is not null + ? new(value.Street, + value.City, + value.County, + value.PostCode, + country) + : null); + } - public static void Map(this EditCompany.Command value, Domain.Model.Entities.Company item, Domain.Model.Entities.Country? country) => - item.Update(value.Name, - value.Email, - value.WebSiteUrl, - country is not null - ? new(value.Street, - value.City, - value.County, - value.PostCode, - country) - : null); + extension(EditCompany.Command value) + { + public void Map(Domain.Model.Entities.Company item, Domain.Model.Entities.Country? country) => + item.Update(value.Name, + value.Email, + value.WebSiteUrl, + country is not null + ? new(value.Street, + value.City, + value.County, + value.PostCode, + country) + : null); + } public static Dictionary>> GetMappedFields() => new() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/Extensions/CountryExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/Extensions/CountryExtensions.cs index 49e7f0f..b689eb7 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/Extensions/CountryExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Country/Extensions/CountryExtensions.cs @@ -5,11 +5,14 @@ namespace Monaco.Template.Backend.Application.Features.Country.Extensions; public static class CountryExtensions { - public static CountryDto? Map(this Domain.Model.Entities.Country? value) => - value is null - ? null - : new(value.Id, - value.Name); + extension(Domain.Model.Entities.Country? value) + { + public CountryDto? Map() => + value is null + ? null + : new(value.Id, + value.Name); + } public static Dictionary>> GetMappedFields() => new() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs index 9462edd..71fb342 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/Extensions/FileExtensions.cs @@ -5,19 +5,22 @@ namespace Monaco.Template.Backend.Application.Features.File.Extensions; public static class FileExtensions { - public static ImageDto? Map(this Image? value) => - value is null - ? null - : new(value.Id, - value.Name, - value.Extension, - value.ContentType, - value.Size, - value.UploadedOn, - value.IsTemp, - value.DateTaken, - value.Dimensions.Width, - value.Dimensions.Height, - value.ThumbnailId, - value.ThumbnailId.HasValue ? value.Thumbnail.Map() : null); + extension(Image? value) + { + public ImageDto? Map() => + value is null + ? null + : new(value.Id, + value.Name, + value.Extension, + value.ContentType, + value.Size, + value.UploadedOn, + value.IsTemp, + value.DateTaken, + value.Dimensions.Width, + value.Dimensions.Height, + value.ThumbnailId, + value.ThumbnailId.HasValue ? value.Thumbnail.Map() : null); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductExtensions.cs index db3437d..9d286bf 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/Extensions/ProductExtensions.cs @@ -14,31 +14,61 @@ namespace Monaco.Template.Backend.Application.Features.Product.Extensions; public static class ProductExtensions { - public static ProductDto? Map(this Domain.Model.Entities.Product? value, - bool expandCompany = false, - bool expandPictures = false, - bool expandDefaultPicture = false) => - value is null - ? null - : new(value.Id, - value.Title, - value.Description, - value.Price, - value.CompanyId, - expandCompany - ? value.Company - .Map() - : null, - expandPictures - ? value.Pictures - .Select(x => x.Map()!) - .ToArray() - : null, - value.DefaultPictureId, - expandDefaultPicture - ? value.DefaultPicture - .Map() - : null); + extension(Domain.Model.Entities.Product? value) + { + public ProductDto? Map(bool expandCompany = false, + bool expandPictures = false, + bool expandDefaultPicture = false) => + value is null + ? null + : new(value.Id, + value.Title, + value.Description, + value.Price, + value.CompanyId, + expandCompany + ? value.Company + .Map() + : null, + expandPictures + ? value.Pictures + .Select(x => x.Map()!) + .ToArray() + : null, + value.DefaultPictureId, + expandDefaultPicture + ? value.DefaultPicture + .Map() + : null); + } +#if (massTransitIntegration) + + extension(Domain.Model.Entities.Product item) + { + internal ProductCreated MapMessage() => + new(item.Id, + item.Title, + item.Description, + item.Price, + item.CompanyId); + } +#endif + + extension(AppDbContext dbContext) + { + internal async Task<(Domain.Model.Entities.Company company , Image[] pics)> GetProductData(Guid companyId, + Guid[] pictures, + CancellationToken cancellationToken) + { + var company = await dbContext.Set() + .SingleAsync(x => x.Id == companyId, cancellationToken); + var pics = await dbContext.Set() + .Include(x => x.Thumbnail) + .Where(x => ((IEnumerable)pictures).Contains(x.Id)) + .ToArrayAsync(cancellationToken); + return (company, pics); + } + } public static Dictionary>> GetMappedFields() => new() @@ -51,27 +81,4 @@ value is null [$"{nameof(ProductDto.Company)}.{nameof(CompanyDto.Name)}"] = x => x.Company.Name, [nameof(ProductDto.DefaultPictureId)] = x => x.DefaultPictureId }; - - internal static async Task<(Domain.Model.Entities.Company company , Image[] pics)> GetProductData(this AppDbContext dbContext, - Guid companyId, - Guid[] pictures, - CancellationToken cancellationToken) - { - var company = await dbContext.Set() - .SingleAsync(x => x.Id == companyId, cancellationToken); - var pics = await dbContext.Set() - .Include(x => x.Thumbnail) - .Where(x => pictures.Contains(x.Id)) - .ToArrayAsync(cancellationToken); - return (company, pics); - } -#if (massTransitIntegration) - - internal static ProductCreated MapMessage(this Domain.Model.Entities.Product item) => - new(item.Id, - item.Title, - item.Description, - item.Price, - item.CompanyId); -#endif } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs index 6baa1a7..b4af343 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs @@ -5,7 +5,10 @@ namespace Monaco.Template.Backend.Application.ResiliencePipelines; public static class ResiliencePipelinesExtensions { - public static IServiceCollection AddResiliencePipelines(this IServiceCollection services) => - // Register additional pipelines chained below - CommonResiliencePipelinesExtensions.AddResiliencePipelines(services); + extension(IServiceCollection services) + { + public IServiceCollection AddResiliencePipelines() => + // Register additional pipelines chained below + CommonResiliencePipelinesExtensions.AddResiliencePipelines(services); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Extensions/ArchUnitExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Extensions/ArchUnitExtensions.cs index fb3ce28..949d2b0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Extensions/ArchUnitExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Extensions/ArchUnitExtensions.cs @@ -4,24 +4,28 @@ namespace Monaco.Template.Backend.ArchitectureTests.Extensions; public static class ArchUnitExtensions { - public static bool IsNestedWithin(this IType type, params IType[] types) => - types.Any(t => type.FullName.StartsWith($"{t.FullName}+")); + extension(IType type) + { + public bool IsNestedWithin(params IType[] types) => + types.Any(t => type.FullName.StartsWith($"{t.FullName}+")); - public static IType? NestType(this IType type, Architecture architecture) => - architecture.Types - .SingleOrDefault(t => type.IsNestedWithin(t)); + public IType? NestType(Architecture architecture) => + architecture.Types + .SingleOrDefault(t => type.IsNestedWithin(t)); + } - public static ClassesShouldConjunction HavePropertySetterWithVisibility(this ClassesShould should, - params Visibility[] visibility) => - should.FollowCustomCondition(c => c.GetPropertyMembers() - .Any(p => visibility.Contains(p.SetterVisibility)), - $"have properties setters with visibility {string.Join(", ", visibility)}", - $"does not have a property setter with visibility {string.Join(", ", visibility)}"); + extension(ClassesShould should) + { + public ClassesShouldConjunction HavePropertySetterWithVisibility(params Visibility[] visibility) => + should.FollowCustomCondition(c => c.GetPropertyMembers() + .Any(p => visibility.Contains(p.SetterVisibility)), + $"have properties setters with visibility {string.Join(", ", visibility)}", + $"does not have a property setter with visibility {string.Join(", ", visibility)}"); - public static ClassesShouldConjunction NotHavePropertySetterWithVisibility(this ClassesShould should, - params Visibility[] visibility) => - should.FollowCustomCondition(c => c.GetPropertyMembers() - .All(p => !visibility.Contains(p.SetterVisibility)), - $"not have properties setters with visibility {string.Join(", ", visibility)}", - $"has property setter with visibility {string.Join(", ", visibility)}"); + public ClassesShouldConjunction NotHavePropertySetterWithVisibility(params Visibility[] visibility) => + should.FollowCustomCondition(c => c.GetPropertyMembers() + .All(p => !visibility.Contains(p.SetterVisibility)), + $"not have properties setters with visibility {string.Join(", ", visibility)}", + $"has property setter with visibility {string.Join(", ", visibility)}"); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs index a45bd60..0dbbe74 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs @@ -10,162 +10,146 @@ namespace Monaco.Template.Backend.Common.Api.Application; public static class MediatorExtensions { - /// - /// Executes the query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the retuned result is null or not - /// - /// The type of the records returned by the query - /// - /// - /// - public static async Task, NotFound>> ExecuteQueryAsync(this ISender sender, - QueryBase query) + extension(ISender sender) { - var result = await sender.Send(query); - return result is null - ? TypedResults.NotFound() - : TypedResults.Ok(result); - } + /// + /// Executes the query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the retuned result is null or not + /// + /// The type of the records returned by the query + /// + /// + public async Task, NotFound>> ExecuteQueryAsync(QueryBase query) + { + var result = await sender.Send(query); + return result is null + ? TypedResults.NotFound() + : TypedResults.Ok(result); + } - /// - /// Executes the paged query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the returned result is null or not - /// - /// The type of the records contained in the page returned by the query - /// - /// - /// - public static async Task>, NotFound>> ExecuteQueryAsync(this ISender sender, - QueryPagedBase query) - { - var result = await sender.Send(query); - return result is null - ? TypedResults.NotFound() - : TypedResults.Ok(result); - } + /// + /// Executes the paged query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the returned result is null or not + /// + /// The type of the records contained in the page returned by the query + /// + /// + public async Task>, NotFound>> ExecuteQueryAsync(QueryPagedBase query) + { + var result = await sender.Send(query); + return result is null + ? TypedResults.NotFound() + : TypedResults.Ok(result); + } - /// - /// Executes the query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the returned item is null or not - /// - /// The type of the item returned by the query - /// - /// - /// - public static async Task, NotFound>> ExecuteQueryAsync(this ISender sender, - QueryByIdBase query) - { - var result = await sender.Send(query); - return result is null - ? TypedResults.NotFound() - : TypedResults.Ok(result); - } + /// + /// Executes the query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the returned item is null or not + /// + /// The type of the item returned by the query + /// + /// + public async Task, NotFound>> ExecuteQueryAsync(QueryByIdBase query) + { + var result = await sender.Send(query); + return result is null + ? TypedResults.NotFound() + : TypedResults.Ok(result); + } - /// - /// Executes the query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the returned item is null or not - /// - /// The type of the item returned by the query - /// The type of the key to search the item by - /// - /// - /// - public static async Task, NotFound>> ExecuteQueryAsync(this ISender sender, - QueryByKeyBase query) - { - var item = await sender.Send(query); - return item is null - ? TypedResults.NotFound() - : TypedResults.Ok(item); - } + /// + /// Executes the query passed and returns the corresponding response that can be either Ok(result) or a NotFound() result depending on whether the returned item is null or not + /// + /// The type of the item returned by the query + /// The type of the key to search the item by + /// + /// + public async Task, NotFound>> ExecuteQueryAsync(QueryByKeyBase query) + { + var item = await sender.Send(query); + return item is null + ? TypedResults.NotFound() + : TypedResults.Ok(item); + } - /// - /// Executes the query passed and returns a FileStreamResult for allowing download of a file or a NotFound() result depending on whether the returned item is null or not - /// - /// - /// - /// - /// - public static async Task> ExecuteFileDownloadAsync(this ISender sender, - QueryBase query) where TResult : FileDownloadDto - { - var item = await sender.Send(query); - return GetFileDownload(item); - } + /// + /// Executes the query passed and returns a FileStreamResult for allowing download of a file or a NotFound() result depending on whether the returned item is null or not + /// + /// + /// + /// + public async Task> ExecuteFileDownloadAsync(QueryBase query) where TResult : FileDownloadDto + { + var item = await sender.Send(query); + return GetFileDownload(item); + } - /// - /// Executes the query passed and returns a FileStreamResult for allowing download of a file or a NotFound() result depending on whether the returned item is null or not - /// - /// - /// - /// - /// - public static async Task> ExecuteFileDownloadAsync(this ISender sender, - QueryByIdBase query) where TResult : FileDownloadDto - { - var item = await sender.Send(query); - return GetFileDownload(item); - } + /// + /// Executes the query passed and returns a FileStreamResult for allowing download of a file or a NotFound() result depending on whether the returned item is null or not + /// + /// + /// + /// + public async Task> ExecuteFileDownloadAsync(QueryByIdBase query) where TResult : FileDownloadDto + { + var item = await sender.Send(query); + return GetFileDownload(item); + } + + /// + /// Executes the command passed and returns the corresponding response that can be either Created(result) or a NotFound() or a ValidationProblem() depending on the validations and processing + /// + /// The type of the result returned by the command + /// + /// The URI to include in the headers of the Created() response + /// The parameters (if any) to pass for concatenating into the resultUri + /// + public async Task, NotFound, ValidationProblem>> ExecuteCommandAsync(CommandBase command, + string resultUri, + params object[]? uriParams) + { + var result = await sender.Send(command); + return result switch + { + { ItemNotFound: true } => TypedResults.NotFound(), + { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), + _ => TypedResults.Created(string.Format(resultUri, + [.. uriParams ?? [], result.Result!]), + result.Result) + }; + } + /// + /// Executes the edit command passed and returns the corresponding response that can be either NoContent() or a NotFound() or a ValidationProblem() depending on the validations and processing + /// + /// + /// + public async Task> ExecuteCommandEditAsync(CommandBase command) + { + var result = await sender.Send(command); + return result switch + { + { ItemNotFound: true } => TypedResults.NotFound(), + { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), + _ => TypedResults.NoContent() + }; + } + + /// + /// Executes the delete command passed and returns the corresponding response that can be either Ok() or a NotFound() or a ValidationProblem() depending on the validations and processing + /// + /// + /// + public async Task> ExecuteCommandDeleteAsync(CommandBase command) + { + var result = await sender.Send(command); + return result switch + { + { ItemNotFound: true } => TypedResults.NotFound(), + { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), + _ => TypedResults.Ok() + }; + } + } private static Results GetFileDownload(TResult? item) where TResult : FileDownloadDto => item is null ? TypedResults.NotFound() : TypedResults.File(item.FileContent, item.ContentType, item.FileName); - - /// - /// Executes the command passed and returns the corresponding response that can be either Created(result) or a NotFound() or a ValidationProblem() depending on the validations and processing - /// - /// The type of the result returned by the command - /// - /// - /// The URI to include in the headers of the Created() response - /// The parameters (if any) to pass for concatenating into the resultUri - /// - public static async Task, NotFound, ValidationProblem>> ExecuteCommandAsync(this ISender sender, - CommandBase command, - string resultUri, - params object[]? uriParams) - { - var result = await sender.Send(command); - return result switch - { - { ItemNotFound: true } => TypedResults.NotFound(), - { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), - _ => TypedResults.Created(string.Format(resultUri, - [.. uriParams ?? [], result.Result!]), - result.Result) - }; - } - - /// - /// Executes the edit command passed and returns the corresponding response that can be either NoContent() or a NotFound() or a ValidationProblem() depending on the validations and processing - /// - /// - /// - /// - public static async Task> ExecuteCommandEditAsync(this ISender sender, - CommandBase command) - { - var result = await sender.Send(command); - return result switch - { - { ItemNotFound: true } => TypedResults.NotFound(), - { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), - _ => TypedResults.NoContent() - }; - } - - /// - /// Executes the delete command passed and returns the corresponding response that can be either Ok() or a NotFound() or a ValidationProblem() depending on the validations and processing - /// - /// - /// - /// - public static async Task> ExecuteCommandDeleteAsync(this ISender sender, - CommandBase command) - { - var result = await sender.Send(command); - return result switch - { - { ItemNotFound: true } => TypedResults.NotFound(), - { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), - _ => TypedResults.Ok() - }; - } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Auth/AuthExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Auth/AuthExtensions.cs index 7f1c57b..c5378a2 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Auth/AuthExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Auth/AuthExtensions.cs @@ -10,41 +10,45 @@ public static class AuthExtensions { public const string ScopeClaimType = "scope"; - public static IServiceCollection AddAuthorizationWithPolicies(this IServiceCollection services, List scopes) => - services.AddAuthorization(cfg => - { // DefaultPolicy will require at least authenticated user by default - cfg.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) - .RequireAuthenticatedUser().Build(); - // Register all listed scopes as policies requiring the existence of such scope in User claims - scopes.ForEach(s => cfg.AddPolicy(s, p => p.RequireScope(s))); - }); + extension(IServiceCollection services) + { + public IServiceCollection AddAuthorizationWithPolicies(List scopes) => + services.AddAuthorization(cfg => + { // DefaultPolicy will require at least authenticated user by default + cfg.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) + .RequireAuthenticatedUser().Build(); + // Register all listed scopes as policies requiring the existence of such scope in User claims + scopes.ForEach(s => cfg.AddPolicy(s, p => p.RequireScope(s))); + }); - public static AuthenticationBuilder AddJwtBearerAuthentication(this IServiceCollection services, - string authority, - string audience, - bool requireHttpsMetadata) => - services.AddTransient() // Add transformer to map scopes correctly in ClaimsPrincipal/Identity - .AddAuthentication() - .AddJwtBearer(options => // Configure validation settings for JWT bearer - { - options.Authority = authority; - options.Audience = audience; - options.RequireHttpsMetadata = requireHttpsMetadata; - options.TokenValidationParameters.NameClaimType = "name"; - options.TokenValidationParameters.RoleClaimType = "roles"; + public AuthenticationBuilder AddJwtBearerAuthentication(string authority, + string audience, + bool requireHttpsMetadata) => + services.AddTransient() // Add transformer to map scopes correctly in ClaimsPrincipal/Identity + .AddAuthentication() + .AddJwtBearer(options => // Configure validation settings for JWT bearer + { + options.Authority = authority; + options.Audience = audience; + options.RequireHttpsMetadata = requireHttpsMetadata; + options.TokenValidationParameters.NameClaimType = "name"; + options.TokenValidationParameters.RoleClaimType = "roles"; - options.TokenHandlers.Clear(); - options.TokenHandlers.Add(new JwtSecurityTokenHandler { MapInboundClaims = false }); + options.TokenHandlers.Clear(); + options.TokenHandlers.Add(new JwtSecurityTokenHandler { MapInboundClaims = false }); - options.TokenValidationParameters.ValidTypes = ["JWT"]; - }); + options.TokenValidationParameters.ValidTypes = ["JWT"]; + }); + } - /// - /// Requires claims of type "scope" with matching values - /// - /// - /// - /// - public static AuthorizationPolicyBuilder RequireScope(this AuthorizationPolicyBuilder builder, params string[] allowedValues) => - builder.RequireClaim(ScopeClaimType, (IEnumerable)allowedValues); + extension(AuthorizationPolicyBuilder builder) + { + /// + /// Requires claims of type "scope" with matching values + /// + /// + /// + public AuthorizationPolicyBuilder RequireScope(params string[] allowedValues) => + builder.RequireClaim(ScopeClaimType, (IEnumerable)allowedValues); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs index f4dfaf4..022654e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs @@ -7,111 +7,105 @@ namespace Monaco.Template.Backend.Common.Api.MinimalApi; public static class MinimalApiExtensions { - public static RouteGroupBuilder CreateApiGroupBuilder(this IEndpointRouteBuilder builder, - ApiVersionSet versionSet, - string collectionName, - int version = 1) => - builder.MapGroup(string.Concat("api/v{apiVersion:apiVersion}/", collectionName)) - .WithName(collectionName) - .WithDisplayName(collectionName) - .WithTags(collectionName) - .WithApiVersionSet(versionSet) + extension(IEndpointRouteBuilder builder) + { + public RouteGroupBuilder CreateApiGroupBuilder(ApiVersionSet versionSet, + string collectionName, + int version = 1) => + builder.MapGroup(string.Concat("api/v{apiVersion:apiVersion}/", collectionName)) + .WithName(collectionName) + .WithDisplayName(collectionName) + .WithTags(collectionName) + .WithApiVersionSet(versionSet) #if (!auth) - .HasApiVersion(version); + .HasApiVersion(version); #else .HasApiVersion(version) .RequireAuthorization(); #endif - public static RouteHandlerBuilder MapGet(this IEndpointRouteBuilder builder, - string pattern, - Delegate handler, - string name, - string summary) => - builder.MapGet(pattern, - handler, - name, - summary, - string.Empty); + public RouteHandlerBuilder MapGet(string pattern, + Delegate handler, + string name, + string summary) => + builder.MapGet(pattern, + handler, + name, + summary, + string.Empty); - public static RouteHandlerBuilder MapGet(this IEndpointRouteBuilder builder, - string pattern, - Delegate handler, - string name, - string summary, - string description) => - builder.MapGet(pattern, - handler) - .WithName(name) - .WithSummary(summary) - .WithDescription(description); + public RouteHandlerBuilder MapGet(string pattern, + Delegate handler, + string name, + string summary, + string description) => + builder.MapGet(pattern, + handler) + .WithName(name) + .WithSummary(summary) + .WithDescription(description); + + public RouteHandlerBuilder MapPost(string pattern, + Delegate handler, + string name, + string summary) => + builder.MapPost(pattern, + handler, + name, + summary, + string.Empty); + + public RouteHandlerBuilder MapPost(string pattern, + Delegate handler, + string name, + string summary, + string description) => + builder.MapPost(pattern, + handler) + .WithName(name) + .WithSummary(summary) + .WithDescription(description); - public static RouteHandlerBuilder MapPost(this IEndpointRouteBuilder builder, - string pattern, - Delegate handler, - string name, - string summary) => - builder.MapPost(pattern, - handler, - name, - summary, - string.Empty); + public RouteHandlerBuilder MapPut(string pattern, + Delegate handler, + string name, + string summary) => + builder.MapPut(pattern, + handler, + name, + summary, + string.Empty); - public static RouteHandlerBuilder MapPost(this IEndpointRouteBuilder builder, - string pattern, - Delegate handler, - string name, - string summary, - string description) => - builder.MapPost(pattern, - handler) - .WithName(name) - .WithSummary(summary) - .WithDescription(description); + public RouteHandlerBuilder MapPut(string pattern, + Delegate handler, + string name, + string summary, + string description) => + builder.MapPut(pattern, + handler) + .WithName(name) + .WithSummary(summary) + .WithDescription(description); - public static RouteHandlerBuilder MapPut(this IEndpointRouteBuilder builder, - string pattern, + public RouteHandlerBuilder MapDelete(string pattern, Delegate handler, string name, string summary) => - builder.MapPut(pattern, - handler, - name, - summary, - string.Empty); + builder.MapDelete(pattern, + handler, + name, + summary, + string.Empty); - public static RouteHandlerBuilder MapPut(this IEndpointRouteBuilder builder, - string pattern, + public RouteHandlerBuilder MapDelete(string pattern, Delegate handler, string name, string summary, string description) => - builder.MapPut(pattern, - handler) - .WithName(name) - .WithSummary(summary) - .WithDescription(description); - - public static RouteHandlerBuilder MapDelete(this IEndpointRouteBuilder builder, - string pattern, - Delegate handler, - string name, - string summary) => - builder.MapDelete(pattern, - handler, - name, - summary, - string.Empty); - - public static RouteHandlerBuilder MapDelete(this IEndpointRouteBuilder builder, - string pattern, - Delegate handler, - string name, - string summary, - string description) => - builder.MapDelete(pattern, - handler) - .WithName(name) - .WithSummary(summary) - .WithDescription(description); + builder.MapDelete(pattern, + handler) + .WithName(name) + .WithSummary(summary) + .WithDescription(description); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs index 36dc45c..1791868 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs @@ -10,82 +10,84 @@ public static class BehaviorExtensions private static readonly Type[]? CommandBaseDerivedTypes = null; private static readonly Type[]? CommandBaseOfResultDerivedTypes = null; - /// - /// Registers command validation behaviors for all command types derived from CommandBase or - /// CommandBase<T> found in the specified assembly. - /// - /// This method scans the provided assembly for types derived from CommandBase and - /// CommandBase<T>. For each detected command type: Scoped - /// instances of are registered for validation existence checks - /// using CommandValidationExistsBehavior. Scoped instances of are registered for validation logic using - /// CommandValidationBehavior. /// The to which the validation behaviors will be added. - /// The assembly to scan for command types. - /// The updated with the registered validation behaviors. - public static IServiceCollection RegisterCommandValidationBehaviors(this IServiceCollection services, Assembly assembly) + extension(IServiceCollection services) { - //Gets the CommandBase derived classes - var commandBaseTypes = GetCommandBaseDerivedTypes(assembly); - //And adds the corresponding scoped behaviors for all the detected commands (for both existance and validation checks) - commandBaseTypes.ForEach(t => services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), - typeof(CommandValidationExistsBehavior<>).MakeGenericType(t)) - .AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), - typeof(CommandValidationBehavior<>).MakeGenericType(t))); - //Gets the CommandBases derived classes - var commandBaseResultTypes = GetCommandBaseOfResultDerivedTypes(assembly); + /// + /// Registers command validation behaviors for all command types derived from CommandBase or + /// CommandBase<T> found in the specified assembly. + /// + /// This method scans the provided assembly for types derived from CommandBase and + /// CommandBase<T>. For each detected command type: Scoped + /// instances of are registered for validation existence checks + /// using CommandValidationExistsBehavior. Scoped instances of are registered for validation logic using + /// CommandValidationBehavior. + /// The assembly to scan for command types. + /// The updated with the registered validation behaviors. + public IServiceCollection RegisterCommandValidationBehaviors(Assembly assembly) + { + //Gets the CommandBase derived classes + var commandBaseTypes = GetCommandBaseDerivedTypes(assembly); + //And adds the corresponding scoped behaviors for all the detected commands (for both existance and validation checks) + commandBaseTypes.ForEach(t => services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), + typeof(CommandValidationExistsBehavior<>).MakeGenericType(t)) + .AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), + typeof(CommandValidationBehavior<>).MakeGenericType(t))); + //Gets the CommandBases derived classes + var commandBaseResultTypes = GetCommandBaseOfResultDerivedTypes(assembly); - //And adds the corresponding scoped behavior for all the detected commands (only for validation checks) - commandBaseResultTypes.ForEach(t => - { - var tResult = t.BaseType!.GenericTypeArguments.First(); - services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult<>).MakeGenericType(tResult)), - typeof(CommandValidationExistsBehavior<,>).MakeGenericType(t, tResult)) - .AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult<>).MakeGenericType(tResult)), - typeof(CommandValidationBehavior<,>).MakeGenericType(t, tResult)); - }); - return services; - } + //And adds the corresponding scoped behavior for all the detected commands (only for validation checks) + commandBaseResultTypes.ForEach(t => + { + var tResult = t.BaseType!.GenericTypeArguments.First(); + services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult<>).MakeGenericType(tResult)), + typeof(CommandValidationExistsBehavior<,>).MakeGenericType(t, tResult)) + .AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult<>).MakeGenericType(tResult)), + typeof(CommandValidationBehavior<,>).MakeGenericType(t, tResult)); + }); + return services; + } - /// - /// Registers pipeline behaviors to handle concurrency exceptions for command types in the specified assembly. - /// - /// This method scans the provided assembly for types derived from CommandBase and - /// CommandBase<TResult>. For each detected command type, it registers a corresponding scoped pipeline - /// behavior to handle concurrency exceptions. - /// The to which the behaviors will be added. - /// The assembly containing the command types to scan for concurrency exception behaviors. - /// The updated with the registered behaviors. - public static IServiceCollection RegisterCommandConcurrencyExceptionBehaviors(this IServiceCollection services, Assembly assembly) - { - //Gets the CommandBase derived classes - var commandBaseTypes = GetCommandBaseDerivedTypes(assembly); - //And adds the corresponding scoped behaviors for all the detected commands - commandBaseTypes.ForEach(t => services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), - typeof(ConcurrencyExceptionBehavior<>).MakeGenericType(t))); - //Gets the CommandBases derived classes - var commandBaseResultTypes = GetCommandBaseOfResultDerivedTypes(assembly); + /// + /// Registers pipeline behaviors to handle concurrency exceptions for command types in the specified assembly. + /// + /// This method scans the provided assembly for types derived from CommandBase and + /// CommandBase<TResult>. For each detected command type, it registers a corresponding scoped pipeline + /// behavior to handle concurrency exceptions. + /// The assembly containing the command types to scan for concurrency exception behaviors. + /// The updated with the registered behaviors. + public IServiceCollection RegisterCommandConcurrencyExceptionBehaviors(Assembly assembly) + { + //Gets the CommandBase derived classes + var commandBaseTypes = GetCommandBaseDerivedTypes(assembly); + //And adds the corresponding scoped behaviors for all the detected commands + commandBaseTypes.ForEach(t => services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult)), + typeof(ConcurrencyExceptionBehavior<>).MakeGenericType(t))); + //Gets the CommandBases derived classes + var commandBaseResultTypes = GetCommandBaseOfResultDerivedTypes(assembly); - //And adds the corresponding scoped behavior for all the detected commands - commandBaseResultTypes.ForEach(t => - { - var tResult = t.BaseType!.GenericTypeArguments.First(); - services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult<>).MakeGenericType(tResult)), - typeof(ConcurrencyExceptionBehavior<,>).MakeGenericType(t, tResult)); - }); - return services; + //And adds the corresponding scoped behavior for all the detected commands + commandBaseResultTypes.ForEach(t => + { + var tResult = t.BaseType!.GenericTypeArguments.First(); + services.AddScoped(typeof(IPipelineBehavior<,>).MakeGenericType(t, typeof(CommandResult<>).MakeGenericType(tResult)), + typeof(ConcurrencyExceptionBehavior<,>).MakeGenericType(t, tResult)); + }); + return services; + } } private static Type[] GetCommandBaseDerivedTypes(Assembly assembly) => CommandBaseDerivedTypes ?? - assembly.GetTypes() - .Where(x => x.BaseType == typeof(CommandBase)) - .ToArray(); + [.. assembly.GetTypes().Where(x => x.BaseType == typeof(CommandBase))]; private static Type[] GetCommandBaseOfResultDerivedTypes(Assembly assembly) => CommandBaseOfResultDerivedTypes ?? - assembly.GetTypes() - .Where(x => (x.BaseType?.IsGenericType ?? false) && x.BaseType?.GetGenericTypeDefinition() == typeof(CommandBase<>)) - .ToArray(); + [ + .. assembly.GetTypes() + .Where(x => (x.BaseType?.IsGenericType ?? false) && + x.BaseType?.GetGenericTypeDefinition() == typeof(CommandBase<>)) + ]; } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/CommandValidationBehavior.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/CommandValidationBehavior.cs index 23acded..04df0ea 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/CommandValidationBehavior.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/CommandValidationBehavior.cs @@ -32,7 +32,7 @@ public virtual async Task Handle(TCommand request, RequestHandler if (!validationResult.IsValid) return CommandResult.ValidationFailed(validationResult); - return await next(); + return await next(cancellationToken); } } @@ -67,7 +67,7 @@ public CommandValidationBehavior(IValidator validator) if (!validationResult.IsValid) return CommandResult.ValidationFailed(validationResult, default); - return await next(); + return await next(cancellationToken); } } @@ -100,7 +100,7 @@ public virtual async Task Handle(TCommand request, if (!validationResult.IsValid) return CommandResult.NotFound(); - return await next(); + return await next(cancellationToken); } } @@ -137,6 +137,6 @@ public CommandValidationExistsBehavior(IValidator validator) if (!validationResult.IsValid) return CommandResult.NotFound(); - return await next(); + return await next(cancellationToken); } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/ConcurrencyExceptionBehavior.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/ConcurrencyExceptionBehavior.cs index 44407ff..9a6b74a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/ConcurrencyExceptionBehavior.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/ConcurrencyExceptionBehavior.cs @@ -30,11 +30,11 @@ public ConcurrencyExceptionBehavior(ResiliencePipelineProvider pipelineP public async Task Handle(TCommand request, RequestHandlerDelegate next, CancellationToken cancellationToken) => - await _dbConcurrentRetryPipeline.ExecuteAsync(async _ => + await _dbConcurrentRetryPipeline.ExecuteAsync(async ct => { try { - return await next(); + return await next(ct); } catch (DbUpdateConcurrencyException) { @@ -69,11 +69,11 @@ public ConcurrencyExceptionBehavior(ResiliencePipelineProvider pipelineP public async Task> Handle(TCommand request, RequestHandlerDelegate> next, CancellationToken cancellationToken) => - await _dbConcurrentRetryPipeline.ExecuteAsync(async _ => + await _dbConcurrentRetryPipeline.ExecuteAsync(async ct => { try { - return await next(); + return await next(ct); } catch (DbUpdateConcurrencyException) { diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Extensions/ClaimsPrincipalExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Extensions/ClaimsPrincipalExtensions.cs index 3f517f3..65ff687 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Extensions/ClaimsPrincipalExtensions.cs @@ -8,44 +8,45 @@ namespace Monaco.Template.Backend.Common.Application.Extensions; public static class ClaimsPrincipalExtensions { private const string ResourceAccessClaimName = "resource_access"; + private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web); - /// - /// Retrieves the User Id from the "sub" claim - /// - /// - /// - public static Guid? GetUserId(this ClaimsPrincipal principal) => - principal.HasClaim(c => c.Type == JwtRegisteredClaimNames.Sub) - ? Guid.Parse(principal.FindFirst(JwtRegisteredClaimNames.Sub)!.Value) - : null; - - /// - /// Determines if the user has the specified role on the specified client - /// - /// - /// - /// - /// - public static bool IsInClientRole(this ClaimsPrincipal principal, string clientName, string roleName) + extension(ClaimsPrincipal principal) { - var resourceAccessClaim = principal.FindFirst(ResourceAccessClaimName); - if (resourceAccessClaim is null) - return false; + /// + /// Retrieves the User Id from the "sub" claim + /// + /// + public Guid? GetUserId() => + principal.HasClaim(c => c.Type == JwtRegisteredClaimNames.Sub) + ? Guid.Parse(principal.FindFirst(JwtRegisteredClaimNames.Sub)!.Value) + : null; - var clients = JsonSerializer.Deserialize>(resourceAccessClaim.Value, - new JsonSerializerOptions(JsonSerializerDefaults.Web)); - return clients is not null && - clients.ContainsKey(clientName) && - (clients[clientName][principal.Identities.First().RoleClaimType]?.Deserialize()?.Contains(roleName) ?? false); - } + /// + /// Determines if the user has the specified role on the specified client + /// + /// + /// + /// + public bool IsInClientRole(string clientName, string roleName) + { + var resourceAccessClaim = principal.FindFirst(ResourceAccessClaimName); + if (resourceAccessClaim is null) + return false; - /// - /// Determines if the user has the specified role in the client specified by the Audience (aud) claim - /// - /// - /// - /// - public static bool IsInClientRole(this ClaimsPrincipal principal, string roleName) => - principal.HasClaim(c => c.Type == JwtRegisteredClaimNames.Aud) && - principal.IsInClientRole(principal.FindFirst(JwtRegisteredClaimNames.Aud)!.Value, roleName); + var clients = JsonSerializer.Deserialize>(resourceAccessClaim.Value, + JsonSerializerOptions); + return clients is not null && + clients.ContainsKey(clientName) && + (clients[clientName][principal.Identities.First().RoleClaimType]?.Deserialize()?.Contains(roleName) ?? false); + } + + /// + /// Determines if the user has the specified role in the client specified by the Audience (aud) claim + /// + /// + /// + public bool IsInClientRole(string roleName) => + principal.HasClaim(c => c.Type == JwtRegisteredClaimNames.Aud) && + principal.IsInClientRole(principal.FindFirst(JwtRegisteredClaimNames.Aud)!.Value, roleName); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Extensions/QueryExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Extensions/QueryExtensions.cs index 749b456..b71c468 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Extensions/QueryExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Queries/Extensions/QueryExtensions.cs @@ -8,216 +8,212 @@ namespace Monaco.Template.Backend.Common.Application.Queries.Extensions; public static class QueryExtensions { - public static async Task> ExecuteQueryAsync(this QueryBase> request, - BaseDbContext dbContext, - Func selector, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - Dictionary>> mappedFieldsSort, - CancellationToken cancellationToken) where T : Entity + extension(QueryBase> request) where T : Entity { - var result = await dbContext.Set() - .AsNoTracking() - .ApplyFilter(request.QueryParams, mappedFieldsFilter) + public async Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + Dictionary>> mappedFieldsSort, + CancellationToken cancellationToken) + { + var result = await dbContext.Set() + .AsNoTracking() + .ApplyFilter(request.QueryParams, mappedFieldsFilter) + .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) + .ToListAsync(cancellationToken); + return [.. result.Select(selector)]; + } + + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + CancellationToken cancellationToken) => + request.ExecuteQueryAsync(dbContext, + selector, + defaultSortField, + mappedFieldsFilter, + mappedFieldsFilter, + cancellationToken); + + public async Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func, IQueryable> queryFunc, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + Dictionary>> mappedFieldsSort, + CancellationToken cancellationToken) + { + var query = dbContext.Set().AsQueryable(); + query = queryFunc.Invoke(query); + + var result = await query.ApplyFilter(request.QueryParams, mappedFieldsFilter) .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) .ToListAsync(cancellationToken); - return result.Select(selector) - .ToList(); + return [.. result.Select(selector)]; + } + + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func, IQueryable> queryFunc, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + CancellationToken cancellationToken) => + request.ExecuteQueryAsync(dbContext, + selector, + queryFunc, + defaultSortField, + mappedFieldsFilter, + mappedFieldsFilter, + cancellationToken); + + public async Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func>, Expression>> expression, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + Dictionary>> mappedFieldsSort, + CancellationToken cancellationToken) + { + var result = await dbContext.Set() + .AsNoTracking() + .Where(expression.Invoke(request)) + .ApplyFilter(request.QueryParams, mappedFieldsFilter) + .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) + .ToListAsync(cancellationToken); + return [.. result.Select(selector)]; + } + + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func>, Expression>> expression, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + CancellationToken cancellationToken) => + request.ExecuteQueryAsync(dbContext, + selector, + expression, + defaultSortField, + mappedFieldsFilter, + mappedFieldsFilter, + cancellationToken); } - public static Task> ExecuteQueryAsync(this QueryBase> request, - BaseDbContext dbContext, - Func selector, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - CancellationToken cancellationToken) where T : Entity => - request.ExecuteQueryAsync(dbContext, - selector, - defaultSortField, - mappedFieldsFilter, - mappedFieldsFilter, - cancellationToken); - - public static async Task> ExecuteQueryAsync(this QueryBase> request, - BaseDbContext dbContext, - Func selector, - Func, IQueryable> queryFunc, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - Dictionary>> mappedFieldsSort, - CancellationToken cancellationToken) where T : Entity - { - var query = dbContext.Set().AsQueryable(); - query = queryFunc.Invoke(query); - - var result = await query.ApplyFilter(request.QueryParams, mappedFieldsFilter) - .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) - .ToListAsync(cancellationToken); - return result.Select(selector).ToList(); - } - - public static Task> ExecuteQueryAsync(this QueryBase> request, - BaseDbContext dbContext, - Func selector, - Func, IQueryable> queryFunc, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - CancellationToken cancellationToken) where T : Entity => - request.ExecuteQueryAsync(dbContext, - selector, - queryFunc, - defaultSortField, - mappedFieldsFilter, - mappedFieldsFilter, - cancellationToken); - - public static async Task> ExecuteQueryAsync(this TReq request, - BaseDbContext dbContext, - Func selector, - Func>> expression, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - Dictionary>> mappedFieldsSort, - CancellationToken cancellationToken) where TReq : QueryBase> - where T : Entity + extension(TReq request) where TReq : QueryBase> where T : Entity { - var result = await dbContext.Set() - .AsNoTracking() - .Where(expression.Invoke(request)) - .ApplyFilter(request.QueryParams, mappedFieldsFilter) - .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) - .ToListAsync(cancellationToken); - return result.Select(selector) - .ToList(); + public async Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func>> expression, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + Dictionary>> mappedFieldsSort, + CancellationToken cancellationToken) + { + var result = await dbContext.Set() + .AsNoTracking() + .Where(expression.Invoke(request)) + .ApplyFilter(request.QueryParams, mappedFieldsFilter) + .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) + .ToListAsync(cancellationToken); + return [.. result.Select(selector)]; + } + + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func>> expression, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + CancellationToken cancellationToken) => + request.ExecuteQueryAsync(dbContext, + selector, + expression, + defaultSortField, + mappedFieldsFilter, + mappedFieldsFilter, + cancellationToken); } - public static Task> ExecuteQueryAsync(this TReq request, - BaseDbContext dbContext, - Func selector, - Func>> expression, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - CancellationToken cancellationToken) where TReq : QueryBase> - where T : Entity => - request.ExecuteQueryAsync(dbContext, - selector, - expression, - defaultSortField, - mappedFieldsFilter, - mappedFieldsFilter, - cancellationToken); - - public static async Task> ExecuteQueryAsync(this QueryBase> request, - BaseDbContext dbContext, - Func selector, - Func>, Expression>> expression, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - Dictionary>> mappedFieldsSort, - CancellationToken cancellationToken) where T : Entity + extension(QueryPagedBase request) where T : Entity { - var result = await dbContext.Set() - .AsNoTracking() - .Where(expression.Invoke(request)) - .ApplyFilter(request.QueryParams, mappedFieldsFilter) - .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) - .ToListAsync(cancellationToken); - return result.Select(selector).ToList(); + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + Dictionary>> mappedFieldsSort, + CancellationToken cancellationToken) => + dbContext.Set() + .AsNoTracking() + .ApplyFilter(request.QueryParams, mappedFieldsFilter) + .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) + .ToPageAsync(request.Offset, request.Limit, selector, cancellationToken); + + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + CancellationToken cancellationToken) => + request.ExecuteQueryAsync(dbContext, + selector, + defaultSortField, + mappedFieldsFilter, + mappedFieldsFilter, + cancellationToken); + + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func, Expression>> expression, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + Dictionary>> mappedFieldsSort, + CancellationToken cancellationToken) => + dbContext.Set() + .AsNoTracking() + .Where(expression.Invoke(request)) + .ApplyFilter(request.QueryParams, mappedFieldsFilter) + .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) + .ToPageAsync(request.Offset, request.Limit, selector, cancellationToken); + + public Task> ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func, Expression>> expression, + string defaultSortField, + Dictionary>> mappedFieldsFilter, + CancellationToken cancellationToken) => + request.ExecuteQueryAsync(dbContext, + selector, + expression, + defaultSortField, + mappedFieldsFilter, + mappedFieldsFilter, + cancellationToken); } - public static Task> ExecuteQueryAsync(this QueryBase> request, - BaseDbContext dbContext, - Func selector, - Func>, Expression>> expression, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - CancellationToken cancellationToken) where T : Entity => - request.ExecuteQueryAsync(dbContext, - selector, - expression, - defaultSortField, - mappedFieldsFilter, - mappedFieldsFilter, - cancellationToken); - - public static Task> ExecuteQueryAsync(this QueryPagedBase request, - BaseDbContext dbContext, - Func selector, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - Dictionary>> mappedFieldsSort, - CancellationToken cancellationToken) where T : Entity => - dbContext.Set() - .AsNoTracking() - .ApplyFilter(request.QueryParams, mappedFieldsFilter) - .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) - .ToPageAsync(request.Offset, request.Limit, selector, cancellationToken); - - public static Task> ExecuteQueryAsync(this QueryPagedBase request, - BaseDbContext dbContext, - Func selector, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - CancellationToken cancellationToken) where T : Entity => - request.ExecuteQueryAsync(dbContext, - selector, - defaultSortField, - mappedFieldsFilter, - mappedFieldsFilter, - cancellationToken); - - public static Task> ExecuteQueryAsync(this QueryPagedBase request, - BaseDbContext dbContext, - Func selector, - Func, Expression>> expression, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - Dictionary>> mappedFieldsSort, - CancellationToken cancellationToken) where T : Entity => - dbContext.Set() - .AsNoTracking() - .Where(expression.Invoke(request)) - .ApplyFilter(request.QueryParams, mappedFieldsFilter) - .ApplySort(request.Sort, defaultSortField, mappedFieldsSort) - .ToPageAsync(request.Offset, request.Limit, selector, cancellationToken); - - public static Task> ExecuteQueryAsync(this QueryPagedBase request, - BaseDbContext dbContext, - Func selector, - Func, Expression>> expression, - string defaultSortField, - Dictionary>> mappedFieldsFilter, - CancellationToken cancellationToken) where T : Entity => - request.ExecuteQueryAsync(dbContext, - selector, - expression, - defaultSortField, - mappedFieldsFilter, - mappedFieldsFilter, - cancellationToken); - - public static async Task ExecuteQueryAsync(this QueryByIdBase request, - BaseDbContext dbContext, - Func selector, - CancellationToken cancellationToken) where T : Entity + extension(QueryByIdBase request) where T : Entity { - var item = await dbContext.Set() - .AsNoTracking() - .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); - return selector.Invoke(item); + public async Task ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + CancellationToken cancellationToken) + { + var item = await dbContext.Set() + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Id == request.Id, cancellationToken); + return selector.Invoke(item); + } } - public static async Task ExecuteQueryAsync(this TReq request, - BaseDbContext dbContext, - Func selector, - Func>> expression, - CancellationToken cancellationToken) where TReq : QueryByIdBase - where T : Entity + extension(TReq request) where TReq : QueryByIdBase where T : Entity { - var item = await dbContext.Set() - .AsNoTracking() - .Where(expression.Invoke(request)) - .SingleOrDefaultAsync(cancellationToken); - return selector.Invoke(item); + public async Task ExecuteQueryAsync(BaseDbContext dbContext, + Func selector, + Func>> expression, + CancellationToken cancellationToken) + { + var item = await dbContext.Set() + .AsNoTracking() + .Where(expression.Invoke(request)) + .SingleOrDefaultAsync(cancellationToken); + return selector.Invoke(item); + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs index 08436d2..f8d67b5 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/ResiliencePipelines/ResiliencePipelinesExtensions.cs @@ -8,15 +8,18 @@ public static class ResiliencePipelinesExtensions { public const string DbConcurrentExceptionPipelineKey = "DbConcurrentExceptionPipeline"; - public static IServiceCollection AddResiliencePipelines(this IServiceCollection services) => - services.AddResiliencePipeline(DbConcurrentExceptionPipelineKey, - builder => builder.AddRetry(new() - { - ShouldHandle = new PredicateBuilder().Handle(), - MaxRetryAttempts = 3, - Delay = TimeSpan.FromSeconds(1), - BackoffType = DelayBackoffType.Linear, - MaxDelay = TimeSpan.FromSeconds(3), - UseJitter = true - })); + extension(IServiceCollection services) + { + public IServiceCollection AddResiliencePipelines() => + services.AddResiliencePipeline(DbConcurrentExceptionPipelineKey, + builder => builder.AddRetry(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + MaxRetryAttempts = 3, + Delay = TimeSpan.FromSeconds(1), + BackoffType = DelayBackoffType.Linear, + MaxDelay = TimeSpan.FromSeconds(3), + UseJitter = true + })); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Extensions/ValidatorsExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Extensions/ValidatorsExtensions.cs index 23b7ed7..2576df1 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Extensions/ValidatorsExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Validators/Extensions/ValidatorsExtensions.cs @@ -11,99 +11,114 @@ public static class ValidatorsExtensions { public static readonly string ExistsRulesetName = "Exists"; - public static void CheckIfExists(this AbstractValidator validator, BaseDbContext dbContext) where TCommand : CommandBase - where TEntity : Entity => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) - .MustExistAsync(dbContext)); - - public static void CheckIfExists(this AbstractValidator validator, BaseDbContext dbContext) where TCommand : CommandBase - where TEntity : Entity => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) - .MustExistAsync(dbContext)); - - public static void CheckIfExists(this AbstractValidator validator, - Func> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) - .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Func> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) - .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Func> predicate) where TCommand : CommandBase => + extension(AbstractValidator validator) + where TCommand : CommandBase + { + public void CheckIfExists(BaseDbContext dbContext) where TEntity : Entity => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) + .MustExistAsync(dbContext)); + + public void CheckIfExists(Func> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) + .MustAsync(predicate)); + + public void CheckIfExists(Func> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) + .MustAsync(predicate)); + + public void CheckIfExists(Expression> selector, + Func> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) + .MustAsync(predicate)); + + public void CheckIfExists(Expression> selector, + Func> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) + .MustAsync(predicate)); + + public void CheckIfExists(Expression> selector, + Func, CancellationToken, Task> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) + .MustAsync(predicate)); + } + + extension(AbstractValidator validator) + where TCommand : CommandBase + where TEntity : Entity + { + public void CheckIfExists(BaseDbContext dbContext) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) + .MustExistAsync(dbContext)); + } + + extension(AbstractValidator validator) + where TCommand : CommandBase + { + public void CheckIfExists(Expression> selector, + Func> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) + .MustAsync(predicate)); + + public void CheckIfExists(Expression> selector, + Func> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) + .MustAsync(predicate)); + + public void CheckIfExists(Expression> selector, + Func, + CancellationToken, + Task> predicate) => + validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) + .MustAsync(predicate)); + } + + extension(IRuleBuilder ruleBuilder) + where TCommand : CommandBase + { + public IRuleBuilderOptions MustExistAsync(BaseDbContext dbContext) where TEntity : Entity => + ruleBuilder.MustAsync(dbContext.ExistsAsync) + .WithMessage("The value {PropertyValue} is not valid"); + } + + extension(IRuleBuilder ruleBuilder) + where TCommand : CommandBase + { + public IRuleBuilderOptions MustExistAsync(BaseDbContext dbContext) where TEntity : Entity => + ruleBuilder.MustAsync(async (id, ct) => id.HasValue && + await dbContext.ExistsAsync(x => x.Id == id.Value, ct)) + .WithMessage("The value {PropertyValue} is not valid"); + } + + extension(IRuleBuilder ruleBuilder) + where TCommand : CommandBase + where TEntity : Entity + { + public IRuleBuilderOptions MustExistAsync(BaseDbContext dbContext) => + ruleBuilder.MustAsync(async (id, ct) => id.HasValue && + await dbContext.ExistsAsync(x => x.Id == id.Value, ct)) + .WithMessage("The value {PropertyValue} is not valid"); + } + + extension(IRuleBuilder ruleBuilder) + where TCommand : CommandBase + where TEntity : Entity + { + public IRuleBuilderOptions MustExistAsync(BaseDbContext dbContext) => + ruleBuilder.MustAsync(dbContext.ExistsAsync) + .WithMessage("The value {PropertyValue} is not valid"); + } + + public static void CheckIfExists(this AbstractValidator validator, + Func> predicate) + where TCommand : CommandBase => validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) .MustAsync(predicate)); - public static void CheckIfExists(this AbstractValidator validator, - Func> predicate) where TCommand : CommandBase => + public static void CheckIfExists(this AbstractValidator validator, + Func> predicate) + where TCommand : CommandBase => validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(x => x.Id) .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Expression> selector, - Func> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) - .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Expression> selector, - Func> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) - .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Expression> selector, - Func, CancellationToken, Task> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) - .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Expression> selector, - Func> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) - .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Expression> selector, - Func> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) - .MustAsync(predicate)); - - public static void CheckIfExists(this AbstractValidator validator, - Expression> selector, - Func, - CancellationToken, - Task> predicate) where TCommand : CommandBase => - validator.RuleSet(ExistsRulesetName, () => validator.RuleFor(selector) - .MustAsync(predicate)); - - public static IRuleBuilderOptions MustExistAsync(this IRuleBuilder ruleBuilder, - BaseDbContext dbContext) where TCommand : CommandBase - where TEntity : Entity => - ruleBuilder.MustAsync(dbContext.ExistsAsync) - .WithMessage("The value {PropertyValue} is not valid"); - - public static IRuleBuilderOptions MustExistAsync(this IRuleBuilder ruleBuilder, - BaseDbContext dbContext) where TCommand : CommandBase - where TEntity : Entity => - ruleBuilder.MustAsync(async (id, ct) => id.HasValue && - await dbContext.ExistsAsync(x => x.Id == id.Value, ct)) - .WithMessage("The value {PropertyValue} is not valid"); - - public static IRuleBuilderOptions MustExistAsync(this IRuleBuilder ruleBuilder, - BaseDbContext dbContext) where TCommand : CommandBase - where TEntity : Entity => - ruleBuilder.MustAsync(async (id, ct) => id.HasValue && - await dbContext.ExistsAsync(x => x.Id == id.Value, ct)) - .WithMessage("The value {PropertyValue} is not valid"); - - public static IRuleBuilderOptions MustExistAsync(this IRuleBuilder ruleBuilder, - BaseDbContext dbContext) where TCommand : CommandBase - where TEntity : Entity => - ruleBuilder.MustAsync(dbContext.ExistsAsync) - .WithMessage("The value {PropertyValue} is not valid"); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Extensions/ServiceCollectionExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Extensions/ServiceCollectionExtensions.cs index dc696ec..5568486 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Extensions/ServiceCollectionExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Extensions/ServiceCollectionExtensions.cs @@ -6,11 +6,14 @@ namespace Monaco.Template.Backend.Common.BlobStorage.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection RegisterBlobStorageService(this IServiceCollection services, Action options) + extension(IServiceCollection services) { - var optionsValue = new BlobStorageServiceOptions(); - options.Invoke(optionsValue); - return services.AddSingleton(new BlobStorageService(new BlobServiceClient(optionsValue.ConnectionString), - optionsValue.ContainerName!)); + public IServiceCollection RegisterBlobStorageService(Action options) + { + var optionsValue = new BlobStorageServiceOptions(); + options.Invoke(optionsValue); + return services.AddSingleton(new BlobStorageService(new BlobServiceClient(optionsValue.ConnectionString), + optionsValue.ContainerName!)); + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Enumeration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Enumeration.cs index bfcdd8f..0322f6c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Enumeration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/Enumeration.cs @@ -27,7 +27,8 @@ public override string ToString() => public static IEnumerable GetAll() where T : Enumeration => typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Select(f => f.GetValue(null)).Cast(); + .Select(f => f.GetValue(null)) + .Cast(); public override int GetHashCode() => Id.GetHashCode(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs index 04851c2..2faf2bb 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/FilterExtensions.cs @@ -6,65 +6,69 @@ namespace Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; public static class FilterExtensions { - /// - /// Applies a filter expression to the query by mapping the querystring with the filter map dictionary - /// - /// The type handled by the query /// The source query - /// The querystring that provides the fields and their values for the filtering - /// A dictionary of field names and the expression that maps it against the domain class - /// Indicates if the default condition of the expression will be TRUE or FALSE - /// Indicates if the filtering must match all the conditions (AND) or just some of them (OR) - /// Returns an IQueryable to which has been applied the predicate matching the filtering criteria - public static IQueryable ApplyFilter(this IQueryable source, - IEnumerable> queryString, - Dictionary>> filterMap, - bool defaultCondition = true, - bool allConditions = true) + /// The type handled by the query + extension(IQueryable source) { - var (filterMapLower, filterList, predicate) = GetData(queryString, filterMap, defaultCondition); - - foreach (var (key, values) in filterList) //and while looping through the list of valid ones to use + /// + /// Applies a filter expression to the query by mapping the querystring with the filter map dictionary + /// + /// The querystring that provides the fields and their values for the filtering + /// A dictionary of field names and the expression that maps it against the domain class + /// Indicates if the default condition of the expression will be TRUE or FALSE + /// Indicates if the filtering must match all the conditions (AND) or just some of them (OR) + /// Returns an IQueryable to which has been applied the predicate matching the filtering criteria + public IQueryable ApplyFilter(IEnumerable> queryString, + Dictionary>> filterMap, + bool defaultCondition = true, + bool allConditions = true) { - //generate the expression equivalent to that querystring with the mapping corresponding to the DB - var predicateKey = PredicateBuilder.New(false); //Declare a PredicateBuilder for the current key values - predicateKey = values.Where(value => ValidateDataType(value, GetBodyExpression(filterMapLower[key]).Type)) - .Select(value => GetOperationExpression(key, filterMapLower[key], value)) //then generate the expression for each value - .Aggregate(predicateKey, (current, expr) => current.Or(expr)); //and chain them all with an OR operator - predicate = allConditions ? predicate.And(predicateKey) : predicate.Or(predicateKey); //then add the resulting expression to the more general predicate - } + var (filterMapLower, filterList, predicate) = GetData(queryString, filterMap, defaultCondition); - return source.Where(predicate); + foreach (var (key, values) in filterList) //and while looping through the list of valid ones to use + { + //generate the expression equivalent to that querystring with the mapping corresponding to the DB + var predicateKey = PredicateBuilder.New(false); //Declare a PredicateBuilder for the current key values + predicateKey = values.Where(value => ValidateDataType(value, GetBodyExpression(filterMapLower[key]).Type)) + .Select(value => GetOperationExpression(key, filterMapLower[key], value)) //then generate the expression for each value + .Aggregate(predicateKey, (current, expr) => current.Or(expr)); //and chain them all with an OR operator + predicate = allConditions ? predicate.And(predicateKey) : predicate.Or(predicateKey); //then add the resulting expression to the more general predicate + } + + return source.Where(predicate); + } } - /// - /// Applies a filter expression to the enumerable by mapping the querystring with the filter map dictionary - /// - /// The type handled by the enumerable /// The source enumerable - /// The querystring that provides the fields and their values for the filtering - /// A dictionary of field names and the expression that maps it against the domain class - /// Indicates if the default condition of the expression will be TRUE or FALSE - /// Indicates if the filtering must match all the conditions (AND) or just some of them (OR) - /// Returns an IEnumerable to which has been applied the predicate matching the filtering criteria - public static IEnumerable ApplyFilter(this IEnumerable source, - IEnumerable> queryString, - Dictionary>> filterMap, - bool defaultCondition = true, - bool allConditions = true) + /// The type handled by the enumerable + extension(IEnumerable source) { - var (filterMapLower, filterList, predicate) = GetData(queryString, filterMap, defaultCondition); - - foreach (var (key, values) in filterList) // and while looping through the list of valid ones to use + /// + /// Applies a filter expression to the enumerable by mapping the querystring with the filter map dictionary + /// + /// The querystring that provides the fields and their values for the filtering + /// A dictionary of field names and the expression that maps it against the domain class + /// Indicates if the default condition of the expression will be TRUE or FALSE + /// Indicates if the filtering must match all the conditions (AND) or just some of them (OR) + /// Returns an IEnumerable to which has been applied the predicate matching the filtering criteria + public IEnumerable ApplyFilter(IEnumerable> queryString, + Dictionary>> filterMap, + bool defaultCondition = true, + bool allConditions = true) { - var predicateKey = PredicateBuilder.New(false); // Declare a PredicateBuilder for the current key values - predicateKey = values.Where(value => ValidateDataType(value, GetBodyExpression(filterMapLower[key]).Type)) - .Select(value => GetOperationExpression(key, filterMapLower[key], value, true)) // then generate the expression for each value - .Aggregate(predicateKey, (current, expr) => current.Or(expr)); // and chain them all with an OR operator - predicate = allConditions ? predicate.And(predicateKey) : predicate.Or(predicateKey); // then add the resulting expression to the more general predicate - } + var (filterMapLower, filterList, predicate) = GetData(queryString, filterMap, defaultCondition); - return source.Where(predicate); + foreach (var (key, values) in filterList) // and while looping through the list of valid ones to use + { + var predicateKey = PredicateBuilder.New(false); // Declare a PredicateBuilder for the current key values + predicateKey = values.Where(value => ValidateDataType(value, GetBodyExpression(filterMapLower[key]).Type)) + .Select(value => GetOperationExpression(key, filterMapLower[key], value, true)) // then generate the expression for each value + .Aggregate(predicateKey, (current, expr) => current.Or(expr)); // and chain them all with an OR operator + predicate = allConditions ? predicate.And(predicateKey) : predicate.Or(predicateKey); // then add the resulting expression to the more general predicate + } + + return source.Where(predicate); + } } private static (Dictionary>> filterMapLower, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs index 52869c7..4954e59 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/MediatorExtension.cs @@ -6,33 +6,36 @@ namespace Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; public static class MediatorExtension { - public static async Task DispatchDomainEventsAsync(this IPublisher publisher, DbContext ctx) + extension(IPublisher publisher) { - while (true) + public async Task DispatchDomainEventsAsync(DbContext ctx) { - var aggregateRoots = ctx.ChangeTracker - .Entries() - .Where(x => x.Entity - .DomainEvents - .Any()) - .ToList(); + while (true) + { + var aggregateRoots = ctx.ChangeTracker + .Entries() + .Where(x => x.Entity + .DomainEvents + .Any()) + .ToList(); - var domainEvents = aggregateRoots.SelectMany(x => x.Entity.DomainEvents).ToList(); + var domainEvents = aggregateRoots.SelectMany(x => x.Entity.DomainEvents).ToList(); - aggregateRoots.ForEach(entity => entity.Entity.ClearDomainEvents()); + aggregateRoots.ForEach(entity => entity.Entity.ClearDomainEvents()); - foreach (var domainEvent in domainEvents) - await publisher.Publish(domainEvent); + foreach (var domainEvent in domainEvents) + await publisher.Publish(domainEvent); - //If event handlers produced more domain events, keep processing them until there's no more - if (ctx.ChangeTracker - .Entries() - .Any(x => x.Entity - .DomainEvents - .Any())) - continue; + //If event handlers produced more domain events, keep processing them until there's no more + if (ctx.ChangeTracker + .Entries() + .Any(x => x.Entity + .DomainEvents + .Any())) + continue; - break; + break; + } } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs index 2fb12a7..0729902 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/OperationsExtensions.cs @@ -6,40 +6,38 @@ namespace Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; public static class OperationsExtensions { - public static Task ExistsAsync(this DbContext dbContext, - Guid id, - CancellationToken cancellationToken) where T : Entity => - dbContext.Set().AnyAsync(x => x.Id == id, cancellationToken); + extension(DbContext dbContext) + { + public Task ExistsAsync(Guid id, + CancellationToken cancellationToken) where T : Entity => + dbContext.Set().AnyAsync(x => x.Id == id, cancellationToken); - public static Task ExistsAsync(this DbContext dbContext, - Expression> predicate, - CancellationToken cancellationToken) where T : class => - dbContext.Set().AnyAsync(predicate, cancellationToken); + public Task ExistsAsync(Expression> predicate, + CancellationToken cancellationToken) where T : class => + dbContext.Set().AnyAsync(predicate, cancellationToken); - public static async Task GetAsync(this DbContext dbContext, - Guid? id, - CancellationToken cancellationToken) where T : class => - id.HasValue - ? await dbContext.GetAsync(id.Value, cancellationToken) - : null; + public async Task GetAsync(Guid? id, + CancellationToken cancellationToken) where T : class => + id.HasValue + ? await dbContext.GetAsync(id.Value, cancellationToken) + : null; - public static async Task GetAsync(this DbContext dbContext, - Guid id, - CancellationToken cancellationToken) where T : class => - (await dbContext.Set().FindAsync([id], cancellationToken))!; + public async Task GetAsync(Guid id, + CancellationToken cancellationToken) where T : class => + (await dbContext.Set().FindAsync([id], cancellationToken))!; - public static IQueryable Set(this DbContext context, Type t) => - (IQueryable)context.GetType() - .GetMethod("Set", Type.EmptyTypes)? - .MakeGenericMethod(t) - .Invoke(context, [])!; + public IQueryable Set(Type t) => + (IQueryable)dbContext.GetType() + .GetMethod("Set", Type.EmptyTypes)? + .MakeGenericMethod(t) + .Invoke(dbContext, [])!; - public static async Task> GetListByIdsAsync(this DbContext dbContext, - Guid[] items, - CancellationToken cancellationToken) where T : Entity => - items.Any() - ? await dbContext.Set() - .Where(x => items.Contains(x.Id)) - .ToListAsync(cancellationToken) - : []; + public async Task> GetListByIdsAsync(List items, + CancellationToken cancellationToken) where T : Entity => + items.Count > 0 + ? await dbContext.Set() + .Where(x => items.Contains(x.Id)) + .ToListAsync(cancellationToken) + : []; + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs index dc72f74..571445e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/PagingExtensions.cs @@ -6,64 +6,69 @@ namespace Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; public static class PagingExtensions { - public static async Task> ToPageAsync(this IQueryable query, - int offset, + extension(IQueryable query) + { + public async Task> ToPageAsync(int offset, + int limit, + Func selector, + CancellationToken cancellationToken = default) + { + var results = await query.Select(x => new + { + Item = x, + TotalCount = query.Count() + }) + .Skip(offset) + .Take(limit) + .ToListAsync(cancellationToken); + return new(results.Select(x => selector(x.Item)), + offset, + limit, + results.FirstOrDefault()?.TotalCount ?? 0); + } + + public async Task> ToPageAsync(int offset, int limit, Func selector, - CancellationToken cancellationToken = default) - { - var results = await query.Select(x => new - { - Item = x, - TotalCount = query.Count() - }) - .Skip(offset) - .Take(limit) - .ToListAsync(cancellationToken); - return new(results.Select(x => selector(x.Item)), - offset, - limit, - results.FirstOrDefault()?.TotalCount ?? 0); - } - - public static async Task> ToPageAsync(this IQueryable query, - int offset, + Expression>? orderBy, + CancellationToken cancellationToken = default) => + await (orderBy is null + ? query + : query.OrderBy(orderBy)).ToPageAsync(offset, + limit, + selector, + cancellationToken); + + public async Task> ToPageAsync(int offset, int limit, Func selector, Expression>? orderBy, + Expression>? thenBy, CancellationToken cancellationToken = default) => - await (orderBy is null - ? query - : query.OrderBy(orderBy)).ToPageAsync(offset, - limit, - selector, - cancellationToken); - - public static async Task> ToPageAsync(this IQueryable query, - int offset, - int limit, - Func selector, - Expression>? orderBy, - Expression>? thenBy, - CancellationToken cancellationToken = default) => - await (orderBy is null - ? query - : thenBy is null - ? query.OrderBy(orderBy) - : query.OrderBy(orderBy) - .ThenBy(thenBy)).ToPageAsync(offset, - limit, - selector, - cancellationToken); + await (orderBy is null + ? query + : thenBy is null + ? query.OrderBy(orderBy) + : query.OrderBy(orderBy) + .ThenBy(thenBy)).ToPageAsync(offset, + limit, + selector, + cancellationToken); + } - public static Page ToPage(this IEnumerable enumerable, int offset, int limit, Func selector) + extension(IEnumerable enumerable) { - var list = enumerable.ToList(); - return new(list.Skip(offset) - .Take(limit) - .Select(selector), - offset, - limit, - list.Count); + public Page ToPage(int offset, + int limit, + Func selector) + { + var list = enumerable.ToList(); + return new(list.Skip(offset) + .Take(limit) + .Select(selector), + offset, + limit, + list.Count); + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs index bf1596a..277cd86 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SelectMapExtensions.cs @@ -6,58 +6,54 @@ namespace Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; public static class SelectMapExtensions { - public static async Task SingleOrDefaultMapAsync(this IQueryable source, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken = default) => - await source.Where(predicate) - .Select(selector) - .DecompileAsync() - .SingleOrDefaultAsync(cancellationToken); - - public static async Task SingleOrDefaultMapAsync(this IQueryable source, - Expression> selector, - CancellationToken cancellationToken = default) => - await source.Select(selector) - .DecompileAsync() - .SingleOrDefaultAsync(cancellationToken); - - public static async Task FirstOrDefaultMapAsync(this IQueryable source, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken = default) => - await source.Where(predicate) - .Select(selector) - .DecompileAsync() - .FirstOrDefaultAsync(cancellationToken); - - public static async Task FirstOrDefaultMapAsync(this IQueryable source, - Expression> selector, - CancellationToken cancellationToken = default) => - await source.Select(selector) - .DecompileAsync() - .FirstOrDefaultAsync(cancellationToken); - - public static async Task SingleOrDefaultAsync(this IQueryable source, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken = default) => - await source.Where(predicate) - .Select(selector) - .SingleOrDefaultAsync(cancellationToken); - - public static async Task FirstOrDefaultAsync(this IQueryable source, - Expression> predicate, - Expression> selector, - CancellationToken cancellationToken = default) => - await source.Where(predicate) - .Select(selector) - .FirstOrDefaultAsync(cancellationToken); - - public static async Task> ToListMapAsync(this IQueryable source, - Expression> selector, - CancellationToken cancellationToken = default) => - await source.Select(selector) - .DecompileAsync() - .ToListAsync(cancellationToken); + extension(IQueryable source) + { + public async Task SingleOrDefaultMapAsync(Expression> predicate, + Expression> selector, + CancellationToken cancellationToken = default) => + await source.Where(predicate) + .Select(selector) + .DecompileAsync() + .SingleOrDefaultAsync(cancellationToken); + + public async Task SingleOrDefaultMapAsync(Expression> selector, + CancellationToken cancellationToken = default) => + await source.Select(selector) + .DecompileAsync() + .SingleOrDefaultAsync(cancellationToken); + + public async Task FirstOrDefaultMapAsync(Expression> predicate, + Expression> selector, + CancellationToken cancellationToken = default) => + await source.Where(predicate) + .Select(selector) + .DecompileAsync() + .FirstOrDefaultAsync(cancellationToken); + + public async Task FirstOrDefaultMapAsync(Expression> selector, + CancellationToken cancellationToken = default) => + await source.Select(selector) + .DecompileAsync() + .FirstOrDefaultAsync(cancellationToken); + + public async Task SingleOrDefaultAsync(Expression> predicate, + Expression> selector, + CancellationToken cancellationToken = default) => + await source.Where(predicate) + .Select(selector) + .SingleOrDefaultAsync(cancellationToken); + + public async Task FirstOrDefaultAsync(Expression> predicate, + Expression> selector, + CancellationToken cancellationToken = default) => + await source.Where(predicate) + .Select(selector) + .FirstOrDefaultAsync(cancellationToken); + + public async Task> ToListMapAsync(Expression> selector, + CancellationToken cancellationToken = default) => + await source.Select(selector) + .DecompileAsync() + .ToListAsync(cancellationToken); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs index 628d189..b8d1b2f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Context/Extensions/SortingExtensions.cs @@ -4,34 +4,75 @@ namespace Monaco.Template.Backend.Common.Infrastructure.Context.Extensions; public static class SortingExtensions { - public static IQueryable ApplySort(this IQueryable source, - string?[] sortFields, - string defaultSortField, - Dictionary>> sortMap) + extension(IQueryable source) { - ArgumentException.ThrowIfNullOrEmpty(defaultSortField, nameof(defaultSortField)); + public IQueryable ApplySort(string?[] sortFields, + string defaultSortField, + Dictionary>> sortMap) + { + ArgumentException.ThrowIfNullOrEmpty(defaultSortField); - var (sortMapLower, lstSort) = GetData(sortFields, defaultSortField, sortMap); + var (sortMapLower, lstSort) = GetData(sortFields, defaultSortField, sortMap); - var query = source.AsQueryable(); - foreach (var (key, value) in lstSort) //Loop through the fields and apply the sorting - query = query.GetOrderedQuery(sortMapLower[key], value, key == lstSort.Keys.First()); - return query; + var query = source.AsQueryable(); + foreach (var (key, value) in lstSort) //Loop through the fields and apply the sorting + query = query.GetOrderedQuery(sortMapLower[key], value, key == lstSort.Keys.First()); + return query; + } + + private IOrderedQueryable GetOrderedQuery(Expression> expression, bool ascending, bool firstSort) + { + var bodyExpression = (MemberExpression)(expression.Body.NodeType == ExpressionType.Convert ? ((UnaryExpression)expression.Body).Operand : expression.Body); + var sortLambda = Expression.Lambda(bodyExpression, expression.Parameters); + Expression>> sortMethod = firstSort + ? ascending + ? () => source.OrderBy(k => null!) + : () => source.OrderByDescending(k => null!) + : ascending + ? () => ((IOrderedQueryable)source).ThenBy(k => null!) + : () => ((IOrderedQueryable)source).ThenByDescending(k => null!); + + var methodCallExpression = (MethodCallExpression)sortMethod.Body; + var method = methodCallExpression.Method.GetGenericMethodDefinition(); + var genericSortMethod = method.MakeGenericMethod(typeof(T), bodyExpression.Type); + return (IOrderedQueryable)genericSortMethod.Invoke(source, [source, sortLambda])!; + } } - public static IEnumerable ApplySort(this IEnumerable source, - string?[] sortFields, - string defaultSortField, - Dictionary>> sortMap) + extension(IEnumerable source) { - ArgumentException.ThrowIfNullOrEmpty(defaultSortField, nameof(defaultSortField)); + public IEnumerable ApplySort(string?[] sortFields, + string defaultSortField, + Dictionary>> sortMap) + { + ArgumentException.ThrowIfNullOrEmpty(defaultSortField); + + var (sortMapLower, lstSort) = GetData(sortFields, defaultSortField, sortMap); - var (sortMapLower, lstSort) = GetData(sortFields, defaultSortField, sortMap); + var query = source.AsEnumerable(); + foreach (var (key, value) in lstSort) //Loop through the fields and apply the sorting + query = query.GetOrderedQuery(sortMapLower[key], value, key == lstSort.Keys.First()); + return query; + } - var query = source.AsEnumerable(); - foreach (var (key, value) in lstSort) //Loop through the fields and apply the sorting - query = query.GetOrderedQuery(sortMapLower[key], value, key == lstSort.Keys.First()); - return query; + private IOrderedEnumerable GetOrderedQuery(Expression> expression, bool ascending, bool firstSort) + { + var bodyExpression = (MemberExpression)(expression.Body.NodeType == ExpressionType.Convert ? ((UnaryExpression)expression.Body).Operand : expression.Body); + var sortLambda = Expression.Lambda(bodyExpression, expression.Parameters); + Expression>> sortMethod = firstSort + ? ascending + ? () => source.OrderBy(k => null!) + : () => source.OrderByDescending(k => null!) + : ascending + ? () => ((IOrderedEnumerable)source).ThenBy(k => null!) + : () => ((IOrderedEnumerable)source).ThenByDescending(k => null!); + if (sortMethod.Body is not MethodCallExpression methodCallExpression) + throw new Exception("oops"); + + var meth = methodCallExpression.Method.GetGenericMethodDefinition(); + var genericSortMethod = meth.MakeGenericMethod(typeof(T), bodyExpression.Type); + return (IOrderedEnumerable)genericSortMethod.Invoke(source, [source, sortLambda.Compile()])!; + } } public static (Dictionary>> sortMapLower, Dictionary lstSort) @@ -47,43 +88,6 @@ public static (Dictionary>> sortMapLower, Dic return (sortMapLower, lstSort); } - private static IOrderedQueryable GetOrderedQuery(this IQueryable source, Expression> expression, bool ascending, bool firstSort) - { - var bodyExpression = (MemberExpression)(expression.Body.NodeType == ExpressionType.Convert ? ((UnaryExpression)expression.Body).Operand : expression.Body); - var sortLambda = Expression.Lambda(bodyExpression, expression.Parameters); - Expression>> sortMethod = firstSort - ? ascending - ? () => source.OrderBy(k => null!) - : () => source.OrderByDescending(k => null!) - : ascending - ? () => ((IOrderedQueryable)source).ThenBy(k => null!) - : () => ((IOrderedQueryable)source).ThenByDescending(k => null!); - - var methodCallExpression = (MethodCallExpression)sortMethod.Body; - var method = methodCallExpression.Method.GetGenericMethodDefinition(); - var genericSortMethod = method.MakeGenericMethod(typeof(T), bodyExpression.Type); - return (IOrderedQueryable)genericSortMethod.Invoke(source, [source, sortLambda])!; - } - - private static IOrderedEnumerable GetOrderedQuery(this IEnumerable source, Expression> expression, bool ascending, bool firstSort) - { - var bodyExpression = (MemberExpression)(expression.Body.NodeType == ExpressionType.Convert ? ((UnaryExpression)expression.Body).Operand : expression.Body); - var sortLambda = Expression.Lambda(bodyExpression, expression.Parameters); - Expression>> sortMethod = firstSort - ? ascending - ? () => source.OrderBy(k => null!) - : () => source.OrderByDescending(k => null!) - : ascending - ? () => ((IOrderedEnumerable)source).ThenBy(k => null!) - : () => ((IOrderedEnumerable)source).ThenByDescending(k => null!); - if (sortMethod.Body is not MethodCallExpression methodCallExpression) - throw new Exception("oops"); - - var meth = methodCallExpression.Method.GetGenericMethodDefinition(); - var genericSortMethod = meth.MakeGenericMethod(typeof(T), bodyExpression.Type); - return (IOrderedEnumerable)genericSortMethod.Invoke(source, [source, sortLambda.Compile()])!; - } - private static Dictionary ProcessSortParam(IEnumerable sortFields, Dictionary>> sortMap) => sortFields.Where(x => x is not null) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs index a67b4dc..b23817f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs @@ -6,42 +6,47 @@ namespace Monaco.Template.Backend.Common.Infrastructure.EntityConfigurations.Ext public static class EntityTypeBuilderExtensions { - public static void ConfigureId(this EntityTypeBuilder builder) where T : Entity + extension(EntityTypeBuilder builder) where T : Entity { - builder.HasKey(x => x.Id); - builder.Property(x => x.Id) - .IsRequired(); - } + public void ConfigureId() + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .IsRequired(); + } - public static void ConfigureIdWithDefaultAndValueGeneratedNever(this EntityTypeBuilder builder) where T : Entity - { - builder.ConfigureId(); - builder.Property(x => x.Id) - .ValueGeneratedNever(); - } + public void ConfigureIdWithDefaultAndValueGeneratedNever() + { + builder.ConfigureId(); + builder.Property(x => x.Id) + .ValueGeneratedNever(); + } - public static void ConfigureIdWithDbGeneratedValue(this EntityTypeBuilder builder) where T : Entity - { - builder.ConfigureId(); - builder.Property(x => x.Id) - .ValueGeneratedOnAdd(); - } + public void ConfigureIdWithDbGeneratedValue() + { + builder.ConfigureId(); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + } - public static void ConfigureIdWithSequence(this EntityTypeBuilder builder) where T : Entity - { - builder.ConfigureId(); - builder.Property(x => x.Id) - .UseHiLo($"{typeof(T).Name}Sequence"); + public void ConfigureIdWithSequence() + { + builder.ConfigureId(); + builder.Property(x => x.Id) + .UseHiLo($"{typeof(T).Name}Sequence"); + } + + public void ConfigureIdWithIdentity() + { + builder.ConfigureId(); + builder.Property(x => x.Id) + .UseIdentityColumn(); + } } - public static void ConfigureIdWithIdentity(this EntityTypeBuilder builder) where T : Entity + extension(EntityTypeBuilder source) where TEntity : class { - builder.ConfigureId(); - builder.Property(x => x.Id) - .UseIdentityColumn(); + public DataBuilder HasData(Func[] dataFuncs) => + source.HasData(dataFuncs.Select(func => func())); } - - public static DataBuilder HasData(this EntityTypeBuilder source, - Func[] dataFuncs) where TEntity : class => - source.HasData(dataFuncs.Select(func => func())); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/SerilogExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/SerilogExtensions.cs index d61c762..51d74ea 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/SerilogExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/SerilogExtensions.cs @@ -6,10 +6,13 @@ namespace Monaco.Template.Backend.Common.Serilog; public static class SerilogExtensions { - public static LoggerConfiguration WithOperationId(this LoggerEnrichmentConfiguration enrichConfiguration) + extension(LoggerEnrichmentConfiguration enrichConfiguration) { - ArgumentNullException.ThrowIfNull(enrichConfiguration, nameof(enrichConfiguration)); + public LoggerConfiguration WithOperationId() + { + ArgumentNullException.ThrowIfNull(enrichConfiguration, nameof(enrichConfiguration)); - return enrichConfiguration.With(); + return enrichConfiguration.With(); + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs index 567e956..d184778 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/DbContextMockExtensions.cs @@ -7,64 +7,53 @@ namespace Monaco.Template.Backend.Common.Tests; public static class DbContextMockExtensions { - public static Mock SetupDbSetMock(this Mock dbContextMock, T entity) - where TDbContext : DbContext - where T : Entity + extension(Mock dbContextMock) where TDbContext : DbContext { - var entityDbSetMock = new List { entity }.BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); - entityDbSetMock.Setup(x => x.FindAsync(new object[] { entity.Id }, It.IsAny())) - .ReturnsAsync(entity); - return dbContextMock; + public Mock SetupDbSetMock(T entity) where T : Entity + { + var entityDbSetMock = new List { entity }.BuildMockDbSet(); + dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); + entityDbSetMock.Setup(x => x.FindAsync(new object[] { entity.Id }, It.IsAny())) + .ReturnsAsync(entity); + return dbContextMock; + } + + public Mock CreateEntityMockAndSetupDbSetMock(out Mock entityMock) where T : Entity + { + entityMock = new Mock(); + var entityDbSetMock = new List { entityMock.Object }.BuildMockDbSet(); + dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); + entityDbSetMock.Setup(x => x.FindAsync(new object[] { It.IsAny() }, It.IsAny())) + .ReturnsAsync(entityMock.Object); + + return dbContextMock; + } + + public Mock CreateEntityMockAndSetupDbSetMock() where T : Entity + => dbContextMock.CreateEntityMockAndSetupDbSetMock(out _); + + public Mock CreateAndSetupDbSetMock(T entity, out Mock> entityDbSetMock) where T : Entity + { + entityDbSetMock = new[] { entity }.BuildMockDbSet(); + dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); + entityDbSetMock.Setup(x => x.FindAsync(new object[] { It.IsAny() }, It.IsAny())) + .ReturnsAsync(entity); + + return dbContextMock; + } + + public Mock CreateAndSetupDbSetMock(T entity) where T : Entity + => dbContextMock.CreateAndSetupDbSetMock(entity, out _); + + public Mock CreateAndSetupDbSetMock(ICollection entities, out Mock> entityDbSetMock) where T : Entity + { + entityDbSetMock = entities.BuildMockDbSet(); + dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); + + return dbContextMock; + } + + public Mock CreateAndSetupDbSetMock(ICollection entities) where T : Entity + => dbContextMock.CreateAndSetupDbSetMock(entities, out _); } - - public static Mock CreateEntityMockAndSetupDbSetMock(this Mock dbContextMock, out Mock entityMock) - where TDbContext : DbContext - where T : Entity - { - entityMock = new Mock(); - var entityDbSetMock = new List { entityMock.Object }.BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); - entityDbSetMock.Setup(x => x.FindAsync(new object[] { It.IsAny() }, It.IsAny())) - .ReturnsAsync(entityMock.Object); - - return dbContextMock; - } - - public static Mock CreateEntityMockAndSetupDbSetMock(this Mock dbContextMock) - where TDbContext : DbContext - where T : Entity - => dbContextMock.CreateEntityMockAndSetupDbSetMock(out _); - - public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, T entity, out Mock> entityDbSetMock) - where TDbContext : DbContext - where T : Entity - { - entityDbSetMock = new[] { entity }.BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); - entityDbSetMock.Setup(x => x.FindAsync(new object[] { It.IsAny() }, It.IsAny())) - .ReturnsAsync(entity); - - return dbContextMock; - } - - public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, T entity) - where TDbContext : DbContext - where T : Entity - => dbContextMock.CreateAndSetupDbSetMock(entity, out _); - - public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, ICollection entities, out Mock> entityDbSetMock) - where TDbContext : DbContext - where T : Entity - { - entityDbSetMock = entities.BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); - - return dbContextMock; - } - - public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, ICollection entities) - where TDbContext : DbContext - where T : Entity - => dbContextMock.CreateAndSetupDbSetMock(entities, out _); } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/FixtureExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/FixtureExtensions.cs index 338a2bd..69fba8c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/FixtureExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/FixtureExtensions.cs @@ -5,27 +5,30 @@ namespace Monaco.Template.Backend.Domain.Tests.Factories; public static class FixtureExtensions { - public static IFixture RegisterEntityFactories(this IFixture fixture) => - fixture.RegisterCompany() - .RegisterAddress() + extension(IFixture fixture) + { + public IFixture RegisterEntityFactories() => + fixture.RegisterCompany() + .RegisterAddress() #if (!filesSupport) - .RegisterCountry(); + .RegisterCountry(); #else - .RegisterCountry() - .RegisterDocument() - .RegisterImage() - .RegisterProduct(); + .RegisterCountry() + .RegisterDocument() + .RegisterImage() + .RegisterProduct(); #endif - public static void RegisterMockFactories(this IFixture fixture) => - fixture.RegisterCompanyMock() - .RegisterAddressMock() + public void RegisterMockFactories() => + fixture.RegisterCompanyMock() + .RegisterAddressMock() #if (!filesSupport) - .RegisterCountryMock(); + .RegisterCountryMock(); #else - .RegisterCountryMock() - .RegisterDocument() - .RegisterImage() - .RegisterProductMock(); + .RegisterCountryMock() + .RegisterDocument() + .RegisterImage() + .RegisterProductMock(); #endif + } } \ No newline at end of file From 2fa537a34f741361173e147ab30e09dc82053032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Sun, 7 Dec 2025 13:55:27 +0100 Subject: [PATCH 09/30] Updated dependencies. Added CompatibilityLevel configuration to EF Core configuration for most recent features usages. --- .../Backend/Solution/Directory.Packages.props | 154 +++++++++--------- .../ServiceCollectionExtensions.cs | 3 +- 2 files changed, 79 insertions(+), 78 deletions(-) diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index a8f7dec..dc6e90a 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -1,79 +1,79 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs index e05e822..8bb3670 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/DependencyInjection/ServiceCollectionExtensions.cs @@ -41,7 +41,8 @@ public IServiceCollection ConfigureApplication(Action option !filter.ValidatorType.IsAbstract, includeInternalTypes: true) .AddDbContext(opts => opts.UseSqlServer(optionsValue.EntityFramework.ConnectionString, - sqlOptions => sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(3), null)) + sqlOptions => sqlOptions.EnableRetryOnFailure(5, TimeSpan.FromSeconds(3), null) + .UseCompatibilityLevel(160)) // SQL Server 2022 = 160 - SQL Server 2025 = 170 .UseLazyLoadingProxies() .EnableSensitiveDataLogging(optionsValue.EntityFramework.EnableEfSensitiveLogging)) .AddScoped(provider => provider.GetRequiredService()); From e3596dc3967e7b6aa6c1c07b6dc84e4868f56e83 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Tue, 9 Dec 2025 22:42:18 +0000 Subject: [PATCH 10/30] chore: move common MSBuild properties to Directory.Build.props file --- src/Content/Backend/Solution/Directory.Build.props | 9 +++++++++ .../Monaco.Template.Backend.Api.csproj | 3 --- .../Monaco.Template.Backend.Application.Tests.csproj | 4 +--- .../Monaco.Template.Backend.Application.csproj | 6 ------ .../Monaco.Template.Backend.ArchitectureTests.csproj | 4 ---- ...Monaco.Template.Backend.Common.Api.Application.csproj | 4 ---- .../Monaco.Template.Backend.Common.Api.csproj | 4 ---- .../Monaco.Template.Backend.Common.ApiGateway.csproj | 3 --- .../Monaco.Template.Backend.Common.Application.csproj | 4 ---- ...naco.Template.Backend.Common.BlobStorage.Tests.csproj | 4 +--- .../Monaco.Template.Backend.Common.BlobStorage.csproj | 4 ---- .../Monaco.Template.Backend.Common.Domain.Tests.csproj | 4 +--- .../Monaco.Template.Backend.Common.Domain.csproj | 4 ---- .../Monaco.Template.Backend.Common.Infrastructure.csproj | 4 ---- .../Monaco.Template.Backend.Common.Serilog.csproj | 4 ---- .../Monaco.Template.Backend.Common.Tests.csproj | 6 ------ .../Monaco.Template.Backend.Domain.Tests.csproj | 4 +--- .../Monaco.Template.Backend.Domain.csproj | 6 ------ .../Monaco.Template.Backend.IntegrationTests.csproj | 3 --- .../Monaco.Template.Backend.Messages.csproj | 4 ---- .../Monaco.Template.Backend.Worker.csproj | 3 --- src/Content/Backend/Solution/Monaco.Template.Backend.sln | 1 + .../Backend/Solution/Monaco.Template.Backend.slnx | 1 + 23 files changed, 15 insertions(+), 78 deletions(-) create mode 100644 src/Content/Backend/Solution/Directory.Build.props diff --git a/src/Content/Backend/Solution/Directory.Build.props b/src/Content/Backend/Solution/Directory.Build.props new file mode 100644 index 0000000..e0ef770 --- /dev/null +++ b/src/Content/Backend/Solution/Directory.Build.props @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj index a0ba7f1..15a8361 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj @@ -1,9 +1,6 @@  - net10.0 - enable - enable 8ac1d4e3-61ef-452f-a386-ff3ec448fbff True linux-x64 diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj index a7b6022..1d604fe 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Monaco.Template.Backend.Application.Tests.csproj @@ -1,10 +1,8 @@  - net10.0 - enable - enable false + true diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj index 5bb0761..f7cff64 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj @@ -1,11 +1,5 @@  - - net10.0 - enable - enable - - diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj index 358153c..066adac 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj @@ -1,10 +1,6 @@ - net10.0 - enable - enable - false true diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj index 722ee9c..1944ee2 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/Monaco.Template.Backend.Common.Api.Application.csproj @@ -1,10 +1,6 @@ - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Api.Application True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj index 1a95d00..42a9f57 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj @@ -1,10 +1,6 @@  - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Api True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj index e3b8be5..d6e6939 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Monaco.Template.Backend.Common.ApiGateway.csproj @@ -1,9 +1,6 @@ - net10.0 - enable - enable 4c76f225-faad-42ec-801b-9ad3b505b7f5 diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj index 395e1d9..07c8f19 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj @@ -1,10 +1,6 @@  - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Application True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj index cd56412..d3c2f48 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage.Tests/Monaco.Template.Backend.Common.BlobStorage.Tests.csproj @@ -1,10 +1,8 @@  - net10.0 - enable - enable false + true diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj index 1f73b24..ef5f12b 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.BlobStorage/Monaco.Template.Backend.Common.BlobStorage.csproj @@ -1,10 +1,6 @@ - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.BlobStorage True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj index 84bd1d9..3077161 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Monaco.Template.Backend.Common.Domain.Tests.csproj @@ -1,10 +1,8 @@ - net10.0 - enable - enable false + true diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj index df5917f..8f8ec7c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Monaco.Template.Backend.Common.Domain.csproj @@ -1,10 +1,6 @@ - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Domain True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj index 4db5036..eba9f83 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/Monaco.Template.Backend.Common.Infrastructure.csproj @@ -1,10 +1,6 @@ - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Infrastructure True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj index 4976643..f7982b0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Serilog/Monaco.Template.Backend.Common.Serilog.csproj @@ -1,10 +1,6 @@ - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Serilog True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj index 78a8bd7..d8ddbd9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Tests/Monaco.Template.Backend.Common.Tests.csproj @@ -1,11 +1,5 @@ - - net10.0 - enable - enable - - $(DefineConstants);auth;commonLibraries;filesSupport diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj index aa5e67e..0c67ecb 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Monaco.Template.Backend.Domain.Tests.csproj @@ -1,10 +1,8 @@  - net10.0 - enable - enable false + true diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj index b82d651..92a4e01 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj @@ -1,11 +1,5 @@ - - net10.0 - enable - enable - - $(DefineConstants);auth;commonLibraries;filesSupport diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj index bce0699..47a5cdd 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj @@ -1,9 +1,6 @@  - net10.0 - enable - enable false true diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj index 204bfc9..d402e22 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Messages/Monaco.Template.Backend.Messages.csproj @@ -1,10 +1,6 @@  - net10.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Messages True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj index 91f1227..37a7a81 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Worker/Monaco.Template.Backend.Worker.csproj @@ -1,9 +1,6 @@  - net10.0 - enable - enable 8783ba16-eb5c-4c28-ae1d-14de7638b5c1 linux-x64 True diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.sln b/src/Content/Backend/Solution/Monaco.Template.Backend.sln index 8384932..423cbfa 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.sln +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.sln @@ -85,6 +85,7 @@ EndProject #endif Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{72505E8B-B8FE-4FB4-BFCC-03C9E7C57643}" ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props #if (apiService) Monaco.Template.Backend.Api.http = Monaco.Template.Backend.Api.http diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.slnx b/src/Content/Backend/Solution/Monaco.Template.Backend.slnx index a81cfc8..2281e19 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.slnx +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.slnx @@ -37,6 +37,7 @@ + From 14a8fc77ac05b90367505d529926893b9361a611 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Tue, 9 Dec 2025 22:45:05 +0000 Subject: [PATCH 11/30] chore: update packages --- .../Backend/Solution/Directory.Packages.props | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index dc6e90a..99bd1af 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -12,16 +12,16 @@ - - - - - - - - - - + + + + + + + + + + @@ -45,7 +45,7 @@ - + From 1a000359c9f440aeb6970459b79d34c3989e2a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Wed, 10 Dec 2025 17:15:32 +0100 Subject: [PATCH 12/30] refactor: Migrate OpenAPI docs from Swagger to Scalar.AspNetCore --- .../Backend/Solution/Directory.Packages.props | 2 +- .../Monaco.Template.Backend.Api/Program.cs | 35 ++-- .../Properties/launchSettings.json | 60 +++--- .../appsettings.json | 22 +-- .../Cors/CorsExtensions.cs | 55 +++--- .../Extensions/MiddlewareExtensions.cs | 48 +++-- .../Monaco.Template.Backend.Common.Api.csproj | 8 +- .../OpenApi/OAuth2DocumentTransformer.cs | 62 ++++++ .../OpenApi/OAuth2OperationTransformer.cs | 39 ++++ .../OpenApi/OpenApiExtensions.cs | 72 +++++++ .../Swagger/AuthorizeCheckOperationFilter.cs | 39 ---- .../Swagger/ConfigureSwaggerExtensions.cs | 182 ------------------ .../Swagger/SwaggerDefaultValues.cs | 40 ---- .../Properties/launchSettings.json | 6 +- .../Keycloak/realm-export-template.json | 8 +- .../Solution/realm-export-template.json | 8 +- 16 files changed, 309 insertions(+), 377 deletions(-) create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2DocumentTransformer.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OpenApiExtensions.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index 99bd1af..bc8fedb 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -24,6 +24,7 @@ + @@ -32,7 +33,6 @@ - diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs index 0d4502b..c6ef4bf 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Program.cs @@ -9,11 +9,11 @@ #endif using Monaco.Template.Backend.Common.Api.Cors; using Monaco.Template.Backend.Common.Api.Middleware.Extensions; -using Monaco.Template.Backend.Common.Api.Swagger; using Monaco.Template.Backend.Common.Serilog; using Monaco.Template.Backend.Common.Serilog.ApplicationInsights.TelemetryConverters; using Monaco.Template.Backend.Api.Endpoints.Extensions; using Monaco.Template.Backend.Application.Persistence; +using Monaco.Template.Backend.Common.Api.OpenApi; using Serilog; var builder = WebApplication.CreateBuilder(args); @@ -33,6 +33,7 @@ .Filter.ByIncludingOnly(x => x.Properties.ContainsKey("AuditEntries"))) .Enrich.WithOperationId() .Enrich.FromLogContext()); +builder.Services.AddSerilogContextEnricher(); // Add services to the container. var configuration = builder.Configuration; @@ -54,19 +55,13 @@ options.BlobStorage.ContainerName = configuration["BlobStorage:Container"]!; #endif }) - .ConfigureApiVersionSwagger(configuration["Swagger:ApiDescription"]!, - configuration["Swagger:Title"]!, - configuration["Swagger:Description"]!, - configuration["Swagger:ContactName"]!, - configuration["Swagger:ContactEmail"]!, -#if (!auth) - configuration["Swagger:TermsOfService"]!) +#if (auth) + .AddOpenApiDocs(configuration["Scalar:AuthEndpoint"]!, + configuration["Scalar:TokenEndpoint"]!, + configuration["SSO:Audience"]!, + Scopes.List) #else - configuration["Swagger:TermsOfService"]!, - configuration["Swagger:AuthEndpoint"], - configuration["Swagger:TokenEndpoint"], - configuration["SSO:Audience"], - Scopes.List) + .AddOpenApiDocs() #endif #if (massTransitIntegration) .AddMassTransit(cfg => @@ -80,7 +75,7 @@ // Disable it in API so only the Worker takes care of this. o.DisableInboxCleanupService(); }); - + var rabbitMqConfig = configuration.GetSection("MessageBus:RabbitMQ"); if (rabbitMqConfig.Exists()) cfg.UsingRabbitMq((_, busCfg) => busCfg.Host(rabbitMqConfig["Host"], @@ -110,11 +105,15 @@ if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage(); -#if (!auth) -app.UseSwaggerConfiguration(); +#if (auth) +app.UseOpenApiDocs(configuration["Scalar:Title"]!, + configuration["Scalar:AuthEndpoint"]!, + configuration["Scalar:TokenEndpoint"]!, + configuration["Scalar:ClientId"]!, + configuration["SSO:Audience"]!, + Scopes.List); #else -app.UseSwaggerConfiguration(configuration["SSO:SwaggerUIClientId"]!, - configuration["Swagger:SwaggerUIAppName"]!); +app.UseOpenApiDocs(configuration["Scalar:Title"]!); #endif app.UseCors() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Properties/launchSettings.json b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Properties/launchSettings.json index d51da1e..abf8634 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Properties/launchSettings.json +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Properties/launchSettings.json @@ -3,42 +3,42 @@ "http": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5050" }, - "https": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7070;http://localhost:5050" - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Container (.Net Sdk)": { - "commandName": "SdkContainer", - "launchBrowser": true, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", - "environmentVariables": { - "ASPNETCORE_HTTPS_PORTS": "8081", - "ASPNETCORE_HTTP_PORTS": "8080" - }, - "publishAllPorts": true, - "useSSL": true - } + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "scalar", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7070;http://localhost:5050" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "scalar", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (.Net Sdk)": { + "commandName": "SdkContainer", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/scalar", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json index 5c1e4b2..656fcb7 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/appsettings.json @@ -17,37 +17,29 @@ //#if (auth) "SSO": { "Authority": "http://localhost:8080/realms/monaco-template", - "Audience": "monaco-template-api", - - "SwaggerUIClientId": "monaco-template-api-swagger-ui", - "SwaggerUIClientSecret": "" + "Audience": "monaco-template-api" }, - + //#endif //#if (massTransitIntegration) "MessageBus": { "ASBConnectionString": "" }, - + //#endif //#if (filesSupport) "BlobStorage": { "ConnectionString": "UseDevelopmentStorage=true", "Container": "files-store" }, - + //#endif - "Swagger": { - "ApiDescription": "Monaco Template API", - "SwaggerUIAppName": "Monaco Template API - Swagger UI", + "Scalar": { "Title": "Monaco Template API", - "Description": "Monaco Template - API", - "ContactName": "One Beyond", - "ContactEmail": "", - "TermsOfService": "https://www.one-beyond.com", //#if (auth) "AuthEndpoint": "http://localhost:8080/realms/monaco-template/protocol/openid-connect/auth", - "TokenEndpoint": "http://localhost:8080/realms/monaco-template/protocol/openid-connect/token" + "TokenEndpoint": "http://localhost:8080/realms/monaco-template/protocol/openid-connect/token", + "ClientId": "monaco-template-api-scalar" //#endif }, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Cors/CorsExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Cors/CorsExtensions.cs index ef89e91..4fef062 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Cors/CorsExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Cors/CorsExtensions.cs @@ -13,37 +13,38 @@ public static class CorsExtensions private const string MethodsSection = "Methods"; private const string HeadersSection = "Headers"; - /// - /// Adds CORS policies configuration from the specified section name - /// /// - /// - /// - /// - public static IServiceCollection AddCorsPolicies(this IServiceCollection services, - IConfiguration configuration, - string sectionName) => - services.AddCors(x => - { - var corsConfigurations = configuration.GetSection(sectionName) - .GetChildren() - .ToList(); + extension(IServiceCollection services) + { + /// + /// Adds CORS policies configuration from the specified section name + /// + /// + /// + /// + public IServiceCollection AddCorsPolicies(IConfiguration configuration, + string sectionName) => + services.AddCors(x => + { + var corsConfigurations = configuration.GetSection(sectionName) + .GetChildren() + .ToList(); - var defaultConfig = corsConfigurations.Find(c => c[NameSection] == CorsDefaultPolicyName); - if (defaultConfig is not null) - x.AddDefaultPolicy(ConfigurePolicy(defaultConfig)); + var defaultConfig = corsConfigurations.Find(c => c[NameSection] == CorsDefaultPolicyName); + if (defaultConfig is not null) + x.AddDefaultPolicy(ConfigurePolicy(defaultConfig)); - corsConfigurations.ForEach(c => x.AddPolicy(c[NameSection]!, ConfigurePolicy(c))); - }); + corsConfigurations.ForEach(c => x.AddPolicy(c[NameSection]!, ConfigurePolicy(c))); + }); - /// - /// Adds CORS policies configuration from the default section name (CorsPolicies) - /// - /// - /// - /// - public static IServiceCollection AddCorsPolicies(this IServiceCollection services, IConfiguration configuration) => - services.AddCorsPolicies(configuration, DefaultCorsPoliciesSectionName); + /// + /// Adds CORS policies configuration from the default section name (CorsPolicies) + /// + /// + /// + public IServiceCollection AddCorsPolicies(IConfiguration configuration) => + services.AddCorsPolicies(configuration, DefaultCorsPoliciesSectionName); + } private static Action ConfigurePolicy(IConfiguration config) => p => p.WithOrigins(config.GetSection(OriginsSection) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/Extensions/MiddlewareExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/Extensions/MiddlewareExtensions.cs index a63ecd3..fd0bf70 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/Extensions/MiddlewareExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Middleware/Extensions/MiddlewareExtensions.cs @@ -1,22 +1,44 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; namespace Monaco.Template.Backend.Common.Api.Middleware.Extensions; public static class MiddlewareExtensions { - /// - /// Uses the Serilog Context Enricher middleware to inject the current user into the Serilog Context. - /// - /// - /// - public static IApplicationBuilder UseSerilogContextEnricher(this IApplicationBuilder app) => - app.UseMiddleware(); + extension(IServiceCollection services) + { + /// + /// Adds the Serilog context enricher middleware to the service collection for dependency injection. + /// + /// This method registers with a scoped lifetime. Call + /// this method during application startup to enable Serilog context enrichment for each request. + /// The same instance so that additional calls can be chained. + public IServiceCollection AddSerilogContextEnricher() => + services.AddScoped(); - /// - /// Uses a middleware for mapping all claims from a JWT token to the Context.User but without running any kind of authentication/authorization middleware - /// + /// + /// Adds the JwtClaimsMapperMiddleware to the service collection for dependency injection. + /// + /// The current IServiceCollection instance with the JwtClaimsMapperMiddleware registered. + public IServiceCollection AddJwtClaimsMapper() => + services.AddScoped(); + } + /// - /// - public static IApplicationBuilder UseJwtClaimsMapper(this IApplicationBuilder app) => - app.UseMiddleware(); + extension(IApplicationBuilder app) + { + /// + /// Uses the Serilog Context Enricher middleware to inject the current user into the Serilog Context. + /// + /// + public IApplicationBuilder UseSerilogContextEnricher() => + app.UseMiddleware(); + + /// + /// Uses a middleware for mapping all claims from a JWT token to the Context.User but without running any kind of authentication/authorization middleware + /// + /// + public IApplicationBuilder UseJwtClaimsMapper() => + app.UseMiddleware(); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj index 42a9f57..99bb627 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Monaco.Template.Backend.Common.Api.csproj @@ -14,13 +14,19 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + $(DefineConstants);auth;commonLibraries;filesSupport;massTransitIntegration + + + + - diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2DocumentTransformer.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2DocumentTransformer.cs new file mode 100644 index 0000000..f6ee96f --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2DocumentTransformer.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Monaco.Template.Backend.Common.Api.OpenApi; + +/// +/// Adds OAuth2 security scheme to the OpenAPI document components. +/// +public sealed class OAuth2DocumentTransformer : IOpenApiDocumentTransformer +{ + public const string SchemeName = "OAuth2"; + + private readonly string _authorizationUrl; + private readonly string _tokenUrl; + private readonly string _audience; + private readonly IReadOnlyList _scopes; + + public OAuth2DocumentTransformer(string authorizationUrl, + string tokenUrl, + string audience, + IReadOnlyList scopes) + { + _authorizationUrl = authorizationUrl; + _tokenUrl = tokenUrl; + _audience = audience; + _scopes = scopes; + } + + public Task TransformAsync(OpenApiDocument document, + OpenApiDocumentTransformerContext context, + CancellationToken cancellationToken) + { + document.Components ??= new(); + document.Components.SecuritySchemes ??= new Dictionary([ + new(SchemeName, + new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new() + { + AuthorizationCode = new() + { + AuthorizationUrl = new(_authorizationUrl), + TokenUrl = new(_tokenUrl), + Scopes = new Dictionary(_scopes.ToDictionary(k => k, _ => "[No description]")) + { + { _audience, "API Audience" } + } + } + } + }) + ]); + + document.Security = [new() { [new(SchemeName, document)] = [] }]; + + // Set the host document for all elements + // including the security scheme references + document.SetReferenceHostDocument(); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs new file mode 100644 index 0000000..efe07f8 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace Monaco.Template.Backend.Common.Api.OpenApi; + +/// +/// Applies per-operation OAuth2 security requirements based on endpoint authorization metadata. +/// +public class OAuth2OperationTransformer : IOpenApiOperationTransformer +{ + private readonly string _audience; + + public OAuth2OperationTransformer(string audience) + { + _audience = audience; + } + + public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken) + { + var metadata = context.Description.ActionDescriptor.EndpointMetadata; + + if (metadata.Any(m => m is IAllowAnonymous)) + { + operation.Security = []; + return Task.CompletedTask; + } + + var requiredScopes = metadata.OfType() + .Where(a => !string.IsNullOrEmpty(a.Policy)) + .Select(a => a.Policy!) + .Distinct() + .ToList(); + + operation.Security = [new() { [new(OAuth2DocumentTransformer.SchemeName, context.Document)] = [..requiredScopes, _audience] }]; + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OpenApiExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OpenApiExtensions.cs new file mode 100644 index 0000000..93c7cbc --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OpenApiExtensions.cs @@ -0,0 +1,72 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Scalar.AspNetCore; + +namespace Monaco.Template.Backend.Common.Api.OpenApi; + +public static class OpenApiExtensions +{ + extension(IServiceCollection services) + { +#if (auth) + public IServiceCollection AddOpenApiDocs(string authEndpoint, + string tokenEndpoint, + string audience, + List scopesList) => +#else + public IServiceCollection AddOpenApiDocs() => +#endif + services.AddApiVersioning(options => + { + options.ReportApiVersions = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + }) + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }) + .Services +#if (auth) + .AddOpenApi(opts => opts.AddDocumentTransformer(new OAuth2DocumentTransformer(authEndpoint, + tokenEndpoint, + audience, + scopesList)) + .AddOperationTransformer(new OAuth2OperationTransformer(audience))); +#else + .AddOpenApi(); +#endif + } + + extension(WebApplication app) + { +#if (auth) + public IApplicationBuilder UseOpenApiDocs(string title, + string authEndpoint, + string tokenEndpoint, + string clientId, + string audience, + List scopesList) +#else + public IApplicationBuilder UseOpenApiDocs(string title) +#endif + { + app.MapOpenApi(); +#if (auth) + app.MapScalarApiReference(opts => opts.WithTitle(title) + .AddAuthorizationCodeFlow("OAuth2", + flow => flow.WithAuthorizationUrl(authEndpoint) + .WithTokenUrl(tokenEndpoint) + .WithPkce(Pkce.Sha256) + .WithClientId(clientId) + .WithSelectedScopes([..scopesList, audience]))); +#else + app.MapScalarApiReference(opts => opts.WithTitle(title)); +#endif + return app; + } + } +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs deleted file mode 100644 index 104ef0f..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; -using System.Net; - -namespace Monaco.Template.Backend.Common.Api.Swagger; - -public class AuthorizeCheckOperationFilter : IOperationFilter -{ - private readonly string _audience; - - public AuthorizeCheckOperationFilter(string audience) - { - _audience = audience; - } - - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - if (context.ApiDescription - .ActionDescriptor - .EndpointMetadata - .Any(m => m is IAllowAnonymous)) - return; - - operation.Responses ??= []; - - var unauthorizedKey = ((int)HttpStatusCode.Unauthorized).ToString(); - if (!operation.Responses.ContainsKey(unauthorizedKey)) - operation.Responses.Add(unauthorizedKey, new OpenApiResponse { Description = HttpStatusCode.Unauthorized.ToString() }); - - var forbiddenKey = ((int)HttpStatusCode.Forbidden).ToString(); - if (!operation.Responses.ContainsKey(forbiddenKey)) - operation.Responses.Add(forbiddenKey, new OpenApiResponse { Description = HttpStatusCode.Forbidden.ToString() }); - - var oAuthScheme = new OpenApiSecuritySchemeReference("oauth2", context.Document); - - operation.Security = [new OpenApiSecurityRequirement { [oAuthScheme] = [_audience] }]; - } -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs deleted file mode 100644 index 97f0114..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/ConfigureSwaggerExtensions.cs +++ /dev/null @@ -1,182 +0,0 @@ -using Asp.Versioning; -using Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Monaco.Template.Backend.Common.Api.Swagger; - -public static class ConfigureSwaggerExtensions -{ - public static IServiceCollection ConfigureApiVersionSwagger(this IServiceCollection services, - string apiDescription, - string title, - string description, - string contactName, - string contactEmail, - string termsOfServiceUrl, - string? authEndpoint = null, - string? tokenEndpoint = null, - string? apiName = null, - List? scopes = null) - { - return services.AddEndpointsApiExplorer() - .AddApiVersioning(options => - { - options.ReportApiVersions = true; - options.DefaultApiVersion = new ApiVersion(1, 0); - options.AssumeDefaultVersionWhenUnspecified = true; - options.ApiVersionReader = new UrlSegmentApiVersionReader(); - }) - .AddApiExplorer(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }) - .Services - .ConfigureSwagger(apiDescription, - title, - description, - contactName, - contactEmail, - termsOfServiceUrl, - authEndpoint, - tokenEndpoint, - apiName, - scopes); - } - - public static IServiceCollection ConfigureSwagger(this IServiceCollection services, - string apiDescription, - string title, - string description, - string contactName, - string contactEmail, - string termsOfServiceUrl, - string? authEndpoint = null, - string? tokenEndpoint = null, - string? apiName = null, - List? scopesList = null) => - services.AddTransient, SwaggerOptions>(provider => new SwaggerOptions(provider.GetRequiredService(), - title, - description, - contactName, - contactEmail, - termsOfServiceUrl)) - .AddSwaggerGen(options => - { - // add a custom operation filter which sets default values - options.OperationFilter(); - options.CustomSchemaIds(x => x.FullName); - // integrate xml comments - var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml"); - foreach (var xmlFile in xmlFiles) - options.IncludeXmlComments(xmlFile); - - if (authEndpoint is not null && tokenEndpoint is not null && apiName is not null && scopesList is not null) - { - // Add security for authenticated APIs - options.AddSecurityDefinition("oauth2", - new OpenApiSecurityScheme - { - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows - { - AuthorizationCode = new OpenApiOAuthFlow - { - AuthorizationUrl = new Uri(authEndpoint), - TokenUrl = new Uri(tokenEndpoint), - Scopes = new Dictionary(scopesList.ToDictionary(x => x, _ => "")) { { apiName, apiDescription } } - } - } - }); - options.OperationFilter(apiName); - } - }); - - public static IApplicationBuilder UseSwaggerConfiguration(this WebApplication app, - string? clientId = null, - string? appName = null) => - app.UseSwagger() // Enable middleware to serve generated Swagger as a JSON endpoint. - .UseSwaggerUI(options => - { // build a swagger endpoint for each discovered API version - var apiVersions = app.DescribeApiVersions(); - foreach (var groupName in apiVersions.Select(x => x.GroupName)) - options.SwaggerEndpoint($"{groupName}/swagger.json", groupName.ToUpperInvariant()); - - if (clientId is not null && appName is not null) - { - options.OAuthClientId(clientId); - options.OAuthAppName(appName); - options.OAuthScopeSeparator(" "); - options.OAuthUsePkce(); - } - }); - - /// - /// Configures the Swagger generation options. - /// - /// This allows API versioning to define a Swagger document per API version after the - /// service has been resolved from the service container. - public class SwaggerOptions : IConfigureOptions - { - private readonly IApiVersionDescriptionProvider _provider; - private readonly string _title; - private readonly string _description; - private readonly string _contactName; - private readonly string _contactEmail; - private readonly string _termsOfServiceUrl; - - /// - /// Initializes a new instance of the class. - /// - /// The provider used to generate Swagger documents. - /// - /// - /// - /// - /// - public SwaggerOptions(IApiVersionDescriptionProvider provider, - string title, - string description, - string contactName, - string contactEmail, - string termsOfServiceUrl) - { - _provider = provider; - _title = title; - _description = description; - _contactName = contactName; - _contactEmail = contactEmail; - _termsOfServiceUrl = termsOfServiceUrl; - } - - /// - public void Configure(SwaggerGenOptions options) - { - // add a swagger document for each discovered API version - // note: you might choose to skip or document deprecated API versions differently - foreach (var description in _provider.ApiVersionDescriptions) - options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); - } - - private OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) - { - var info = new OpenApiInfo - { - Title = _title, - Version = description.ApiVersion.ToString(), - Description = _description, - Contact = new OpenApiContact { Name = _contactName, Email = _contactEmail }, - TermsOfService = new Uri(_termsOfServiceUrl) - }; - - if (description.IsDeprecated) - info.Description += " This API version has been deprecated."; - - return info; - } - } -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs deleted file mode 100644 index f443b9f..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Monaco.Template.Backend.Common.Api.Swagger; - -/// -/// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. -/// -/// -/// This is only required due to bugs in the . -/// Once they are fixed and published, this class can be removed. -/// -public class SwaggerDefaultValues : IOperationFilter -{ - /// - /// Applies the filter to the specified operation using the given context. - /// - /// The operation to apply the filter to. - /// The current operation filter context. - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - var apiDescription = context.ApiDescription; - operation.Deprecated |= apiDescription.IsDeprecated(); - - if (operation.Parameters == null || operation.Parameters.Count == 0) - return; - - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412 - // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413 - foreach (var parameter in operation.Parameters) - { - var description = apiDescription.ParameterDescriptions.FirstOrDefault(p => p.Name == parameter.Name); - if (description is null) - continue; - - parameter.Description ??= description.ModelMetadata?.Description; - } - } -} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Properties/launchSettings.json b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Properties/launchSettings.json index f273864..90a8e59 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Properties/launchSettings.json +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.ApiGateway/Properties/launchSettings.json @@ -3,7 +3,7 @@ "http": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -13,7 +13,7 @@ "https": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -23,7 +23,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "scalar", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Imports/Keycloak/realm-export-template.json b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Imports/Keycloak/realm-export-template.json index 24a3536..c3d4c3b 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Imports/Keycloak/realm-export-template.json +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Imports/Keycloak/realm-export-template.json @@ -270,7 +270,7 @@ "attributes": {} } ], - "monaco-template-api-swagger-ui": [], + "monaco-template-api-scalar": [], "monaco-template-front": [], "security-admin-console": [], "monaco-template-backend": [], @@ -684,9 +684,9 @@ }, { "id": "b6049a46-fbd1-4368-a72b-364c284f01e0", - "clientId": "monaco-template-api-swagger-ui", - "name": "Monaco Template API Swagger", - "description": "Monaco Template API - Swagger client", + "clientId": "monaco-template-api-scalar", + "name": "Monaco Template API Scalar", + "description": "Monaco Template API - Scalar client", "rootUrl": "", "adminUrl": "", "baseUrl": "", diff --git a/src/Content/Backend/Solution/realm-export-template.json b/src/Content/Backend/Solution/realm-export-template.json index 98d22c4..244f6f3 100644 --- a/src/Content/Backend/Solution/realm-export-template.json +++ b/src/Content/Backend/Solution/realm-export-template.json @@ -246,7 +246,7 @@ "containerId" : "05648945-7e90-42ec-a4dc-233fd52cddfe", "attributes" : { } } ], - "monaco-template-api-swagger-ui" : [ ], + "monaco-template-api-scalar" : [ ], "monaco-template-front" : [ ], "security-admin-console" : [ ], "monaco-template-backend" : [ ], @@ -625,9 +625,9 @@ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id" : "b6049a46-fbd1-4368-a72b-364c284f01e0", - "clientId" : "monaco-template-api-swagger-ui", - "name" : "Monaco Template API Swagger", - "description" : "Monaco Template API - Swagger client", + "clientId" : "monaco-template-api-scalar", + "name" : "Monaco Template API Scalar", + "description" : "Monaco Template API - Scalar client", "rootUrl" : "", "adminUrl" : "", "baseUrl" : "", From 3d515d7ce4e8d4f57f8b4334ad0a189c58d2e0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Thu, 11 Dec 2025 01:53:27 +0100 Subject: [PATCH 13/30] refactor: finished converting the last missing extension methods into block extension methods. --- .../Endpoints/Companies.cs | 93 ++++++------- .../Endpoints/Countries.cs | 33 ++--- .../Endpoints/Files.cs | 37 +++--- .../Endpoints/Products.cs | 123 +++++++++--------- .../Factories/Entities/EntityFactory.cs | 28 ++-- .../Factories/Entities/EnumerationFactory.cs | 49 ++++--- .../Factories/FixtureExtensions.cs | 9 +- .../Factories/Entities/AddressFactory.cs | 45 ++++--- .../Factories/Entities/CompanyFactory.cs | 50 +++---- .../Factories/Entities/CountryFactory.cs | 34 ++--- .../Factories/Entities/DocumentFactory.cs | 25 ++-- .../Factories/Entities/FileFactory.cs | 89 +++++++------ .../Factories/Entities/ImageFactory.cs | 54 ++++---- .../Factories/Entities/ProductFactory.cs | 80 ++++++------ .../ApiRoutes.cs | 26 ++-- 15 files changed, 397 insertions(+), 378 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs index a87908c..baf97d7 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs @@ -17,72 +17,75 @@ namespace Monaco.Template.Backend.Api.Endpoints; internal static class Companies { - public static IEndpointRouteBuilder AddCompanies(this IEndpointRouteBuilder builder, ApiVersionSet versionSet) + extension(IEndpointRouteBuilder builder) { - var companies = builder.CreateApiGroupBuilder(versionSet, "Companies"); + public IEndpointRouteBuilder AddCompanies(ApiVersionSet versionSet) + { + var companies = builder.CreateApiGroupBuilder(versionSet, "Companies"); - companies.MapGet("", - Task>, NotFound>> ([FromServices] ISender sender, - HttpRequest request) => - sender.ExecuteQueryAsync(new GetCompanyPage.Query(request.Query)), - "GetCompanies", + companies.MapGet("", + Task>, NotFound>> ([FromServices] ISender sender, + HttpRequest request) => + sender.ExecuteQueryAsync(new GetCompanyPage.Query(request.Query)), + "GetCompanies", #if (!auth) - "Gets a page of companies"); + "Gets a page of companies"); #else - "Gets a page of companies") - .RequireAuthorization(Scopes.CompaniesRead); + "Gets a page of companies") + .RequireAuthorization(Scopes.CompaniesRead); #endif - companies.MapGet("{id:guid}", - Task, NotFound>> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteQueryAsync(new GetCompanyById.Query(id)), - "GetCompany", + companies.MapGet("{id:guid}", + Task, NotFound>> ([FromServices] ISender sender, + [FromRoute] Guid id) => + sender.ExecuteQueryAsync(new GetCompanyById.Query(id)), + "GetCompany", #if (!auth) - "Gets a company by Id"); + "Gets a company by Id"); #else - "Gets a company by Id") - .RequireAuthorization(Scopes.CompaniesRead); + "Gets a company by Id") + .RequireAuthorization(Scopes.CompaniesRead); #endif - companies.MapPost("", - Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, - [FromBody] CompanyCreateEditDto dto, - HttpContext context) => - sender.ExecuteCommandAsync(dto.MapCreateCommand(), "api/v{0}/Companies/{1}", context.GetRequestedApiVersion()!), - "CreateCompany", + companies.MapPost("", + Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, + [FromBody] CompanyCreateEditDto dto, + HttpContext context) => + sender.ExecuteCommandAsync(dto.MapCreateCommand(), "api/v{0}/Companies/{1}", context.GetRequestedApiVersion()!), + "CreateCompany", #if (!auth) - "Create a new company"); + "Create a new company"); #else - "Create a new company") - .RequireAuthorization(Scopes.CompaniesWrite); + "Create a new company") + .RequireAuthorization(Scopes.CompaniesWrite); #endif - companies.MapPut("{id:guid}", - Task> ([FromServices] ISender sender, - [FromRoute] Guid id, - [FromBody] CompanyCreateEditDto dto) => - sender.ExecuteCommandEditAsync(dto.MapEditCommand(id)), - "EditCompany", + companies.MapPut("{id:guid}", + Task> ([FromServices] ISender sender, + [FromRoute] Guid id, + [FromBody] CompanyCreateEditDto dto) => + sender.ExecuteCommandEditAsync(dto.MapEditCommand(id)), + "EditCompany", #if (!auth) - "Edit an existing company by Id"); + "Edit an existing company by Id"); #else - "Edit an existing company by Id") - .RequireAuthorization(Scopes.CompaniesWrite); + "Edit an existing company by Id") + .RequireAuthorization(Scopes.CompaniesWrite); #endif - companies.MapDelete("{id:guid}", - Task> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteCommandDeleteAsync(new DeleteCompany.Command(id)), - "DeleteCompany", + companies.MapDelete("{id:guid}", + Task> ([FromServices] ISender sender, + [FromRoute] Guid id) => + sender.ExecuteCommandDeleteAsync(new DeleteCompany.Command(id)), + "DeleteCompany", #if (!auth) - "Delete an existing company by Id"); + "Delete an existing company by Id"); #else - "Delete an existing company by Id") - .RequireAuthorization(Scopes.CompaniesWrite); + "Delete an existing company by Id") + .RequireAuthorization(Scopes.CompaniesWrite); #endif - return builder; + return builder; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Countries.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Countries.cs index 7895332..0901a27 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Countries.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Countries.cs @@ -11,24 +11,27 @@ namespace Monaco.Template.Backend.Api.Endpoints; internal static class Countries { - public static IEndpointRouteBuilder AddCountries(this IEndpointRouteBuilder builder, ApiVersionSet versionSet) + extension(IEndpointRouteBuilder builder) { - var countries = builder.CreateApiGroupBuilder(versionSet, "Countries"); + public IEndpointRouteBuilder AddCountries(ApiVersionSet versionSet) + { + var countries = builder.CreateApiGroupBuilder(versionSet, "Countries"); - countries.MapGet("", - Task>, NotFound>> ([FromServices] ISender sender, - HttpRequest request) => - sender.ExecuteQueryAsync(new GetCountryList.Query(request.Query)), - "GetCountries", - "Gets a list of countries"); + countries.MapGet("", + Task>, NotFound>> ([FromServices] ISender sender, + HttpRequest request) => + sender.ExecuteQueryAsync(new GetCountryList.Query(request.Query)), + "GetCountries", + "Gets a list of countries"); - countries.MapGet("{id:guid}", - Task, NotFound>> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteQueryAsync(new GetCountryById.Query(id)), - "GetCountry", - "Gets a country by Id"); + countries.MapGet("{id:guid}", + Task, NotFound>> ([FromServices] ISender sender, + [FromRoute] Guid id) => + sender.ExecuteQueryAsync(new GetCountryById.Query(id)), + "GetCountry", + "Gets a country by Id"); - return builder; + return builder; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs index c45c325..c5abef9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs @@ -13,28 +13,31 @@ namespace Monaco.Template.Backend.Api.Endpoints; internal static class Files { - public static IEndpointRouteBuilder AddFiles(this IEndpointRouteBuilder builder, ApiVersionSet versionSet) + extension(IEndpointRouteBuilder builder) { - var files = builder.CreateApiGroupBuilder(versionSet, "Files"); + public IEndpointRouteBuilder AddFiles(ApiVersionSet versionSet) + { + var files = builder.CreateApiGroupBuilder(versionSet, "Files"); - files.MapPost("", - Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, - IFormFile file, - HttpContext context) => - sender.ExecuteCommandAsync(new CreateFile.Command(file.OpenReadStream(), - file.FileName, - file.ContentType), - "api/v{0}/Files/{1}", - context.GetRequestedApiVersion()!), - "CreateFile", - "Upload and create a new file") + files.MapPost("", + Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, + IFormFile file, + HttpContext context) => + sender.ExecuteCommandAsync(new CreateFile.Command(file.OpenReadStream(), + file.FileName, + file.ContentType), + "api/v{0}/Files/{1}", + context.GetRequestedApiVersion()!), + "CreateFile", + "Upload and create a new file") #if (!auth) - .DisableAntiforgery(); + .DisableAntiforgery(); #else - .DisableAntiforgery() - .RequireAuthorization(Scopes.FilesWrite); + .DisableAntiforgery() + .RequireAuthorization(Scopes.FilesWrite); #endif - return builder; + return builder; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs index cac1c29..bc827af 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs @@ -17,91 +17,94 @@ namespace Monaco.Template.Backend.Api.Endpoints; internal static class Products { - public static IEndpointRouteBuilder AddProducts(this IEndpointRouteBuilder builder, ApiVersionSet versionSet) + extension(IEndpointRouteBuilder builder) { - var products = builder.CreateApiGroupBuilder(versionSet, "Products"); + public IEndpointRouteBuilder AddProducts(ApiVersionSet versionSet) + { + var products = builder.CreateApiGroupBuilder(versionSet, "Products"); - products.MapGet("", - Task>, NotFound>> ([FromServices] ISender sender, - HttpRequest request) => - sender.ExecuteQueryAsync(new GetProductPage.Query(request.Query)), - "GetProducts", + products.MapGet("", + Task>, NotFound>> ([FromServices] ISender sender, + HttpRequest request) => + sender.ExecuteQueryAsync(new GetProductPage.Query(request.Query)), + "GetProducts", #if (!auth) - "Gets a page of products"); + "Gets a page of products"); #else - "Gets a page of products") - .AllowAnonymous(); + "Gets a page of products") + .AllowAnonymous(); #endif - products.MapGet("{id:guid}", - Task, NotFound>> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteQueryAsync(new GetProductById.Query(id)), - "GetProduct", + products.MapGet("{id:guid}", + Task, NotFound>> ([FromServices] ISender sender, + [FromRoute] Guid id) => + sender.ExecuteQueryAsync(new GetProductById.Query(id)), + "GetProduct", #if (!auth) - "Gets a product by Id"); + "Gets a product by Id"); #else - "Gets a product by Id") - .AllowAnonymous(); + "Gets a product by Id") + .AllowAnonymous(); #endif - products.MapPost("", - Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, - [FromBody] ProductCreateEditDto dto, - HttpContext context) => - sender.ExecuteCommandAsync(dto.Map(), - "api/v{0}/Products/{1}", - context.GetRequestedApiVersion()!), - "CreateProduct", + products.MapPost("", + Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, + [FromBody] ProductCreateEditDto dto, + HttpContext context) => + sender.ExecuteCommandAsync(dto.Map(), + "api/v{0}/Products/{1}", + context.GetRequestedApiVersion()!), + "CreateProduct", #if (!auth) - "Create a new product"); + "Create a new product"); #else - "Create a new product") - .RequireAuthorization(Scopes.ProductsWrite); + "Create a new product") + .RequireAuthorization(Scopes.ProductsWrite); #endif - products.MapPut("{id:guid}", - Task> ([FromServices] ISender sender, - [FromRoute] Guid id, - [FromBody] ProductCreateEditDto dto) => - sender.ExecuteCommandEditAsync(dto.Map(id)), - "EditProduct", + products.MapPut("{id:guid}", + Task> ([FromServices] ISender sender, + [FromRoute] Guid id, + [FromBody] ProductCreateEditDto dto) => + sender.ExecuteCommandEditAsync(dto.Map(id)), + "EditProduct", #if (!auth) - "Edit an existing product by Id"); + "Edit an existing product by Id"); #else - "Edit an existing product by Id") - .RequireAuthorization(Scopes.ProductsWrite); + "Edit an existing product by Id") + .RequireAuthorization(Scopes.ProductsWrite); #endif - products.MapDelete("{id:guid}", - Task> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteCommandDeleteAsync(new DeleteProduct.Command(id)), - "DeleteProduct", + products.MapDelete("{id:guid}", + Task> ([FromServices] ISender sender, + [FromRoute] Guid id) => + sender.ExecuteCommandDeleteAsync(new DeleteProduct.Command(id)), + "DeleteProduct", #if (!auth) - "Delete an existing product by Id"); + "Delete an existing product by Id"); #else - "Delete an existing product by Id") - .RequireAuthorization(Scopes.ProductsWrite); + "Delete an existing product by Id") + .RequireAuthorization(Scopes.ProductsWrite); #endif - products.MapGet("{productId:guid}/Pictures/{pictureId:guid}", - Task> ([FromServices] ISender sender, - [FromRoute] Guid productId, - [FromRoute] Guid pictureId, - HttpRequest request) => - sender.ExecuteFileDownloadAsync(new DownloadProductPicture.Query(productId, - pictureId, - request.Query)), - "DownloadProductPicture", - "Download a picture from a product by Id") + products.MapGet("{productId:guid}/Pictures/{pictureId:guid}", + Task> ([FromServices] ISender sender, + [FromRoute] Guid productId, + [FromRoute] Guid pictureId, + HttpRequest request) => + sender.ExecuteFileDownloadAsync(new DownloadProductPicture.Query(productId, + pictureId, + request.Query)), + "DownloadProductPicture", + "Download a picture from a product by Id") #if (!auth) - .Produces(StatusCodes.Status200OK); + .Produces(StatusCodes.Status200OK); #else - .Produces(StatusCodes.Status200OK) - .AllowAnonymous(); + .Produces(StatusCodes.Status200OK) + .AllowAnonymous(); #endif - return builder; + return builder; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EntityFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EntityFactory.cs index 0b2ed37..3c363ba 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EntityFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EntityFactory.cs @@ -10,20 +10,20 @@ public static class EntityFactory public static Entity CreateMock(Guid? id = null) => FixtureFactory.Create(f => f.RegisterEntityMock(id)) .Create(); -} - -public static class EntityFactoryExtension -{ - public static IFixture RegisterEntityMock(this IFixture fixture, Guid? id = null) + + extension(IFixture fixture) { - fixture.Register(() => - { - var mock = new Mock(id ?? fixture.Create()) - { - CallBase = true - }; - return mock.Object; - }); - return fixture; + public IFixture RegisterEntityMock(Guid? id = null) + { + fixture.Register(() => + { + var mock = new Mock(id ?? fixture.Create()) + { + CallBase = true + }; + return mock.Object; + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EnumerationFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EnumerationFactory.cs index 29f9cc0..40b2b4a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EnumerationFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/Entities/EnumerationFactory.cs @@ -10,31 +10,30 @@ public static class EnumerationFactory public static Enumeration CreateMock((Guid Id, string Name)? value = null) => FixtureFactory.Create(f => f.RegisterEnumerationMock(value)) .Create(); -} - -public static class EnumerationFactoryExtension -{ - public static IFixture RegisterEnumerationMock(this IFixture fixture, - (Guid Id, string Name)? value = null) + + extension(IFixture fixture) { - fixture.Register(() => - { - var mock = new Mock(value.HasValue - ? - [ - value.Value.Id, - value.Value.Name - ] - : - [ - fixture.Create(), - fixture.Create() - ]) - { - CallBase = true - }; - return mock.Object; - }); - return fixture; + public IFixture RegisterEnumerationMock((Guid Id, string Name)? value = null) + { + fixture.Register(() => + { + var mock = new Mock(value.HasValue + ? + [ + value.Value.Id, + value.Value.Name + ] + : + [ + fixture.Create(), + fixture.Create() + ]) + { + CallBase = true + }; + return mock.Object; + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/FixtureExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/FixtureExtensions.cs index 4988856..dbc4af6 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/FixtureExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain.Tests/Factories/FixtureExtensions.cs @@ -5,7 +5,10 @@ namespace Monaco.Template.Backend.Common.Domain.Tests.Factories; public static class FixtureExtensions { - public static IFixture RegisterMockFactories(this IFixture fixture) => - fixture.RegisterEntityMock() - .RegisterEnumerationMock(); + extension(IFixture fixture) + { + public IFixture RegisterMockFactories() => + fixture.RegisterEntityMock() + .RegisterEnumerationMock(); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/AddressFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/AddressFactory.cs index 4371d3e..a366c11 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/AddressFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/AddressFactory.cs @@ -21,28 +21,31 @@ public static IEnumerable
CreateMany() => public static class AddressFactoryExtensions { - public static IFixture RegisterAddress(this IFixture fixture) + extension(IFixture fixture) { - fixture.Register(() => new Address(fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create()?[..Address.PostCodeLength], - fixture.Create())); - return fixture; - } + public IFixture RegisterAddress() + { + fixture.Register(() => new Address(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create()?[..Address.PostCodeLength], + fixture.Create())); + return fixture; + } - public static IFixture RegisterAddressMock(this IFixture fixture) - { - fixture.Register(() => - { - var country = fixture.Create(); - var mock = new Mock
(fixture.Create()!, - fixture.Create()!, - fixture.Create()!, - fixture.Create()?[..Address.PostCodeLength]!, - country); - return mock.Object; - }); - return fixture; + public IFixture RegisterAddressMock() + { + fixture.Register(() => + { + var country = fixture.Create(); + var mock = new Mock
(fixture.Create()!, + fixture.Create()!, + fixture.Create()!, + fixture.Create()?[..Address.PostCodeLength]!, + country); + return mock.Object; + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CompanyFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CompanyFactory.cs index ae86396..44143d9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CompanyFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CompanyFactory.cs @@ -30,33 +30,33 @@ public static Mock Mock() return mock; } -} - -public static class CompanyFactoryExtension -{ - public static IFixture RegisterCompany(this IFixture fixture) - { - fixture.Register(() => new Company(fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create
())); - return fixture; - } - - public static IFixture RegisterCompanyMock(this IFixture fixture) + + extension(IFixture fixture) { - fixture.Register(() => - { - var mock = new Mock(fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create
()); - mock.SetupGet(x => x.Id).Returns(Guid.NewGuid()); + public IFixture RegisterCompany() + { + fixture.Register(() => new Company(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create
())); + return fixture; + } + + public IFixture RegisterCompanyMock() + { + fixture.Register(() => + { + var mock = new Mock(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create
()); + mock.SetupGet(x => x.Id).Returns(Guid.NewGuid()); #if (filesSupport) - mock.SetupGet(x => x.Products).Returns(new List()); + mock.SetupGet(x => x.Products).Returns(new List()); #endif - return mock.Object; - }); - return fixture; + return mock.Object; + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CountryFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CountryFactory.cs index 5762d49..d2fcf7c 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CountryFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/CountryFactory.cs @@ -14,25 +14,25 @@ public static Country Create() => public static IEnumerable CreateMany() => FixtureFactory.Create(f => f.RegisterCountryMock()) .CreateMany(); -} - -public static class CountryFactoryExtensions -{ - public static IFixture RegisterCountry(this IFixture fixture) + + extension(IFixture fixture) { - fixture.Register(() => new Country(fixture.Create())); + public IFixture RegisterCountry() + { + fixture.Register(() => new Country(fixture.Create())); - return fixture; - } + return fixture; + } - public static IFixture RegisterCountryMock(this IFixture fixture) - { - fixture.Register(() => - { - var mock = new Mock(fixture.Create()); - mock.SetupGet(x => x.Id).Returns(Guid.NewGuid()); - return mock.Object; - }); - return fixture; + public IFixture RegisterCountryMock() + { + fixture.Register(() => + { + var mock = new Mock(fixture.Create()); + mock.SetupGet(x => x.Id).Returns(Guid.NewGuid()); + return mock.Object; + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/DocumentFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/DocumentFactory.cs index d07f50b..8492383 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/DocumentFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/DocumentFactory.cs @@ -1,6 +1,7 @@ using AutoFixture; using Monaco.Template.Backend.Common.Tests; using Monaco.Template.Backend.Domain.Model.Entities; +using File = Monaco.Template.Backend.Domain.Model.Entities.File; namespace Monaco.Template.Backend.Domain.Tests.Factories.Entities; @@ -13,18 +14,18 @@ public static Document Create() => public static IEnumerable CreateMany() => FixtureFactory.Create(f => f.RegisterDocument()) .CreateMany(); -} - -public static class DocumentFactoryExtension -{ - public static IFixture RegisterDocument(this IFixture fixture) + + extension(IFixture fixture) { - fixture.Register(() => new Document(fixture.Create(), - fixture.Create(), - fixture.Create()[..Document.ExtensionLength], - fixture.Create(), - fixture.Create(), - false)); - return fixture; + public IFixture RegisterDocument() + { + fixture.Register(() => new Document(fixture.Create(), + fixture.Create(), + fixture.Create()[..File.ExtensionLength], + fixture.Create(), + fixture.Create(), + false)); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/FileFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/FileFactory.cs index 1f65892..245b554 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/FileFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/FileFactory.cs @@ -16,53 +16,52 @@ public static File CreateMock((Guid Id, DateTime UploadedOn)? value = null) => FixtureFactory.Create(f => f.RegisterFileMock(value)) .Create(); -} - -public static class FileFactoryExtensions -{ - public static IFixture RegisterFileMock(this IFixture fixture, - (Guid Id, - string Name, - string Extension, - long Size, - string ContentType, - bool IsTemp, - DateTime UploadedOn)? value = null) + + extension(IFixture fixture) { - fixture.Register(() => - { - try - { - var mock = new Mock(value.HasValue - ? - [ - value.Value.Id, - value.Value.Name, - value.Value.Extension, - value.Value.Size, - value.Value.ContentType, - value.Value.IsTemp - ] - : - [ - fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create() - ]) { CallBase = true }; - mock.SetupGet(x => x.UploadedOn) - .Returns(value?.UploadedOn ?? fixture.Create()); - return mock.Object; - } - catch (Exception e) + public IFixture RegisterFileMock((Guid Id, + string Name, + string Extension, + long Size, + string ContentType, + bool IsTemp, + DateTime UploadedOn)? value = null) + { + fixture.Register(() => { - Console.WriteLine(e); - throw; - } + try + { + var mock = new Mock(value.HasValue + ? + [ + value.Value.Id, + value.Value.Name, + value.Value.Extension, + value.Value.Size, + value.Value.ContentType, + value.Value.IsTemp + ] + : + [ + fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create() + ]) { CallBase = true }; + mock.SetupGet(x => x.UploadedOn) + .Returns(value?.UploadedOn ?? fixture.Create()); + return mock.Object; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } - }); - return fixture; + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ImageFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ImageFactory.cs index f058d7d..a28e670 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ImageFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ImageFactory.cs @@ -13,34 +13,34 @@ public static Image Create() => public static IEnumerable CreateMany() => FixtureFactory.Create(f => f.RegisterImage()) .CreateMany(); -} - -public static class ImageFactoryExtension -{ - public static IFixture RegisterImage(this IFixture fixture) + + extension(IFixture fixture) { - fixture.Register(() => - { - var name = fixture.Create(); - const string extension = ".png"; - var size = fixture.Create(); - const string contentType = "image/png"; + public IFixture RegisterImage() + { + fixture.Register(() => + { + var name = fixture.Create(); + const string extension = ".png"; + var size = fixture.Create(); + const string contentType = "image/png"; - return new Image(fixture.Create(), - name, - extension, - size, - contentType, - false, - (fixture.Create(), - fixture.Create()), - fixture.Create(), - null, - (fixture.Create(), - size, - (fixture.Create(), - fixture.Create()))); - }); - return fixture; + return new Image(fixture.Create(), + name, + extension, + size, + contentType, + false, + (fixture.Create(), + fixture.Create()), + fixture.Create(), + null, + (fixture.Create(), + size, + (fixture.Create(), + fixture.Create()))); + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs index 3e530b9..2d40733 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs @@ -20,49 +20,49 @@ public static IEnumerable CreateMany() => .RegisterCompanyMock() .RegisterProductMock()) .CreateMany(); -} - -public static class ProductFactoryExtension -{ - public static IFixture RegisterProduct(this IFixture fixture) + + extension(IFixture fixture) { - fixture.Register(() => - { - var images = fixture.CreateMany().ToList(); - var product = new Product(fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create(), - images, - images.First()); + public IFixture RegisterProduct() + { + fixture.Register(() => + { + var images = fixture.CreateMany().ToList(); + var product = new Product(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + images, + images.First()); - return product; - }); - return fixture; - } + return product; + }); + return fixture; + } - public static IFixture RegisterProductMock(this IFixture fixture) - { - fixture.Register(() => - { - var images = fixture.CreateMany().ToList(); - var mock = new Mock(fixture.Create(), - fixture.Create(), - fixture.Create(), - fixture.Create(), - images, - images.First()); - mock.SetupGet(x => x.Id) - .Returns(Guid.NewGuid()); - mock.SetupGet(x => x.Company) - .Returns(fixture.Create()); - mock.SetupGet(x => x.Pictures) - .Returns([.. images]); - mock.SetupGet(x => x.DefaultPicture) - .Returns(images.First()); + public IFixture RegisterProductMock() + { + fixture.Register(() => + { + var images = fixture.CreateMany().ToList(); + var mock = new Mock(fixture.Create(), + fixture.Create(), + fixture.Create(), + fixture.Create(), + images, + images.First()); + mock.SetupGet(x => x.Id) + .Returns(Guid.NewGuid()); + mock.SetupGet(x => x.Company) + .Returns(fixture.Create()); + mock.SetupGet(x => x.Pictures) + .Returns([.. images]); + mock.SetupGet(x => x.DefaultPicture) + .Returns(images.First()); - return mock.Object; - }); - return fixture; + return mock.Object; + }); + return fixture; + } } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/ApiRoutes.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/ApiRoutes.cs index 5015ba6..7f71b8a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/ApiRoutes.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/ApiRoutes.cs @@ -13,18 +13,20 @@ internal static class ApiRoutes private const string OffsetParamName = "offset"; private const string LimitParamName = "limit"; - private static Url Expand(this Url url, - bool expand, - string paramName) => - expand - ? url.AppendQueryParam(ExpandParamName, paramName) - : url; - - private static Url Offset(this Url url, int? offset = null) => - url.SetQueryParam(OffsetParamName, offset); - - private static Url Limit(this Url url, int? limit = null) => - url.SetQueryParam(LimitParamName, limit); + extension(Url url) + { + private Url Expand(bool expand, + string paramName) => + expand + ? url.AppendQueryParam(ExpandParamName, paramName) + : url; + + private Url Offset(int? offset = null) => + url.SetQueryParam(OffsetParamName, offset); + + private Url Limit(int? limit = null) => + url.SetQueryParam(LimitParamName, limit); + } public static class Companies From 226b0e6e9ff8af867abb4280b6334be92b10f92f Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 15 Dec 2025 17:54:07 +0000 Subject: [PATCH 14/30] chore: update packages --- src/Content/Backend/Solution/Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index bc8fedb..160c452 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -24,7 +24,7 @@ - + @@ -71,7 +71,7 @@ - + From 67f4af3b686acd7e4e8f8c8136f9e471d3233435 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 15 Dec 2025 18:02:18 +0000 Subject: [PATCH 15/30] chore: remove RequiresAuthentication from integration tests when the auth flag is false as base property is also removed --- .../IntegrationTest.cs | 2 +- .../Tests/CountriesTests.cs | 4 +--- .../Tests/FilesTests.cs | 2 -- .../Tests/ProductsTests.cs | 8 +++----- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs index 861c1a8..5fdf7b9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs @@ -91,7 +91,7 @@ public virtual async Task InitializeAsync() #if (apiService && auth) protected virtual async Task SetupAccessToken(string audienceClientId, string[] roles, - string[] scopes) + string[] scopes) { if (!RequiresAuthentication) return; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs index a13bf97..4ff26ea 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs @@ -17,8 +17,6 @@ public CountriesTests(AppFixture fixture) : base(fixture) #if (auth) protected override bool RequiresAuthentication => true; -#else - protected override bool RequiresAuthentication => false; #endif public override async Task InitializeAsync() @@ -34,7 +32,7 @@ public override async Task InitializeAsync() public async Task GetCountriesSucceeds() { var response = await CreateRequest(ApiRoutes.Countries.Query()).GetAsync(); - + response.StatusCode .Should() .Be((int)HttpStatusCode.OK); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs index a61c11f..691691d 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs @@ -18,8 +18,6 @@ public FilesTests(AppFixture fixture) : base(fixture) #if (auth) protected override bool RequiresAuthentication => true; -#else - protected override bool RequiresAuthentication => false; #endif public override async Task InitializeAsync() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index c986ee7..4a55307 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -29,8 +29,6 @@ public ProductsTests(AppFixture fixture) : base(fixture) #if (auth) protected override bool RequiresAuthentication => true; -#else - protected override bool RequiresAuthentication => false; #endif public override async Task InitializeAsync() @@ -224,14 +222,14 @@ public async Task DownloadProductPictureThumbnailSucceeds() await DownloadProductPictureTest(productId, pictureId, true); } - private async Task DownloadProductPictureTest(Guid productId, + private async Task DownloadProductPictureTest(Guid productId, Guid pictureId, bool? isThumbnail = null) { var response = await CreateRequest(ApiRoutes.Products.DownloadPicture(productId, pictureId, isThumbnail)).GetAsync(); - + var picture = await GetDbContext().Set() .AsNoTracking() .Where(x => x.Id == pictureId) @@ -326,7 +324,7 @@ [.. tempImages.Select(i => i.Id)], { i.Should() .BeOneOf(tempImages); - + i.IsTemp .Should() .BeFalse(); From ba2d991e560e9597be405b3e15db4fbc36b85808 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 15 Dec 2025 18:11:44 +0000 Subject: [PATCH 16/30] feat: remove sln file --- .../Solution/.template.config/template.json | 3 - .../Solution/Monaco.Template.Backend.sln | 258 ------------------ 2 files changed, 261 deletions(-) delete mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.sln diff --git a/src/Content/Backend/Solution/.template.config/template.json b/src/Content/Backend/Solution/.template.config/template.json index 49be5a4..adc4ba2 100644 --- a/src/Content/Backend/Solution/.template.config/template.json +++ b/src/Content/Backend/Solution/.template.config/template.json @@ -14,9 +14,6 @@ "type": "project" }, "primaryOutputs": [ - { - "path": "Monaco.Template.Backend.sln" - }, { "path": "Monaco.Template.Backend.slnx" } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.sln b/src/Content/Backend/Solution/Monaco.Template.Backend.sln deleted file mode 100644 index 423cbfa..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.sln +++ /dev/null @@ -1,258 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31808.319 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{1095FB23-B2A1-4FD6-BC12-433529EEA56E}" - ProjectSection(SolutionItems) = preProject - nuget.config = nuget.config - EndProjectSection -EndProject -#if (commonLibraries) -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{8BFE9C37-2620-4156-88B8-286537954C5E}" -EndProject -#if (tests) -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{35484293-234F-40DA-B430-A95170EDE449}" -EndProject -#endif -#if (keycloakConfig) -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "KeyCloak", "KeyCloak", "{29DFBC35-3DA6-4C68-AA97-5CCE06D80917}" - ProjectSection(SolutionItems) = preProject - realm-export-template.json = realm-export-template.json - EndProjectSection -EndProject -#endif -#if (tests) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Domain.Tests", "Monaco.Template.Backend.Common.Domain.Tests\Monaco.Template.Backend.Common.Domain.Tests.csproj", "{426A6AB4-95F6-46AC-AB6D-98C4612F74E5}" -EndProject -#endif -#if (apiService) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Api", "Monaco.Template.Backend.Common.Api\Monaco.Template.Backend.Common.Api.csproj", "{78120DEF-B581-4D6B-92D7-1735070ACB7F}" -EndProject -#endif -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Application", "Monaco.Template.Backend.Common.Application\Monaco.Template.Backend.Common.Application.csproj", "{BFD5F082-8402-45DE-8ABA-EBB354BD2858}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Infrastructure", "Monaco.Template.Backend.Common.Infrastructure\Monaco.Template.Backend.Common.Infrastructure.csproj", "{23E5BE73-89F8-4EFE-8F28-27987C4E2DCD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Serilog", "Monaco.Template.Backend.Common.Serilog\Monaco.Template.Backend.Common.Serilog.csproj", "{605588FC-AC96-43A2-BFD9-2A6F27790202}" -EndProject -#if (tests) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Tests", "Monaco.Template.Backend.Common.Tests\Monaco.Template.Backend.Common.Tests.csproj", "{B82D784A-81A2-46C5-A966-B472B8FCAD46}" -EndProject -#endif -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Domain", "Monaco.Template.Backend.Common.Domain\Monaco.Template.Backend.Common.Domain.csproj", "{0FB378C2-9DE1-437F-8A62-774747105CE5}" -EndProject -#if (apiService) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.Api.Application", "Monaco.Template.Backend.Common.Api.Application\Monaco.Template.Backend.Common.Api.Application.csproj", "{F733BD0F-4C55-4E65-92E8-837191C37357}" -EndProject -#endif -#endif -#if (apiGateway) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.ApiGateway", "Monaco.Template.Backend.Common.ApiGateway\Monaco.Template.Backend.Common.ApiGateway.csproj", "{815EF0B0-5FDA-4D1E-BACE-DA7E440D5D34}" -EndProject -#endif -#if (tests) -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{920A23AF-59DA-4453-B825-0549B1C04F5B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Application.Tests", "Monaco.Template.Backend.Application.Tests\Monaco.Template.Backend.Application.Tests.csproj", "{E5781B96-E0CA-4762-9412-C4202E0CA08F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Domain.Tests", "Monaco.Template.Backend.Domain.Tests\Monaco.Template.Backend.Domain.Tests.csproj", "{B09E0906-8522-4B70-8C55-958415DAF21D}" -EndProject -#endif -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Application", "Monaco.Template.Backend.Application\Monaco.Template.Backend.Application.csproj", "{D43A4BF3-8D08-4EC2-8A99-5C3B92EC71A2}" -EndProject -#if (apiService) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Api", "Monaco.Template.Backend.Api\Monaco.Template.Backend.Api.csproj", "{895C8DDB-DD81-4CED-BE86-EFED36DD3732}" -EndProject -#endif -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Domain", "Monaco.Template.Backend.Domain\Monaco.Template.Backend.Domain.csproj", "{6B21DD04-2484-490C-B108-CC01148B9FC4}" -EndProject -#if (filesSupport) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.BlobStorage", "Monaco.Template.Backend.Common.BlobStorage\Monaco.Template.Backend.Common.BlobStorage.csproj", "{42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}" -EndProject -#if (tests) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Common.BlobStorage.Tests", "Monaco.Template.Backend.Common.BlobStorage.Tests\Monaco.Template.Backend.Common.BlobStorage.Tests.csproj", "{D8623B90-59C1-4753-A0E6-F2DBD4305C9B}" -EndProject -#endif -#endif -#if (workerService) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Worker", "Monaco.Template.Backend.Worker\Monaco.Template.Backend.Worker.csproj", "{BAF0203F-B036-46B3-AC04-E3847315B616}" -EndProject -#endif -#if (massTransitIntegration) -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monaco.Template.Backend.Messages", "Monaco.Template.Backend.Messages\Monaco.Template.Backend.Messages.csproj", "{CCAA345B-EDE0-441F-8D19-DDAFE52EFB9D}" -EndProject -#endif -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{72505E8B-B8FE-4FB4-BFCC-03C9E7C57643}" - ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props - Directory.Packages.props = Directory.Packages.props - #if (apiService) - Monaco.Template.Backend.Api.http = Monaco.Template.Backend.Api.http - #endif - EndProjectSection -EndProject -#if (tests) -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monaco.Template.Backend.ArchitectureTests", "Monaco.Template.Backend.ArchitectureTests\Monaco.Template.Backend.ArchitectureTests.csproj", "{60C8C40D-9BB1-491E-B364-8B22F668408A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monaco.Template.Backend.IntegrationTests", "Monaco.Template.Backend.IntegrationTests\Monaco.Template.Backend.IntegrationTests.csproj", "{2624756F-BBBB-4317-A3DF-B5F683700661}" -EndProject -#endif -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - #if (commonLibraries) - #if (tests) - {426A6AB4-95F6-46AC-AB6D-98C4612F74E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {426A6AB4-95F6-46AC-AB6D-98C4612F74E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {426A6AB4-95F6-46AC-AB6D-98C4612F74E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {426A6AB4-95F6-46AC-AB6D-98C4612F74E5}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - #if (apiService) - {78120DEF-B581-4D6B-92D7-1735070ACB7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {78120DEF-B581-4D6B-92D7-1735070ACB7F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {78120DEF-B581-4D6B-92D7-1735070ACB7F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {78120DEF-B581-4D6B-92D7-1735070ACB7F}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - {BFD5F082-8402-45DE-8ABA-EBB354BD2858}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFD5F082-8402-45DE-8ABA-EBB354BD2858}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFD5F082-8402-45DE-8ABA-EBB354BD2858}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFD5F082-8402-45DE-8ABA-EBB354BD2858}.Release|Any CPU.Build.0 = Release|Any CPU - {23E5BE73-89F8-4EFE-8F28-27987C4E2DCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {23E5BE73-89F8-4EFE-8F28-27987C4E2DCD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {23E5BE73-89F8-4EFE-8F28-27987C4E2DCD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {23E5BE73-89F8-4EFE-8F28-27987C4E2DCD}.Release|Any CPU.Build.0 = Release|Any CPU - {605588FC-AC96-43A2-BFD9-2A6F27790202}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {605588FC-AC96-43A2-BFD9-2A6F27790202}.Debug|Any CPU.Build.0 = Debug|Any CPU - {605588FC-AC96-43A2-BFD9-2A6F27790202}.Release|Any CPU.ActiveCfg = Release|Any CPU - {605588FC-AC96-43A2-BFD9-2A6F27790202}.Release|Any CPU.Build.0 = Release|Any CPU - #if (tests) - {B82D784A-81A2-46C5-A966-B472B8FCAD46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B82D784A-81A2-46C5-A966-B472B8FCAD46}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B82D784A-81A2-46C5-A966-B472B8FCAD46}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B82D784A-81A2-46C5-A966-B472B8FCAD46}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - {0FB378C2-9DE1-437F-8A62-774747105CE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0FB378C2-9DE1-437F-8A62-774747105CE5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0FB378C2-9DE1-437F-8A62-774747105CE5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0FB378C2-9DE1-437F-8A62-774747105CE5}.Release|Any CPU.Build.0 = Release|Any CPU - #if (apiService) - {F733BD0F-4C55-4E65-92E8-837191C37357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F733BD0F-4C55-4E65-92E8-837191C37357}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F733BD0F-4C55-4E65-92E8-837191C37357}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F733BD0F-4C55-4E65-92E8-837191C37357}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - #endif - #if (apiGateway) - {815EF0B0-5FDA-4D1E-BACE-DA7E440D5D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {815EF0B0-5FDA-4D1E-BACE-DA7E440D5D34}.Debug|Any CPU.Build.0 = Debug|Any CPU - {815EF0B0-5FDA-4D1E-BACE-DA7E440D5D34}.Release|Any CPU.ActiveCfg = Release|Any CPU - {815EF0B0-5FDA-4D1E-BACE-DA7E440D5D34}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - #if (tests) - {E5781B96-E0CA-4762-9412-C4202E0CA08F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5781B96-E0CA-4762-9412-C4202E0CA08F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5781B96-E0CA-4762-9412-C4202E0CA08F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5781B96-E0CA-4762-9412-C4202E0CA08F}.Release|Any CPU.Build.0 = Release|Any CPU - {B09E0906-8522-4B70-8C55-958415DAF21D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B09E0906-8522-4B70-8C55-958415DAF21D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B09E0906-8522-4B70-8C55-958415DAF21D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B09E0906-8522-4B70-8C55-958415DAF21D}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - {D43A4BF3-8D08-4EC2-8A99-5C3B92EC71A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D43A4BF3-8D08-4EC2-8A99-5C3B92EC71A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D43A4BF3-8D08-4EC2-8A99-5C3B92EC71A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D43A4BF3-8D08-4EC2-8A99-5C3B92EC71A2}.Release|Any CPU.Build.0 = Release|Any CPU - #if (apiService) - {895C8DDB-DD81-4CED-BE86-EFED36DD3732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {895C8DDB-DD81-4CED-BE86-EFED36DD3732}.Debug|Any CPU.Build.0 = Debug|Any CPU - {895C8DDB-DD81-4CED-BE86-EFED36DD3732}.Release|Any CPU.ActiveCfg = Release|Any CPU - {895C8DDB-DD81-4CED-BE86-EFED36DD3732}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - {6B21DD04-2484-490C-B108-CC01148B9FC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B21DD04-2484-490C-B108-CC01148B9FC4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B21DD04-2484-490C-B108-CC01148B9FC4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B21DD04-2484-490C-B108-CC01148B9FC4}.Release|Any CPU.Build.0 = Release|Any CPU - #if (filesSupport) - {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0}.Release|Any CPU.Build.0 = Release|Any CPU - #if (tests) - {D8623B90-59C1-4753-A0E6-F2DBD4305C9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D8623B90-59C1-4753-A0E6-F2DBD4305C9B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D8623B90-59C1-4753-A0E6-F2DBD4305C9B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D8623B90-59C1-4753-A0E6-F2DBD4305C9B}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - #endif - #if (workerService) - {BAF0203F-B036-46B3-AC04-E3847315B616}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BAF0203F-B036-46B3-AC04-E3847315B616}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BAF0203F-B036-46B3-AC04-E3847315B616}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BAF0203F-B036-46B3-AC04-E3847315B616}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - #if (massTransitIntegration) - {CCAA345B-EDE0-441F-8D19-DDAFE52EFB9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CCAA345B-EDE0-441F-8D19-DDAFE52EFB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CCAA345B-EDE0-441F-8D19-DDAFE52EFB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CCAA345B-EDE0-441F-8D19-DDAFE52EFB9D}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - #if (tests) - {60C8C40D-9BB1-491E-B364-8B22F668408A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60C8C40D-9BB1-491E-B364-8B22F668408A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60C8C40D-9BB1-491E-B364-8B22F668408A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60C8C40D-9BB1-491E-B364-8B22F668408A}.Release|Any CPU.Build.0 = Release|Any CPU - {2624756F-BBBB-4317-A3DF-B5F683700661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2624756F-BBBB-4317-A3DF-B5F683700661}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2624756F-BBBB-4317-A3DF-B5F683700661}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2624756F-BBBB-4317-A3DF-B5F683700661}.Release|Any CPU.Build.0 = Release|Any CPU - #endif - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - #if (commonLibraries) - #if (tests) - {35484293-234F-40DA-B430-A95170EDE449} = {8BFE9C37-2620-4156-88B8-286537954C5E} - {426A6AB4-95F6-46AC-AB6D-98C4612F74E5} = {35484293-234F-40DA-B430-A95170EDE449} - #endif - #if (apiService) - {78120DEF-B581-4D6B-92D7-1735070ACB7F} = {8BFE9C37-2620-4156-88B8-286537954C5E} - #endif - {BFD5F082-8402-45DE-8ABA-EBB354BD2858} = {8BFE9C37-2620-4156-88B8-286537954C5E} - {23E5BE73-89F8-4EFE-8F28-27987C4E2DCD} = {8BFE9C37-2620-4156-88B8-286537954C5E} - {605588FC-AC96-43A2-BFD9-2A6F27790202} = {8BFE9C37-2620-4156-88B8-286537954C5E} - #if (tests) - {B82D784A-81A2-46C5-A966-B472B8FCAD46} = {8BFE9C37-2620-4156-88B8-286537954C5E} - #endif - {0FB378C2-9DE1-437F-8A62-774747105CE5} = {8BFE9C37-2620-4156-88B8-286537954C5E} - #if (apiService) - {F733BD0F-4C55-4E65-92E8-837191C37357} = {8BFE9C37-2620-4156-88B8-286537954C5E} - #endif - #endif - #if (apiGateway) - {815EF0B0-5FDA-4D1E-BACE-DA7E440D5D34} = {8BFE9C37-2620-4156-88B8-286537954C5E} - #endif - #if (tests) - {E5781B96-E0CA-4762-9412-C4202E0CA08F} = {920A23AF-59DA-4453-B825-0549B1C04F5B} - {B09E0906-8522-4B70-8C55-958415DAF21D} = {920A23AF-59DA-4453-B825-0549B1C04F5B} - #endif - #if (filesSupport) - {42E51D47-B82F-4A92-B1E5-CD8BE44DE6F0} = {8BFE9C37-2620-4156-88B8-286537954C5E} - #if (tests) - {D8623B90-59C1-4753-A0E6-F2DBD4305C9B} = {35484293-234F-40DA-B430-A95170EDE449} - #endif - #endif - #if (tests) - {60C8C40D-9BB1-491E-B364-8B22F668408A} = {920A23AF-59DA-4453-B825-0549B1C04F5B} - {2624756F-BBBB-4317-A3DF-B5F683700661} = {920A23AF-59DA-4453-B825-0549B1C04F5B} - #endif - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {42C5F44E-1221-43DF-A6F5-4CB2CBEF8D72} - EndGlobalSection -EndGlobal \ No newline at end of file From dcc6cdbe5141a45250d1e4020372cfc9d09d638b Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Mon, 15 Dec 2025 18:15:17 +0000 Subject: [PATCH 17/30] chore: match condition to base class --- .../Tests/CountriesTests.cs | 2 +- .../Tests/FilesTests.cs | 2 +- .../Tests/ProductsTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs index 4ff26ea..a8ce700 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs @@ -15,7 +15,7 @@ public class CountriesTests : IntegrationTest public CountriesTests(AppFixture fixture) : base(fixture) { } -#if (auth) +#if (apiService && auth) protected override bool RequiresAuthentication => true; #endif diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs index 691691d..b8d8675 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs @@ -16,7 +16,7 @@ public class FilesTests : IntegrationTest public FilesTests(AppFixture fixture) : base(fixture) { } -#if (auth) +#if (apiService && auth) protected override bool RequiresAuthentication => true; #endif diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index 4a55307..142e9cc 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -27,7 +27,7 @@ public class ProductsTests : IntegrationTest public ProductsTests(AppFixture fixture) : base(fixture) { } -#if (auth) +#if (apiService && auth) protected override bool RequiresAuthentication => true; #endif From afa65983910fb317fe21019e0f656c7c128e5aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Tue, 23 Dec 2025 02:12:00 +0100 Subject: [PATCH 18/30] Refactor entity ID generation to use app-side GUIDs EntityTypeConfiguration now uses ValueGeneratedNever for IDs, shifting ID generation from the database to the application (via Guid.NewGuid()). Updated entity constructors, command handlers, and tests to align with this approach. Adjusted EF Core migrations and model snapshots to remove ValueGeneratedOnAdd. Cleaned up related test and factory code for consistency. --- .../Features/Company/CreateCompanyHandlerTests.cs | 2 +- .../Company/DeleteCompanyValidatorTests.cs | 11 +++-------- .../Features/File/CreateFileHandlerTests.cs | 4 ++-- .../Features/Product/CreateProductHandlerTests.cs | 2 +- .../Features/Company/CreateCompany.cs | 4 +++- .../Features/File/CreateFile.cs | 4 ++-- .../Features/Product/CreateProduct.cs | 3 ++- .../CompanyEntityConfiguration.cs | 2 +- .../CountryEntityConfiguration.cs | 14 ++++++-------- .../FileEntityConfiguration.cs | 2 +- .../ProductEntityConfiguration.cs | 2 +- ...Designer.cs => 20251220200916_Init.Designer.cs} | 7 ++----- ...250828213955_Init.cs => 20251220200916_Init.cs} | 0 .../Migrations/AppDbContextModelSnapshot.cs | 5 +---- .../ApplicationTests.cs | 8 +------- .../Model/AggregateRoot.cs | 2 +- .../Extensions/EntityTypeBuilderExtensions.cs | 2 +- .../Factories/Entities/ProductFactory.cs | 7 +++++-- .../Model/Entities/Company.cs | 2 +- .../Model/Entities/Product.cs | 12 +++++------- .../Tests/ProductsTests.cs | 12 ++++-------- 21 files changed, 44 insertions(+), 63 deletions(-) rename src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/{20250828213955_Init.Designer.cs => 20251220200916_Init.Designer.cs} (99%) rename src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/{20250828213955_Init.cs => 20251220200916_Init.cs} (100%) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs index 3de1ca1..101e9c4 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/CreateCompanyHandlerTests.cs @@ -40,7 +40,7 @@ public async Task CreateNewCompanySucceeds(Domain.Model.Entities.Country country var sut = new CreateCompany.Handler(_dbContextMock.Object); var result = await sut.Handle(Command, CancellationToken.None); - companyDbSetMock.Verify(x => x.Attach(It.IsAny()), Times.Once); + companyDbSetMock.Verify(x => x.Add(It.IsAny()), Times.Once); _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), Times.Once); result.ValidationResult .IsValid diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs index 95bedf9..5212218 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Company/DeleteCompanyValidatorTests.cs @@ -66,17 +66,12 @@ public async Task NonExistingCompanyGenertesError(Domain.Model.Entities.Company #if filesSupport [Theory(DisplayName = "Company assigned to Product generates error")] - [AutoDomainData] + [AutoDomainData(true)] public async Task CompanyAssignedToProductGeneratesError(Domain.Model.Entities.Company company, Domain.Model.Entities.Product product) { - var command = Command with { Id = company.Id }; - - product.Update(product.Title, - product.Description, - product.Price, - company); + var command = Command with { Id = product.CompanyId }; - _dbContextMock.CreateAndSetupDbSetMock(company); + _dbContextMock.CreateAndSetupDbSetMock(product.Company); _dbContextMock.CreateAndSetupDbSetMock(product); var sut = new DeleteCompany.Validator(_dbContextMock.Object); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs index e9b99f9..1d6819f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/File/CreateFileHandlerTests.cs @@ -47,7 +47,7 @@ public async Task CreateNewFileSucceeds(Document file) It.IsAny(), It.IsAny()), Times.Once); - fileDbSetMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), + fileDbSetMock.Verify(x => x.Add(It.IsAny()), Times.Once); _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), Times.Once); @@ -88,7 +88,7 @@ await action.Should() It.IsAny(), It.IsAny()), Times.Once); - fileDbSetMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), + fileDbSetMock.Verify(x => x.Add(It.IsAny()), Times.Once); } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs index bbedd8e..4e633c1 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application.Tests/Features/Product/CreateProductHandlerTests.cs @@ -66,7 +66,7 @@ public async Task CreateNewProductSucceeds(Domain.Model.Entities.Company company var result = await sut.Handle(command, CancellationToken.None); - productDbSetMock.Verify(x => x.Attach(It.IsAny()), Times.Once); + productDbSetMock.Verify(x => x.Add(It.IsAny()), Times.Once); _dbContextMock.Verify(x => x.SaveEntitiesAsync(It.IsAny()), Times.Once); #if (massTransitIntegration) _publishEndpointMock.Verify(x => x.Publish(It.IsAny(), It.IsAny()), Times.Once); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/CreateCompany.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/CreateCompany.cs index eb23ac8..2d6552a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/CreateCompany.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Company/CreateCompany.cs @@ -73,7 +73,9 @@ public async Task> Handle(Command request, CancellationToken var country = await _dbContext.GetAsync(request.CountryId, cancellationToken); var item = request.Map(country); - _dbContext.Set().Attach(item); + _dbContext.Set() + .Add(item); + await _dbContext.SaveEntitiesAsync(cancellationToken); return CommandResult.Success(item.Id); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs index 2add265..cee3b4f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/File/CreateFile.cs @@ -48,8 +48,8 @@ public async Task> Handle(Command request, CancellationToken try { - await _dbContext.Set() - .AddAsync(file, cancellationToken); + _dbContext.Set() + .Add(file); await _dbContext.SaveEntitiesAsync(cancellationToken); } catch diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs index 995566a..4c60357 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Features/Product/CreateProduct.cs @@ -91,7 +91,8 @@ public async Task> Handle(Command request, CancellationToken [.. pictures], pictures.Single(x => x.Id == request.DefaultPictureId)); - _dbContext.Set().Attach(item); + _dbContext.Set() + .Add(item); #if (massTransitIntegration) await _publishEndpoint.Publish(item.MapMessage(), cancellationToken); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CompanyEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CompanyEntityConfiguration.cs index 2e23ad9..40fa508 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CompanyEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CompanyEntityConfiguration.cs @@ -10,7 +10,7 @@ internal sealed class CompanyEntityConfiguration : IEntityTypeConfiguration builder) { - builder.ConfigureIdWithDbGeneratedValue(); + builder.ConfigureIdWithValueGeneratedNever(); builder.Property(x => x.Name) .IsRequired() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CountryEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CountryEntityConfiguration.cs index 8ba488c..6394983 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CountryEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/CountryEntityConfiguration.cs @@ -1,23 +1,21 @@ -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.Extensions.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Monaco.Template.Backend.Application.Persistence.EntityConfigurations.Seeds; -using Monaco.Template.Backend.Common.Infrastructure.EntityConfigurations; using Monaco.Template.Backend.Common.Infrastructure.EntityConfigurations.Extensions; using Monaco.Template.Backend.Domain.Model.Entities; namespace Monaco.Template.Backend.Application.Persistence.EntityConfigurations; -internal sealed class CountryEntityConfiguration(IHostEnvironment env) : EntityTypeConfigurationBase(env) +internal sealed class CountryEntityConfiguration : IEntityTypeConfiguration { - public override void Configure(EntityTypeBuilder builder) + public void Configure(EntityTypeBuilder builder) { - builder.ConfigureIdWithDbGeneratedValue(); + builder.ConfigureIdWithValueGeneratedNever(); builder.Property(x => x.Name) .IsRequired() .HasMaxLength(Country.NameLength); - if (CanRunSeed) - builder.HasData(CountrySeed.GetCountries()); + builder.HasData(CountrySeed.GetCountries()); } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/FileEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/FileEntityConfiguration.cs index 2fde3f4..bb5c026 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/FileEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/FileEntityConfiguration.cs @@ -10,7 +10,7 @@ internal sealed class FileEntityConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.ConfigureIdWithDefaultAndValueGeneratedNever(); + builder.ConfigureIdWithValueGeneratedNever(); builder.ToTable(nameof(File)) .HasDiscriminator("Discriminator") diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs index b070c86..4102661 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/EntityConfigurations/ProductEntityConfiguration.cs @@ -9,7 +9,7 @@ internal sealed class ProductEntityConfiguration : IEntityTypeConfiguration builder) { - builder.ConfigureIdWithDbGeneratedValue(); + builder.ConfigureIdWithValueGeneratedNever(); builder.Property(x => x.Title) .IsRequired() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20250828213955_Init.Designer.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20251220200916_Init.Designer.cs similarity index 99% rename from src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20250828213955_Init.Designer.cs rename to src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20251220200916_Init.Designer.cs index 1e4f0a6..3fb08fb 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20250828213955_Init.Designer.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20251220200916_Init.Designer.cs @@ -12,7 +12,7 @@ namespace Monaco.Template.Backend.Application.Persistence.Migrations { [DbContext(typeof(AppDbContext))] - [Migration("20250828213955_Init")] + [Migration("20251220200916_Init")] partial class Init { /// @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("ProductVersion", "10.0.1") .HasAnnotation("Proxies:ChangeTracking", false) .HasAnnotation("Proxies:CheckEquality", false) .HasAnnotation("Proxies:LazyLoading", true) @@ -201,7 +201,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Monaco.Template.Backend.Domain.Model.Entities.Company", b => { b.Property("Id") - .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property("Email") @@ -232,7 +231,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Monaco.Template.Backend.Domain.Model.Entities.Country", b => { b.Property("Id") - .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property("Name") @@ -1268,7 +1266,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Monaco.Template.Backend.Domain.Model.Entities.Product", b => { b.Property("Id") - .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property("CompanyId") diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20250828213955_Init.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20251220200916_Init.cs similarity index 100% rename from src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20250828213955_Init.cs rename to src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/20251220200916_Init.cs diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/AppDbContextModelSnapshot.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/AppDbContextModelSnapshot.cs index 33a9515..6e1d9d0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("ProductVersion", "10.0.1") .HasAnnotation("Proxies:ChangeTracking", false) .HasAnnotation("Proxies:CheckEquality", false) .HasAnnotation("Proxies:LazyLoading", true) @@ -198,7 +198,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Monaco.Template.Backend.Domain.Model.Entities.Company", b => { b.Property("Id") - .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property("Email") @@ -229,7 +228,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Monaco.Template.Backend.Domain.Model.Entities.Country", b => { b.Property("Id") - .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property("Name") @@ -1265,7 +1263,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Monaco.Template.Backend.Domain.Model.Entities.Product", b => { b.Property("Id") - .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); b.Property("CompanyId") diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/ApplicationTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/ApplicationTests.cs index 78d2afc..28909cd 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/ApplicationTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/ApplicationTests.cs @@ -5,7 +5,6 @@ using Monaco.Template.Backend.ArchitectureTests.Extensions; using Monaco.Template.Backend.Common.Application.Commands; using Monaco.Template.Backend.Common.Domain.Model; -using Monaco.Template.Backend.Common.Infrastructure.EntityConfigurations; namespace Monaco.Template.Backend.ArchitectureTests; @@ -74,8 +73,7 @@ public class ApplicationTests : BaseTest .As("Entities"); private static readonly Interface EntityTypeConfiguration = Architecture.GetInterfaceOfType(typeof(IEntityTypeConfiguration<>)); - private static readonly Class EntityTypeConfigurationBase = Architecture.GetClassOfType(typeof(EntityTypeConfigurationBase<>)); - + private readonly GivenClassesConjunctionWithDescription _entityConfiguration = Classes().That() .AreAssignableTo(EntityTypeConfiguration) .And() @@ -195,10 +193,6 @@ public void EntitiesHaveEntityConfiguration() => .Any(etc => etc.GetImplementsInterfaceDependencies() .Any(i => i.Target.Equals(EntityTypeConfiguration) && i.TargetGenericArguments - .Any(g => g.Type.Equals(c))) || - etc.GetInheritsBaseClassDependencies() - .Any(b => b.Target.Equals(EntityTypeConfigurationBase) && - b.TargetGenericArguments .Any(g => g.Type.Equals(c)))), "have their corresponding EntityTypeConfiguration", "does not have its corresponding EntityTypeConfiguration") diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs index 5e3cf67..6f49fc3 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Domain/Model/AggregateRoot.cs @@ -33,7 +33,7 @@ protected AggregateRoot(Guid id) : base(id) /// protected void AddDomainEvent(DomainEvent eventItem, bool unique = false) { - if (unique && _domainEvents.Any(x => x.GetType() == eventItem.GetType())) + if (unique && DomainEvents.Any(x => x.GetType() == eventItem.GetType())) return; _domainEvents.Add(eventItem); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs index b23817f..ae571ac 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Infrastructure/EntityConfigurations/Extensions/EntityTypeBuilderExtensions.cs @@ -15,7 +15,7 @@ public void ConfigureId() .IsRequired(); } - public void ConfigureIdWithDefaultAndValueGeneratedNever() + public void ConfigureIdWithValueGeneratedNever() { builder.ConfigureId(); builder.Property(x => x.Id) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs index 2d40733..457da6a 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain.Tests/Factories/Entities/ProductFactory.cs @@ -45,16 +45,19 @@ public IFixture RegisterProductMock() fixture.Register(() => { var images = fixture.CreateMany().ToList(); + var company = fixture.Create(); var mock = new Mock(fixture.Create(), fixture.Create(), fixture.Create(), - fixture.Create(), + company, images, images.First()); mock.SetupGet(x => x.Id) .Returns(Guid.NewGuid()); mock.SetupGet(x => x.Company) - .Returns(fixture.Create()); + .Returns(company); + mock.SetupGet(x => x.CompanyId) + .Returns(company.Id); mock.SetupGet(x => x.Pictures) .Returns([.. images]); mock.SetupGet(x => x.DefaultPicture) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Company.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Company.cs index 82935ea..dcca707 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Company.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Company.cs @@ -17,7 +17,7 @@ protected Company() public Company(string name, string email, string? webSiteUrl, - Address? address) + Address? address) : base(Guid.NewGuid()) { (Name, Email, WebSiteUrl) = Validate(name, email, diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Product.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Product.cs index c3602e8..210bbcf 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Product.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Model/Entities/Product.cs @@ -17,7 +17,7 @@ public Product(string title, decimal price, Company company, List pictures, - Image defaultPicture) + Image defaultPicture) : base(Guid.NewGuid()) { Company = company; @@ -27,19 +27,17 @@ public Product(string title, pictures.ToList() .Throw() .IfEmpty(); - - defaultPicture.Throw() - .IfFalse(pictures.Contains); - + pictures.ForEach(AddPicture); - SetDefaultPicture(defaultPicture); + SetDefaultPicture(defaultPicture.Throw() + .IfFalse(pictures.Contains)); } public string Title { get; private set; } = null!; public string Description { get; private set; } = null!; public decimal Price { get; private set; } - public Guid CompanyId { get; private set; } + public virtual Guid CompanyId { get; private set; } public virtual Company Company { get; private set; } = null!; private readonly List _pictures = []; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index 142e9cc..3475bcc 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -301,14 +301,14 @@ [.. tempImages.Select(i => i.Id)], .Include(x => x.DefaultPicture) .ToListAsync(); products.Should() - .HaveCount(4); + .HaveCount(4); var newProduct = products.SingleOrDefault(c => c.Id == id); newProduct.Should() .NotBeNull(); - newProduct!.Title - .Should() - .Be(dto.Title); + newProduct.Title + .Should() + .Be(dto.Title); newProduct.Description .Should() .Be(dto.Description); @@ -346,10 +346,6 @@ [.. tempImages.Select(i => i.Id)], #endif #if (workerService) - (await serviceTestHarness.Consumed.Any()) - .Should() - .BeTrue(); - var consumerHarness = serviceTestHarness.GetConsumerHarness(); (await consumerHarness.Consumed.Any()) .Should() From 6b6c540115c58a38c9f2cdd586a2d1ac634ffb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Sun, 28 Dec 2025 21:47:39 +0100 Subject: [PATCH 19/30] Refactor Mediator API for CancellationToken & CreatedResponse Refactored MediatorExtensions to support CancellationToken in all methods, improving async and cancellation handling. Introduced CreatedResponse record for standardized creation responses. Updated Companies, Products, and Files endpoints to use new Mediator methods and return Created. Adjusted integration tests to validate CreatedResponse. Improved code clarity and authorization policy checks. --- .../Endpoints/Companies.cs | 45 +++--- .../Endpoints/Files.cs | 29 ++-- .../Endpoints/Products.cs | 53 +++++--- .../CreatedResponse.cs | 3 + .../MediatorExtensions.cs | 128 ++++++++++++++---- .../OpenApi/OAuth2OperationTransformer.cs | 11 +- .../Tests/CompaniesTests.cs | 42 ++++-- .../Tests/ProductsTests.cs | 81 ++++++----- 8 files changed, 263 insertions(+), 129 deletions(-) create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/CreatedResponse.cs diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs index baf97d7..4ec14b3 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs @@ -2,9 +2,6 @@ using MediatR; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; -#if (auth) -using Monaco.Template.Backend.Api.Auth; -#endif using Monaco.Template.Backend.Api.DTOs; using Monaco.Template.Backend.Api.DTOs.Extensions; using Monaco.Template.Backend.Application.Features.Company; @@ -12,6 +9,9 @@ using Monaco.Template.Backend.Common.Api.Application; using Monaco.Template.Backend.Common.Api.MinimalApi; using Monaco.Template.Backend.Common.Domain.Model; +#if (auth) +using Monaco.Template.Backend.Api.Auth; +#endif namespace Monaco.Template.Backend.Api.Endpoints; @@ -21,12 +21,15 @@ internal static class Companies { public IEndpointRouteBuilder AddCompanies(ApiVersionSet versionSet) { - var companies = builder.CreateApiGroupBuilder(versionSet, "Companies"); + var companies = builder.CreateApiGroupBuilder(versionSet, + "Companies"); companies.MapGet("", Task>, NotFound>> ([FromServices] ISender sender, - HttpRequest request) => - sender.ExecuteQueryAsync(new GetCompanyPage.Query(request.Query)), + HttpRequest request, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetCompanyPage.Query(request.Query), + cancellationToken), "GetCompanies", #if (!auth) "Gets a page of companies"); @@ -37,8 +40,10 @@ public IEndpointRouteBuilder AddCompanies(ApiVersionSet versionSet) companies.MapGet("{id:guid}", Task, NotFound>> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteQueryAsync(new GetCompanyById.Query(id)), + [FromRoute] Guid id, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetCompanyById.Query(id), + cancellationToken), "GetCompany", #if (!auth) "Gets a company by Id"); @@ -48,10 +53,14 @@ public IEndpointRouteBuilder AddCompanies(ApiVersionSet versionSet) #endif companies.MapPost("", - Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, - [FromBody] CompanyCreateEditDto dto, - HttpContext context) => - sender.ExecuteCommandAsync(dto.MapCreateCommand(), "api/v{0}/Companies/{1}", context.GetRequestedApiVersion()!), + Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, + [FromBody] CompanyCreateEditDto dto, + HttpContext context, + CancellationToken cancellationToken) => + sender.ExecuteCommandCreatedAsync(dto.MapCreateCommand(), + "api/v{0}/Companies/{1}", + [context.GetRequestedApiVersion()!], + cancellationToken), "CreateCompany", #if (!auth) "Create a new company"); @@ -63,8 +72,10 @@ public IEndpointRouteBuilder AddCompanies(ApiVersionSet versionSet) companies.MapPut("{id:guid}", Task> ([FromServices] ISender sender, [FromRoute] Guid id, - [FromBody] CompanyCreateEditDto dto) => - sender.ExecuteCommandEditAsync(dto.MapEditCommand(id)), + [FromBody] CompanyCreateEditDto dto, + CancellationToken cancellationToken) => + sender.ExecuteCommandNoContentAsync(dto.MapEditCommand(id), + cancellationToken), "EditCompany", #if (!auth) "Edit an existing company by Id"); @@ -75,8 +86,10 @@ public IEndpointRouteBuilder AddCompanies(ApiVersionSet versionSet) companies.MapDelete("{id:guid}", Task> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteCommandDeleteAsync(new DeleteCompany.Command(id)), + [FromRoute] Guid id, + CancellationToken cancellationToken) => + sender.ExecuteCommandOkAsync(new DeleteCompany.Command(id), + cancellationToken), "DeleteCompany", #if (!auth) "Delete an existing company by Id"); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs index c5abef9..650b8e0 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs @@ -2,12 +2,12 @@ using MediatR; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; -#if (auth) -using Monaco.Template.Backend.Api.Auth; -#endif using Monaco.Template.Backend.Application.Features.File; using Monaco.Template.Backend.Common.Api.Application; using Monaco.Template.Backend.Common.Api.MinimalApi; +#if (auth) +using Monaco.Template.Backend.Api.Auth; +#endif namespace Monaco.Template.Backend.Api.Endpoints; @@ -15,19 +15,22 @@ internal static class Files { extension(IEndpointRouteBuilder builder) { - public IEndpointRouteBuilder AddFiles(ApiVersionSet versionSet) + public IEndpointRouteBuilder AddFiles(ApiVersionSet versionSet) { - var files = builder.CreateApiGroupBuilder(versionSet, "Files"); + var files = builder.CreateApiGroupBuilder(versionSet, + "Files"); files.MapPost("", - Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, - IFormFile file, - HttpContext context) => - sender.ExecuteCommandAsync(new CreateFile.Command(file.OpenReadStream(), - file.FileName, - file.ContentType), - "api/v{0}/Files/{1}", - context.GetRequestedApiVersion()!), + Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, + IFormFile file, + HttpContext context, + CancellationToken cancellationToken) => + sender.ExecuteCommandCreatedAsync(new CreateFile.Command(file.OpenReadStream(), + file.FileName, + file.ContentType), + "api/v{0}/Files/{1}", + [context.GetRequestedApiVersion()!], + cancellationToken), "CreateFile", "Upload and create a new file") #if (!auth) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs index bc827af..1614ee2 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Products.cs @@ -2,9 +2,6 @@ using MediatR; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; -#if (auth) -using Monaco.Template.Backend.Api.Auth; -#endif using Monaco.Template.Backend.Api.DTOs; using Monaco.Template.Backend.Api.DTOs.Extensions; using Monaco.Template.Backend.Application.Features.Product; @@ -12,6 +9,9 @@ using Monaco.Template.Backend.Common.Api.Application; using Monaco.Template.Backend.Common.Api.MinimalApi; using Monaco.Template.Backend.Common.Domain.Model; +#if (auth) +using Monaco.Template.Backend.Api.Auth; +#endif namespace Monaco.Template.Backend.Api.Endpoints; @@ -21,12 +21,15 @@ internal static class Products { public IEndpointRouteBuilder AddProducts(ApiVersionSet versionSet) { - var products = builder.CreateApiGroupBuilder(versionSet, "Products"); + var products = builder.CreateApiGroupBuilder(versionSet, + "Products"); products.MapGet("", Task>, NotFound>> ([FromServices] ISender sender, - HttpRequest request) => - sender.ExecuteQueryAsync(new GetProductPage.Query(request.Query)), + HttpRequest request, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetProductPage.Query(request.Query), + cancellationToken), "GetProducts", #if (!auth) "Gets a page of products"); @@ -37,8 +40,10 @@ public IEndpointRouteBuilder AddProducts(ApiVersionSet versionSet) products.MapGet("{id:guid}", Task, NotFound>> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteQueryAsync(new GetProductById.Query(id)), + [FromRoute] Guid id, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetProductById.Query(id), + cancellationToken), "GetProduct", #if (!auth) "Gets a product by Id"); @@ -48,12 +53,14 @@ public IEndpointRouteBuilder AddProducts(ApiVersionSet versionSet) #endif products.MapPost("", - Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, - [FromBody] ProductCreateEditDto dto, - HttpContext context) => - sender.ExecuteCommandAsync(dto.Map(), - "api/v{0}/Products/{1}", - context.GetRequestedApiVersion()!), + Task, NotFound, ValidationProblem>> ([FromServices] ISender sender, + [FromBody] ProductCreateEditDto dto, + HttpContext context, + CancellationToken cancellationToken) => + sender.ExecuteCommandCreatedAsync(dto.Map(), + "api/v{0}/Products/{1}", + [context.GetRequestedApiVersion()!], + cancellationToken), "CreateProduct", #if (!auth) "Create a new product"); @@ -65,8 +72,10 @@ public IEndpointRouteBuilder AddProducts(ApiVersionSet versionSet) products.MapPut("{id:guid}", Task> ([FromServices] ISender sender, [FromRoute] Guid id, - [FromBody] ProductCreateEditDto dto) => - sender.ExecuteCommandEditAsync(dto.Map(id)), + [FromBody] ProductCreateEditDto dto, + CancellationToken cancellationToken) => + sender.ExecuteCommandNoContentAsync(dto.Map(id), + cancellationToken), "EditProduct", #if (!auth) "Edit an existing product by Id"); @@ -77,8 +86,10 @@ public IEndpointRouteBuilder AddProducts(ApiVersionSet versionSet) products.MapDelete("{id:guid}", Task> ([FromServices] ISender sender, - [FromRoute] Guid id) => - sender.ExecuteCommandDeleteAsync(new DeleteProduct.Command(id)), + [FromRoute] Guid id, + CancellationToken cancellationToken) => + sender.ExecuteCommandOkAsync(new DeleteProduct.Command(id), + cancellationToken), "DeleteProduct", #if (!auth) "Delete an existing product by Id"); @@ -91,10 +102,12 @@ public IEndpointRouteBuilder AddProducts(ApiVersionSet versionSet) Task> ([FromServices] ISender sender, [FromRoute] Guid productId, [FromRoute] Guid pictureId, - HttpRequest request) => + HttpRequest request, + CancellationToken cancellationToken) => sender.ExecuteFileDownloadAsync(new DownloadProductPicture.Query(productId, pictureId, - request.Query)), + request.Query), + cancellationToken), "DownloadProductPicture", "Download a picture from a product by Id") #if (!auth) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/CreatedResponse.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/CreatedResponse.cs new file mode 100644 index 0000000..9242dc1 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/CreatedResponse.cs @@ -0,0 +1,3 @@ +namespace Monaco.Template.Backend.Common.Api.Application; + +public record CreatedResponse(Guid Id); \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs index 0dbbe74..88ae43d 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs @@ -10,6 +10,13 @@ namespace Monaco.Template.Backend.Common.Api.Application; public static class MediatorExtensions { + private static Results GetFileDownload(TResult? item) where TResult : FileDownloadDto => + item is null + ? TypedResults.NotFound() + : TypedResults.File(item.FileContent, + item.ContentType, + item.FileName); + extension(ISender sender) { /// @@ -17,10 +24,12 @@ public static class MediatorExtensions /// /// The type of the records returned by the query /// + /// /// - public async Task, NotFound>> ExecuteQueryAsync(QueryBase query) + public async Task, NotFound>> ExecuteQueryAsync(QueryBase query, CancellationToken cancellationToken = default) { - var result = await sender.Send(query); + var result = await sender.Send(query, + cancellationToken); return result is null ? TypedResults.NotFound() : TypedResults.Ok(result); @@ -31,10 +40,12 @@ public async Task, NotFound>> ExecuteQueryAsync(Que /// /// The type of the records contained in the page returned by the query /// + /// /// - public async Task>, NotFound>> ExecuteQueryAsync(QueryPagedBase query) + public async Task>, NotFound>> ExecuteQueryAsync(QueryPagedBase query, CancellationToken cancellationToken = default) { - var result = await sender.Send(query); + var result = await sender.Send(query, + cancellationToken); return result is null ? TypedResults.NotFound() : TypedResults.Ok(result); @@ -45,10 +56,12 @@ public async Task>, NotFound>> ExecuteQueryAsync /// The type of the item returned by the query /// + /// /// - public async Task, NotFound>> ExecuteQueryAsync(QueryByIdBase query) + public async Task, NotFound>> ExecuteQueryAsync(QueryByIdBase query, CancellationToken cancellationToken = default) { - var result = await sender.Send(query); + var result = await sender.Send(query, + cancellationToken); return result is null ? TypedResults.NotFound() : TypedResults.Ok(result); @@ -60,10 +73,12 @@ public async Task, NotFound>> ExecuteQueryAsync(Que /// The type of the item returned by the query /// The type of the key to search the item by /// + /// /// - public async Task, NotFound>> ExecuteQueryAsync(QueryByKeyBase query) + public async Task, NotFound>> ExecuteQueryAsync(QueryByKeyBase query, CancellationToken cancellationToken = default) { - var item = await sender.Send(query); + var item = await sender.Send(query, + cancellationToken); return item is null ? TypedResults.NotFound() : TypedResults.Ok(item); @@ -74,10 +89,13 @@ public async Task, NotFound>> ExecuteQueryAsync /// /// + /// /// - public async Task> ExecuteFileDownloadAsync(QueryBase query) where TResult : FileDownloadDto + public async Task> ExecuteFileDownloadAsync(QueryBase query, + CancellationToken cancellationToken = default) where TResult : FileDownloadDto { - var item = await sender.Send(query); + var item = await sender.Send(query, + cancellationToken); return GetFileDownload(item); } @@ -86,44 +104,51 @@ public async Task> ExecuteFileDownloadAs /// /// /// + /// /// - public async Task> ExecuteFileDownloadAsync(QueryByIdBase query) where TResult : FileDownloadDto + public async Task> ExecuteFileDownloadAsync(QueryByIdBase query, + CancellationToken cancellationToken = default) where TResult : FileDownloadDto { - var item = await sender.Send(query); + var item = await sender.Send(query, + cancellationToken); return GetFileDownload(item); } /// - /// Executes the command passed and returns the corresponding response that can be either Created(result) or a NotFound() or a ValidationProblem() depending on the validations and processing + /// Executes the command passed and returns the corresponding response that can be either or a or a depending on the validations and processing /// - /// The type of the result returned by the command /// /// The URI to include in the headers of the Created() response + /// /// The parameters (if any) to pass for concatenating into the resultUri /// - public async Task, NotFound, ValidationProblem>> ExecuteCommandAsync(CommandBase command, - string resultUri, - params object[]? uriParams) + public async Task, NotFound, ValidationProblem>> ExecuteCommandCreatedAsync(CommandBase command, + string resultUri, + object[]? uriParams = null, + CancellationToken cancellationToken = default) { - var result = await sender.Send(command); + var result = await sender.Send(command, + cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), _ => TypedResults.Created(string.Format(resultUri, - [.. uriParams ?? [], result.Result!]), - result.Result) + [.. uriParams ?? [], result.Result]), + new CreatedResponse(result.Result)) }; } /// - /// Executes the edit command passed and returns the corresponding response that can be either NoContent() or a NotFound() or a ValidationProblem() depending on the validations and processing + /// Executes the command passed and returns the corresponding response that can be either or a or a depending on the validations and processing /// /// + /// /// - public async Task> ExecuteCommandEditAsync(CommandBase command) + public async Task> ExecuteCommandNoContentAsync(CommandBase command, CancellationToken cancellationToken = default) { - var result = await sender.Send(command); + var result = await sender.Send(command, + cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), @@ -133,13 +158,15 @@ public async Task> ExecuteComman } /// - /// Executes the delete command passed and returns the corresponding response that can be either Ok() or a NotFound() or a ValidationProblem() depending on the validations and processing + /// Executes the command passed and returns the corresponding response that can be either or a or a depending on the validations and processing /// /// + /// /// - public async Task> ExecuteCommandDeleteAsync(CommandBase command) + public async Task> ExecuteCommandOkAsync(CommandBase command, CancellationToken cancellationToken = default) { - var result = await sender.Send(command); + var result = await sender.Send(command, + cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), @@ -147,9 +174,50 @@ public async Task> ExecuteCommandDelete _ => TypedResults.Ok() }; } + + /// + /// Executes the command passed and returns the corresponding response that can be either or a or a user-defined depending on the validations and processing + /// + /// + /// + /// + /// + /// + public async Task> ExecuteCommandAsync(CommandBase command, + TResponse response, + CancellationToken cancellationToken = default) where TResponse : IResult + { + var result = await sender.Send(command, + cancellationToken); + return result switch + { + { ItemNotFound: true } => TypedResults.NotFound(), + { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), + _ => response + }; + } + + /// + /// Executes the command passed and returns the corresponding response that can be either or a or an calculated based on a function, depending on the validations and processing + /// + /// The type of the result returned by the command + /// + /// + /// A function to convert the result to the desired response type + /// + /// + public async Task> ExecuteCommandAsync(CommandBase command, + Func func, + CancellationToken cancellationToken = default) where TResponse : IResult + { + var result = await sender.Send(command, + cancellationToken); + return result switch + { + { ItemNotFound: true } => TypedResults.NotFound(), + { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), + _ => func(result.Result) + }; + } } - private static Results GetFileDownload(TResult? item) where TResult : FileDownloadDto => - item is null - ? TypedResults.NotFound() - : TypedResults.File(item.FileContent, item.ContentType, item.FileName); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs index efe07f8..d336a70 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs @@ -27,12 +27,19 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform } var requiredScopes = metadata.OfType() - .Where(a => !string.IsNullOrEmpty(a.Policy)) + .Where(a => !string.IsNullOrWhiteSpace(a.Policy)) .Select(a => a.Policy!) .Distinct() .ToList(); - operation.Security = [new() { [new(OAuth2DocumentTransformer.SchemeName, context.Document)] = [..requiredScopes, _audience] }]; + operation.Security = + [ + new() + { + [new(OAuth2DocumentTransformer.SchemeName, + context.Document)] = [..requiredScopes, _audience] + } + ]; return Task.CompletedTask; } diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs index a3afebe..6a21536 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs @@ -1,20 +1,22 @@ -using AutoFixture.Xunit2; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Mail; +using AutoFixture.Xunit2; using AwesomeAssertions; using Flurl.Http; using Microsoft.EntityFrameworkCore; using Monaco.Template.Backend.Api.DTOs; using Monaco.Template.Backend.Application.Features.Company.DTOs; +using Monaco.Template.Backend.Common.Api.Application; using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Domain.Model.Entities; using Monaco.Template.Backend.Domain.Model.ValueObjects; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Mail; namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] -[Trait("Integration Tests", "Companies")] +[Trait("Integration Tests", + "Companies")] public class CompaniesTests : IntegrationTest { public CompaniesTests(AppFixture fixture) : base(fixture) @@ -37,15 +39,23 @@ public override async Task InitializeAsync() } [Theory(DisplayName = "Get Companies page succeeds")] - [InlineData(false, null, null, 3)] - [InlineData(true, 1, 5, 2)] + [InlineData(false, + null, + null, + 3)] + [InlineData(true, + 1, + 5, + 2)] public async Task GetCompaniesPageSucceeds(bool expandCountry, int? offset, int? limit, int expectedItemsCount) { - var response = await CreateRequest(ApiRoutes.Companies.Query(expandCountry, offset, limit)).GetAsync(); - + var response = await CreateRequest(ApiRoutes.Companies.Query(expandCountry, + offset, + limit)).GetAsync(); + response.StatusCode .Should() .Be((int)HttpStatusCode.OK); @@ -145,21 +155,23 @@ public async Task CreateNewCompanySucceeds(string name, .Should() .Be((int)HttpStatusCode.Created); - var result = await response.GetStringAsync(); + var result = await response.GetJsonAsync(); - var id = Guid.Empty; result.Should() - .Match(value => Guid.TryParse(value.Replace("\"", ""), out id)); + .NotBeNull(); + result.Id + .Should() + .NotBeEmpty(); response.Headers .Should() - .Contain(("Location", ApiRoutes.Companies.Get(id).ToString())); - + .Contain(("Location", ApiRoutes.Companies.Get(result.Id).ToString())); + var companies = await GetDbContext().Set() .ToListAsync(); companies.Should() .HaveCount(4); - var newCompany = companies.SingleOrDefault(c => c.Id == id); + var newCompany = companies.SingleOrDefault(c => c.Id == result.Id); newCompany.Should() .NotBeNull(); newCompany!.Name diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index 3475bcc..0035adc 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -1,27 +1,29 @@ -using AutoFixture.Xunit2; +#if (massTransitIntegration && (apiService || workerService)) +using Monaco.Template.Backend.Messages.V1; +#endif +#if (massTransitIntegration || workerService) +using Monaco.Template.Backend.Worker.Consumers; +#endif +using System.Diagnostics.CodeAnalysis; +using System.Net; +using AutoFixture.Xunit2; +using AwesomeAssertions; using Azure.Storage.Blobs; using Dasync.Collections; -using AwesomeAssertions; using Flurl.Http; using Microsoft.EntityFrameworkCore; using Monaco.Template.Backend.Api.DTOs; -using Monaco.Template.Backend.Common.Domain.Model; -using System.Diagnostics.CodeAnalysis; -using System.Net; using Monaco.Template.Backend.Application.Features.Product.DTOs; +using Monaco.Template.Backend.Common.Api.Application; +using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Domain.Model.Entities; -#if (massTransitIntegration && (apiService || workerService)) -using Monaco.Template.Backend.Messages.V1; -#endif -#if (massTransitIntegration || workerService) -using Monaco.Template.Backend.Worker.Consumers; -#endif using File = System.IO.File; namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] -[Trait("Integration Tests", "Products")] +[Trait("Integration Tests", + "Products")] public class ProductsTests : IntegrationTest { public ProductsTests(AppFixture fixture) : base(fixture) @@ -48,8 +50,7 @@ public override async Task InitializeAsync() } #if (auth) - private Task SetupAccessToken() => - SetupAccessToken([Auth.Auth.Roles.Administrator]); + private Task SetupAccessToken() => SetupAccessToken([Auth.Auth.Roles.Administrator]); #endif private BlobContainerClient GetBlobContainerClient() => @@ -57,8 +58,18 @@ private BlobContainerClient GetBlobContainerClient() => AppFixture.StorageContainer); [Theory(DisplayName = "Get Products page succeeds")] - [InlineData(false, false, false, null, null, 3)] - [InlineData(true, true, true, 1, 5, 2)] + [InlineData(false, + false, + false, + null, + null, + 3)] + [InlineData(true, + true, + true, + 1, + 5, + 2)] public async Task GetProductsPageSucceeds(bool expandCompany, bool expandPictures, bool expandDefaultPicture, @@ -210,7 +221,8 @@ public async Task DownloadProductPictureSucceeds() var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); var pictureId = Guid.Parse("7D5C57BA-05F4-44FD-832E-5145C5AB0486"); - await DownloadProductPictureTest(productId, pictureId); + await DownloadProductPictureTest(productId, + pictureId); } [Fact(DisplayName = "Download Product's Picture Thumbnail succeeds")] @@ -219,7 +231,9 @@ public async Task DownloadProductPictureThumbnailSucceeds() var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); var pictureId = Guid.Parse("7D5C57BA-05F4-44FD-832E-5145C5AB0486"); - await DownloadProductPictureTest(productId, pictureId, true); + await DownloadProductPictureTest(productId, + pictureId, + true); } private async Task DownloadProductPictureTest(Guid productId, @@ -285,25 +299,27 @@ [.. tempImages.Select(i => i.Id)], .Should() .Be((int)HttpStatusCode.Created); - var result = await response.GetStringAsync(); + var result = await response.GetJsonAsync(); - var id = Guid.Empty; result.Should() - .Match(value => Guid.TryParse(value.Replace("\"", ""), out id)); + .NotBeNull(); + result.Id + .Should() + .NotBeEmpty(); response.Headers .Should() - .Contain(("Location", ApiRoutes.Products.Get(id).ToString())); + .Contain(("Location", ApiRoutes.Products.Get(result.Id).ToString())); var products = await GetDbContext().Set() - .Include(x => x.Company) - .Include(x => x.Pictures) - .ThenInclude(x => x.Thumbnail) - .Include(x => x.DefaultPicture) - .ToListAsync(); + .Include(x => x.Company) + .Include(x => x.Pictures) + .ThenInclude(x => x.Thumbnail) + .Include(x => x.DefaultPicture) + .ToListAsync(); products.Should() .HaveCount(4); - var newProduct = products.SingleOrDefault(c => c.Id == id); + var newProduct = products.SingleOrDefault(c => c.Id == result.Id); newProduct.Should() .NotBeNull(); newProduct.Title @@ -340,14 +356,14 @@ [.. tempImages.Select(i => i.Id)], #if (massTransitIntegration) #if (apiService) - (await apiTestHarness.Published.Any()) + (await apiTestHarness.Published.SelectAsync().AnyAsync()) .Should() .BeTrue(); #endif #if (workerService) var consumerHarness = serviceTestHarness.GetConsumerHarness(); - (await consumerHarness.Consumed.Any()) + (await consumerHarness.Consumed.SelectAsync().AnyAsync()) .Should() .BeTrue(); #endif @@ -445,7 +461,6 @@ public async Task EditExistingProductSucceeds(string title, .Should() .NotBeNull(); }); - } [Fact(DisplayName = "Delete existing Product succeeds")] @@ -462,10 +477,10 @@ public async Task DeleteExistingProductSucceeds() .Be((int)HttpStatusCode.OK); var products = await GetDbContext().Set() - .ToListAsync(); + .ToListAsync(); products.Should() - .HaveCount(2); + .HaveCount(2); products.Should() .NotContain(x => x.Id == productId); } From fa691aaf73ae97a0e3dcb2bd70c93649afb56832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Sun, 28 Dec 2025 22:03:13 +0100 Subject: [PATCH 20/30] Undo accidental cleanup. --- .../MediatorExtensions.cs | 68 +++++++++---------- .../Tests/CompaniesTests.cs | 25 +++---- .../Tests/ProductsTests.cs | 48 +++++-------- 3 files changed, 56 insertions(+), 85 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs index 88ae43d..3e4a957 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs @@ -10,13 +10,6 @@ namespace Monaco.Template.Backend.Common.Api.Application; public static class MediatorExtensions { - private static Results GetFileDownload(TResult? item) where TResult : FileDownloadDto => - item is null - ? TypedResults.NotFound() - : TypedResults.File(item.FileContent, - item.ContentType, - item.FileName); - extension(ISender sender) { /// @@ -26,10 +19,10 @@ item is null /// /// /// - public async Task, NotFound>> ExecuteQueryAsync(QueryBase query, CancellationToken cancellationToken = default) + public async Task, NotFound>> ExecuteQueryAsync(QueryBase query, + CancellationToken cancellationToken = default) { - var result = await sender.Send(query, - cancellationToken); + var result = await sender.Send(query, cancellationToken); return result is null ? TypedResults.NotFound() : TypedResults.Ok(result); @@ -42,10 +35,10 @@ public async Task, NotFound>> ExecuteQueryAsync(Que /// /// /// - public async Task>, NotFound>> ExecuteQueryAsync(QueryPagedBase query, CancellationToken cancellationToken = default) + public async Task>, NotFound>> ExecuteQueryAsync(QueryPagedBase query, + CancellationToken cancellationToken = default) { - var result = await sender.Send(query, - cancellationToken); + var result = await sender.Send(query, cancellationToken); return result is null ? TypedResults.NotFound() : TypedResults.Ok(result); @@ -58,10 +51,10 @@ public async Task>, NotFound>> ExecuteQueryAsync /// /// - public async Task, NotFound>> ExecuteQueryAsync(QueryByIdBase query, CancellationToken cancellationToken = default) + public async Task, NotFound>> ExecuteQueryAsync(QueryByIdBase query, + CancellationToken cancellationToken = default) { - var result = await sender.Send(query, - cancellationToken); + var result = await sender.Send(query, cancellationToken); return result is null ? TypedResults.NotFound() : TypedResults.Ok(result); @@ -75,10 +68,10 @@ public async Task, NotFound>> ExecuteQueryAsync(Que /// /// /// - public async Task, NotFound>> ExecuteQueryAsync(QueryByKeyBase query, CancellationToken cancellationToken = default) + public async Task, NotFound>> ExecuteQueryAsync(QueryByKeyBase query, + CancellationToken cancellationToken = default) { - var item = await sender.Send(query, - cancellationToken); + var item = await sender.Send(query, cancellationToken); return item is null ? TypedResults.NotFound() : TypedResults.Ok(item); @@ -94,8 +87,7 @@ public async Task, NotFound>> ExecuteQueryAsync> ExecuteFileDownloadAsync(QueryBase query, CancellationToken cancellationToken = default) where TResult : FileDownloadDto { - var item = await sender.Send(query, - cancellationToken); + var item = await sender.Send(query, cancellationToken); return GetFileDownload(item); } @@ -109,8 +101,7 @@ public async Task> ExecuteFileDownloadAs public async Task> ExecuteFileDownloadAsync(QueryByIdBase query, CancellationToken cancellationToken = default) where TResult : FileDownloadDto { - var item = await sender.Send(query, - cancellationToken); + var item = await sender.Send(query, cancellationToken); return GetFileDownload(item); } @@ -127,14 +118,12 @@ public async Task, NotFound, ValidationProblem> object[]? uriParams = null, CancellationToken cancellationToken = default) { - var result = await sender.Send(command, - cancellationToken); + var result = await sender.Send(command, cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), - _ => TypedResults.Created(string.Format(resultUri, - [.. uriParams ?? [], result.Result]), + _ => TypedResults.Created(string.Format(resultUri, [.. uriParams ?? [], result.Result]), new CreatedResponse(result.Result)) }; } @@ -145,10 +134,10 @@ public async Task, NotFound, ValidationProblem> /// /// /// - public async Task> ExecuteCommandNoContentAsync(CommandBase command, CancellationToken cancellationToken = default) + public async Task> ExecuteCommandNoContentAsync(CommandBase command, + CancellationToken cancellationToken = default) { - var result = await sender.Send(command, - cancellationToken); + var result = await sender.Send(command, cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), @@ -163,10 +152,10 @@ public async Task> ExecuteComman /// /// /// - public async Task> ExecuteCommandOkAsync(CommandBase command, CancellationToken cancellationToken = default) + public async Task> ExecuteCommandOkAsync(CommandBase command, + CancellationToken cancellationToken = default) { - var result = await sender.Send(command, - cancellationToken); + var result = await sender.Send(command, cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), @@ -187,8 +176,7 @@ public async Task> ExecuteComman TResponse response, CancellationToken cancellationToken = default) where TResponse : IResult { - var result = await sender.Send(command, - cancellationToken); + var result = await sender.Send(command, cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), @@ -210,8 +198,7 @@ public async Task> ExecuteComman Func func, CancellationToken cancellationToken = default) where TResponse : IResult { - var result = await sender.Send(command, - cancellationToken); + var result = await sender.Send(command, cancellationToken); return result switch { { ItemNotFound: true } => TypedResults.NotFound(), @@ -220,4 +207,11 @@ public async Task> ExecuteComman }; } } + + private static Results GetFileDownload(TResult? item) where TResult : FileDownloadDto => + item is null + ? TypedResults.NotFound() + : TypedResults.File(item.FileContent, + item.ContentType, + item.FileName); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs index 6a21536..be1a250 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs @@ -1,7 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Mail; -using AutoFixture.Xunit2; +using AutoFixture.Xunit2; using AwesomeAssertions; using Flurl.Http; using Microsoft.EntityFrameworkCore; @@ -11,12 +8,14 @@ using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Domain.Model.Entities; using Monaco.Template.Backend.Domain.Model.ValueObjects; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Mail; namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] -[Trait("Integration Tests", - "Companies")] +[Trait("Integration Tests", "Companies")] public class CompaniesTests : IntegrationTest { public CompaniesTests(AppFixture fixture) : base(fixture) @@ -39,22 +38,14 @@ public override async Task InitializeAsync() } [Theory(DisplayName = "Get Companies page succeeds")] - [InlineData(false, - null, - null, - 3)] - [InlineData(true, - 1, - 5, - 2)] + [InlineData(false, null, null, 3)] + [InlineData(true, 1, 5, 2)] public async Task GetCompaniesPageSucceeds(bool expandCountry, int? offset, int? limit, int expectedItemsCount) { - var response = await CreateRequest(ApiRoutes.Companies.Query(expandCountry, - offset, - limit)).GetAsync(); + var response = await CreateRequest(ApiRoutes.Companies.Query(expandCountry, offset, limit)).GetAsync(); response.StatusCode .Should() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index 0035adc..5cb134f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -1,12 +1,4 @@ -#if (massTransitIntegration && (apiService || workerService)) -using Monaco.Template.Backend.Messages.V1; -#endif -#if (massTransitIntegration || workerService) -using Monaco.Template.Backend.Worker.Consumers; -#endif -using System.Diagnostics.CodeAnalysis; -using System.Net; -using AutoFixture.Xunit2; +using AutoFixture.Xunit2; using AwesomeAssertions; using Azure.Storage.Blobs; using Dasync.Collections; @@ -17,13 +9,20 @@ using Monaco.Template.Backend.Common.Api.Application; using Monaco.Template.Backend.Common.Domain.Model; using Monaco.Template.Backend.Domain.Model.Entities; +#if (massTransitIntegration && (apiService || workerService)) +using Monaco.Template.Backend.Messages.V1; +#endif +#if (massTransitIntegration || workerService) +using Monaco.Template.Backend.Worker.Consumers; +#endif +using System.Diagnostics.CodeAnalysis; +using System.Net; using File = System.IO.File; namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] -[Trait("Integration Tests", - "Products")] +[Trait("Integration Tests", "Products")] public class ProductsTests : IntegrationTest { public ProductsTests(AppFixture fixture) : base(fixture) @@ -50,26 +49,16 @@ public override async Task InitializeAsync() } #if (auth) - private Task SetupAccessToken() => SetupAccessToken([Auth.Auth.Roles.Administrator]); + private Task SetupAccessToken() => + SetupAccessToken([Auth.Auth.Roles.Administrator]); #endif private BlobContainerClient GetBlobContainerClient() => - new(Fixture.StorageConnectionString, - AppFixture.StorageContainer); + new(Fixture.StorageConnectionString, AppFixture.StorageContainer); [Theory(DisplayName = "Get Products page succeeds")] - [InlineData(false, - false, - false, - null, - null, - 3)] - [InlineData(true, - true, - true, - 1, - 5, - 2)] + [InlineData(false, false, false, null, null, 3)] + [InlineData(true, true, true, 1, 5, 2)] public async Task GetProductsPageSucceeds(bool expandCompany, bool expandPictures, bool expandDefaultPicture, @@ -221,8 +210,7 @@ public async Task DownloadProductPictureSucceeds() var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); var pictureId = Guid.Parse("7D5C57BA-05F4-44FD-832E-5145C5AB0486"); - await DownloadProductPictureTest(productId, - pictureId); + await DownloadProductPictureTest(productId, pictureId); } [Fact(DisplayName = "Download Product's Picture Thumbnail succeeds")] @@ -231,9 +219,7 @@ public async Task DownloadProductPictureThumbnailSucceeds() var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); var pictureId = Guid.Parse("7D5C57BA-05F4-44FD-832E-5145C5AB0486"); - await DownloadProductPictureTest(productId, - pictureId, - true); + await DownloadProductPictureTest(productId, pictureId, true); } private async Task DownloadProductPictureTest(Guid productId, From 72e45472c8728611933e2f4c7248079fd2268453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Wed, 21 Jan 2026 00:22:50 +0100 Subject: [PATCH 21/30] Refactor integration test infra: shared fixture & Respawn - Use [Collection("IntegrationTests")] to share AppFixture across all test classes, starting containers only once per run - Add Respawn for fast, reliable DB resets between tests - Apply DB migrations once in fixture, not per test - Remove redundant DbContext/migration logic from test base - Disable test collection parallelization via xunit.runner.schema.json - Update dependencies and project files for Respawn and config - Simplify and clarify test setup/teardown for better reliability --- .../Backend/Solution/Directory.Packages.props | 1 + .../AppFixture.cs | 73 ++++++++++++++++++ .../IntegrationTest.cs | 76 +++++-------------- .../IntegrationTestsCollection.cs | 15 ++++ ...o.Template.Backend.IntegrationTests.csproj | 7 ++ .../Tests/CompaniesTests.cs | 21 +++-- .../Tests/CountriesTests.cs | 11 ++- .../Tests/FilesTests.cs | 8 +- .../Tests/ProductsTests.cs | 65 +++++++++------- .../xunit.runner.schema.json | 5 ++ 10 files changed, 179 insertions(+), 103 deletions(-) create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTestsCollection.cs create mode 100644 src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/xunit.runner.schema.json diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index 160c452..a2b628e 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -24,6 +24,7 @@ + diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs index cbcbf99..3b2afad 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs @@ -5,14 +5,25 @@ #if (auth) using Testcontainers.Keycloak; #endif +using System.Diagnostics.CodeAnalysis; using Flurl; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; +using Monaco.Template.Backend.Domain.Model.Entities; +using Respawn; using Testcontainers.MsSql; #if (massTransitIntegration) using Testcontainers.RabbitMq; +using Monaco.Template.Backend.IntegrationTests.Factories; +using Monaco.Template.Backend.Application.Persistence; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; #endif namespace Monaco.Template.Backend.IntegrationTests; +[ExcludeFromCodeCoverage] public class AppFixture : IAsyncLifetime { #if (massTransitIntegration) @@ -44,6 +55,17 @@ public class AppFixture : IAsyncLifetime .Build(); #endif +#if (apiService) + public ApiWebApplicationFactory WebAppFactory = null!; + +#endif +#if (workerService) + public WorkerServiceFactory WorkerServiceFactory = null!; + public IHost WorkerServiceInstance = null!; + +#endif + private Respawner? _respawner; + public async Task InitializeAsync() { await SqlContainer.StartAsync(); @@ -58,8 +80,32 @@ public async Task InitializeAsync() await InitStorage(); #endif + +#if (apiService) + WebAppFactory = new ApiWebApplicationFactory(this); +#endif +#if (workerService) + WorkerServiceFactory = new WorkerServiceFactory(this); + WorkerServiceInstance = WorkerServiceFactory.GetHostInstance(); +#endif + + await ApplyDbMigrationsAsync(); } + public virtual AppDbContext GetDbContext() => +#if (apiService) + WebAppFactory.Services +#elif (workerService) + WorkerServiceInstance.Services +#endif + .CreateScope() + .ServiceProvider + .GetRequiredService(); + + protected virtual async Task ApplyDbMigrationsAsync(string? targetMigration = null) => + await GetDbContext().GetService() + .MigrateAsync(targetMigration); + public string SqlConnectionString => SqlContainer.GetConnectionString(); #if (massTransitIntegration) @@ -127,4 +173,31 @@ private async Task InitStorage() .CreateAsync(); } #endif + + /// + /// Resets database data using Respawn. This is much faster than rolling back migrations. + /// Respawner is lazily initialized on first call. + /// + public async Task ResetDatabaseDataAsync() + { + var connection = GetDbContext().Database + .GetDbConnection(); + + if (connection.State != System.Data.ConnectionState.Open) + await connection.OpenAsync(); + + _respawner ??= await Respawner.CreateAsync(connection, + new RespawnerOptions + { + DbAdapter = DbAdapter.SqlServer, + TablesToIgnore = + [ + "__EFMigrationsHistory", + nameof(Country) + ], + SchemasToInclude = ["dbo"] + }); + + await _respawner.ResetAsync(connection); + } } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs index 5fdf7b9..2510581 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs @@ -1,18 +1,13 @@ using Flurl.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.Extensions.DependencyInjection; #if (workerService) using Microsoft.Extensions.Hosting; #endif using System.Diagnostics.CodeAnalysis; -using Monaco.Template.Backend.Application.Persistence; #if (massTransitIntegration) using MassTransit.Testing; #endif -using Monaco.Template.Backend.IntegrationTests.Factories; #if (apiService && auth) using Monaco.Template.Backend.IntegrationTests.Auth; #endif @@ -20,16 +15,13 @@ namespace Monaco.Template.Backend.IntegrationTests; [ExcludeFromCodeCoverage] -[Collection("IntegrationTests")] -public abstract class IntegrationTest : IClassFixture, IAsyncLifetime +public abstract class IntegrationTest : IAsyncLifetime { protected readonly AppFixture Fixture; #if (apiService) - protected ApiWebApplicationFactory WebAppFactory; protected IFlurlClient Client; #endif #if (workerService) - protected WorkerServiceFactory WorkerServiceFactory; protected IHost WorkerServiceInstance; #endif #if (apiService && auth) @@ -42,12 +34,6 @@ public abstract class IntegrationTest : IClassFixture, IAsyncLifetim protected IntegrationTest(AppFixture fixture) { Fixture = fixture; -#if (apiService) - WebAppFactory = new ApiWebApplicationFactory(Fixture); -#endif -#if (workerService) - WorkerServiceFactory = new WorkerServiceFactory(Fixture); -#endif #if (apiService) var clientOptions = new WebApplicationFactoryClientOptions @@ -55,7 +41,7 @@ protected IntegrationTest(AppFixture fixture) AllowAutoRedirect = false }; - Client = new FlurlClient(WebAppFactory.CreateClient(clientOptions)) + Client = new FlurlClient(Fixture.WebAppFactory.CreateClient(clientOptions)) #if (auth) .AllowAnyHttpStatus() .BeforeCall(call => @@ -75,7 +61,7 @@ protected IntegrationTest(AppFixture fixture) #endif #endif #if (workerService) - WorkerServiceInstance = WorkerServiceFactory.GetHostInstance(); + WorkerServiceInstance = Fixture.WorkerServiceInstance; #endif } @@ -83,10 +69,8 @@ protected IntegrationTest(AppFixture fixture) protected IFlurlRequest CreateRequest(string endpoint) => Client.Request(endpoint); #endif - public virtual async Task InitializeAsync() - { - await ApplyDbMigrations(); - } + public virtual Task InitializeAsync() => + Task.CompletedTask; #if (apiService && auth) protected virtual async Task SetupAccessToken(string audienceClientId, @@ -106,52 +90,26 @@ protected virtual Task SetupAccessToken(string[] roles) => Auth.Auth.Scopes); #endif - protected virtual AppDbContext GetDbContext() => -#if (apiService) - WebAppFactory.Services -#elif (workerService) - WorkerServiceInstance.Services -#endif - .CreateScope() - .ServiceProvider - .GetRequiredService(); - protected virtual async Task ApplyDbMigrations(string? targetMigration = null) => - await GetDbContext().GetService() - .MigrateAsync(targetMigration); - - protected virtual async Task RollbackDbMigrations() => - await ApplyDbMigrations("0"); - - protected virtual async Task RunScriptAsync(string filePath) - { - var script = await File.ReadAllTextAsync(filePath); - await GetDbContext().Database - .ExecuteSqlRawAsync(script); - } + protected virtual async Task RunScriptAsync(string filePath) => + await Fixture.GetDbContext() + .Database + .ExecuteSqlRawAsync(await File.ReadAllTextAsync(filePath)); #if (apiService && massTransitIntegration) protected virtual ITestHarness GetApiTestHarness() => - WebAppFactory.Services - .GetTestHarness(); + Fixture.WebAppFactory + .Services + .GetTestHarness(); #endif #if (workerService && massTransitIntegration) protected virtual ITestHarness GetServiceTestHarness() => - WorkerServiceInstance.Services - .GetTestHarness(); + Fixture.WorkerServiceInstance + .Services + .GetTestHarness(); #endif - public virtual async Task DisposeAsync() - { - await RollbackDbMigrations(); -#if (apiService) - await WebAppFactory.DisposeAsync(); -#endif -#if (workerService) - - await WorkerServiceInstance.StopAsync(); - WorkerServiceInstance.Dispose(); -#endif - } + public virtual async Task DisposeAsync() => + await Fixture.ResetDatabaseDataAsync(); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTestsCollection.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTestsCollection.cs new file mode 100644 index 0000000..71e5114 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTestsCollection.cs @@ -0,0 +1,15 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Monaco.Template.Backend.IntegrationTests; + +/// +/// Defines the "IntegrationTests" collection so that all test classes +/// sharing this collection use a single instance. +/// This ensures containers start once and migrations run once per test run. +/// +[ExcludeFromCodeCoverage] +[CollectionDefinition("IntegrationTests")] +public class IntegrationTestsCollection : ICollectionFixture +{ + // No code required; this class ties AppFixture to the "IntegrationTests" collection. +} \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj index 47a5cdd..4c5994f 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Monaco.Template.Backend.IntegrationTests.csproj @@ -20,6 +20,7 @@ + @@ -62,4 +63,10 @@ + + + Always + + + diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs index be1a250..e81f970 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs @@ -15,6 +15,7 @@ namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Companies")] public class CompaniesTests : IntegrationTest { @@ -90,8 +91,9 @@ public async Task GetCompanySucceeds() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var company = await GetDbContext().Set() - .SingleAsync(c => c.Id == companyId); + var company = await Fixture.GetDbContext() + .Set() + .SingleAsync(c => c.Id == companyId); result.Should() .NotBeNull(); @@ -157,8 +159,9 @@ public async Task CreateNewCompanySucceeds(string name, .Should() .Contain(("Location", ApiRoutes.Companies.Get(result.Id).ToString())); - var companies = await GetDbContext().Set() - .ToListAsync(); + var companies = await Fixture.GetDbContext() + .Set() + .ToListAsync(); companies.Should() .HaveCount(4); @@ -217,8 +220,9 @@ public async Task EditExistingCompanySucceeds(string name, .Should() .Be((int)HttpStatusCode.NoContent); - var company = await GetDbContext().Set() - .SingleOrDefaultAsync(c => c.Id == companyId); + var company = await Fixture.GetDbContext() + .Set() + .SingleOrDefaultAsync(c => c.Id == companyId); company.Should() .NotBeNull(); company!.Name @@ -257,8 +261,9 @@ public async Task DeleteExistingCompanySucceeds() .Should() .Be((int)HttpStatusCode.OK); - var companies = await GetDbContext().Set() - .ToListAsync(); + var companies = await Fixture.GetDbContext() + .Set() + .ToListAsync(); companies.Should() .HaveCount(2); companies.Should() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs index a8ce700..3acde68 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs @@ -9,6 +9,7 @@ namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Countries")] public class CountriesTests : IntegrationTest { @@ -38,8 +39,9 @@ public async Task GetCountriesSucceeds() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var countriesCount = await GetDbContext().Set() - .CountAsync(); + var countriesCount = await Fixture.GetDbContext() + .Set() + .CountAsync(); result.Should() .NotBeNull(); @@ -59,8 +61,9 @@ public async Task GetCountrySucceeds() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var country = await GetDbContext().Set() - .SingleAsync(c => c.Id == countryId); + var country = await Fixture.GetDbContext() + .Set() + .SingleAsync(c => c.Id == countryId); result.Should() .NotBeNull(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs index b8d8675..f3f6b59 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs @@ -10,6 +10,7 @@ namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Files")] public class FilesTests : IntegrationTest { @@ -46,9 +47,10 @@ public async Task UploadFileSuccceeds() .Should() .Be((int)HttpStatusCode.Created); - var files = await GetDbContext().Set() - .AsNoTracking() - .ToListAsync(); + var files = await Fixture.GetDbContext() + .Set() + .AsNoTracking() + .ToListAsync(); files.Should() .AllBeAssignableTo() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index 5cb134f..c11fefa 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -22,6 +22,7 @@ namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Products")] public class ProductsTests : IntegrationTest { @@ -36,9 +37,10 @@ public override async Task InitializeAsync() { await base.InitializeAsync(); await RunScriptAsync(@"Scripts\Products.sql"); - var images = await GetDbContext().Set() - .AsNoTracking() - .ToListAsync(); + var images = await Fixture.GetDbContext() + .Set() + .AsNoTracking() + .ToListAsync(); var blobContainerClient = GetBlobContainerClient(); foreach (var image in images) @@ -141,12 +143,13 @@ public async Task GetProductSucceeds() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var product = await GetDbContext().Set() - .Include(x => x.Company) - .Include(x => x.DefaultPicture) - .Include(x => x.Pictures) - .ThenInclude(x => x.Thumbnail) - .SingleAsync(c => c.Id == productId); + var product = await Fixture.GetDbContext() + .Set() + .Include(x => x.Company) + .Include(x => x.DefaultPicture) + .Include(x => x.Pictures) + .ThenInclude(x => x.Thumbnail) + .SingleAsync(c => c.Id == productId); result.Should() .NotBeNull(); @@ -230,11 +233,12 @@ private async Task DownloadProductPictureTest(Guid productId, pictureId, isThumbnail)).GetAsync(); - var picture = await GetDbContext().Set() - .AsNoTracking() - .Where(x => x.Id == pictureId) - .Select(x => isThumbnail.HasValue && isThumbnail.Value ? x.Thumbnail! : x) - .SingleAsync(); + var picture = await Fixture.GetDbContext() + .Set() + .AsNoTracking() + .Where(x => x.Id == pictureId) + .Select(x => isThumbnail.HasValue && isThumbnail.Value ? x.Thumbnail! : x) + .SingleAsync(); response.StatusCode .Should() @@ -263,10 +267,10 @@ public async Task CreateNewProductSucceeds(string title, var apiTestHarness = GetApiTestHarness(); #endif #if (workerService && massTransitIntegration) - var serviceTestHarness = GetServiceTestHarness(); + var serviceTestHarness = GetServiceTestHarness(); #endif - var dbContext = GetDbContext(); + var dbContext = Fixture.GetDbContext(); var tempImages = await dbContext.Set() .Where(i => i.IsTemp && i.ThumbnailId.HasValue) .ToListAsync(); @@ -296,12 +300,13 @@ [.. tempImages.Select(i => i.Id)], .Should() .Contain(("Location", ApiRoutes.Products.Get(result.Id).ToString())); - var products = await GetDbContext().Set() - .Include(x => x.Company) - .Include(x => x.Pictures) - .ThenInclude(x => x.Thumbnail) - .Include(x => x.DefaultPicture) - .ToListAsync(); + var products = await Fixture.GetDbContext() + .Set() + .Include(x => x.Company) + .Include(x => x.Pictures) + .ThenInclude(x => x.Thumbnail) + .Include(x => x.DefaultPicture) + .ToListAsync(); products.Should() .HaveCount(4); @@ -365,7 +370,7 @@ public async Task EditExistingProductSucceeds(string title, #if (auth) await SetupAccessToken(); #endif - var dbContext = GetDbContext(); + var dbContext = Fixture.GetDbContext(); var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); var productPictures = await dbContext.Set() .AsNoTracking() @@ -390,10 +395,11 @@ public async Task EditExistingProductSucceeds(string title, .Should() .Be((int)HttpStatusCode.NoContent); - var product = await GetDbContext().Set() - .Include(x => x.Pictures) - .Include(x => x.DefaultPicture) - .SingleOrDefaultAsync(c => c.Id == productId); + var product = await Fixture.GetDbContext() + .Set() + .Include(x => x.Pictures) + .Include(x => x.DefaultPicture) + .SingleOrDefaultAsync(c => c.Id == productId); product.Should() .NotBeNull(); product!.Title @@ -462,8 +468,9 @@ public async Task DeleteExistingProductSucceeds() .Should() .Be((int)HttpStatusCode.OK); - var products = await GetDbContext().Set() - .ToListAsync(); + var products = await Fixture.GetDbContext() + .Set() + .ToListAsync(); products.Should() .HaveCount(2); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/xunit.runner.schema.json b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/xunit.runner.schema.json new file mode 100644 index 0000000..e98438d --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/xunit.runner.schema.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "parallelizeTestCollections": false, + "maxParallelThreads": 1 +} From badcdbbcda111ac1a052d43146c78d02849682d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Wed, 21 Jan 2026 00:29:19 +0100 Subject: [PATCH 22/30] Update NuGet package versions to latest releases --- .../Backend/Solution/Directory.Packages.props | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index a2b628e..c23bef3 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -3,7 +3,7 @@ true - + @@ -12,25 +12,25 @@ - - - - - - - - - - + + + + + + + + + + - + - + @@ -40,17 +40,17 @@ - + - + - + - + @@ -59,7 +59,7 @@ - + @@ -70,8 +70,8 @@ - - + + From 5a7d6971503b7eec462c96bb76236b4dc99f89f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Wed, 21 Jan 2026 00:56:27 +0100 Subject: [PATCH 23/30] Formatting. --- .../Tests/ProductsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index c11fefa..b78e734 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -267,7 +267,7 @@ public async Task CreateNewProductSucceeds(string title, var apiTestHarness = GetApiTestHarness(); #endif #if (workerService && massTransitIntegration) - var serviceTestHarness = GetServiceTestHarness(); + var serviceTestHarness = GetServiceTestHarness(); #endif var dbContext = Fixture.GetDbContext(); From b6fb8d80a2665eef934d5930af420ddd7de2909f Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Sun, 1 Feb 2026 18:28:08 +0000 Subject: [PATCH 24/30] fix: revert change to product test --- .../Tests/ProductsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index b78e734..7d9b66d 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -347,14 +347,14 @@ [.. tempImages.Select(i => i.Id)], #if (massTransitIntegration) #if (apiService) - (await apiTestHarness.Published.SelectAsync().AnyAsync()) + (await apiTestHarness.Published.Any()) .Should() .BeTrue(); #endif #if (workerService) var consumerHarness = serviceTestHarness.GetConsumerHarness(); - (await consumerHarness.Consumed.SelectAsync().AnyAsync()) + (await consumerHarness.Consumed.Any()) .Should() .BeTrue(); #endif From 27a68de1333f810e99152cd7eda051ce970f4517 Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Sun, 1 Feb 2026 18:28:44 +0000 Subject: [PATCH 25/30] chore: update packages and comment formatting --- src/Content/Backend/Solution/Directory.Packages.props | 8 ++++---- .../MediatorExtensions.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Content/Backend/Solution/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index c23bef3..405a614 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -9,7 +9,7 @@ - + @@ -25,7 +25,7 @@ - + @@ -68,11 +68,11 @@ - + - + diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs index 3e4a957..1b84036 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api.Application/MediatorExtensions.cs @@ -110,8 +110,8 @@ public async Task> ExecuteFileDownloadAs /// /// /// The URI to include in the headers of the Created() response - /// /// The parameters (if any) to pass for concatenating into the resultUri + /// /// public async Task, NotFound, ValidationProblem>> ExecuteCommandCreatedAsync(CommandBase command, string resultUri, @@ -207,7 +207,7 @@ public async Task> ExecuteComman }; } } - + private static Results GetFileDownload(TResult? item) where TResult : FileDownloadDto => item is null ? TypedResults.NotFound() From 470b72f45ad8cc57ef19ad09e1d84a566cef24bf Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Sun, 1 Feb 2026 22:47:00 +0000 Subject: [PATCH 26/30] fix: integration test issues --- .../AppFixture.cs | 12 +++++++----- .../Tests/CompaniesTests.cs | 4 +--- .../Tests/ProductsTests.cs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs index 3b2afad..678c863 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs @@ -8,17 +8,19 @@ using System.Diagnostics.CodeAnalysis; using Flurl; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.DependencyInjection; +#if (workerService) using Microsoft.Extensions.Hosting; +#endif +using Monaco.Template.Backend.Application.Persistence; using Monaco.Template.Backend.Domain.Model.Entities; +using Monaco.Template.Backend.IntegrationTests.Factories; using Respawn; using Testcontainers.MsSql; #if (massTransitIntegration) using Testcontainers.RabbitMq; -using Monaco.Template.Backend.IntegrationTests.Factories; -using Monaco.Template.Backend.Application.Persistence; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; #endif namespace Monaco.Template.Backend.IntegrationTests; diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs index e81f970..eb3de9e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs @@ -22,10 +22,8 @@ public class CompaniesTests : IntegrationTest public CompaniesTests(AppFixture fixture) : base(fixture) { } -#if (auth) +#if (apiService && auth) protected override bool RequiresAuthentication => true; -#else - protected override bool RequiresAuthentication => false; #endif public override async Task InitializeAsync() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index 7d9b66d..70395a3 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -12,7 +12,7 @@ #if (massTransitIntegration && (apiService || workerService)) using Monaco.Template.Backend.Messages.V1; #endif -#if (massTransitIntegration || workerService) +#if (massTransitIntegration && workerService) using Monaco.Template.Backend.Worker.Consumers; #endif using System.Diagnostics.CodeAnalysis; From f1b52376b911ef81d00ae0cae6a20792a68e990a Mon Sep 17 00:00:00 2001 From: Matthew Toghill Date: Sun, 1 Feb 2026 22:55:17 +0000 Subject: [PATCH 27/30] chore: remove template config special custom operations for slnx files as no longer needed --- .../Solution/.template.config/template.json | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/Content/Backend/Solution/.template.config/template.json b/src/Content/Backend/Solution/.template.config/template.json index adc4ba2..ec2b038 100644 --- a/src/Content/Backend/Solution/.template.config/template.json +++ b/src/Content/Backend/Solution/.template.config/template.json @@ -18,34 +18,6 @@ "path": "Monaco.Template.Backend.slnx" } ], - "SpecialCustomOperations": { - "**.slnx": { - "operations": [ - { - "type": "conditional", - "configuration": { - "actionableIf": [ "", - "pseudoEndToken": "-- >", - "id": "fixPseudoNestedComments", - "resetFlag": "_TestResetFlag_" - } - } - ] - } - }, "symbols": { "apiGateway": { "type": "parameter", From 52c486cd86906c6e17109d14f4409aa96c1a54da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Mon, 2 Mar 2026 17:48:28 +0100 Subject: [PATCH 28/30] Refactor integration test infra: DI, containers, MassTransit Refactor and modernize integration test infrastructure: - Pin Testcontainers images for SQL Server, RabbitMQ, Azurite, Keycloak - Require IServiceProvider for DbContext resolution; update all usages - Move MassTransit test harness setup to extension methods - Add GetCustomFactory for flexible Web/Worker factory config - Remove direct IHost usage for worker; use DI instead - Use per-test FlurlClient with proper disposal - Improve resource cleanup in AppFixture.DisposeAsync - Use Parallel.ForEachAsync for blob cleanup in ProductsTests - Enhance MassTransit message assertions for content matching - General code cleanup and modernization for maintainability and test isolation --- .../AppFixture.cs | 81 ++++++++++-------- .../Factories/ApiWebApplicationFactory.cs | 47 ++++++----- .../Factories/WorkerServiceFactory.cs | 59 ++++++------- .../IntegrationTest.cs | 57 ++++--------- .../Tests/CompaniesTests.cs | 31 +++++-- .../Tests/CountriesTests.cs | 12 ++- .../Tests/FilesTests.cs | 13 +-- .../Tests/ProductsTests.cs | 84 ++++++++++++------- 8 files changed, 208 insertions(+), 176 deletions(-) diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs index 3b2afad..992f37e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/AppFixture.cs @@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis; using Flurl; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Hosting; using Monaco.Template.Backend.Domain.Model.Entities; using Respawn; using Testcontainers.MsSql; @@ -38,21 +37,20 @@ public class AppFixture : IAsyncLifetime public const string KeycloakRealmPassword = "admin"; #endif - public MsSqlContainer SqlContainer = new MsSqlBuilder().Build(); + public MsSqlContainer SqlContainer = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04").Build(); #if (massTransitIntegration) - public RabbitMqContainer RabbitMqContainer = new RabbitMqBuilder().WithEnvironment("RABBITMQ_DEFAULT_VHOST", RabbitMqVHost) - .Build(); + public RabbitMqContainer RabbitMqContainer = new RabbitMqBuilder("rabbitmq:3.11").WithEnvironment("RABBITMQ_DEFAULT_VHOST", RabbitMqVHost) + .Build(); #endif #if (filesSupport) - public AzuriteContainer AzuriteContainer = new AzuriteBuilder().WithCommand("--skipApiVersionCheck") - .Build(); + public AzuriteContainer AzuriteContainer = new AzuriteBuilder("mcr.microsoft.com/azure-storage/azurite:3.28.0").WithCommand("--skipApiVersionCheck") + .Build(); #endif #if (auth) - public KeycloakContainer KeycloakContainer = new KeycloakBuilder().WithImage("quay.io/keycloak/keycloak:25.0.6") - .WithResourceMapping(new FileInfo("./Imports/Keycloak/realm-export-template.json"), - new FileInfo("/opt/keycloak/data/import/realm-export-template.json")) - .WithCommand("--import-realm") - .Build(); + public KeycloakContainer KeycloakContainer = new KeycloakBuilder("quay.io/keycloak/keycloak:25.0.6").WithResourceMapping(new FileInfo("./Imports/Keycloak/realm-export-template.json"), + new FileInfo("/opt/keycloak/data/import/realm-export-template.json")) + .WithCommand("--import-realm") + .Build(); #endif #if (apiService) @@ -61,7 +59,6 @@ public class AppFixture : IAsyncLifetime #endif #if (workerService) public WorkerServiceFactory WorkerServiceFactory = null!; - public IHost WorkerServiceInstance = null!; #endif private Respawner? _respawner; @@ -86,25 +83,24 @@ public async Task InitializeAsync() #endif #if (workerService) WorkerServiceFactory = new WorkerServiceFactory(this); - WorkerServiceInstance = WorkerServiceFactory.GetHostInstance(); #endif await ApplyDbMigrationsAsync(); } - public virtual AppDbContext GetDbContext() => + public virtual AppDbContext GetDbContext(IServiceProvider services) => + services.CreateScope() + .ServiceProvider + .GetRequiredService(); + + protected virtual async Task ApplyDbMigrationsAsync(string? targetMigration = null) => #if (apiService) - WebAppFactory.Services + await GetDbContext(WebAppFactory.Services) #elif (workerService) - WorkerServiceInstance.Services + await GetDbContext(WorkerServiceInstance.Services) #endif - .CreateScope() - .ServiceProvider - .GetRequiredService(); - - protected virtual async Task ApplyDbMigrationsAsync(string? targetMigration = null) => - await GetDbContext().GetService() - .MigrateAsync(targetMigration); + .GetService() + .MigrateAsync(targetMigration); public string SqlConnectionString => SqlContainer.GetConnectionString(); @@ -141,30 +137,38 @@ await GetDbContext().GetService() .AppendPathSegments("realms", KeycloakRealm); #endif - public async Task DisposeAsync() - { - await SqlContainer.StopAsync(); + public async Task DisposeAsync() + { +#if (apiService) + await WebAppFactory.DisposeAsync(); + +#endif +#if (workerService) + await WorkerServiceFactory.DisposeAsync(); + +#endif + await SqlContainer.StopAsync(); #if (massTransitIntegration) - await RabbitMqContainer.StopAsync(); + await RabbitMqContainer.StopAsync(); #endif #if (filesSupport) - await AzuriteContainer.StopAsync(); + await AzuriteContainer.StopAsync(); #endif #if (auth) - await KeycloakContainer.StopAsync(); + await KeycloakContainer.StopAsync(); #endif - await SqlContainer.DisposeAsync(); + await SqlContainer.DisposeAsync(); #if (massTransitIntegration) - await RabbitMqContainer.DisposeAsync(); + await RabbitMqContainer.DisposeAsync(); #endif #if (filesSupport) - await AzuriteContainer.DisposeAsync(); + await AzuriteContainer.DisposeAsync(); #endif #if (auth) - await KeycloakContainer.DisposeAsync(); + await KeycloakContainer.DisposeAsync(); #endif - } + } #if (filesSupport) private async Task InitStorage() @@ -180,8 +184,13 @@ private async Task InitStorage() /// public async Task ResetDatabaseDataAsync() { - var connection = GetDbContext().Database - .GetDbConnection(); +#if (apiService) + var services = WebAppFactory.Services; +#elif (workerService) + var services = WorkerServiceInstance.Services; +#endif + var connection = GetDbContext(services).Database + .GetDbConnection(); if (connection.State != System.Data.ConnectionState.Open) await connection.OpenAsync(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/ApiWebApplicationFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/ApiWebApplicationFactory.cs index 0dc06ee..74f707e 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/ApiWebApplicationFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/ApiWebApplicationFactory.cs @@ -33,27 +33,32 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) => ["MessageBus:RabbitMQ:Password"] = _fixture.RabbitMqPassword #endif }) -#if (massTransitIntegration) - .UseSetting("https_port", "8080") - .ConfigureServices((context, services) => - { - var configuration = context.Configuration; - services.AddMassTransitTestHarness(cfg => - { - var rabbitMqConfig = configuration.GetSection("MessageBus:RabbitMQ"); - if (rabbitMqConfig.Exists()) - cfg.UsingRabbitMq((_, busCfg) => busCfg.Host(rabbitMqConfig["Host"], - ushort.Parse(rabbitMqConfig["Port"] ?? "5672"), - rabbitMqConfig["VHost"], - h => - { - h.Username(rabbitMqConfig["Username"]!); - h.Password(rabbitMqConfig["Password"]!); - })); - }); - }); -#else .UseSetting("https_port", "8080"); -#endif + public WebApplicationFactory GetCustomFactory(Action configure) => + WithWebHostBuilder(configure); +} +#if (massTransitIntegration) + +public static class WebAppFactoryExtensions +{ + extension(IWebHostBuilder builder) + { + public IWebHostBuilder AddMassTransitTestHarnessForWebApp() => + builder.ConfigureServices((context, services) => + services.AddMassTransitTestHarness(cfg => + { + var rabbitMqConfig = context.Configuration.GetSection("MessageBus:RabbitMQ"); + if (rabbitMqConfig.Exists()) + cfg.UsingRabbitMq((_, busCfg) => busCfg.Host(rabbitMqConfig["Host"], + ushort.Parse(rabbitMqConfig["Port"] ?? "5672"), + rabbitMqConfig["VHost"], + h => + { + h.Username(rabbitMqConfig["Username"]!); + h.Password(rabbitMqConfig["Password"]!); + })); + })); + } } +#endif \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/WorkerServiceFactory.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/WorkerServiceFactory.cs index baeb3a0..e792c75 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/WorkerServiceFactory.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Factories/WorkerServiceFactory.cs @@ -4,8 +4,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace Monaco.Template.Backend.IntegrationTests.Factories; @@ -32,32 +30,37 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) => ["MessageBus:RabbitMQ:Password"] = _fixture.RabbitMqPassword #endif }) + .Configure(_ => { }); + + public WebApplicationFactory GetCustomFactory(Action configure) => + WithWebHostBuilder(configure); +} #if (massTransitIntegration) - .ConfigureServices((context, services) => - { - var configuration = context.Configuration; - services.AddMassTransitTestHarness(cfg => - { - var rabbitMqConfig = configuration.GetSection("MessageBus:RabbitMQ"); - if (rabbitMqConfig.Exists()) - cfg.UsingRabbitMq((ctx, busCfg) => - { - busCfg.Host(rabbitMqConfig["Host"], - ushort.Parse(rabbitMqConfig["Port"] ?? "5672"), - rabbitMqConfig["VHost"], - h => - { - h.Username(rabbitMqConfig["Username"]!); - h.Password(rabbitMqConfig["Password"]!); - }); - busCfg.ConfigureEndpoints(ctx, new DefaultEndpointNameFormatter(true)); - }); - }); - }) -#endif - .Configure(_ => { }); +public static class WorkerServiceFactoryExtensions +{ + extension(IWebHostBuilder builder) + { + public IWebHostBuilder AddMassTransitTestHarnessForWorker() => + builder.ConfigureServices((context, services) => + services.AddMassTransitTestHarness(cfg => + { + var rabbitMqConfig = context.Configuration.GetSection("MessageBus:RabbitMQ"); + if (rabbitMqConfig.Exists()) + cfg.UsingRabbitMq((ctx, busCfg) => + { + busCfg.Host(rabbitMqConfig["Host"], + ushort.Parse(rabbitMqConfig["Port"] ?? "5672"), + rabbitMqConfig["VHost"], + h => + { + h.Username(rabbitMqConfig["Username"]!); + h.Password(rabbitMqConfig["Password"]!); + }); - public IHost GetHostInstance() => - Services.GetRequiredService(); -} \ No newline at end of file + busCfg.ConfigureEndpoints(ctx, new DefaultEndpointNameFormatter(true)); + }); + })); + } +} +#endif \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs index 2510581..c32b8ba 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs @@ -1,13 +1,7 @@ using Flurl.Http; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; -#if (workerService) -using Microsoft.Extensions.Hosting; -#endif using System.Diagnostics.CodeAnalysis; -#if (massTransitIntegration) -using MassTransit.Testing; -#endif #if (apiService && auth) using Monaco.Template.Backend.IntegrationTests.Auth; #endif @@ -18,12 +12,6 @@ namespace Monaco.Template.Backend.IntegrationTests; public abstract class IntegrationTest : IAsyncLifetime { protected readonly AppFixture Fixture; -#if (apiService) - protected IFlurlClient Client; -#endif -#if (workerService) - protected IHost WorkerServiceInstance; -#endif #if (apiService && auth) protected KeycloakService? KeycloakService; protected AccessTokenDto? AccessToken; @@ -36,19 +24,7 @@ protected IntegrationTest(AppFixture fixture) Fixture = fixture; #if (apiService) - var clientOptions = new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false - }; - - Client = new FlurlClient(Fixture.WebAppFactory.CreateClient(clientOptions)) #if (auth) - .AllowAnyHttpStatus() - .BeforeCall(call => - { - if (AccessToken is not null) - call.Request.WithOAuthBearerToken(AccessToken.AccessToken); - }); if (RequiresAuthentication) KeycloakService = new KeycloakService(Fixture.KeycloakContainer.GetBaseAddress(), @@ -59,15 +35,24 @@ protected IntegrationTest(AppFixture fixture) .AllowAnyHttpStatus(); #endif -#endif -#if (workerService) - WorkerServiceInstance = Fixture.WorkerServiceInstance; #endif } #if (apiService) - protected IFlurlRequest CreateRequest(string endpoint) => Client.Request(endpoint); + protected FlurlClient GetClient(WebApplicationFactory factory) => + new FlurlClient(factory.CreateClient(new() { AllowAutoRedirect = false })) +#if (auth) + .AllowAnyHttpStatus() + .BeforeCall(call => + { + if (AccessToken is not null) + call.Request.WithOAuthBearerToken(AccessToken.AccessToken); + }); + +#else + .AllowAnyHttpStatus(); +#endif #endif public virtual Task InitializeAsync() => Task.CompletedTask; @@ -92,24 +77,10 @@ protected virtual Task SetupAccessToken(string[] roles) => #endif protected virtual async Task RunScriptAsync(string filePath) => - await Fixture.GetDbContext() + await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Database .ExecuteSqlRawAsync(await File.ReadAllTextAsync(filePath)); -#if (apiService && massTransitIntegration) - protected virtual ITestHarness GetApiTestHarness() => - Fixture.WebAppFactory - .Services - .GetTestHarness(); - -#endif -#if (workerService && massTransitIntegration) - protected virtual ITestHarness GetServiceTestHarness() => - Fixture.WorkerServiceInstance - .Services - .GetTestHarness(); - -#endif public virtual async Task DisposeAsync() => await Fixture.ResetDatabaseDataAsync(); } \ No newline at end of file diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs index e81f970..d2d7640 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CompaniesTests.cs @@ -46,7 +46,9 @@ public async Task GetCompaniesPageSucceeds(bool expandCountry, int? limit, int expectedItemsCount) { - var response = await CreateRequest(ApiRoutes.Companies.Query(expandCountry, offset, limit)).GetAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Companies.Query(expandCountry, offset, limit)) + .GetAsync(); response.StatusCode .Should() @@ -84,14 +86,16 @@ public async Task GetCompanySucceeds() { var companyId = Guid.Parse("8CEFE8FA-F747-4A3A-D8C9-08DC18C76CDC"); - var response = await CreateRequest(ApiRoutes.Companies.Get(companyId)).GetAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Companies.Get(companyId)) + .GetAsync(); response.StatusCode .Should() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var company = await Fixture.GetDbContext() + var company = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .SingleAsync(c => c.Id == companyId); @@ -142,7 +146,10 @@ public async Task CreateNewCompanySucceeds(string name, county, postCode[..Address.PostCodeLength], spainId); - var response = await CreateRequest(ApiRoutes.Companies.Post()).PostJsonAsync(dto); + + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Companies.Post()) + .PostJsonAsync(dto); response.StatusCode .Should() @@ -159,7 +166,7 @@ public async Task CreateNewCompanySucceeds(string name, .Should() .Contain(("Location", ApiRoutes.Companies.Get(result.Id).ToString())); - var companies = await Fixture.GetDbContext() + var companies = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .ToListAsync(); companies.Should() @@ -214,13 +221,16 @@ public async Task EditExistingCompanySucceeds(string name, county, postCode[..Address.PostCodeLength], countryId); - var response = await CreateRequest(ApiRoutes.Companies.Put(companyId)).PutJsonAsync(dto); + + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Companies.Put(companyId)) + .PutJsonAsync(dto); response.StatusCode .Should() .Be((int)HttpStatusCode.NoContent); - var company = await Fixture.GetDbContext() + var company = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .SingleOrDefaultAsync(c => c.Id == companyId); company.Should() @@ -255,13 +265,16 @@ public async Task EditExistingCompanySucceeds(string name, public async Task DeleteExistingCompanySucceeds() { var companyId = Guid.Parse("EDEDB1E8-FD3A-4579-9EF8-A0BBEF2A6F95"); - var response = await CreateRequest(ApiRoutes.Companies.Delete(companyId)).DeleteAsync(); + + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Companies.Delete(companyId)) + .DeleteAsync(); response.StatusCode .Should() .Be((int)HttpStatusCode.OK); - var companies = await Fixture.GetDbContext() + var companies = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .ToListAsync(); companies.Should() diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs index 3acde68..618dc89 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/CountriesTests.cs @@ -32,14 +32,16 @@ public override async Task InitializeAsync() [Fact(DisplayName = "Get Countries succeeds")] public async Task GetCountriesSucceeds() { - var response = await CreateRequest(ApiRoutes.Countries.Query()).GetAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Countries.Query()) + .GetAsync(); response.StatusCode .Should() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var countriesCount = await Fixture.GetDbContext() + var countriesCount = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .CountAsync(); @@ -54,14 +56,16 @@ public async Task GetCountrySucceeds() { var countryId = Guid.Parse("534A826B-70EF-2128-1A4C-52E23B7D5447"); - var response = await CreateRequest(ApiRoutes.Countries.Get(countryId)).GetAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Countries.Get(countryId)) + .GetAsync(); response.StatusCode .Should() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var country = await Fixture.GetDbContext() + var country = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .SingleAsync(c => c.Id == countryId); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs index f3f6b59..1da1be4 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/FilesTests.cs @@ -6,7 +6,6 @@ using System.Net; using File = Monaco.Template.Backend.Domain.Model.Entities.File; - namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] @@ -16,8 +15,8 @@ public class FilesTests : IntegrationTest { public FilesTests(AppFixture fixture) : base(fixture) { } - #if (apiService && auth) + protected override bool RequiresAuthentication => true; #endif @@ -38,16 +37,18 @@ public async Task UploadFileSuccceeds() const string file = $@"Imports\Pictures\{fileName}"; const string contentType = "image/png"; - var response = await CreateRequest(ApiRoutes.Files.Post()).PostMultipartAsync(b => b.AddFile("file", - System.IO.File.OpenRead(file), - fileName, contentType)); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Files.Post()) + .PostMultipartAsync(b => b.AddFile("file", + System.IO.File.OpenRead(file), + fileName, contentType)); var uploadDate = DateTime.UtcNow; response.StatusCode .Should() .Be((int)HttpStatusCode.Created); - var files = await Fixture.GetDbContext() + var files = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .AsNoTracking() .ToListAsync(); diff --git a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs index b78e734..38c05a9 100644 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/Tests/ProductsTests.cs @@ -1,7 +1,6 @@ using AutoFixture.Xunit2; using AwesomeAssertions; using Azure.Storage.Blobs; -using Dasync.Collections; using Flurl.Http; using Microsoft.EntityFrameworkCore; using Monaco.Template.Backend.Api.DTOs; @@ -17,6 +16,8 @@ #endif using System.Diagnostics.CodeAnalysis; using System.Net; +using MassTransit.Testing; +using Monaco.Template.Backend.IntegrationTests.Factories; using File = System.IO.File; namespace Monaco.Template.Backend.IntegrationTests.Tests; @@ -37,7 +38,7 @@ public override async Task InitializeAsync() { await base.InitializeAsync(); await RunScriptAsync(@"Scripts\Products.sql"); - var images = await Fixture.GetDbContext() + var images = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .AsNoTracking() .ToListAsync(); @@ -68,11 +69,13 @@ public async Task GetProductsPageSucceeds(bool expandCompany, int? limit, int expectedItemsCount) { - var response = await CreateRequest(ApiRoutes.Products.Query(expandCompany, - expandPictures, - expandDefaultPicture, - offset, - limit)).GetAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Products.Query(expandCompany, + expandPictures, + expandDefaultPicture, + offset, + limit)) + .GetAsync(); response.StatusCode .Should() @@ -136,14 +139,16 @@ public async Task GetProductSucceeds() { var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); - var response = await CreateRequest(ApiRoutes.Products.Get(productId)).GetAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Products.Get(productId)) + .GetAsync(); response.StatusCode .Should() .Be((int)HttpStatusCode.OK); var result = await response.GetJsonAsync(); - var product = await Fixture.GetDbContext() + var product = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .Include(x => x.Company) .Include(x => x.DefaultPicture) @@ -229,11 +234,13 @@ private async Task DownloadProductPictureTest(Guid productId, Guid pictureId, bool? isThumbnail = null) { - var response = await CreateRequest(ApiRoutes.Products.DownloadPicture(productId, - pictureId, - isThumbnail)).GetAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Products.DownloadPicture(productId, + pictureId, + isThumbnail)) + .GetAsync(); - var picture = await Fixture.GetDbContext() + var picture = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .AsNoTracking() .Where(x => x.Id == pictureId) @@ -260,17 +267,21 @@ public async Task CreateNewProductSucceeds(string title, string description, decimal price) { + var webAppFactory = Fixture.WebAppFactory.GetCustomFactory(b => b.AddMassTransitTestHarnessForWebApp()); + var workerServiceFactory = Fixture.WorkerServiceFactory.GetCustomFactory(b => b.AddMassTransitTestHarnessForWorker()); + + using var client = GetClient(webAppFactory); #if (auth) await SetupAccessToken(); #endif #if (apiService && massTransitIntegration) - var apiTestHarness = GetApiTestHarness(); + var apiTestHarness = webAppFactory.Services.GetTestHarness(); #endif #if (workerService && massTransitIntegration) - var serviceTestHarness = GetServiceTestHarness(); + var serviceTestHarness = workerServiceFactory.Services.GetTestHarness(); #endif - var dbContext = Fixture.GetDbContext(); + var dbContext = Fixture.GetDbContext(webAppFactory.Services); var tempImages = await dbContext.Set() .Where(i => i.IsTemp && i.ThumbnailId.HasValue) .ToListAsync(); @@ -283,7 +294,7 @@ public async Task CreateNewProductSucceeds(string title, [.. tempImages.Select(i => i.Id)], tempImages.Last().Id); - var response = await CreateRequest(ApiRoutes.Products.Post()).PostJsonAsync(dto); + var response = await client.Request(ApiRoutes.Products.Post()).PostJsonAsync(dto); response.StatusCode .Should() @@ -300,7 +311,7 @@ [.. tempImages.Select(i => i.Id)], .Should() .Contain(("Location", ApiRoutes.Products.Get(result.Id).ToString())); - var products = await Fixture.GetDbContext() + var products = await Fixture.GetDbContext(webAppFactory.Services) .Set() .Include(x => x.Company) .Include(x => x.Pictures) @@ -347,14 +358,25 @@ [.. tempImages.Select(i => i.Id)], #if (massTransitIntegration) #if (apiService) - (await apiTestHarness.Published.SelectAsync().AnyAsync()) - .Should() - .BeTrue(); + var message = await apiTestHarness.Published + .SelectAsync() + .SingleOrDefaultAsync(x => x.Context.Message.Id == result.Id, + CancellationToken.None); + + message.Should().NotBeNull(); + + var (msgId, msgTitle, msgDescription, msgPrice, msgCompanyId) = message.Context.Message; + + msgId.Should().Be(result.Id); + msgTitle.Should().Be(dto.Title); + msgDescription.Should().Be(dto.Description); + msgCompanyId.Should().Be(dto.CompanyId); + msgPrice.Should().Be(dto.Price); #endif #if (workerService) var consumerHarness = serviceTestHarness.GetConsumerHarness(); - (await consumerHarness.Consumed.SelectAsync().AnyAsync()) + (await consumerHarness.Consumed.SelectAsync().AnyAsync(c => c.Context.Message.Id == result.Id)) .Should() .BeTrue(); #endif @@ -370,7 +392,7 @@ public async Task EditExistingProductSucceeds(string title, #if (auth) await SetupAccessToken(); #endif - var dbContext = Fixture.GetDbContext(); + var dbContext = Fixture.GetDbContext(Fixture.WebAppFactory.Services); var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); var productPictures = await dbContext.Set() .AsNoTracking() @@ -389,13 +411,15 @@ public async Task EditExistingProductSucceeds(string title, [.. productPictures], newPictureId); - var response = await CreateRequest(ApiRoutes.Products.Put(productId)).PutJsonAsync(dto); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Products.Put(productId)) + .PutJsonAsync(dto); response.StatusCode .Should() .Be((int)HttpStatusCode.NoContent); - var product = await Fixture.GetDbContext() + var product = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .Include(x => x.Pictures) .Include(x => x.DefaultPicture) @@ -462,13 +486,15 @@ public async Task DeleteExistingProductSucceeds() await SetupAccessToken(); #endif var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); - var response = await CreateRequest(ApiRoutes.Products.Delete(productId)).DeleteAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Products.Delete(productId)) + .DeleteAsync(); response.StatusCode .Should() .Be((int)HttpStatusCode.OK); - var products = await Fixture.GetDbContext() + var products = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) .Set() .ToListAsync(); @@ -481,8 +507,8 @@ public async Task DeleteExistingProductSucceeds() public override async Task DisposeAsync() { var container = GetBlobContainerClient(); - await container.GetBlobs() - .ParallelForEachAsync(blob => container.DeleteBlobAsync(blob.Name)); + await Parallel.ForEachAsync(container.GetBlobs(), + async (blob, ct) => await container.DeleteBlobAsync(blob.Name, cancellationToken: ct)); await base.DisposeAsync(); } From e94cbe2e028496e043603b4dae3af99ae3f51e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Mon, 2 Mar 2026 20:14:14 +0100 Subject: [PATCH 29/30] Bump Monaco.Template version to 2.8.0 Updated the version number in Monaco.Template.nuspec from 2.7.0 to 2.8.0 to prepare for the next release. No other changes were made. --- src/Monaco.Template.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Monaco.Template.nuspec b/src/Monaco.Template.nuspec index 8195a30..faf9ad8 100644 --- a/src/Monaco.Template.nuspec +++ b/src/Monaco.Template.nuspec @@ -2,7 +2,7 @@ Monaco.Template - 2.7.0 + 2.8.0 Monaco Template Templates for .NET projects From ce3142697a186b01336b00c40093e0b1b741f61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Demicheli?= Date: Tue, 3 Mar 2026 11:22:28 +0100 Subject: [PATCH 30/30] Exclude .idea directory in template.json patterns Added .idea to the list of excluded file patterns in template.json to prevent JetBrains IDE configuration files from being included. No other exclusion changes were made. --- src/Content/Backend/Solution/.template.config/template.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Content/Backend/Solution/.template.config/template.json b/src/Content/Backend/Solution/.template.config/template.json index ec2b038..716ed63 100644 --- a/src/Content/Backend/Solution/.template.config/template.json +++ b/src/Content/Backend/Solution/.template.config/template.json @@ -172,7 +172,8 @@ "**/.vs/**/*", "**/logs/**", "**/TestResults/**", - "**/[Pp]ublish/**/*" + "**/[Pp]ublish/**/*", + "**/.idea/**/*" ], "modifiers": [ {