diff --git a/.editorconfig b/.editorconfig index f92bd13..656e2b8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -175,7 +175,7 @@ dotnet_diagnostic.CA1304.severity = error # CA1307: Specify StringComparison for clarity dotnet_diagnostic.CA1307.severity = error # CA1308: Normalize strings to uppercase -dotnet_diagnostic.CA1308.severity = error +dotnet_diagnostic.CA1308.severity = none # CA1309: Use ordinal StringComparison dotnet_diagnostic.CA1309.severity = error # CA1724: Type names should not match namespaces @@ -236,7 +236,7 @@ dotnet_diagnostic.IDE0052.severity = error # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/dotnet-formatting-options # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0055 -dotnet_diagnostic.IDE0055.severity = error +dotnet_diagnostic.IDE0055.severity = suggestion # CS1574: XML comment on 'construct' has syntactically incorrect cref attribute 'name' dotnet_diagnostic.CS1574.severity = error diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 8b03264..da2bee0 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -122,15 +122,15 @@ autolabeler: title: - '/^(feat)(\([a-z ]+\))?: .+/' - label: 'minor' - branch: + branch: - '/^(feat)(\([a-z ]+\))?\/.+/' - title: + title: - '/^(feat)(\([a-z ]+\))?: .+/' - label: 'patch' - branch: + branch: - '/^(fix)(\([a-z ]+\))?\/.+/' - '/^(ci)(\([a-z ]+\))?\/.+/' - title: + title: - '/^(fix)(\([a-z ]+\))?: .+/' - '/^(ci)(\([a-z ]+\))?: .+/' diff --git a/readme.md b/readme.md index 299d1c5..308c927 100644 --- a/readme.md +++ b/readme.md @@ -8,74 +8,82 @@ > This is a An Asp.Net Core `template` based on `Vertical Slice Architecture`, CQRS, Minimal APIs, API Versioning and Swagger. Create a new project based on this template by clicking the above **Use this template** button or by installing and running the associated NuGet package (see Getting Started for full details). -# ⭐ Support +## ⭐ Support If you like feel free to ⭐ this repository, It helps out :) Thanks a bunch for supporting me! -# Table of Contents -- [Install](#install) -- [Features](#features) -- [Libraries](#libraries) -- [Getting Started](#getting-started) -- [Setup](#setup) - - [Dev Certificate](#dev-certificate) - - [Conventional Commit](#conventional-commit) - - [Formatting](#formatting) - - [Analizers](#analizers) -- [Application Structure](#application-structure) -- [Vertical Slice Flow](#vertical-slice-flow) -- [How to Run](#how-to-run) - - [Using PM2](#using-pm2) - - [Using Tye](#using-tye) -- [Contribution](#contribution) -- [License](#license) +## Table of Contents + +- [Vertical Slice API Template](#vertical-slice-api-template) + - [⭐ Support](#-support) + - [Table of Contents](#table-of-contents) + - [Install](#install) + - [Features](#features) + - [Libraries](#libraries) + - [Getting Started](#getting-started) + - [Setup](#setup) + - [Dev Certificate](#dev-certificate) + - [Conventional Commit](#conventional-commit) + - [Formatting](#formatting) + - [Analizers](#analizers) + - [Application Structure](#application-structure) + - [High Level Structure](#high-level-structure) + - [Modules Structure](#modules-structure) + - [Folder Structure](#folder-structure) + - [Vertical Slice Flow](#vertical-slice-flow) + - [How to Run](#how-to-run) + - [Using PM2](#using-pm2) + - [Using Tye](#using-tye) + - [Contribution](#contribution) + - [License](#license) ## Install For installing `vertical slice api template` from [nuget container registry](https://www.nuget.org/packages/Vertical.Slice.Template) run this dotnet cli command: -``` bash +```bash dotnet new install Vertical.Slice.Template ``` Or for installing the template locally you can clone the project and run this command in the root of this repository: -``` bash +```bash dotnet new install . ``` ## Features -- ✅ Using `Vertical Slice Architecture` as a high-level architecture -- ✅ Using `CQRS Pattern` on top of `MediatR` library -- ✅ Using `Mapperly` source generator for the mappings -- ✅ Using `Minimal APIs` for handling requests -- ✅ Using `Fluent Validation` and a [Validation Pipeline Behaviour](./src/BuildingBlocks/BuildingBlocks.Validation/RequestValidationBehavior.cs) on top of MediatR -- ✅ Using `Postgres` On Top of EfCore -- ✅ Using different levels of tests like `Unit Tests`, `Integration Tests` and `End-To-End Tests` -- ✅ Logging with `Serilog` and `Elasticsearch` and `Kibana` for collecting and searching structured logs -- ✅ Using [Microsoft Tye](https://github.com/dotnet/tye) and `Pm2` for running the application -- ✅ Using docker and `docker-compose` for deployment -- 🚧 Using `OpenTelemetry` for collection `Metrics` and `Distributed Tracing` +- ✅ Using `Vertical Slice Architecture` as a high-level architecture +- ✅ Using `CQRS Pattern` on top of `MediatR` library +- ✅ Using `Mapperly` source generator for the mappings +- ✅ Using `Minimal APIs` for handling requests +- ✅ Using `Fluent Validation` and a [Validation Pipeline Behaviour](./src/BuildingBlocks/BuildingBlocks.Validation/RequestValidationBehavior.cs) on top of MediatR +- ✅ Using `Postgres` On Top of EfCore +- ✅ Using different levels of tests like `Unit Tests`, `Integration Tests` and `End-To-End Tests` +- ✅ Logging with `Serilog` and `Elasticsearch` and `Kibana` for collecting and searching structured logs +- ✅ Using [Microsoft Tye](https://github.com/dotnet/tye) and `Pm2` for running the application +- ✅ Using docker and `docker-compose` for deployment +- 🚧 Using `OpenTelemetry` for collection `Metrics` and `Distributed Tracing` ## Libraries -- ✔️ **[`.NET 8`](https://dotnet.microsoft.com/download)** - .NET Framework and .NET Core, including ASP.NET and ASP.NET Core -- ✔️ **[`Npgsql Entity Framework Core Provider`](https://www.npgsql.org/efcore/)** - Npgsql has an Entity Framework (EF) Core provider. It behaves like other EF Core providers (e.g. SQL Server), so the general EF Core docs apply here as well -- ✔️ **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules -- ✔️ **[`Swagger & Swagger UI`](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)** - Swagger tools for documenting API's built on ASP.NET Core -- ✔️ **[`Serilog`](https://github.com/serilog/serilog)** - Simple .NET logging with fully-structured events -- ✔️ **[`Polly`](https://github.com/App-vNext/Polly)** - Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner -- ✔️ **[`Scrutor`](https://github.com/khellang/Scrutor)** - Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection -- ✔️ **[`Opentelemetry-dotnet`](https://github.com/open-telemetry/opentelemetry-dotnet)** - The OpenTelemetry .NET Client -- ✔️ **[`Newtonsoft.Json`](https://github.com/JamesNK/Newtonsoft.Json)** - Json.NET is a popular high-performance JSON framework for .NET -- ✔️ **[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks)** - Enterprise HealthChecks for ASP.NET Core Diagnostics Package -- ✔️ **[`NSubstitute`](https://github.com/nsubstitute/NSubstitute)** - A friendly substitute for .NET mocking libraries. -- ✔️ **[`StyleCopAnalyzers`](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)** - An implementation of StyleCop rules using the .NET Compiler Platform -- ✔️ **[`Mapperly`](https://github.com/riok/mapperly)** - A .NET source generator for generating object mappings, No runtime reflection. -- ✔️ **[`NewID`](https://masstransit.io/documentation/patterns/newid)** - NewId generates sequential unique identifiers that are 128-bit (16-bytes) and fit nicely into a Guid +- ✔️ **[`.NET 8`](https://dotnet.microsoft.com/download)** - .NET Framework and .NET Core, including ASP.NET and ASP.NET Core +- ✔️ **[`Npgsql Entity Framework Core Provider`](https://www.npgsql.org/efcore/)** - Npgsql has an Entity Framework (EF) Core provider. It behaves like other EF Core providers (e.g. SQL Server), so the general EF Core docs apply here as well +- ✔️ **[`FluentValidation`](https://github.com/FluentValidation/FluentValidation)** - Popular .NET validation library for building strongly-typed validation rules +- ✔️ **[`Swagger & Swagger UI`](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)** - Swagger tools for documenting API's built on ASP.NET Core +- ✔️ **[`Serilog`](https://github.com/serilog/serilog)** - Simple .NET logging with fully-structured events +- ✔️ **[`Polly`](https://github.com/App-vNext/Polly)** - Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner +- ✔️ **[`Scrutor`](https://github.com/khellang/Scrutor)** - Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection +- ✔️ **[`Opentelemetry-dotnet`](https://github.com/open-telemetry/opentelemetry-dotnet)** - The OpenTelemetry .NET Client +- ✔️ **[`Newtonsoft.Json`](https://github.com/JamesNK/Newtonsoft.Json)** - Json.NET is a popular high-performance JSON framework for .NET +- ✔️ **[`AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks)** - Enterprise HealthChecks for ASP.NET Core Diagnostics Package +- ✔️ **[`NSubstitute`](https://github.com/nsubstitute/NSubstitute)** - A friendly substitute for .NET mocking libraries. +- ✔️ **[`StyleCopAnalyzers`](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)** - An implementation of StyleCop rules using the .NET Compiler Platform +- ✔️ **[`Mapperly`](https://github.com/riok/mapperly)** - A .NET source generator for generating object mappings, No runtime reflection. +- ✔️ **[`Mediator`](https://github.com/martinothamar/Mediator)** - A high performance implementation of Mediator pattern in .NET using source generators. +- ✔️ **[`NewID`](https://masstransit.io/documentation/patterns/newid)** - NewId generates sequential unique identifiers that are 128-bit (16-bytes) and fit nicely into a Guid ## Getting Started @@ -89,7 +97,7 @@ dotnet new install . 8. Run `dotnet new vsa` for short name or `dotnet new Vertical.Slice.Template -n ` to create a new project template. 9. Open [.sln](./Vertical.Slice.Template.sln) solution, make sure that's compiling. 10. Navigate to `src/App/.Api` and run `dotnet run` to launch the back end (ASP.NET Core Web API) -11. Open web browser https://localhost:5158/swagger Swagger UI +11. Open web browser https://localhost:4000/swagger Swagger UI ## Setup @@ -97,7 +105,7 @@ dotnet new install . This application uses `Https` for hosting apis, to setup a valid certificate on your machine, you can create a [Self-Signed Certificate](https://learn.microsoft.com/en-us/aspnet/core/security/docker-https#macos-or-linux), see more about enforce certificate [here](https://learn.microsoft.com/en-us/dotnet/core/additional-tools/self-signed-certificates-guide) and [here](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl). -- Setup on windows and [`powershell`](https://learn.microsoft.com/en-us/dotnet/core/additional-tools/self-signed-certificates-guide#with-dotnet-dev-certs): +- Setup on windows and [`powershell`](https://learn.microsoft.com/en-us/dotnet/core/additional-tools/self-signed-certificates-guide#with-dotnet-dev-certs): ```powershell dotnet dev-certs https --clean @@ -105,7 +113,7 @@ dotnet dev-certs https -ep $env:USERPROFILE\.aspnet\https\aspnetapp.pfx -p + { + x.OpenApiRoutePattern = "/swagger/v1/swagger.json"; + }); } - // #endif + // #endif await app.RunAsync(); } catch (Exception ex) diff --git a/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj b/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj index a832a99..70f267a 100644 --- a/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj +++ b/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj @@ -7,5 +7,5 @@ - + diff --git a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProduct.cs b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProduct.cs index e9f50d6..42e7cce 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProduct.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProduct.cs @@ -1,5 +1,4 @@ using FluentValidation; -using MediatR; using Microsoft.Extensions.Logging; using Shared.Abstractions.Core.CQRS; using Shared.Abstractions.Persistence.Ef; @@ -19,7 +18,7 @@ namespace Vertical.Slice.Template.Products.Features.CreatingProduct.v1; // https://codeopinion.com/leaking-value-objects-from-your-domain/ // https://www.youtube.com/watch?v=CdanF8PWJng // we don't pass value-objects and domains to our commands and events, just primitive types -internal record CreateProduct(string Name, Guid CategoryId, decimal Price, string? Description = null) +public record CreateProduct(string Name, Guid CategoryId, decimal Price, string? Description = null) : ICommand { public Guid Id { get; } = IdGenerator.NewId(); @@ -55,7 +54,7 @@ internal class CreateProductHandler( ILogger logger ) : ICommandHandler { - public async Task Handle(CreateProduct request, CancellationToken cancellationToken) + public async ValueTask Handle(CreateProduct request, CancellationToken cancellationToken) { request.NotBeNull(); @@ -72,7 +71,7 @@ public async Task Handle(CreateProduct request, Cancellatio } } -internal record CreateProductResult(Guid Id); +public record CreateProductResult(Guid Id); internal class DbExecuters : IDbExecutors { diff --git a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs index 76117d5..d442e22 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/CreateProductEndpoint.cs @@ -1,11 +1,11 @@ using Humanizer; -using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Shared.Abstractions.Web; +using Shared.Core.Extensions; using Shared.Web.Minimal.Extensions; using Shared.Web.ProblemDetail.HttpResults; @@ -37,6 +37,8 @@ async Task< { var (request, context, mediator, cancellationToken) = requestParameters; + request.NotBeNull(); + var command = request.ToCreateProduct(); var result = await mediator.Send(command, cancellationToken); diff --git a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/ProductCreated.cs b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/ProductCreated.cs index bd4771d..f872e1d 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/ProductCreated.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/CreatingProduct/v1/ProductCreated.cs @@ -11,7 +11,7 @@ namespace Vertical.Slice.Template.Products.Features.CreatingProduct.v1; // https://codeopinion.com/leaking-value-objects-from-your-domain/ // https://www.youtube.com/watch?v=CdanF8PWJng // we don't pass value-objects and domains to our commands and events, just primitive types -internal record ProductCreated(Guid Id, string Name, Guid CategoryId, decimal Price, string? Description = null) +public record ProductCreated(Guid Id, string Name, Guid CategoryId, decimal Price, string? Description = null) : DomainEvent { public static ProductCreated Of(Guid id, string? name, Guid categoryId, decimal price, string? description = null) diff --git a/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductById.cs b/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductById.cs index 7c34a4b..6acb63e 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductById.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductById.cs @@ -14,7 +14,7 @@ namespace Vertical.Slice.Template.Products.Features.GettingProductById.v1; -internal record GetProductById(Guid Id) : CacheQuery +public record GetProductById(Guid Id) : CacheQuery { /// /// GetProductById query with validation. @@ -43,7 +43,7 @@ public GetProductByIdValidator() internal class GetProductByIdHandler(DbExecutors.GetProductByIdExecutor getProductByIdExecutor) : IQueryHandler { - public async Task Handle(GetProductById request, CancellationToken cancellationToken) + public async ValueTask Handle(GetProductById request, CancellationToken cancellationToken) { request.NotBeNull(); diff --git a/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs b/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs index 48ff58b..3ef1cfc 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/GettingProductById/v1/GetProductByIdEndpoint.cs @@ -1,5 +1,4 @@ using Humanizer; -using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPage.cs b/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPage.cs index a4f1adf..df09170 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPage.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPage.cs @@ -14,7 +14,7 @@ namespace Vertical.Slice.Template.Products.Features.GettingProductsByPage.v1; -internal record GetProductsByPage : PageQuery +public record GetProductsByPage : PageQuery { /// /// GetProductById query with validation. @@ -56,7 +56,10 @@ internal class GetProductByPageHandler( ISieveProcessor sieveProcessor ) : IQueryHandler { - public async Task Handle(GetProductsByPage request, CancellationToken cancellationToken) + public async ValueTask Handle( + GetProductsByPage request, + CancellationToken cancellationToken + ) { request.NotBeNull(); diff --git a/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs b/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs index 9febcb5..aaf3079 100644 --- a/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs +++ b/src/App/Vertical.Slice.Template/Products/Features/GettingProductsByPage/v1/GetProductsByPageEndpoint.cs @@ -1,5 +1,4 @@ using Humanizer; -using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Infrastrcture.cs b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Infrastrcture.cs index 941a56f..8fd815b 100644 --- a/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Infrastrcture.cs +++ b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.Infrastrcture.cs @@ -1,5 +1,5 @@ using CorrelationId.DependencyInjection; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Builder; using Shared.Abstractions.Persistence.Ef.Repository; using Shared.Cache; @@ -47,7 +47,13 @@ public static WebApplicationBuilder AddInfrastructures(this WebApplicationBuilde builder.Services.AddHttpContextAccessor(); - builder.Services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(CatalogsMetadata).Assembly)); + // https://github.com/martinothamar/Mediator + builder.Services.AddMediator(options => + { + options.ServiceLifetime = ServiceLifetime.Transient; + options.Namespace = "Vertical.Slice.Template"; + }); + builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(StreamLoggingBehavior<,>)); builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>)); diff --git a/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationExtensions/WebApplicationExtensions.Infrastructure.cs b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationExtensions/WebApplicationExtensions.Infrastructure.cs index 3d15cb5..9b6bf1e 100644 --- a/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationExtensions/WebApplicationExtensions.Infrastructure.cs +++ b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationExtensions/WebApplicationExtensions.Infrastructure.cs @@ -6,7 +6,7 @@ namespace Vertical.Slice.Template.Shared.Extensions.WebApplicationExtensions; public static partial class WebApplicationExtensions { - public static Task UseInfrastructure(this WebApplication app) + public static async Task UseInfrastructure(this WebApplication app) { app.UseCustomCors(); @@ -17,6 +17,6 @@ public static Task UseInfrastructure(this WebApplication app) // https://github.com/stevejgordon/CorrelationId app.UseCorrelationId(); - return Task.CompletedTask; + await app.MigrateDatabases(); } } diff --git a/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationExtensions/WebApplicationExtensions.Migration.cs b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationExtensions/WebApplicationExtensions.Migration.cs new file mode 100644 index 0000000..7d3353d --- /dev/null +++ b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationExtensions/WebApplicationExtensions.Migration.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Builder; +using Shared.Abstractions.Persistence; + +namespace Vertical.Slice.Template.Shared.Extensions.WebApplicationExtensions; + +public static partial class WebApplicationExtensions +{ + public static async Task MigrateDatabases(this WebApplication app) + { + using var scope = app.Services.CreateScope(); + var migrationManager = scope.ServiceProvider.GetRequiredService(); + + await migrationManager.ExecuteAsync(CancellationToken.None); + } +} diff --git a/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsers.cs b/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsers.cs index a98710c..e4492e5 100644 --- a/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsers.cs +++ b/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsers.cs @@ -6,7 +6,7 @@ namespace Vertical.Slice.Template.Users.GetUsers; -internal record GetUsersByPage : PageQuery +public record GetUsersByPage : PageQuery { /// /// GetUsersByPage query with validation. @@ -29,7 +29,7 @@ public static GetUsersByPage Of(PageRequest pageRequest) internal class GetUsersHandler(IUsersHttpClient usersHttpClient) : IQueryHandler { - public async Task Handle(GetUsersByPage request, CancellationToken cancellationToken) + public async ValueTask Handle(GetUsersByPage request, CancellationToken cancellationToken) { var usersList = await usersHttpClient.GetAllUsersAsync(request, cancellationToken); diff --git a/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsersEndpoint.cs b/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsersEndpoint.cs index f7cba60..0120b84 100644 --- a/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsersEndpoint.cs +++ b/src/App/Vertical.Slice.Template/Users/GetUsers/GetUsersEndpoint.cs @@ -1,5 +1,5 @@ using Humanizer; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/src/App/Vertical.Slice.Template/Usings.cs b/src/App/Vertical.Slice.Template/Usings.cs new file mode 100644 index 0000000..0fdf12b --- /dev/null +++ b/src/App/Vertical.Slice.Template/Usings.cs @@ -0,0 +1 @@ +global using IMediator = Mediator.IMediator; diff --git a/src/App/Vertical.Slice.Template/Vertical.Slice.Template.csproj b/src/App/Vertical.Slice.Template/Vertical.Slice.Template.csproj index 301d5ef..5f1e584 100644 --- a/src/App/Vertical.Slice.Template/Vertical.Slice.Template.csproj +++ b/src/App/Vertical.Slice.Template/Vertical.Slice.Template.csproj @@ -17,5 +17,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d190313..3a2f05b 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -17,6 +17,8 @@ + + @@ -55,6 +57,7 @@ + @@ -148,9 +151,6 @@ - - - @@ -207,4 +207,4 @@ - + \ No newline at end of file diff --git a/src/Shared/Abstractions/Caching/ICacheRequest.cs b/src/Shared/Abstractions/Caching/ICacheRequest.cs index f459a18..fe0eec4 100644 --- a/src/Shared/Abstractions/Caching/ICacheRequest.cs +++ b/src/Shared/Abstractions/Caching/ICacheRequest.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Caching; diff --git a/src/Shared/Abstractions/Caching/IInvalidateCacheRequest.cs b/src/Shared/Abstractions/Caching/IInvalidateCacheRequest.cs index b79be2e..a7e6f97 100644 --- a/src/Shared/Abstractions/Caching/IInvalidateCacheRequest.cs +++ b/src/Shared/Abstractions/Caching/IInvalidateCacheRequest.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Caching; diff --git a/src/Shared/Abstractions/Core/CQRS/ICommand.cs b/src/Shared/Abstractions/Core/CQRS/ICommand.cs index e634619..d096062 100644 --- a/src/Shared/Abstractions/Core/CQRS/ICommand.cs +++ b/src/Shared/Abstractions/Core/CQRS/ICommand.cs @@ -1,8 +1,8 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Core.CQRS; public interface ICommand : IRequest; -public interface ICommand : IRequest +public interface ICommand : IRequest where TResponse : class; diff --git a/src/Shared/Abstractions/Core/CQRS/ICommandHandler.cs b/src/Shared/Abstractions/Core/CQRS/ICommandHandler.cs index dfab597..6f053d9 100644 --- a/src/Shared/Abstractions/Core/CQRS/ICommandHandler.cs +++ b/src/Shared/Abstractions/Core/CQRS/ICommandHandler.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Core.CQRS; diff --git a/src/Shared/Abstractions/Core/CQRS/IQuery.cs b/src/Shared/Abstractions/Core/CQRS/IQuery.cs index f732eac..2582a02 100644 --- a/src/Shared/Abstractions/Core/CQRS/IQuery.cs +++ b/src/Shared/Abstractions/Core/CQRS/IQuery.cs @@ -1,10 +1,9 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Core.CQRS; public interface IQuery : IRequest where TResponse : class; -// https://jimmybogard.com/mediatr-10-0-released/ public interface IStreamQuery : IStreamRequest where T : notnull; diff --git a/src/Shared/Abstractions/Core/CQRS/IQueryHandler.cs b/src/Shared/Abstractions/Core/CQRS/IQueryHandler.cs index 2789219..e7e6d7a 100644 --- a/src/Shared/Abstractions/Core/CQRS/IQueryHandler.cs +++ b/src/Shared/Abstractions/Core/CQRS/IQueryHandler.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Core.CQRS; @@ -6,7 +6,6 @@ public interface IQueryHandler : IRequestHandler where TResponse : class; -// https://jimmybogard.com/mediatr-10-0-released/ public interface IStreamQueryHandler : IStreamRequestHandler where TQuery : IStreamQuery where TResponse : class; diff --git a/src/Shared/Abstractions/Core/Domain/Events/IEvent.cs b/src/Shared/Abstractions/Core/Domain/Events/IEvent.cs index 1b30780..ca79a4e 100644 --- a/src/Shared/Abstractions/Core/Domain/Events/IEvent.cs +++ b/src/Shared/Abstractions/Core/Domain/Events/IEvent.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Core.Domain.Events; diff --git a/src/Shared/Abstractions/Core/Domain/Events/IEventHandler.cs b/src/Shared/Abstractions/Core/Domain/Events/IEventHandler.cs index 242b519..13b44a9 100644 --- a/src/Shared/Abstractions/Core/Domain/Events/IEventHandler.cs +++ b/src/Shared/Abstractions/Core/Domain/Events/IEventHandler.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; namespace Shared.Abstractions.Core.Domain.Events; diff --git a/src/Shared/Abstractions/Web/IHttpCommand.cs b/src/Shared/Abstractions/Web/IHttpCommand.cs index d9764ed..d0ca9b8 100644 --- a/src/Shared/Abstractions/Web/IHttpCommand.cs +++ b/src/Shared/Abstractions/Web/IHttpCommand.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Microsoft.AspNetCore.Http; namespace Shared.Abstractions.Web; diff --git a/src/Shared/Abstractions/Web/IHttpQuery.cs b/src/Shared/Abstractions/Web/IHttpQuery.cs index 657e837..3f5662d 100644 --- a/src/Shared/Abstractions/Web/IHttpQuery.cs +++ b/src/Shared/Abstractions/Web/IHttpQuery.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Microsoft.AspNetCore.Http; namespace Shared.Abstractions.Web; diff --git a/src/Shared/Abstractions/Web/IMinimalEndpoint.cs b/src/Shared/Abstractions/Web/IMinimalEndpoint.cs index 9ab0a5b..1a4cbb5 100644 --- a/src/Shared/Abstractions/Web/IMinimalEndpoint.cs +++ b/src/Shared/Abstractions/Web/IMinimalEndpoint.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; diff --git a/src/Shared/Cache/Behaviours/CachingBehavior.cs b/src/Shared/Cache/Behaviours/CachingBehavior.cs index 1707f51..b6b4256 100644 --- a/src/Shared/Cache/Behaviours/CachingBehavior.cs +++ b/src/Shared/Cache/Behaviours/CachingBehavior.cs @@ -1,5 +1,5 @@ using EasyCaching.Core; -using MediatR; +using Mediator; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Shared.Abstractions.Caching; @@ -7,62 +7,53 @@ namespace Shared.Cache.Behaviours; // Ref: https://anderly.com/2019/12/12/cross-cutting-concerns-with-mediatr-pipeline-behaviors/ -public class CachingBehavior : IPipelineBehavior +public class CachingBehavior( + ILogger> logger, + IEasyCachingProviderFactory cachingProviderFactory, + IOptions cacheOptions +) : IPipelineBehavior where TRequest : IRequest where TResponse : class { - private readonly ILogger> _logger; - private readonly IEasyCachingProvider _cacheProvider; - private readonly CacheOptions _cacheOptions; - - public CachingBehavior( - ILogger> logger, - IEasyCachingProviderFactory cachingProviderFactory, - IOptions cacheOptions - ) - { - _cacheOptions = cacheOptions.Value; - _logger = logger; - _cacheProvider = cachingProviderFactory.GetCachingProvider(cacheOptions.Value.DefaultCacheType); - } + private readonly IEasyCachingProvider _cacheProvider = cachingProviderFactory.GetCachingProvider( + cacheOptions.Value.DefaultCacheType + ); + + private readonly CacheOptions _cacheOptions = cacheOptions.Value; - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken + public async ValueTask Handle( + TRequest message, + CancellationToken cancellationToken, + MessageHandlerDelegate next ) { - if (request is not ICacheRequest cacheRequest) + if (message is not ICacheRequest cacheRequest) { // No cache policy found, so just continue through the pipeline - return await next(); + return await next(message, cancellationToken); } - var cacheKey = cacheRequest.CacheKey(request); + var cacheKey = cacheRequest.CacheKey(message); var cachedResponse = await _cacheProvider.GetAsync(cacheKey, cancellationToken); if (cachedResponse.Value != null) { - _logger.LogDebug( + logger.LogDebug( "Response retrieved {TRequest} from cache. CacheKey: {CacheKey}", typeof(TRequest).FullName, cacheKey ); + return cachedResponse.Value; } - var response = await next(); + var response = await next(message, cancellationToken); - var expiredTimeSpan = - cacheRequest.AbsoluteExpirationRelativeToNow != TimeSpan.FromMinutes(5) - ? cacheRequest.AbsoluteExpirationRelativeToNow - : TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute) != TimeSpan.FromMinutes(5) - ? TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute) - : cacheRequest.AbsoluteExpirationRelativeToNow; + var expiredTimeSpan = GetExpirationTime(cacheRequest); await _cacheProvider.SetAsync(cacheKey, response, expiredTimeSpan, cancellationToken); - _logger.LogDebug( + logger.LogDebug( "Caching response for {TRequest} with cache key: {CacheKey}", typeof(TRequest).FullName, cacheKey @@ -70,37 +61,45 @@ CancellationToken cancellationToken return response; } + + private TimeSpan GetExpirationTime(ICacheRequest cacheRequest) + { + if (cacheRequest.AbsoluteExpirationRelativeToNow != TimeSpan.FromMinutes(5)) + { + return cacheRequest.AbsoluteExpirationRelativeToNow; + } + else if (TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute) != TimeSpan.FromMinutes(5)) + { + return TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute); + } + + return cacheRequest.AbsoluteExpirationRelativeToNow; + } } -public class StreamCachingBehavior : IStreamPipelineBehavior +public class StreamCachingBehavior( + ILogger> logger, + IEasyCachingProviderFactory cachingProviderFactory, + IOptions cacheOptions +) : IStreamPipelineBehavior where TRequest : IStreamRequest where TResponse : class { - private readonly ILogger> _logger; - private readonly IEasyCachingProvider _cacheProvider; - private readonly CacheOptions _cacheOptions; - - public StreamCachingBehavior( - ILogger> logger, - IEasyCachingProviderFactory cachingProviderFactory, - IOptions cacheOptions - ) - { - _cacheOptions = cacheOptions.Value; - _logger = logger; - _cacheProvider = cachingProviderFactory.GetCachingProvider(cacheOptions.Value.DefaultCacheType); - } + private readonly IEasyCachingProvider _cacheProvider = cachingProviderFactory.GetCachingProvider( + cacheOptions.Value.DefaultCacheType + ); + private readonly CacheOptions _cacheOptions = cacheOptions.Value; public async IAsyncEnumerable Handle( - TRequest request, - StreamHandlerDelegate next, - CancellationToken cancellationToken + TRequest message, + CancellationToken cancellationToken, + StreamHandlerDelegate next ) { - if (request is not IStreamCacheRequest cacheRequest) + if (message is not IStreamCacheRequest cacheRequest) { // If the request does not implement IStreamCacheRequest, go to the next pipeline - await foreach (var response in next().WithCancellation(cancellationToken)) + await foreach (var response in next(message, cancellationToken)) { yield return response; } @@ -108,12 +107,12 @@ CancellationToken cancellationToken yield break; } - var cacheKey = cacheRequest.CacheKey(request); - var cachedResponse = _cacheProvider.Get(cacheKey); + var cacheKey = cacheRequest.CacheKey(message); + var cachedResponse = await _cacheProvider.GetAsync(cacheKey, cancellationToken); if (cachedResponse != null) { - _logger.LogDebug( + logger.LogDebug( "Response retrieved {TRequest} from cache. CacheKey: {CacheKey}", typeof(TRequest).FullName, cacheKey @@ -123,18 +122,13 @@ CancellationToken cancellationToken yield break; } - var expiredTimeSpan = - cacheRequest.AbsoluteExpirationRelativeToNow != TimeSpan.FromMinutes(5) - ? cacheRequest.AbsoluteExpirationRelativeToNow - : TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute) != TimeSpan.FromMinutes(5) - ? TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute) - : cacheRequest.AbsoluteExpirationRelativeToNow; + var expiredTimeSpan = GetExpirationTime(cacheRequest); - await foreach (var response in next().WithCancellation(cancellationToken)) + await foreach (var response in next(message, cancellationToken)) { - _cacheProvider.SetAsync(cacheKey, response, expiredTimeSpan, cancellationToken).GetAwaiter().GetResult(); + await _cacheProvider.SetAsync(cacheKey, response, expiredTimeSpan, cancellationToken); - _logger.LogDebug( + logger.LogDebug( "Caching response for {TRequest} with cache key: {CacheKey}", typeof(TRequest).FullName, cacheKey @@ -143,4 +137,18 @@ CancellationToken cancellationToken yield return response; } } + + private TimeSpan GetExpirationTime(IStreamCacheRequest cacheRequest) + { + if (cacheRequest.AbsoluteExpirationRelativeToNow != TimeSpan.FromMinutes(5)) + { + return cacheRequest.AbsoluteExpirationRelativeToNow; + } + else if (TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute) != TimeSpan.FromMinutes(5)) + { + return TimeSpan.FromMinutes(_cacheOptions.ExpirationTimeInMinute); + } + + return cacheRequest.AbsoluteExpirationRelativeToNow; + } } diff --git a/src/Shared/Cache/Behaviours/InvalidateCachingBehavior.cs b/src/Shared/Cache/Behaviours/InvalidateCachingBehavior.cs index cd91b60..92159d9 100644 --- a/src/Shared/Cache/Behaviours/InvalidateCachingBehavior.cs +++ b/src/Shared/Cache/Behaviours/InvalidateCachingBehavior.cs @@ -1,80 +1,70 @@ using EasyCaching.Core; -using MediatR; +using Mediator; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Shared.Abstractions.Caching; namespace Shared.Cache.Behaviours; -public class InvalidateCachingBehavior : IPipelineBehavior +public class InvalidateCachingBehavior( + ILogger> logger, + IEasyCachingProviderFactory cachingProviderFactory, + IOptions cacheOptions +) : IPipelineBehavior where TRequest : IRequest where TResponse : class { - private readonly ILogger> _logger; - private readonly IEasyCachingProvider _cacheProvider; + private readonly IEasyCachingProvider _cacheProvider = cachingProviderFactory.GetCachingProvider( + cacheOptions.Value.DefaultCacheType + ); - public InvalidateCachingBehavior( - ILogger> logger, - IEasyCachingProviderFactory cachingProviderFactory, - IOptions cacheOptions + public async ValueTask Handle( + TRequest message, + CancellationToken cancellationToken, + MessageHandlerDelegate next ) { - _logger = logger; - _cacheProvider = cachingProviderFactory.GetCachingProvider(cacheOptions.Value.DefaultCacheType); - } - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken - ) - { - if (request is not IInvalidateCacheRequest cacheRequest) + if (message is not IInvalidateCacheRequest cacheRequest) { // No cache policy found, so just continue through the pipeline - return await next(); + return await next(message, cancellationToken); } - var cacheKeys = cacheRequest.CacheKeys(request); - var response = await next(); + var cacheKeys = cacheRequest.CacheKeys(message); + var response = await next(message, cancellationToken); foreach (var cacheKey in cacheKeys) { await _cacheProvider.RemoveAsync(cacheKey, cancellationToken); - _logger.LogDebug("Cache data with cache key: {CacheKey} invalidated", cacheKey); + logger.LogDebug("Cache data with cache key: {CacheKey} invalidated", cacheKey); } return response; } } -public class StreamInvalidateCachingBehavior : IStreamPipelineBehavior +public class StreamInvalidateCachingBehavior( + ILogger> logger, + IEasyCachingProviderFactory cachingProviderFactory, + IOptions cacheOptions +) : IStreamPipelineBehavior where TRequest : IStreamRequest where TResponse : class { - private readonly ILogger> _logger; - private readonly IEasyCachingProvider _cacheProvider; - - public StreamInvalidateCachingBehavior( - ILogger> logger, - IEasyCachingProviderFactory cachingProviderFactory, - IOptions cacheOptions - ) - { - _logger = logger; - _cacheProvider = cachingProviderFactory.GetCachingProvider(cacheOptions.Value.DefaultCacheType); - } + private readonly IEasyCachingProvider _cacheProvider = cachingProviderFactory.GetCachingProvider( + cacheOptions.Value.DefaultCacheType + ); public async IAsyncEnumerable Handle( - TRequest request, - StreamHandlerDelegate next, - CancellationToken cancellationToken + TRequest message, + CancellationToken cancellationToken, + StreamHandlerDelegate next ) { - if (request is not IStreamInvalidateCacheRequest cacheRequest) + if (message is not IStreamInvalidateCacheRequest cacheRequest) { // If the request does not implement IStreamCacheRequest, go to the next pipeline - await foreach (var response in next().WithCancellation(cancellationToken)) + await foreach (var response in next(message, cancellationToken)) { yield return response; } @@ -82,14 +72,14 @@ CancellationToken cancellationToken yield break; } - await foreach (var response in next().WithCancellation(cancellationToken)) + await foreach (var response in next(message, cancellationToken)) { - var cacheKeys = cacheRequest.CacheKeys(request); + var cacheKeys = cacheRequest.CacheKeys(message); foreach (var cacheKey in cacheKeys) { await _cacheProvider.RemoveAsync(cacheKey, cancellationToken); - _logger.LogDebug("Cache data with cache key: {CacheKey} invalidated", cacheKey); + logger.LogDebug("Cache data with cache key: {CacheKey} invalidated", cacheKey); } yield return response; diff --git a/src/Shared/Cache/CacheRequest.cs b/src/Shared/Cache/CacheRequest.cs index 24ffdc7..0a6ca88 100644 --- a/src/Shared/Cache/CacheRequest.cs +++ b/src/Shared/Cache/CacheRequest.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Shared.Abstractions.Caching; namespace Shared.Cache; diff --git a/src/Shared/Cache/InvalidateCacheRequest.cs b/src/Shared/Cache/InvalidateCacheRequest.cs index 70be410..3aad6f0 100644 --- a/src/Shared/Cache/InvalidateCacheRequest.cs +++ b/src/Shared/Cache/InvalidateCacheRequest.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Shared.Abstractions.Caching; namespace Shared.Cache; diff --git a/src/Shared/Cache/StreamCacheRequest.cs b/src/Shared/Cache/StreamCacheRequest.cs index 2ae90e8..c4ded9b 100644 --- a/src/Shared/Cache/StreamCacheRequest.cs +++ b/src/Shared/Cache/StreamCacheRequest.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Shared.Abstractions.Caching; namespace Shared.Cache; diff --git a/src/Shared/Core/Domain/Events/DomainEvent.cs b/src/Shared/Core/Domain/Events/DomainEvent.cs index 09fa685..c919f6f 100644 --- a/src/Shared/Core/Domain/Events/DomainEvent.cs +++ b/src/Shared/Core/Domain/Events/DomainEvent.cs @@ -2,7 +2,8 @@ namespace Shared.Core.Domain.Events; -public record DomainEvent : Event, IDomainEvent +// https://github.com/martinothamar/Mediator/issues/138 +public abstract record DomainEvent : Event, IDomainEvent { public dynamic AggregateId { get; private set; } = default!; public long AggregateSequenceNumber { get; private set; } diff --git a/src/Shared/Core/Domain/Events/DomainEventPublisher.cs b/src/Shared/Core/Domain/Events/DomainEventPublisher.cs index 8a22274..2178b66 100644 --- a/src/Shared/Core/Domain/Events/DomainEventPublisher.cs +++ b/src/Shared/Core/Domain/Events/DomainEventPublisher.cs @@ -1,37 +1,26 @@ -using MediatR; +using Mediator; using Microsoft.Extensions.Logging; using Shared.Abstractions.Core.Domain.Events; using Shared.Core.Extensions; namespace Shared.Core.Domain.Events; -public class DomainEventPublisher : IDomainEventPublisher +public class DomainEventPublisher( + IDomainEventsAccessor domainEventsAccessor, + IMediator mediator, + ILogger logger +) : IDomainEventPublisher { - private readonly IDomainEventsAccessor _domainEventsAccessor; - private readonly IMediator _mediator; - private readonly ILogger _logger; - - public DomainEventPublisher( - IDomainEventsAccessor domainEventsAccessor, - IMediator mediator, - ILogger logger - ) - { - _domainEventsAccessor = domainEventsAccessor; - _mediator = mediator; - _logger = logger; - } - public Task PublishAsync(IDomainEvent domainEvent, CancellationToken cancellationToken = default) { - return PublishAsync(new[] { domainEvent }, cancellationToken); + return PublishAsync([domainEvent], cancellationToken); } public async Task PublishAsync(IDomainEvent[] domainEvents, CancellationToken cancellationToken = default) { domainEvents.NotBeNull(); - if (!domainEvents.Any()) + if (domainEvents.Length == 0) return; // https://github.com/dotnet-architecture/eShopOnContainers/issues/700#issuecomment-461807560 @@ -43,16 +32,16 @@ public async Task PublishAsync(IDomainEvent[] domainEvents, CancellationToken ca // Dispatch our domain events before commit var eventsToDispatch = domainEvents.ToList(); - if (!eventsToDispatch.Any()) + if (eventsToDispatch.Count == 0) { - eventsToDispatch = new List(_domainEventsAccessor.UnCommittedDomainEvents); + eventsToDispatch = [.. domainEventsAccessor.UnCommittedDomainEvents]; } foreach (var domainEvent in eventsToDispatch) { - await _mediator.Publish(domainEvent, cancellationToken); + await mediator.Publish(domainEvent, cancellationToken); - _logger.LogDebug( + logger.LogDebug( "Dispatched domain event {DomainEventName} with payload {DomainEventContent}", domainEvent.GetType().FullName, domainEvent diff --git a/src/Shared/Core/Domain/Events/Event.cs b/src/Shared/Core/Domain/Events/Event.cs index 200b3bb..eec8ff2 100644 --- a/src/Shared/Core/Domain/Events/Event.cs +++ b/src/Shared/Core/Domain/Events/Event.cs @@ -2,7 +2,8 @@ namespace Shared.Core.Domain.Events; -public record Event : IEvent +// https://github.com/martinothamar/Mediator/issues/138 +public abstract record Event : IEvent { public Guid EventId { get; } = Guid.NewGuid(); public long EventVersion => -1; diff --git a/src/Shared/EF/EfTxBehavior.cs b/src/Shared/EF/EfTxBehavior.cs index 9ad4628..362e144 100644 --- a/src/Shared/EF/EfTxBehavior.cs +++ b/src/Shared/EF/EfTxBehavior.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Shared.Abstractions.Core.Domain.Events; @@ -8,83 +8,73 @@ namespace Shared.EF; // Ref: https://github.com/thangchung/clean-architecture-dotnet/blob/main/src/N8T.Infrastructure.EfCore/TxBehavior.cs -public class EfTxBehavior : IPipelineBehavior +public class EfTxBehavior( + IDbFacadeResolver dbFacadeResolver, + ILogger> logger, + IDomainEventPublisher domainEventPublisher, + IDomainEventContext domainEventContext +) : IPipelineBehavior where TRequest : class, IRequest where TResponse : notnull { - private readonly IDbFacadeResolver _dbFacadeResolver; - private readonly ILogger> _logger; - private readonly IDomainEventPublisher _domainEventPublisher; - private readonly IDomainEventContext _domainEventContext; - - public EfTxBehavior( - IDbFacadeResolver dbFacadeResolver, - ILogger> logger, - IDomainEventPublisher domainEventPublisher, - IDomainEventContext domainEventContext + public async ValueTask Handle( + TRequest message, + CancellationToken cancellationToken, + MessageHandlerDelegate next ) { - _dbFacadeResolver = dbFacadeResolver; - _logger = logger; - _domainEventPublisher = domainEventPublisher; - _domainEventContext = domainEventContext; - } - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken - ) - { - if (request is not ITxRequest) - return await next(); + if (message is not ITxRequest) + return await next(message, cancellationToken); - _logger.LogInformation( + logger.LogInformation( "{Prefix} Handled command {MediatrRequest}", nameof(EfTxBehavior), typeof(TRequest).FullName ); - _logger.LogDebug( + logger.LogDebug( "{Prefix} Handled command {MediatrRequest} with content {RequestContent}", nameof(EfTxBehavior), typeof(TRequest).FullName, - JsonSerializer.Serialize(request) + JsonSerializer.Serialize(message) ); - _logger.LogInformation( + logger.LogInformation( "{Prefix} Open the transaction for {MediatrRequest}", nameof(EfTxBehavior), typeof(TRequest).FullName ); - var strategy = _dbFacadeResolver.Database.CreateExecutionStrategy(); + var strategy = dbFacadeResolver.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync(async () => { // https://www.thinktecture.com/en/entity-framework-core/use-transactionscope-with-caution-in-2-1/ // https://github.com/dotnet/efcore/issues/6233#issuecomment-242693262 - var isInnerTransaction = _dbFacadeResolver.Database.CurrentTransaction is not null; + var isInnerTransaction = dbFacadeResolver.Database.CurrentTransaction is not null; + var transaction = - _dbFacadeResolver.Database.CurrentTransaction - ?? await _dbFacadeResolver.Database.BeginTransactionAsync(cancellationToken); + dbFacadeResolver.Database.CurrentTransaction + ?? await dbFacadeResolver.Database.BeginTransactionAsync(cancellationToken); + try { - var response = await next(); + var response = await next(message, cancellationToken); - _logger.LogInformation( + logger.LogInformation( "{Prefix} Executed the {MediatrRequest} request", nameof(EfTxBehavior), typeof(TRequest).FullName ); - var domainEvents = _domainEventContext.GetAllUncommittedEvents(); - await _domainEventPublisher.PublishAsync(domainEvents.ToArray(), cancellationToken); + var domainEvents = domainEventContext.GetAllUncommittedEvents(); + + await domainEventPublisher.PublishAsync(domainEvents.ToArray(), cancellationToken); if (isInnerTransaction == false) await transaction.CommitAsync(cancellationToken); - _domainEventContext.MarkUncommittedDomainEventAsCommitted(); + domainEventContext.MarkUncommittedDomainEventAsCommitted(); return response; } @@ -92,6 +82,7 @@ CancellationToken cancellationToken { if (isInnerTransaction == false) await transaction.RollbackAsync(cancellationToken); + throw; } }); diff --git a/src/Shared/Logging/Extensions/DependencyInjectionExtensions.cs b/src/Shared/Logging/Extensions/DependencyInjectionExtensions.cs index 8bc0602..92a7499 100644 --- a/src/Shared/Logging/Extensions/DependencyInjectionExtensions.cs +++ b/src/Shared/Logging/Extensions/DependencyInjectionExtensions.cs @@ -30,13 +30,19 @@ public static WebApplicationBuilder AddCustomSerilog( // add option to the dependency injection builder.Services.AddValidationOptions(opt => configurator?.Invoke(opt)); - // https://andrewlock.net/creating-a-rolling-file-logging-provider-for-asp-net-core-2-0/ // https://github.com/serilog/serilog-extensions-hosting - // https://andrewlock.net/adding-serilog-to-the-asp-net-core-generic-host/ - // Serilog replace `ILoggerFactory`,It replaces microsoft `LoggerFactory` class with `SerilogLoggerFactory`, so `ConsoleLoggerProvider` and other default microsoft logger providers don't instantiate at all with serilog - builder.Host.UseSerilog( - (context, serviceProvider, loggerConfiguration) => + // https://github.com/serilog/serilog-aspnetcore#two-stage-initialization + // Routes framework log messages through Serilog - get other sinks from top level definition + builder.Services.AddSerilog( + (sp, loggerConfiguration) => { + // The downside of initializing Serilog in top level is that services from the ASP.NET Core host, including the appsettings.json configuration and dependency injection, aren't available yet. + // setup sinks that related to `configuration` here instead of top level serilog configuration + // https://github.com/serilog/serilog-settings-configuration + loggerConfiguration.ReadFrom.Configuration( + builder.Configuration, + new ConfigurationReaderOptions {SectionName = nameof(SerilogOptions)}); + extraConfigure?.Invoke(loggerConfiguration); loggerConfiguration @@ -50,22 +56,16 @@ public static WebApplicationBuilder AddCustomSerilog( .Enrich.WithExceptionDetails( new DestructuringOptionsBuilder() .WithDefaultDestructurers() - .WithDestructurers(new[] { new DbUpdateExceptionDestructurer() }) - ); + .WithDestructurers(new[] {new DbUpdateExceptionDestructurer()})); - // https://github.com/serilog/serilog-settings-configuration - loggerConfiguration.ReadFrom.Configuration( - context.Configuration, - new ConfigurationReaderOptions { SectionName = nameof(SerilogOptions) } - ); if (serilogOptions.UseConsole) { // https://github.com/serilog/serilog-sinks-async // https://github.com/lucadecamillis/serilog-sinks-spectre - loggerConfiguration.WriteTo.Async(writeTo => - writeTo.Spectre(outputTemplate: serilogOptions.LogTemplate) - ); + loggerConfiguration.WriteTo.Async( + writeTo => + writeTo.Spectre(outputTemplate: serilogOptions.LogTemplate)); } // https://github.com/serilog/serilog-sinks-async @@ -74,19 +74,27 @@ public static WebApplicationBuilder AddCustomSerilog( // elasticsearch sink internally is async // https://www.nuget.org/packages/Elastic.Serilog.Sinks loggerConfiguration.WriteTo.Elasticsearch( - new[] { new Uri(serilogOptions.ElasticSearchUrl) }, + new[] {new Uri(serilogOptions.ElasticSearchUrl),}, opts => { opts.DataStream = new DataStreamName( - $"{builder.Environment.ApplicationName}-{builder.Environment.EnvironmentName}-{DateTime.Now:yyyy-MM}" - ); + $"{ + builder.Environment.ApplicationName + }-{ + builder.Environment.EnvironmentName + }-{ + DateTime.Now + :yyyy-MM}"); + opts.BootstrapMethod = BootstrapMethod.Failure; + opts.ConfigureChannel = channelOpts => - { - channelOpts.BufferOptions = new BufferOptions { ExportMaxConcurrency = 10 }; - }; - } - ); + { + channelOpts.BufferOptions = + new BufferOptions + {ExportMaxConcurrency = 10}; + }; + }); } // https://github.com/serilog-contrib/serilog-sinks-grafana-loki @@ -96,10 +104,9 @@ public static WebApplicationBuilder AddCustomSerilog( serilogOptions.GrafanaLokiUrl, new[] { - new LokiLabel { Key = "service", Value = "food-delivery" }, + new LokiLabel {Key = "service", Value = "food-delivery"}, }, - ["app"] - ); + ["app"]); } if (!string.IsNullOrEmpty(serilogOptions.SeqUrl)) @@ -117,17 +124,15 @@ public static WebApplicationBuilder AddCustomSerilog( if (!string.IsNullOrEmpty(serilogOptions.LogPath)) { - loggerConfiguration.WriteTo.Async(writeTo => - writeTo.File( - serilogOptions.LogPath, - outputTemplate: serilogOptions.LogTemplate, - rollingInterval: RollingInterval.Day, - rollOnFileSizeLimit: true - ) - ); + loggerConfiguration.WriteTo.Async( + writeTo => + writeTo.File( + serilogOptions.LogPath, + outputTemplate: serilogOptions.LogTemplate, + rollingInterval: RollingInterval.Day, + rollOnFileSizeLimit: true)); } - } - ); + }); return builder; } diff --git a/src/Shared/Logging/LoggingBehavior.cs b/src/Shared/Logging/LoggingBehavior.cs index 68a056d..13a5785 100644 --- a/src/Shared/Logging/LoggingBehavior.cs +++ b/src/Shared/Logging/LoggingBehavior.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using MediatR; +using Mediator; using Microsoft.Extensions.Logging; namespace Shared.Logging; @@ -9,10 +9,10 @@ public class LoggingBehavior(ILogger where TResponse : class { - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken + public async ValueTask Handle( + TRequest message, + CancellationToken cancellationToken, + MessageHandlerDelegate next ) { const string prefix = nameof(LoggingBehavior); @@ -27,10 +27,11 @@ CancellationToken cancellationToken var timer = new Stopwatch(); timer.Start(); - var response = await next(); + var response = await next(message, cancellationToken); timer.Stop(); var timeTaken = timer.Elapsed; + if (timeTaken.Seconds > 3) { logger.LogWarning( @@ -62,9 +63,9 @@ public class StreamLoggingBehavior(ILogger Handle( - TRequest request, - StreamHandlerDelegate next, - CancellationToken cancellationToken + TRequest message, + CancellationToken cancellationToken, + StreamHandlerDelegate next ) { const string prefix = nameof(StreamLoggingBehavior); @@ -79,10 +80,11 @@ CancellationToken cancellationToken var timer = new Stopwatch(); timer.Start(); - await foreach (var response in next().WithCancellation(cancellationToken)) + await foreach (var response in next(message, cancellationToken)) { timer.Stop(); var timeTaken = timer.Elapsed; + if (timeTaken.Seconds > 3) { logger.LogWarning( @@ -94,6 +96,7 @@ CancellationToken cancellationToken } logger.LogInformation("[{Prefix}] Handled '{RequestData}'", prefix, typeof(TRequest).Name); + yield return response; } } diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index 0d71116..f794391 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -22,7 +22,7 @@ - + @@ -41,6 +41,7 @@ + diff --git a/src/Shared/Validation/RequestValidationBehavior.cs b/src/Shared/Validation/RequestValidationBehavior.cs index 1bdd8e5..9b3fb91 100644 --- a/src/Shared/Validation/RequestValidationBehavior.cs +++ b/src/Shared/Validation/RequestValidationBehavior.cs @@ -1,86 +1,73 @@ using System.Text.Json; using FluentValidation; -using MediatR; +using Mediator; using Microsoft.Extensions.Logging; using Shared.Validation.Extensions; namespace Shared.Validation; -public class RequestValidationBehavior : IPipelineBehavior +public class RequestValidationBehavior( + IServiceProvider serviceProvider, + ILogger> logger +) : IPipelineBehavior where TRequest : IRequest where TResponse : class { - private readonly ILogger> _logger; - private readonly IServiceProvider _serviceProvider; - - public RequestValidationBehavior( - IServiceProvider serviceProvider, - ILogger> logger - ) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken + public async ValueTask Handle( + TRequest message, + CancellationToken cancellationToken, + MessageHandlerDelegate next ) { - var validator = _serviceProvider.GetService>()!; + var validator = serviceProvider.GetService>()!; if (validator is null) - return await next(); + return await next(message, cancellationToken); - _logger.LogInformation( + logger.LogInformation( "[{Prefix}] Handle request={RequestData} and response={ResponseData}", nameof(RequestValidationBehavior), typeof(TRequest).Name, typeof(TResponse).Name ); - _logger.LogDebug( + logger.LogDebug( "Handling {FullName} with content {Request}", typeof(TRequest).FullName, - JsonSerializer.Serialize(request) + JsonSerializer.Serialize(message) ); - await validator.HandleValidationAsync(request, cancellationToken); + await validator.HandleValidationAsync(message, cancellationToken); - var response = await next(); + var response = await next(message, cancellationToken); - _logger.LogInformation("Handled {FullName}", typeof(TRequest).FullName); + logger.LogInformation("Handled {FullName}", typeof(TRequest).FullName); return response; } } -public class StreamRequestValidationBehavior : IStreamPipelineBehavior +public class StreamRequestValidationBehavior( + IServiceProvider serviceProvider, + ILogger> logger +) : IStreamPipelineBehavior where TRequest : IStreamRequest where TResponse : class { - private readonly ILogger> _logger; - private readonly IServiceProvider _serviceProvider; - private IValidator _validator; - - public StreamRequestValidationBehavior( - IServiceProvider serviceProvider, - ILogger> logger - ) - { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly ILogger> _logger = + logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IServiceProvider _serviceProvider = + serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); public async IAsyncEnumerable Handle( - TRequest request, - StreamHandlerDelegate next, - CancellationToken cancellationToken + TRequest message, + CancellationToken cancellationToken, + StreamHandlerDelegate next ) { - _validator = _serviceProvider.GetService>()!; - if (_validator is null) + var validator = _serviceProvider.GetService>()!; + + if (validator is null) { - await foreach (var response in next().WithCancellation(cancellationToken)) + await foreach (var response in next(message, cancellationToken)) { yield return response; } @@ -98,12 +85,12 @@ CancellationToken cancellationToken _logger.LogDebug( "Handling {FullName} with content {Request}", typeof(TRequest).FullName, - JsonSerializer.Serialize(request) + JsonSerializer.Serialize(message) ); - _validator.HandleValidation(request); + await validator.HandleValidationAsync(message, cancellationToken: cancellationToken); - await foreach (var response in next().WithCancellation(cancellationToken)) + await foreach (var response in next(message, cancellationToken)) { yield return response; _logger.LogInformation("Handled {FullName}", typeof(TRequest).FullName); diff --git a/src/Shared/Web/Minimal/Extensions/EndpointRouteBuilderExtensions.cs b/src/Shared/Web/Minimal/Extensions/EndpointRouteBuilderExtensions.cs index 322f227..066f374 100644 --- a/src/Shared/Web/Minimal/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/Shared/Web/Minimal/Extensions/EndpointRouteBuilderExtensions.cs @@ -1,5 +1,5 @@ using Humanizer; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/src/Shared/Web/Minimal/HttpCommand.cs b/src/Shared/Web/Minimal/HttpCommand.cs index af42b4c..1271773 100644 --- a/src/Shared/Web/Minimal/HttpCommand.cs +++ b/src/Shared/Web/Minimal/HttpCommand.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Microsoft.AspNetCore.Http; using Shared.Abstractions.Web; diff --git a/src/Shared/Web/Minimal/HttpQuery.cs b/src/Shared/Web/Minimal/HttpQuery.cs index 94124af..4fccfe0 100644 --- a/src/Shared/Web/Minimal/HttpQuery.cs +++ b/src/Shared/Web/Minimal/HttpQuery.cs @@ -1,4 +1,4 @@ -using MediatR; +using Mediator; using Microsoft.AspNetCore.Http; using Shared.Abstractions.Web; diff --git a/tests/Vertical.Slice.Template.IntegrationTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs b/tests/Vertical.Slice.Template.IntegrationTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs index f18b627..9fff264 100644 --- a/tests/Vertical.Slice.Template.IntegrationTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs +++ b/tests/Vertical.Slice.Template.IntegrationTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs @@ -23,7 +23,7 @@ public async Task should_create_new_product_with_valid_input_in_sql_db() var command = new CreateProductFake(fakeCategoryId).Generate(); // Act - var createdCustomerResponse = await SharedFixture.SendAsync(command); + var createdCustomerResponse = await SharedFixture.SendAsync(command, CancellationToken.None); // Assert createdCustomerResponse.Should().NotBeNull(); diff --git a/tests/Vertical.Slice.Template.TestsShared/Fixtures/MsSqlContainerFixture.cs b/tests/Vertical.Slice.Template.TestsShared/Fixtures/MsSqlContainerFixture.cs index e56dc27..62f20f6 100644 --- a/tests/Vertical.Slice.Template.TestsShared/Fixtures/MsSqlContainerFixture.cs +++ b/tests/Vertical.Slice.Template.TestsShared/Fixtures/MsSqlContainerFixture.cs @@ -2,6 +2,7 @@ using Respawn; using Shared.Core.Extensions; using Testcontainers.MsSql; +using Xunit; using Xunit.Sdk; namespace Vertical.Slice.Template.TestsShared.Fixtures; diff --git a/tests/Vertical.Slice.Template.TestsShared/Fixtures/PostgresContainerFixture.cs b/tests/Vertical.Slice.Template.TestsShared/Fixtures/PostgresContainerFixture.cs index d53652d..dfe6bc0 100644 --- a/tests/Vertical.Slice.Template.TestsShared/Fixtures/PostgresContainerFixture.cs +++ b/tests/Vertical.Slice.Template.TestsShared/Fixtures/PostgresContainerFixture.cs @@ -3,6 +3,7 @@ using Shared.Core.Extensions; using Testcontainers.PostgreSql; using Vertical.Slice.Template.TestsShared.Helpers; +using Xunit; using Xunit.Sdk; namespace Vertical.Slice.Template.TestsShared.Fixtures; diff --git a/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixture.cs b/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixture.cs index 0ff42a8..b838a79 100644 --- a/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixture.cs +++ b/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixture.cs @@ -2,7 +2,7 @@ using AutoBogus; using FluentAssertions; using FluentAssertions.Extensions; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -10,6 +10,7 @@ using Serilog; using Shared.EF; using Vertical.Slice.Template.TestsShared.Factory; +using Xunit; using Xunit.Sdk; namespace Vertical.Slice.Template.TestsShared.Fixtures; diff --git a/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixtureWithEfCore.cs b/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixtureWithEfCore.cs index 158e1f0..1343251 100644 --- a/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixtureWithEfCore.cs +++ b/tests/Vertical.Slice.Template.TestsShared/Fixtures/SharedFixtureWithEfCore.cs @@ -1,4 +1,3 @@ -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit.Sdk; diff --git a/tests/Vertical.Slice.Template.TestsShared/TestBase/IntegrationTestBase.cs b/tests/Vertical.Slice.Template.TestsShared/TestBase/IntegrationTestBase.cs index 8d29ae2..d5761ae 100644 --- a/tests/Vertical.Slice.Template.TestsShared/TestBase/IntegrationTestBase.cs +++ b/tests/Vertical.Slice.Template.TestsShared/TestBase/IntegrationTestBase.cs @@ -5,6 +5,7 @@ using Shared.Abstractions.Persistence; using Shared.Abstractions.Persistence.Ef; using Vertical.Slice.Template.TestsShared.Fixtures; +using Xunit; namespace Vertical.Slice.Template.TestsShared.TestBase; diff --git a/tests/Vertical.Slice.Template.TestsShared/Usings.cs b/tests/Vertical.Slice.Template.TestsShared/Usings.cs index c802f44..0fdf12b 100644 --- a/tests/Vertical.Slice.Template.TestsShared/Usings.cs +++ b/tests/Vertical.Slice.Template.TestsShared/Usings.cs @@ -1 +1 @@ -global using Xunit; +global using IMediator = Mediator.IMediator; diff --git a/tests/Vertical.Slice.Template.TestsShared/XunitFramework/CustomTestFramework.cs b/tests/Vertical.Slice.Template.TestsShared/XunitFramework/CustomTestFramework.cs index 26fab4f..40d0f86 100644 --- a/tests/Vertical.Slice.Template.TestsShared/XunitFramework/CustomTestFramework.cs +++ b/tests/Vertical.Slice.Template.TestsShared/XunitFramework/CustomTestFramework.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Xunit; using Xunit.Sdk; [assembly: TestFramework( diff --git a/tests/Vertical.Slice.Template.UnitTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs b/tests/Vertical.Slice.Template.UnitTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs index 3a9aa32..d4b769d 100644 --- a/tests/Vertical.Slice.Template.UnitTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs +++ b/tests/Vertical.Slice.Template.UnitTests/Products/Features/CreatingProduct/v1/CreateProductTests.cs @@ -1,6 +1,6 @@ using AutoBogus; using FluentAssertions; -using MediatR; +using Mediator; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Vertical.Slice.Template.Products.Features.CreatingProduct.v1; diff --git a/tests/Vertical.Slice.Template.UnitTests/Products/Features/GettingProductById/v1/GetProductByIdTests.cs b/tests/Vertical.Slice.Template.UnitTests/Products/Features/GettingProductById/v1/GetProductByIdTests.cs index 33231d9..0398539 100644 --- a/tests/Vertical.Slice.Template.UnitTests/Products/Features/GettingProductById/v1/GetProductByIdTests.cs +++ b/tests/Vertical.Slice.Template.UnitTests/Products/Features/GettingProductById/v1/GetProductByIdTests.cs @@ -48,10 +48,10 @@ public async Task must_throw_not_found_exception_when_product_with_given_id_not_ var notExistProductQuery = new GetProductById(notExistsProductId); // Act - Func> act = () => handler.Handle(notExistProductQuery, CancellationToken.None); + Func action = async () => await handler.Handle(notExistProductQuery, CancellationToken.None); // Assert - await act.Should().ThrowAsync(); + await action.Should().ThrowAsync(); await executor.Received(1).Invoke(Arg.Is(notExistsProductId), Arg.Any()); } @@ -65,10 +65,10 @@ public async Task must_throw_exception_when_input_is_null() var handler = new GetProductByIdHandler(executor); // Act - Func> act = () => handler.Handle(null!, CancellationToken.None); + Func action = async () => await handler.Handle(null!, CancellationToken.None); // Assert - await act.Should().ThrowAsync(); + await action.Should().ThrowAsync(); await executor.Received(0).Invoke(Arg.Is(product.Id), Arg.Any()); } } diff --git a/tests/Vertical.Slice.Template.UnitTests/Users/Features/GetUsers/v1/GetUsersHandlerTests.cs b/tests/Vertical.Slice.Template.UnitTests/Users/Features/GetUsers/v1/GetUsersHandlerTests.cs index b499f72..c230d8a 100644 --- a/tests/Vertical.Slice.Template.UnitTests/Users/Features/GetUsers/v1/GetUsersHandlerTests.cs +++ b/tests/Vertical.Slice.Template.UnitTests/Users/Features/GetUsers/v1/GetUsersHandlerTests.cs @@ -13,7 +13,7 @@ namespace Vertical.Slice.Template.UnitTests.Users.Features.GetUsers.v1; // https://www.testwithspring.com/lesson/the-best-practices-of-nested-unit-tests/ -public class GetUsersHandlerTests() +public class GetUsersHandlerTests { [Fact] public async Task handle_should_call_users_http_client_once() @@ -80,9 +80,9 @@ public async Task handle_with_http_response_exception_should_returns_correct_err var query = GetUsersByPage.Of(new PageRequest { PageNumber = page, PageSize = pageSize }); - var act = () => handler.Handle(query, cancellationToken); + var action = async () => await handler.Handle(query, cancellationToken); - await act.Should().ThrowAsync(); + await action.Should().ThrowAsync(); } [Fact] @@ -100,8 +100,8 @@ public async Task handle_with_exception_should_returns_correct_error_result() var handler = new GetUsersHandler(usersHttpClient); var query = GetUsersByPage.Of(new PageRequest { PageNumber = page, PageSize = pageSize }); - var act = () => handler.Handle(query, cancellationToken); + var action = async () => await handler.Handle(query, cancellationToken); - await act.Should().ThrowAsync(); + await action.Should().ThrowAsync(); } }