From eeb878c2a057b16d796e80871a3ff0a2586a7811 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sat, 15 Aug 2026 17:33:15 +0200 Subject: [PATCH] Add CQRS and MediatR in ASP.NET Core sample Products API demonstrating CQRS with MediatR: queries, commands, notifications, and a logging pipeline behavior. Targets net10.0 and pins MediatR 12.5.0 (last Apache-2.0 release). Adds an xUnit test project covering the handlers and the logging behavior. --- .../CqrsAndMediatRInAspNetCore.sln | 53 +++++++++++++ .../Behaviors/LoggingBehavior.cs | 25 +++++++ .../Commands/AddProductCommand.cs | 6 ++ .../Controllers/ProductsController.cs | 43 +++++++++++ .../CqrsAndMediatRInAspNetCore.csproj | 13 ++++ .../DataStore/FakeDataStore.cs | 38 ++++++++++ .../Handlers/AddProductHandler.cs | 20 +++++ .../Handlers/CacheInvalidationHandler.cs | 24 ++++++ .../Handlers/EmailHandler.cs | 24 ++++++ .../Handlers/GetProductByIdHandler.cs | 17 +++++ .../Handlers/GetProductsHandler.cs | 16 ++++ .../Notifications/ProductAddedNotification.cs | 6 ++ .../CqrsAndMediatRInAspNetCore/Product.cs | 8 ++ .../CqrsAndMediatRInAspNetCore/Program.cs | 25 +++++++ .../Properties/launchSettings.json | 13 ++++ .../Queries/GetProductByIdQuery.cs | 6 ++ .../Queries/GetProductsQuery.cs | 6 ++ .../appsettings.Development.json | 8 ++ .../appsettings.json | 9 +++ .../CqrsAndMediatRInAspNetCoreTests.csproj | 30 ++++++++ .../LoggingBehaviorTests.cs | 32 ++++++++ .../ProductHandlersTests.cs | 74 +++++++++++++++++++ .../CqrsAndMediatRInAspNetCoreTests/Usings.cs | 1 + 23 files changed, 497 insertions(+) create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.sln create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Behaviors/LoggingBehavior.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Commands/AddProductCommand.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Controllers/ProductsController.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.csproj create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/DataStore/FakeDataStore.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/AddProductHandler.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/CacheInvalidationHandler.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/EmailHandler.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductByIdHandler.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductsHandler.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Notifications/ProductAddedNotification.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Product.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Program.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Properties/launchSettings.json create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductByIdQuery.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductsQuery.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.Development.json create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.json create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/CqrsAndMediatRInAspNetCoreTests.csproj create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/LoggingBehaviorTests.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/ProductHandlersTests.cs create mode 100644 dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/Usings.cs diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.sln b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.sln new file mode 100644 index 0000000000..f28df052d0 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CqrsAndMediatRInAspNetCore", "CqrsAndMediatRInAspNetCore\CqrsAndMediatRInAspNetCore.csproj", "{A35A7661-54DB-466D-BFB1-F743CB102862}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CqrsAndMediatRInAspNetCoreTests", "Tests\CqrsAndMediatRInAspNetCoreTests\CqrsAndMediatRInAspNetCoreTests.csproj", "{1AAD829F-350B-49F1-B9CC-9451038A08CF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A35A7661-54DB-466D-BFB1-F743CB102862}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Debug|x64.ActiveCfg = Debug|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Debug|x64.Build.0 = Debug|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Debug|x86.ActiveCfg = Debug|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Debug|x86.Build.0 = Debug|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Release|Any CPU.Build.0 = Release|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Release|x64.ActiveCfg = Release|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Release|x64.Build.0 = Release|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Release|x86.ActiveCfg = Release|Any CPU + {A35A7661-54DB-466D-BFB1-F743CB102862}.Release|x86.Build.0 = Release|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Debug|x64.Build.0 = Debug|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Debug|x86.Build.0 = Debug|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Release|Any CPU.Build.0 = Release|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Release|x64.ActiveCfg = Release|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Release|x64.Build.0 = Release|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Release|x86.ActiveCfg = Release|Any CPU + {1AAD829F-350B-49F1-B9CC-9451038A08CF}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1AAD829F-350B-49F1-B9CC-9451038A08CF} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + EndGlobalSection +EndGlobal diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Behaviors/LoggingBehavior.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Behaviors/LoggingBehavior.cs new file mode 100644 index 0000000000..6aa23cd8a8 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Behaviors/LoggingBehavior.cs @@ -0,0 +1,25 @@ +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Behaviors +{ + public class LoggingBehavior : IPipelineBehavior + where TRequest : IRequest + { + private readonly ILogger> _logger; + + public LoggingBehavior(ILogger> logger) + => _logger = logger; + + public async Task Handle(TRequest request, RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + _logger.LogInformation($"Handling {typeof(TRequest).Name}"); + + var response = await next(); + + _logger.LogInformation($"Handled {typeof(TResponse).Name}"); + + return response; + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Commands/AddProductCommand.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Commands/AddProductCommand.cs new file mode 100644 index 0000000000..ee5642641c --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Commands/AddProductCommand.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Commands +{ + public record AddProductCommand(Product Product) : IRequest; +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Controllers/ProductsController.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Controllers/ProductsController.cs new file mode 100644 index 0000000000..e589bc1c5b --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Controllers/ProductsController.cs @@ -0,0 +1,43 @@ +using CqrsAndMediatRInAspNetCore.Commands; +using CqrsAndMediatRInAspNetCore.Notifications; +using CqrsAndMediatRInAspNetCore.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace CqrsAndMediatRInAspNetCore.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ProductsController : ControllerBase + { + private readonly IMediator _mediator; + + public ProductsController(IMediator mediator) => _mediator = mediator; + + [HttpGet] + public async Task GetProducts() + { + var products = await _mediator.Send(new GetProductsQuery()); + + return Ok(products); + } + + [HttpGet("{id:int}", Name = "GetProductById")] + public async Task GetProductById(int id) + { + var product = await _mediator.Send(new GetProductByIdQuery(id)); + + return Ok(product); + } + + [HttpPost] + public async Task AddProduct([FromBody] Product product) + { + var productToReturn = await _mediator.Send(new AddProductCommand(product)); + + await _mediator.Publish(new ProductAddedNotification(productToReturn)); + + return CreatedAtRoute("GetProductById", new { id = productToReturn.Id }, productToReturn); + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.csproj b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.csproj new file mode 100644 index 0000000000..5f6ce769d1 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/DataStore/FakeDataStore.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/DataStore/FakeDataStore.cs new file mode 100644 index 0000000000..fa66254dfb --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/DataStore/FakeDataStore.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CqrsAndMediatRInAspNetCore.DataStore +{ + public class FakeDataStore + { + private static List _products; + + public FakeDataStore() + { + _products = new List + { + new Product { Id = 1, Name = "Test Product 1" }, + new Product { Id = 2, Name = "Test Product 2" }, + new Product { Id = 3, Name = "Test Product 3" } + }; + } + + public async Task AddProduct(Product product) + { + _products.Add(product); + await Task.CompletedTask; + } + + public async Task> GetAllProducts() => await Task.FromResult(_products); + + public async Task GetProductById(int id) => + await Task.FromResult(_products.Single(p => p.Id == id)); + + public async Task EventOccured(Product product, string evt) + { + _products.Single(p => p.Id == product.Id).Name = $"{product.Name} evt: {evt}"; + await Task.CompletedTask; + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/AddProductHandler.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/AddProductHandler.cs new file mode 100644 index 0000000000..8a13dd60f2 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/AddProductHandler.cs @@ -0,0 +1,20 @@ +using CqrsAndMediatRInAspNetCore.Commands; +using CqrsAndMediatRInAspNetCore.DataStore; +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Handlers +{ + public class AddProductHandler : IRequestHandler + { + private readonly FakeDataStore _fakeDataStore; + + public AddProductHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore; + + public async Task Handle(AddProductCommand request, CancellationToken cancellationToken) + { + await _fakeDataStore.AddProduct(request.Product); + + return request.Product; + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/CacheInvalidationHandler.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/CacheInvalidationHandler.cs new file mode 100644 index 0000000000..bd5262f81e --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/CacheInvalidationHandler.cs @@ -0,0 +1,24 @@ +using CqrsAndMediatRInAspNetCore.DataStore; +using CqrsAndMediatRInAspNetCore.Notifications; +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace CqrsAndMediatRInAspNetCore.Handlers +{ + public class CacheInvalidationHandler : INotificationHandler + { + private readonly FakeDataStore _fakeDataStore; + + public CacheInvalidationHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore; + + public async Task Handle(ProductAddedNotification notification, CancellationToken cancellationToken) + { + await _fakeDataStore.EventOccured(notification.Product, "Cache Invalidated"); + await Task.CompletedTask; + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/EmailHandler.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/EmailHandler.cs new file mode 100644 index 0000000000..3dfbbf4b0c --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/EmailHandler.cs @@ -0,0 +1,24 @@ +using CqrsAndMediatRInAspNetCore.DataStore; +using CqrsAndMediatRInAspNetCore.Notifications; +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace CqrsAndMediatRInAspNetCore.Handlers +{ + public class EmailHandler : INotificationHandler + { + private readonly FakeDataStore _fakeDataStore; + + public EmailHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore; + + public async Task Handle(ProductAddedNotification notification, CancellationToken cancellationToken) + { + await _fakeDataStore.EventOccured(notification.Product, "Email sent"); + await Task.CompletedTask; + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductByIdHandler.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductByIdHandler.cs new file mode 100644 index 0000000000..08fb1077ff --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductByIdHandler.cs @@ -0,0 +1,17 @@ +using CqrsAndMediatRInAspNetCore.DataStore; +using CqrsAndMediatRInAspNetCore.Queries; +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Handlers +{ + public class GetProductByIdHandler : IRequestHandler + { + private readonly FakeDataStore _fakeDataStore; + + public GetProductByIdHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore; + + public async Task Handle(GetProductByIdQuery request, CancellationToken cancellationToken) => + await _fakeDataStore.GetProductById(request.Id); + + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductsHandler.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductsHandler.cs new file mode 100644 index 0000000000..ce989405f0 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Handlers/GetProductsHandler.cs @@ -0,0 +1,16 @@ +using CqrsAndMediatRInAspNetCore.DataStore; +using CqrsAndMediatRInAspNetCore.Queries; +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Handlers +{ + public class GetProductsHandler : IRequestHandler> + { + private readonly FakeDataStore _fakeDataStore; + + public GetProductsHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore; + + public async Task> Handle(GetProductsQuery request, + CancellationToken cancellationToken) => await _fakeDataStore.GetAllProducts(); + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Notifications/ProductAddedNotification.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Notifications/ProductAddedNotification.cs new file mode 100644 index 0000000000..848cfbe2fc --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Notifications/ProductAddedNotification.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Notifications +{ + public record ProductAddedNotification(Product Product) : INotification; +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Product.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Product.cs new file mode 100644 index 0000000000..d33e92360f --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Product.cs @@ -0,0 +1,8 @@ +namespace CqrsAndMediatRInAspNetCore +{ + public class Product + { + public int Id { get; set; } + public string? Name { get; set; } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Program.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Program.cs new file mode 100644 index 0000000000..f590f00bfc --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Program.cs @@ -0,0 +1,25 @@ +using CqrsAndMediatRInAspNetCore.Behaviors; +using CqrsAndMediatRInAspNetCore.DataStore; +using MediatR; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); + +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Properties/launchSettings.json b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000000..9cebb3fd40 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductByIdQuery.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductByIdQuery.cs new file mode 100644 index 0000000000..4a3f3bc9d7 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductByIdQuery.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Queries +{ + public record GetProductByIdQuery(int Id) : IRequest; +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductsQuery.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductsQuery.cs new file mode 100644 index 0000000000..74fafe3e87 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/Queries/GetProductsQuery.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace CqrsAndMediatRInAspNetCore.Queries +{ + public record GetProductsQuery() : IRequest>; +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.Development.json b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.json b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/CqrsAndMediatRInAspNetCore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/CqrsAndMediatRInAspNetCoreTests.csproj b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/CqrsAndMediatRInAspNetCoreTests.csproj new file mode 100644 index 0000000000..1cc12c4d16 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/CqrsAndMediatRInAspNetCoreTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/LoggingBehaviorTests.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/LoggingBehaviorTests.cs new file mode 100644 index 0000000000..7d60c8911b --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/LoggingBehaviorTests.cs @@ -0,0 +1,32 @@ +using CqrsAndMediatRInAspNetCore; +using CqrsAndMediatRInAspNetCore.Behaviors; +using CqrsAndMediatRInAspNetCore.Queries; +using MediatR; +using Microsoft.Extensions.Logging; +using Moq; + +namespace CqrsAndMediatRInAspNetCoreTests; + +public class LoggingBehaviorTests +{ + [Fact] + public async Task WhenHandle_ThenPassesResponseThroughAndLogsBeforeAndAfter() + { + var loggerMock = new Mock>>>(); + var behavior = new LoggingBehavior>(loggerMock.Object); + var expected = new List { new() { Id = 1, Name = "Test Product 1" } }; + RequestHandlerDelegate> next = _ => Task.FromResult>(expected); + + var result = await behavior.Handle(new GetProductsQuery(), next, CancellationToken.None); + + Assert.Same(expected, result); + loggerMock.Verify( + l => l.Log( + LogLevel.Information, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Exactly(2)); + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/ProductHandlersTests.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/ProductHandlersTests.cs new file mode 100644 index 0000000000..99e51c2390 --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/ProductHandlersTests.cs @@ -0,0 +1,74 @@ +using CqrsAndMediatRInAspNetCore; +using CqrsAndMediatRInAspNetCore.Commands; +using CqrsAndMediatRInAspNetCore.DataStore; +using CqrsAndMediatRInAspNetCore.Handlers; +using CqrsAndMediatRInAspNetCore.Notifications; +using CqrsAndMediatRInAspNetCore.Queries; + +namespace CqrsAndMediatRInAspNetCoreTests; + +public class ProductHandlersTests +{ + private static FakeDataStore NewSeededStore() => new(); + + [Fact] + public async Task WhenHandleGetProductsQuery_ThenReturnsAllSeededProducts() + { + var handler = new GetProductsHandler(NewSeededStore()); + + var result = await handler.Handle(new GetProductsQuery(), CancellationToken.None); + + Assert.Equal(3, result.Count()); + } + + [Fact] + public async Task WhenHandleGetProductByIdQuery_ThenReturnsMatchingProduct() + { + var handler = new GetProductByIdHandler(NewSeededStore()); + + var result = await handler.Handle(new GetProductByIdQuery(2), CancellationToken.None); + + Assert.Equal(2, result.Id); + Assert.Equal("Test Product 2", result.Name); + } + + [Fact] + public async Task WhenHandleAddProductCommand_ThenProductIsPersistedAndReturned() + { + var store = NewSeededStore(); + var addHandler = new AddProductHandler(store); + var newProduct = new Product { Id = 4, Name = "Test Product 4" }; + + var returned = await addHandler.Handle(new AddProductCommand(newProduct), CancellationToken.None); + + Assert.Equal(newProduct, returned); + var all = await new GetProductsHandler(store).Handle(new GetProductsQuery(), CancellationToken.None); + Assert.Contains(all, p => p.Id == 4 && p.Name == "Test Product 4"); + } + + [Fact] + public async Task WhenEmailHandlerHandlesNotification_ThenProductRecordsTheEvent() + { + var store = NewSeededStore(); + var handler = new EmailHandler(store); + var product = new Product { Id = 1, Name = "Test Product 1" }; + + await handler.Handle(new ProductAddedNotification(product), CancellationToken.None); + + var stored = await store.GetProductById(1); + Assert.Equal("Test Product 1 evt: Email sent", stored.Name); + } + + [Fact] + public async Task WhenCacheInvalidationHandlerHandlesNotification_ThenProductRecordsTheEvent() + { + var store = NewSeededStore(); + var handler = new CacheInvalidationHandler(store); + var product = new Product { Id = 3, Name = "Test Product 3" }; + + await handler.Handle(new ProductAddedNotification(product), CancellationToken.None); + + var stored = await store.GetProductById(3); + Assert.Equal("Test Product 3 evt: Cache Invalidated", stored.Name); + } +} diff --git a/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/Usings.cs b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/Usings.cs new file mode 100644 index 0000000000..c802f4480b --- /dev/null +++ b/dotnet-client-libraries/CqrsAndMediatRInAspNetCore/Tests/CqrsAndMediatRInAspNetCoreTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit;