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