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 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 diff --git a/src/Content/Backend/Solution/.template.config/template.json b/src/Content/Backend/Solution/.template.config/template.json index 49be5a4..716ed63 100644 --- a/src/Content/Backend/Solution/.template.config/template.json +++ b/src/Content/Backend/Solution/.template.config/template.json @@ -14,41 +14,10 @@ "type": "project" }, "primaryOutputs": [ - { - "path": "Monaco.Template.Backend.sln" - }, { "path": "Monaco.Template.Backend.slnx" } ], - "SpecialCustomOperations": { - "**.slnx": { - "operations": [ - { - "type": "conditional", - "configuration": { - "actionableIf": [ "", - "pseudoEndToken": "-- >", - "id": "fixPseudoNestedComments", - "resetFlag": "_TestResetFlag_" - } - } - ] - } - }, "symbols": { "apiGateway": { "type": "parameter", @@ -203,7 +172,8 @@ "**/.vs/**/*", "**/logs/**", "**/TestResults/**", - "**/[Pp]ublish/**/*" + "**/[Pp]ublish/**/*", + "**/.idea/**/*" ], "modifiers": [ { 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/Directory.Packages.props b/src/Content/Backend/Solution/Directory.Packages.props index a6f913d..405a614 100644 --- a/src/Content/Backend/Solution/Directory.Packages.props +++ b/src/Content/Backend/Solution/Directory.Packages.props @@ -3,77 +3,78 @@ true - + - - - + + + - + - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + + + - - - + + - + - - - + + + - + - - - - + + + + - + - - - + + + - - - + + + - + \ No newline at end of file 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/Companies.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Companies.cs index a87908c..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,77 +9,96 @@ 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; 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, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetCompanyPage.Query(request.Query), + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetCompanyById.Query(id), + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteCommandCreatedAsync(dto.MapCreateCommand(), + "api/v{0}/Companies/{1}", + [context.GetRequestedApiVersion()!], + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteCommandNoContentAsync(dto.MapEditCommand(id), + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteCommandOkAsync(new DeleteCompany.Command(id), + cancellationToken), + "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/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.Api/Endpoints/Files.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Endpoints/Files.cs index 7e25b0a..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,39 +2,45 @@ 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; 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, - [FromForm] 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, + 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) - .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..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,96 +9,115 @@ 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; 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, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetProductPage.Query(request.Query), + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteQueryAsync(new GetProductById.Query(id), + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteCommandCreatedAsync(dto.Map(), + "api/v{0}/Products/{1}", + [context.GetRequestedApiVersion()!], + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteCommandNoContentAsync(dto.Map(id), + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteCommandOkAsync(new DeleteProduct.Command(id), + cancellationToken), + "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, + CancellationToken cancellationToken) => + sender.ExecuteFileDownloadAsync(new DownloadProductPicture.Query(productId, + pictureId, + request.Query), + cancellationToken), + "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.Api/Monaco.Template.Backend.Api.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Api/Monaco.Template.Backend.Api.csproj index 41c418f..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 @@  - net9.0 - enable - enable 8ac1d4e3-61ef-452f-a386-ff3ec448fbff True linux-x64 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.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/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.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.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..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 @@  - net9.0 - enable - enable false + true 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..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 @@ -19,43 +19,45 @@ 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) + .UseCompatibilityLevel(160)) // SQL Server 2022 = 160 - SQL Server 2025 = 170 + .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/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/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/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/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/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/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/Monaco.Template.Backend.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Application/Monaco.Template.Backend.Application.csproj index c9a3fe9..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 @@  - - net9.0 - enable - enable - - 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/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 07bbcce..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() @@ -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/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.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.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.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.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.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.ArchitectureTests/Monaco.Template.Backend.ArchitectureTests.csproj index 3005b29..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 @@ - net9.0 - enable - enable - false true 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 a45bd60..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 @@ -10,162 +10,208 @@ 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, + CancellationToken cancellationToken = default) + { + var result = await sender.Send(query, cancellationToken); + 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, + CancellationToken cancellationToken = default) + { + var result = await sender.Send(query, cancellationToken); + 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, + CancellationToken cancellationToken = default) + { + var result = await sender.Send(query, cancellationToken); + 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, + CancellationToken cancellationToken = default) + { + var item = await sender.Send(query, cancellationToken); + 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, + CancellationToken cancellationToken = default) where TResult : FileDownloadDto + { + var item = await sender.Send(query, cancellationToken); + 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, + CancellationToken cancellationToken = default) where TResult : FileDownloadDto + { + var item = await sender.Send(query, cancellationToken); + return GetFileDownload(item); + } - 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 or a or a depending on the validations and processing + /// + /// + /// 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, + object[]? uriParams = null, + CancellationToken cancellationToken = default) + { + 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]), + new CreatedResponse(result.Result)) + }; + } - /// - /// 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 command passed and returns the corresponding response that can be either or a or a depending on the validations and processing + /// + /// + /// + /// + public async Task> ExecuteCommandNoContentAsync(CommandBase command, + CancellationToken cancellationToken = default) + { + var result = await sender.Send(command, cancellationToken); + return result switch + { + { ItemNotFound: true } => TypedResults.NotFound(), + { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), + _ => TypedResults.NoContent() + }; + } - /// - /// 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 command passed and returns the corresponding response that can be either or a or a depending on the validations and processing + /// + /// + /// + /// + public async Task> ExecuteCommandOkAsync(CommandBase command, + CancellationToken cancellationToken = default) + { + var result = await sender.Send(command, cancellationToken); + return result switch + { + { ItemNotFound: true } => TypedResults.NotFound(), + { ValidationResult.IsValid: false } => TypedResults.ValidationProblem(result.ValidationResult.ToDictionary()), + _ => TypedResults.Ok() + }; + } - /// - /// 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() - }; + /// + /// 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.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..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 @@ - net9.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/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/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/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.Api/MinimalApi/MinimalApiExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/MinimalApi/MinimalApiExtensions.cs index 7ed5bf7..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,115 +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) - .WithOpenApi() - .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) - .WithOpenApi() - .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) - .WithOpenApi() - .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) - .WithOpenApi() - .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.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..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 @@ -1,10 +1,6 @@  - net9.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Api True @@ -18,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..d336a70 --- /dev/null +++ b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/OpenApi/OAuth2OperationTransformer.cs @@ -0,0 +1,46 @@ +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.IsNullOrWhiteSpace(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 1489023..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/AuthorizeCheckOperationFilter.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi.Models; -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; - - 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 } }]; - } -} \ 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 cfbdc20..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.Models; -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 de1befc..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Api/Swagger/SwaggerDefaultValues.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; -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) - 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); - - 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.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..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 @@ - net9.0 - enable - enable 4c76f225-faad-42ec-801b-9ad3b505b7f5 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.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.Common.Application/Commands/Behaviors/BehaviorExtensions.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Commands/Behaviors/BehaviorExtensions.cs index 36dc45c..9171e50 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/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/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/Monaco.Template.Backend.Common.Application.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Common.Application/Monaco.Template.Backend.Common.Application.csproj index fa669d4..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 @@  - net9.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Application True 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/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/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) 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/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.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.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..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 @@  - net9.0 - enable - enable false + true 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.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.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..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 @@ - net9.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/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.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..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 @@ - net9.0 - enable - enable false + true 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..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 @@ -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; @@ -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.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.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..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 @@ - net9.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/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..9f7dfbe 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..2dec160 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..c3b6565 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,86 +4,90 @@ 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) 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 + if (lstSort.Count == 0) // if there's none remaining, load the default ones lstSort = ProcessSortParam([defaultSortField], sortMapLower); 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..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 @@ -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 ConfigureIdWithValueGeneratedNever() + { + 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.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..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 @@ - net9.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/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); } 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..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 @@ - net9.0 - enable - enable - 0.0.1-alpha1 Monaco.Template.Backend.Common.Serilog True 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 ad337ad..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 }.AsQueryable().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 }.AsQueryable().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 }.AsQueryable().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, IEnumerable entities, out Mock> entityDbSetMock) - where TDbContext : DbContext - where T : Entity - { - entityDbSetMock = entities.AsQueryable().BuildMockDbSet(); - dbContextMock.Setup(x => x.Set()).Returns(entityDbSetMock.Object); - - return dbContextMock; - } - - public static Mock CreateAndSetupDbSetMock(this Mock dbContextMock, IEnumerable entities) - where TDbContext : DbContext - where T : Entity - => dbContextMock.CreateAndSetupDbSetMock(entities, out _); } 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..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 @@ - - net9.0 - enable - enable - - $(DefineConstants);auth;commonLibraries;filesSupport 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..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 @@ -20,49 +20,52 @@ 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 company = fixture.Create(); + var mock = new Mock(fixture.Create(), + fixture.Create(), + fixture.Create(), + company, + images, + images.First()); + mock.SetupGet(x => x.Id) + .Returns(Guid.NewGuid()); + mock.SetupGet(x => x.Company) + .Returns(company); + mock.SetupGet(x => x.CompanyId) + .Returns(company.Id); + 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.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 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..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 @@  - net9.0 - enable - enable false + true 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.Domain/Monaco.Template.Backend.Domain.csproj b/src/Content/Backend/Solution/Monaco.Template.Backend.Domain/Monaco.Template.Backend.Domain.csproj index bf65174..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 @@ - - net9.0 - enable - enable - - $(DefineConstants);auth;commonLibraries;filesSupport 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 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..992f37e 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,24 @@ #if (auth) using Testcontainers.Keycloak; #endif +using System.Diagnostics.CodeAnalysis; using Flurl; +using Microsoft.EntityFrameworkCore; +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) @@ -27,23 +37,32 @@ 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) + public ApiWebApplicationFactory WebAppFactory = null!; + +#endif +#if (workerService) + public WorkerServiceFactory WorkerServiceFactory = null!; + +#endif + private Respawner? _respawner; + public async Task InitializeAsync() { await SqlContainer.StartAsync(); @@ -58,8 +77,31 @@ public async Task InitializeAsync() await InitStorage(); #endif + +#if (apiService) + WebAppFactory = new ApiWebApplicationFactory(this); +#endif +#if (workerService) + WorkerServiceFactory = new WorkerServiceFactory(this); +#endif + + await ApplyDbMigrationsAsync(); } + public virtual AppDbContext GetDbContext(IServiceProvider services) => + services.CreateScope() + .ServiceProvider + .GetRequiredService(); + + protected virtual async Task ApplyDbMigrationsAsync(string? targetMigration = null) => +#if (apiService) + await GetDbContext(WebAppFactory.Services) +#elif (workerService) + await GetDbContext(WorkerServiceInstance.Services) +#endif + .GetService() + .MigrateAsync(targetMigration); + public string SqlConnectionString => SqlContainer.GetConnectionString(); #if (massTransitIntegration) @@ -95,30 +137,38 @@ public async Task InitializeAsync() .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() @@ -127,4 +177,36 @@ 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() + { +#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(); + + _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/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/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/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs b/src/Content/Backend/Solution/Monaco.Template.Backend.IntegrationTests/IntegrationTest.cs index 861c1a8..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,18 +1,7 @@ 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,18 +9,9 @@ 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) protected KeycloakService? KeycloakService; protected AccessTokenDto? AccessToken; @@ -42,27 +22,9 @@ 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 - { - AllowAutoRedirect = false - }; - - Client = new FlurlClient(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(), @@ -73,25 +35,32 @@ protected IntegrationTest(AppFixture fixture) .AllowAnyHttpStatus(); #endif -#endif -#if (workerService) - WorkerServiceInstance = WorkerServiceFactory.GetHostInstance(); #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 - public virtual async Task InitializeAsync() - { - await ApplyDbMigrations(); - } +#endif + public virtual Task InitializeAsync() => + Task.CompletedTask; #if (apiService && auth) protected virtual async Task SetupAccessToken(string audienceClientId, string[] roles, - string[] scopes) + string[] scopes) { if (!RequiresAuthentication) return; @@ -106,52 +75,12 @@ 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); - } - -#if (apiService && massTransitIntegration) - protected virtual ITestHarness GetApiTestHarness() => - WebAppFactory.Services - .GetTestHarness(); -#endif -#if (workerService && massTransitIntegration) - protected virtual ITestHarness GetServiceTestHarness() => - WorkerServiceInstance.Services - .GetTestHarness(); - -#endif - public virtual async Task DisposeAsync() - { - await RollbackDbMigrations(); -#if (apiService) - await WebAppFactory.DisposeAsync(); -#endif -#if (workerService) + protected virtual async Task RunScriptAsync(string filePath) => + await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Database + .ExecuteSqlRawAsync(await File.ReadAllTextAsync(filePath)); - 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 b3909af..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 @@ -1,9 +1,6 @@  - net9.0 - enable - enable false true @@ -23,6 +20,7 @@ + @@ -65,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 a3afebe..e741e25 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 @@ -4,6 +4,7 @@ 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; @@ -14,16 +15,15 @@ namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Companies")] 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() @@ -44,8 +44,10 @@ 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() .Be((int)HttpStatusCode.OK); @@ -82,15 +84,18 @@ 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 GetDbContext().Set() - .SingleAsync(c => c.Id == companyId); + var company = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Set() + .SingleAsync(c => c.Id == companyId); result.Should() .NotBeNull(); @@ -139,27 +144,33 @@ 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() .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())); - - var companies = await GetDbContext().Set() - .ToListAsync(); + .Contain(("Location", ApiRoutes.Companies.Get(result.Id).ToString())); + + var companies = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .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 @@ -208,14 +219,18 @@ 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 GetDbContext().Set() - .SingleOrDefaultAsync(c => c.Id == companyId); + var company = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Set() + .SingleOrDefaultAsync(c => c.Id == companyId); company.Should() .NotBeNull(); company!.Name @@ -248,14 +263,18 @@ 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 GetDbContext().Set() - .ToListAsync(); + var companies = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .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 a13bf97..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 @@ -9,16 +9,15 @@ namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Countries")] public class CountriesTests : IntegrationTest { public CountriesTests(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() @@ -33,15 +32,18 @@ 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 GetDbContext().Set() - .CountAsync(); + var countriesCount = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Set() + .CountAsync(); result.Should() .NotBeNull(); @@ -54,15 +56,18 @@ 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 GetDbContext().Set() - .SingleAsync(c => c.Id == countryId); + var country = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .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 a61c11f..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,20 +6,18 @@ using System.Net; using File = Monaco.Template.Backend.Domain.Model.Entities.File; - namespace Monaco.Template.Backend.IntegrationTests.Tests; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Files")] public class FilesTests : IntegrationTest { public FilesTests(AppFixture fixture) : base(fixture) { } +#if (apiService && auth) -#if (auth) protected override bool RequiresAuthentication => true; -#else - protected override bool RequiresAuthentication => false; #endif public override async Task InitializeAsync() @@ -39,18 +37,21 @@ 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 GetDbContext().Set() - .AsNoTracking() - .ToListAsync(); + var files = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .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 c986ee7..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,14 +1,12 @@ using AutoFixture.Xunit2; -using Azure.Storage.Blobs; -using Dasync.Collections; using AwesomeAssertions; +using Azure.Storage.Blobs; 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; @@ -16,30 +14,34 @@ #if (massTransitIntegration || workerService) using Monaco.Template.Backend.Worker.Consumers; #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; [ExcludeFromCodeCoverage] +[Collection("IntegrationTests")] [Trait("Integration Tests", "Products")] public class ProductsTests : IntegrationTest { public ProductsTests(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() { await base.InitializeAsync(); await RunScriptAsync(@"Scripts\Products.sql"); - var images = await GetDbContext().Set() - .AsNoTracking() - .ToListAsync(); + var images = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Set() + .AsNoTracking() + .ToListAsync(); var blobContainerClient = GetBlobContainerClient(); foreach (var image in images) @@ -55,8 +57,7 @@ private Task SetupAccessToken() => #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)] @@ -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,19 +139,22 @@ 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 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(Fixture.WebAppFactory.Services) + .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(); @@ -224,19 +230,22 @@ 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) - .Select(x => isThumbnail.HasValue && isThumbnail.Value ? x.Thumbnail! : x) - .SingleAsync(); + using var client = GetClient(Fixture.WebAppFactory); + var response = await client.Request(ApiRoutes.Products.DownloadPicture(productId, + pictureId, + isThumbnail)) + .GetAsync(); + + var picture = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Set() + .AsNoTracking() + .Where(x => x.Id == pictureId) + .Select(x => isThumbnail.HasValue && isThumbnail.Value ? x.Thumbnail! : x) + .SingleAsync(); response.StatusCode .Should() @@ -258,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 = GetDbContext(); + var dbContext = Fixture.GetDbContext(webAppFactory.Services); var tempImages = await dbContext.Set() .Where(i => i.IsTemp && i.ThumbnailId.HasValue) .ToListAsync(); @@ -281,36 +294,39 @@ 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() .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())); - - var products = await GetDbContext().Set() - .Include(x => x.Company) - .Include(x => x.Pictures) - .ThenInclude(x => x.Thumbnail) - .Include(x => x.DefaultPicture) - .ToListAsync(); + .Contain(("Location", ApiRoutes.Products.Get(result.Id).ToString())); + + var products = await Fixture.GetDbContext(webAppFactory.Services) + .Set() + .Include(x => x.Company) + .Include(x => x.Pictures) + .ThenInclude(x => x.Thumbnail) + .Include(x => x.DefaultPicture) + .ToListAsync(); products.Should() - .HaveCount(4); + .HaveCount(4); - var newProduct = products.SingleOrDefault(c => c.Id == id); + var newProduct = products.SingleOrDefault(c => c.Id == result.Id); newProduct.Should() .NotBeNull(); - newProduct!.Title - .Should() - .Be(dto.Title); + newProduct.Title + .Should() + .Be(dto.Title); newProduct.Description .Should() .Be(dto.Description); @@ -326,7 +342,7 @@ [.. tempImages.Select(i => i.Id)], { i.Should() .BeOneOf(tempImages); - + i.IsTemp .Should() .BeFalse(); @@ -342,18 +358,25 @@ [.. tempImages.Select(i => i.Id)], #if (massTransitIntegration) #if (apiService) - (await apiTestHarness.Published.Any()) - .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) - (await serviceTestHarness.Consumed.Any()) - .Should() - .BeTrue(); - var consumerHarness = serviceTestHarness.GetConsumerHarness(); - (await consumerHarness.Consumed.Any()) + (await consumerHarness.Consumed.SelectAsync().AnyAsync(c => c.Context.Message.Id == result.Id)) .Should() .BeTrue(); #endif @@ -369,7 +392,7 @@ public async Task EditExistingProductSucceeds(string title, #if (auth) await SetupAccessToken(); #endif - var dbContext = GetDbContext(); + var dbContext = Fixture.GetDbContext(Fixture.WebAppFactory.Services); var productId = Guid.Parse("FA934D1C-1E6D-4DD4-ADC2-08DC18C8810C"); var productPictures = await dbContext.Set() .AsNoTracking() @@ -388,16 +411,19 @@ 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 GetDbContext().Set() - .Include(x => x.Pictures) - .Include(x => x.DefaultPicture) - .SingleOrDefaultAsync(c => c.Id == productId); + var product = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Set() + .Include(x => x.Pictures) + .Include(x => x.DefaultPicture) + .SingleOrDefaultAsync(c => c.Id == productId); product.Should() .NotBeNull(); product!.Title @@ -451,7 +477,6 @@ public async Task EditExistingProductSucceeds(string title, .Should() .NotBeNull(); }); - } [Fact(DisplayName = "Delete existing Product succeeds")] @@ -461,17 +486,20 @@ 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 GetDbContext().Set() - .ToListAsync(); + var products = await Fixture.GetDbContext(Fixture.WebAppFactory.Services) + .Set() + .ToListAsync(); products.Should() - .HaveCount(2); + .HaveCount(2); products.Should() .NotContain(x => x.Id == productId); } @@ -479,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(); } 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 +} 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..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 @@  - net9.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 90ce0e8..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 @@  - net9.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 deleted file mode 100644 index 8384932..0000000 --- a/src/Content/Backend/Solution/Monaco.Template.Backend.sln +++ /dev/null @@ -1,257 +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.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 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 @@ + 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" : "", 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