├── .github └── workflows │ └── dotnetcore.yml ├── .gitignore ├── App └── Todo │ └── CQRS.App.Todo.WebApi │ ├── CQRS.App.Todo.WebApi.csproj │ ├── Controllers │ └── Items │ │ ├── ItemsGetController.cs │ │ └── ItemsPostController.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── CQRS.sln ├── README.md ├── Src └── CQRS.Todo │ ├── CQRS.Todo.csproj │ ├── Items │ ├── Application │ │ ├── Create │ │ │ ├── CreateItemCommand.cs │ │ │ └── CreateItemCommandHandler.cs │ │ ├── Find │ │ │ ├── FindItemQuery.cs │ │ │ └── FindItemQueryHandler.cs │ │ └── ItemResponse.cs │ ├── Domain │ │ ├── Item.cs │ │ └── ItemRepository.cs │ └── Infrastructure │ │ └── InMemoryItemRepository.cs │ └── Shared │ ├── CommandServiceExtension.cs │ ├── Domain │ └── Bus │ │ ├── Commands │ │ ├── Command.cs │ │ ├── CommandBus.cs │ │ ├── CommandHandler.cs │ │ └── CommandNotRegisteredError.cs │ │ └── Queries │ │ ├── Query.cs │ │ ├── QueryBus.cs │ │ ├── QueryHandler.cs │ │ └── QueryNotRegisteredError.cs │ ├── Infrastructure │ └── Bus │ │ ├── Commands │ │ ├── CommandHandlerWrapper.cs │ │ └── InMemoryCommandBus.cs │ │ └── Queries │ │ ├── InMemoryQueryBus.cs │ │ └── QueryHandlerWrapper.cs │ └── QueryServiceExtension.cs └── Test └── Src └── CQRS.Test.Src.Todo ├── Application └── Items │ ├── Create │ └── CreateItemCommandHandlerShould.cs │ ├── Find │ └── FindItemQueryHandlerShould.cs │ └── ItemModuleUnitCase.cs └── CQRS.Test.Src.Todo.csproj /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ develop, master ] 6 | pull_request: 7 | branches: [ develop, master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: '6.0.x' 20 | - name: Install dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --configuration Release --no-restore 24 | 25 | test: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Setup .NET 30 | uses: actions/setup-dotnet@v1 31 | with: 32 | dotnet-version: '6.0.x' 33 | - name: run tests 34 | run: dotnet test CQRS.sln -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/CQRS.App.Todo.WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | CQRS.App.WebApi 6 | CQRS.App.WebApi 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/Controllers/Items/ItemsGetController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CQRS.Todo.Items.Application; 4 | using CQRS.Todo.Items.Application.Find; 5 | using CQRS.Todo.Shared.Domain.Bus.Queries; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace CQRS.App.WebApi.Controllers.Items; 9 | 10 | [Route("Items")] 11 | public class ItemsGetController : Controller 12 | { 13 | private readonly QueryBus _bus; 14 | 15 | public ItemsGetController(QueryBus bus) 16 | { 17 | _bus = bus; 18 | } 19 | 20 | [HttpGet("{id}")] 21 | public async Task Index(string id) 22 | { 23 | var itemResponse = await _bus.Send(new FindItemQuery(new Guid(id))); 24 | 25 | return Ok(itemResponse); 26 | } 27 | } -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/Controllers/Items/ItemsPostController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CQRS.Todo.Items.Application.Create; 4 | using CQRS.Todo.Shared.Domain.Bus.Commands; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Newtonsoft.Json; 7 | 8 | namespace CQRS.App.WebApi.Controllers.Items; 9 | 10 | [Route("Items")] 11 | public class ItemsPostController : Controller 12 | { 13 | private readonly CommandBus _bus; 14 | 15 | public ItemsPostController(CommandBus bus) 16 | { 17 | _bus = bus; 18 | } 19 | 20 | [HttpPost("{id}")] 21 | public async Task Index(string id, [FromBody] dynamic body) 22 | { 23 | body = JsonConvert.DeserializeObject(Convert.ToString(body)); 24 | 25 | if (body == null) 26 | return BadRequest("body is empty"); 27 | 28 | await _bus.Dispatch(new CreateItemCommand(new Guid(id), body["name"].ToString())); 29 | 30 | return StatusCode(201); 31 | } 32 | } -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace CQRS.App.WebApi; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 16 | } -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:4190", 7 | "sslPort": 44369 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": false, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WebApi": { 19 | "commandName": "Project", 20 | "launchBrowser": false, 21 | "applicationUrl": "https://localhost:5001/;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using CQRS.Todo.Items.Domain; 2 | using CQRS.Todo.Items.Infrastructure; 3 | using CQRS.Todo.Shared; 4 | using CQRS.Todo.Shared.Domain.Bus.Commands; 5 | using CQRS.Todo.Shared.Domain.Bus.Queries; 6 | using CQRS.Todo.Shared.Infrastructure.Bus.Commands; 7 | using CQRS.Todo.Shared.Infrastructure.Bus.Queries; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | 13 | namespace CQRS.App.WebApi; 14 | 15 | public class Startup 16 | { 17 | // This method gets called by the runtime. Use this method to add services to the container. 18 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddControllersWithViews(); 22 | 23 | services.AddCommandServices(typeof(Command).Assembly); 24 | services.AddScoped(); 25 | 26 | services.AddQueryServices(typeof(Query).Assembly); 27 | services.AddScoped(); 28 | 29 | services.AddSingleton(); 30 | } 31 | 32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 33 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 34 | { 35 | if (env.IsDevelopment()) 36 | { 37 | app.UseDeveloperExceptionPage(); 38 | } 39 | 40 | app.UseRouting(); 41 | 42 | app.UseEndpoints(endpoints => 43 | { 44 | endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); 45 | }); 46 | } 47 | } -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /App/Todo/CQRS.App.Todo.WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /CQRS.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRS.App.Todo.WebApi", "App\Todo\CQRS.App.Todo.WebApi\CQRS.App.Todo.WebApi.csproj", "{159A8940-1E7A-49D3-9B17-F06F716F6420}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{409A8894-466A-4505-9B52-828E2F097B66}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{8B0C8C36-C79D-446F-A237-C9CF07C10AB3}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "App", "App", "{EED4D9B3-EFBA-4D90-AA2D-73AA82F432F5}" 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Todo", "Todo", "{9DA33454-C2E2-42DC-8E88-209EB238FD8A}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRS.Todo", "Src\CQRS.Todo\CQRS.Todo.csproj", "{D7C475BB-DEBB-4292-84E5-5AF26BEA7071}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{CD734F6B-61FB-4C3E-98C9-2FC7333D3492}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRS.Test.Src.Todo", "Test\Src\CQRS.Test.Src.Todo\CQRS.Test.Src.Todo.csproj", "{AA7EB481-3699-43C8-8223-472EDD05E864}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {159A8940-1E7A-49D3-9B17-F06F716F6420}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {159A8940-1E7A-49D3-9B17-F06F716F6420}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {159A8940-1E7A-49D3-9B17-F06F716F6420}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {159A8940-1E7A-49D3-9B17-F06F716F6420}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {D7C475BB-DEBB-4292-84E5-5AF26BEA7071}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {D7C475BB-DEBB-4292-84E5-5AF26BEA7071}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {D7C475BB-DEBB-4292-84E5-5AF26BEA7071}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {D7C475BB-DEBB-4292-84E5-5AF26BEA7071}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {AA7EB481-3699-43C8-8223-472EDD05E864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {AA7EB481-3699-43C8-8223-472EDD05E864}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {AA7EB481-3699-43C8-8223-472EDD05E864}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {AA7EB481-3699-43C8-8223-472EDD05E864}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(NestedProjects) = preSolution 39 | {9DA33454-C2E2-42DC-8E88-209EB238FD8A} = {EED4D9B3-EFBA-4D90-AA2D-73AA82F432F5} 40 | {D7C475BB-DEBB-4292-84E5-5AF26BEA7071} = {409A8894-466A-4505-9B52-828E2F097B66} 41 | {159A8940-1E7A-49D3-9B17-F06F716F6420} = {9DA33454-C2E2-42DC-8E88-209EB238FD8A} 42 | {CD734F6B-61FB-4C3E-98C9-2FC7333D3492} = {8B0C8C36-C79D-446F-A237-C9CF07C10AB3} 43 | {AA7EB481-3699-43C8-8223-472EDD05E864} = {CD734F6B-61FB-4C3E-98C9-2FC7333D3492} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-cqrs 2 | 3 | This project shows a clean way to use CQRS without using the MediatR library. 4 | 5 | In C# is common to use a library named [MediatR](https://github.com/jbogard/MediatR) to implement CQRS. This is an amazing library but forces you to implement the interface INotification, INotificationHandler and IRequestHandler in your domain/application layer coupling this with an infrastructure library. 6 | This is a different approach to avoid add this coupling. 7 | 8 | ## Commands 9 | 10 | ### A command example 11 | 12 | ```csharp 13 | public class CreateItemCommand : Command 14 | { 15 | public CreateItemCommand(Guid id, string name) 16 | { 17 | Id = id; 18 | Name = name; 19 | } 20 | } 21 | ``` 22 | 23 | ### A handler example 24 | 25 | ```csharp 26 | public class CreateItemCommandHandler : CommandHandler 27 | { 28 | private readonly ItemRepository _repository; 29 | 30 | public CreateItemCommandHandler(ItemRepository repository) 31 | { 32 | _repository = repository; 33 | } 34 | 35 | public async Task Handle(CreateItemCommand command) 36 | { 37 | await _repository.Add(new Item(command.Id, command.Name)); 38 | } 39 | } 40 | ``` 41 | 42 | ### Interfaces to add in Domain Layer 43 | 44 | [Command Interfaces](https://github.com/Leanwit/dotnet-cqrs/tree/master/Src/CQRS.Shared/Domain/Bus/Command) 45 | 46 | ## Queries 47 | 48 | ### A query example: 49 | 50 | ```csharp 51 | public class FindItemQuery : Query 52 | { 53 | public Guid Id { get; private set; } 54 | 55 | public FindItemQuery(Guid id) 56 | { 57 | Id = id; 58 | } 59 | } 60 | ``` 61 | 62 | ### A handler example 63 | 64 | ```csharp 65 | public class FindItemQueryHandler : QueryHandler 66 | { 67 | private readonly ItemRepository _repository; 68 | 69 | public FindItemQueryHandler(ItemRepository repository) 70 | { 71 | _repository = repository; 72 | } 73 | 74 | public async Task Handle(FindItemQuery query) 75 | { 76 | Item item = await _repository.GetById(query.Id); 77 | 78 | return new ItemResponse(item.Id, item.Name, item.IsCompleted); 79 | } 80 | } 81 | ``` 82 | 83 | ### Interfaces to add in Domain Layer 84 | 85 | [Query Interfaces](https://github.com/Leanwit/dotnet-cqrs/tree/master/Src/CQRS.Shared/Domain/Bus/Query) 86 | 87 | ## InMemoryBus implementation 88 | 89 | [InMemoryCommandBus](https://github.com/Leanwit/dotnet-cqrs/blob/master/Src/CQRS.Shared/Infrastructure/Bus/Command/InMemoryCommandBus.cs) 90 | 91 | [InMemoryQueryBus](https://github.com/Leanwit/dotnet-cqrs/blob/master/Src/CQRS.Shared/Infrastructure/Bus/Query/InMemoryQueryBus.cs) 92 | 93 | ## Dependency Injection 94 | 95 | ### Command 96 | 97 | ```csharp 98 | services.AddScoped, CreateItemCommandHandler>(); 99 | ``` 100 | 101 | ### Query 102 | 103 | ```csharp 104 | services.AddScoped, FindItemQueryHandler>(); 105 | ``` 106 | 107 | ### Automatic Load 108 | 109 | ```csharp 110 | services.AddCommandServices(typeof(Command).Assembly); 111 | services.AddQueryServices(typeof(Query).Assembly); 112 | ``` -------------------------------------------------------------------------------- /Src/CQRS.Todo/CQRS.Todo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | CQRS.Todo 6 | CQRS.Todo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Application/Create/CreateItemCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CQRS.Todo.Shared.Domain.Bus.Commands; 3 | 4 | namespace CQRS.Todo.Items.Application.Create; 5 | 6 | public class CreateItemCommand : Command 7 | { 8 | public Guid Id { get;} 9 | public string Name { get;} 10 | 11 | public CreateItemCommand(Guid id, string name) 12 | { 13 | Id = id; 14 | Name = name; 15 | } 16 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Application/Create/CreateItemCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CQRS.Todo.Items.Domain; 3 | using CQRS.Todo.Shared.Domain.Bus.Commands; 4 | 5 | namespace CQRS.Todo.Items.Application.Create; 6 | public class CreateItemCommandHandler : CommandHandler 7 | { 8 | private readonly ItemRepository _repository; 9 | 10 | public CreateItemCommandHandler(ItemRepository repository) 11 | { 12 | _repository = repository; 13 | } 14 | 15 | public async Task Handle(CreateItemCommand command) 16 | { 17 | await _repository.Add(new Item(command.Id, command.Name)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Application/Find/FindItemQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CQRS.Todo.Shared.Domain.Bus.Queries; 3 | 4 | namespace CQRS.Todo.Items.Application.Find; 5 | 6 | public class FindItemQuery : Query 7 | { 8 | public Guid Id { get; } 9 | 10 | public FindItemQuery(Guid id) 11 | { 12 | Id = id; 13 | } 14 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Application/Find/FindItemQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CQRS.Todo.Items.Domain; 3 | using CQRS.Todo.Shared.Domain.Bus.Queries; 4 | 5 | namespace CQRS.Todo.Items.Application.Find; 6 | 7 | public class FindItemQueryHandler : QueryHandler 8 | { 9 | private readonly ItemRepository _repository; 10 | 11 | public FindItemQueryHandler(ItemRepository repository) 12 | { 13 | _repository = repository; 14 | } 15 | 16 | public async Task Handle(FindItemQuery query) 17 | { 18 | Item item = await _repository.GetById(query.Id); 19 | 20 | return new ItemResponse(item.Id, item.Name, item.IsCompleted); 21 | } 22 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Application/ItemResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CQRS.Todo.Items.Application; 4 | 5 | public class ItemResponse 6 | { 7 | public Guid Id { get; } 8 | public string Name { get; } 9 | public bool IsCompleted { get; } 10 | 11 | public ItemResponse(Guid id, string name, bool isCompleted) 12 | { 13 | Id = id; 14 | Name = name; 15 | IsCompleted = isCompleted; 16 | } 17 | 18 | public override bool Equals(object obj) 19 | { 20 | if (this == obj) return true; 21 | 22 | var item = obj as ItemResponse; 23 | if (item == null) return false; 24 | 25 | return Id == item.Id && Name == item.Name && IsCompleted == item.IsCompleted; 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | return HashCode.Combine(Id, Name, IsCompleted); 31 | } 32 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Domain/Item.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CQRS.Todo.Items.Domain; 4 | 5 | public class Item 6 | { 7 | public Guid Id { get; } 8 | public string Name { get; } 9 | 10 | public bool IsCompleted { get;} 11 | 12 | public Item(Guid id, string name) 13 | { 14 | Id = id; 15 | Name = name; 16 | IsCompleted = false; 17 | } 18 | 19 | public override bool Equals(object obj) 20 | { 21 | if (this == obj) return true; 22 | 23 | var item = obj as Item; 24 | if (item == null) return false; 25 | 26 | return Id == item.Id && Name == item.Name && IsCompleted == item.IsCompleted; 27 | } 28 | 29 | public override int GetHashCode() 30 | { 31 | return HashCode.Combine(Id, Name, IsCompleted); 32 | } 33 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Domain/ItemRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace CQRS.Todo.Items.Domain; 5 | 6 | public interface ItemRepository 7 | { 8 | Task GetById(Guid id); 9 | Task Add(Item item); 10 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Items/Infrastructure/InMemoryItemRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CQRS.Todo.Items.Domain; 6 | 7 | namespace CQRS.Todo.Items.Infrastructure; 8 | 9 | public class InMemoryItemRepository : ItemRepository 10 | { 11 | private readonly List _context; 12 | 13 | public InMemoryItemRepository() 14 | { 15 | _context = new List(); 16 | } 17 | 18 | public Task GetById(Guid id) 19 | { 20 | return Task.FromResult(_context.FirstOrDefault(x => x.Id.Equals(id))); 21 | } 22 | 23 | public Task Add(Item item) 24 | { 25 | _context.Add(item); 26 | return Task.CompletedTask; 27 | } 28 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/CommandServiceExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using CQRS.Todo.Shared.Domain.Bus.Commands; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace CQRS.Todo.Shared; 7 | 8 | public static class CommandServiceExtension 9 | { 10 | public static IServiceCollection AddCommandServices(this IServiceCollection services, 11 | Assembly assembly) 12 | { 13 | var classTypes = assembly.ExportedTypes.Select(t => t.GetTypeInfo()).Where(t => t.IsClass && !t.IsAbstract); 14 | 15 | foreach (var type in classTypes) 16 | { 17 | var interfaces = type.ImplementedInterfaces.Select(i => i.GetTypeInfo()); 18 | 19 | foreach (var handlerInterfaceType in interfaces.Where(i => 20 | i.IsGenericType && i.GetGenericTypeDefinition() == typeof(CommandHandler<>))) 21 | { 22 | services.AddScoped(handlerInterfaceType.AsType(), type.AsType()); 23 | } 24 | } 25 | 26 | return services; 27 | } 28 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Commands/Command.cs: -------------------------------------------------------------------------------- 1 | namespace CQRS.Todo.Shared.Domain.Bus.Commands; 2 | 3 | public abstract class Command 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Commands/CommandBus.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CQRS.Todo.Shared.Domain.Bus.Commands; 4 | 5 | public interface CommandBus 6 | { 7 | Task Dispatch(Command command); 8 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Commands/CommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CQRS.Todo.Shared.Domain.Bus.Commands; 4 | 5 | public interface CommandHandler where TCommand : Command 6 | { 7 | Task Handle(TCommand domainEvent); 8 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Commands/CommandNotRegisteredError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CQRS.Todo.Shared.Domain.Bus.Commands; 4 | 5 | public class CommandNotRegisteredError : Exception 6 | { 7 | public CommandNotRegisteredError(Command command) : base( 8 | $"The command {command} has not a command handler associated") 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Queries/Query.cs: -------------------------------------------------------------------------------- 1 | namespace CQRS.Todo.Shared.Domain.Bus.Queries; 2 | 3 | public abstract class Query 4 | { 5 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Queries/QueryBus.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CQRS.Todo.Shared.Domain.Bus.Queries; 4 | 5 | public interface QueryBus 6 | { 7 | Task Send(Query request); 8 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Queries/QueryHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CQRS.Todo.Shared.Domain.Bus.Queries; 4 | 5 | public interface QueryHandler where TQuery : Query 6 | { 7 | Task Handle(TQuery query); 8 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Domain/Bus/Queries/QueryNotRegisteredError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CQRS.Todo.Shared.Domain.Bus.Queries; 4 | 5 | public class QueryNotRegisteredError : Exception 6 | { 7 | public QueryNotRegisteredError(Query query) : base( 8 | $"The query {query} has not a query handler associated") 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Infrastructure/Bus/Commands/CommandHandlerWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CQRS.Todo.Shared.Domain.Bus.Commands; 4 | 5 | namespace CQRS.Todo.Shared.Infrastructure.Bus.Commands; 6 | 7 | internal abstract class CommandHandlerWrapper 8 | { 9 | public abstract Task Handle(Command command, IServiceProvider provider); 10 | } 11 | 12 | internal class CommandHandlerWrapper : CommandHandlerWrapper 13 | where TCommand : Command 14 | { 15 | public override async Task Handle(Command domainEvent, IServiceProvider provider) 16 | { 17 | var handler = (CommandHandler) provider.GetService(typeof(CommandHandler)); 18 | await handler.Handle((TCommand) domainEvent); 19 | } 20 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Infrastructure/Bus/Commands/InMemoryCommandBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using CQRS.Todo.Shared.Domain.Bus.Commands; 8 | 9 | namespace CQRS.Todo.Shared.Infrastructure.Bus.Commands; 10 | 11 | public class InMemoryCommandBus : CommandBus 12 | { 13 | private readonly IServiceProvider _provider; 14 | private static readonly ConcurrentDictionary> _commandHandlers = new(); 15 | 16 | public InMemoryCommandBus(IServiceProvider provider) 17 | { 18 | _provider = provider; 19 | } 20 | 21 | public async Task Dispatch(Command command) 22 | { 23 | var wrappedHandlers = GetWrappedHandlers(command); 24 | 25 | if(wrappedHandlers == null) throw new CommandNotRegisteredError(command); 26 | 27 | foreach (CommandHandlerWrapper handler in wrappedHandlers) 28 | { 29 | await handler.Handle(command, _provider); 30 | } 31 | } 32 | 33 | private IEnumerable GetWrappedHandlers(Command command) 34 | { 35 | Type handlerType = typeof(CommandHandler<>).MakeGenericType(command.GetType()); 36 | Type wrapperType = typeof(CommandHandlerWrapper<>).MakeGenericType(command.GetType()); 37 | 38 | IEnumerable handlers = 39 | (IEnumerable) _provider.GetService(typeof(IEnumerable<>).MakeGenericType(handlerType)); 40 | 41 | var wrappedHandlers = _commandHandlers.GetOrAdd(command.GetType(), handlers.Cast() 42 | .Select(_ => (CommandHandlerWrapper) Activator.CreateInstance(wrapperType))); 43 | 44 | return wrappedHandlers; 45 | } 46 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Infrastructure/Bus/Queries/InMemoryQueryBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using CQRS.Todo.Shared.Domain.Bus.Queries; 8 | 9 | namespace CQRS.Todo.Shared.Infrastructure.Bus.Queries; 10 | 11 | public class InMemoryQueryBus : QueryBus 12 | { 13 | private readonly IServiceProvider _provider; 14 | private static readonly ConcurrentDictionary _queryHandlers = new(); 15 | 16 | public InMemoryQueryBus(IServiceProvider provider) 17 | { 18 | _provider = provider; 19 | } 20 | 21 | public async Task Send(Query query) 22 | { 23 | var handler = GetWrappedHandlers(query); 24 | 25 | if(handler == null) throw new QueryNotRegisteredError(query); 26 | 27 | return await handler.Handle(query, _provider); 28 | } 29 | 30 | private QueryHandlerWrapper GetWrappedHandlers(Query query) 31 | { 32 | Type[] typeArgs = {query.GetType(), typeof(TResponse)}; 33 | 34 | var handlerType = typeof(QueryHandler<,>).MakeGenericType(typeArgs); 35 | Type wrapperType = typeof(QueryHandlerWrapper<,>).MakeGenericType(typeArgs); 36 | 37 | IEnumerable handlers = 38 | (IEnumerable) _provider.GetService(typeof(IEnumerable<>).MakeGenericType(handlerType)); 39 | 40 | 41 | var wrappedHandlers = (QueryHandlerWrapper)_queryHandlers.GetOrAdd(query.GetType(), handlers.Cast() 42 | .Select(_ => (QueryHandlerWrapper) Activator.CreateInstance(wrapperType)).FirstOrDefault()); 43 | 44 | return wrappedHandlers; 45 | } 46 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/Infrastructure/Bus/Queries/QueryHandlerWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CQRS.Todo.Shared.Domain.Bus.Queries; 4 | 5 | namespace CQRS.Todo.Shared.Infrastructure.Bus.Queries; 6 | 7 | internal abstract class QueryHandlerWrapper 8 | { 9 | public abstract Task Handle(Query query, IServiceProvider provider); 10 | } 11 | 12 | internal class QueryHandlerWrapper : QueryHandlerWrapper 13 | where TQuery : Query 14 | { 15 | public override async Task Handle(Query query, IServiceProvider provider) 16 | { 17 | var handler = (QueryHandler)provider.GetService(typeof(QueryHandler)); 18 | 19 | if (handler == null) 20 | throw new NullReferenceException($"{nameof(QueryHandlerWrapper)} Handler not found"); 21 | 22 | return await handler.Handle((TQuery)query); 23 | } 24 | } -------------------------------------------------------------------------------- /Src/CQRS.Todo/Shared/QueryServiceExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using CQRS.Todo.Shared.Domain.Bus.Queries; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace CQRS.Todo.Shared; 7 | 8 | public static class QueryServiceExtension 9 | { 10 | public static IServiceCollection AddQueryServices(this IServiceCollection services, 11 | Assembly assembly) 12 | { 13 | var classTypes = assembly.ExportedTypes.Select(t => t.GetTypeInfo()).Where(t => t.IsClass && !t.IsAbstract); 14 | 15 | foreach (var type in classTypes) 16 | { 17 | var interfaces = type.ImplementedInterfaces.Select(i => i.GetTypeInfo()); 18 | 19 | foreach (var handlerInterfaceType in interfaces.Where(i => 20 | i.IsGenericType && i.GetGenericTypeDefinition() == typeof(QueryHandler<,>))) 21 | { 22 | services.AddScoped(handlerInterfaceType.AsType(), type.AsType()); 23 | } 24 | } 25 | 26 | return services; 27 | } 28 | } -------------------------------------------------------------------------------- /Test/Src/CQRS.Test.Src.Todo/Application/Items/Create/CreateItemCommandHandlerShould.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CQRS.Todo.Items.Application.Create; 3 | using Xunit; 4 | 5 | namespace CQRS.Test.Src.Todo.Application.Items.Create; 6 | 7 | public class CreateItemCommandHandlerShould : ItemModuleUnitCase 8 | { 9 | private readonly CreateItemCommandHandler _handler; 10 | 11 | public CreateItemCommandHandlerShould() 12 | { 13 | _handler = new CreateItemCommandHandler(Repository.Object); 14 | } 15 | 16 | [Fact] 17 | public void Create_a_valid_item() 18 | { 19 | var id = Guid.NewGuid(); 20 | var name = "Create a new task"; 21 | 22 | var item = new CQRS.Todo.Items.Domain.Item(id, name); 23 | var command = new CreateItemCommand(id, name); 24 | 25 | _handler.Handle(command); 26 | 27 | ShouldHaveSave(item); 28 | } 29 | } -------------------------------------------------------------------------------- /Test/Src/CQRS.Test.Src.Todo/Application/Items/Find/FindItemQueryHandlerShould.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CQRS.Todo.Items.Application; 4 | using CQRS.Todo.Items.Application.Find; 5 | using CQRS.Todo.Items.Domain; 6 | using Xunit; 7 | 8 | namespace CQRS.Test.Src.Todo.Application.Items.Find; 9 | 10 | public class FindItemQueryHandlerShould : ItemModuleUnitCase 11 | { 12 | private readonly FindItemQueryHandler _handler; 13 | 14 | public FindItemQueryHandlerShould() 15 | { 16 | _handler = new FindItemQueryHandler(Repository.Object); 17 | } 18 | 19 | [Fact] 20 | public async Task It_should_find_an_existing_item() 21 | { 22 | var id = Guid.NewGuid(); 23 | var name = "Create a new task"; 24 | 25 | var item = new Item(id, name); 26 | var itemResponse = new ItemResponse(id, name, false); 27 | var query = new FindItemQuery(id); 28 | 29 | ShouldSearch(item); 30 | Assert.Equal(itemResponse, await _handler.Handle(query)); 31 | } 32 | } -------------------------------------------------------------------------------- /Test/Src/CQRS.Test.Src.Todo/Application/Items/ItemModuleUnitCase.cs: -------------------------------------------------------------------------------- 1 | using CQRS.Todo.Items.Domain; 2 | using Moq; 3 | 4 | namespace CQRS.Test.Src.Todo.Application.Items; 5 | 6 | public class ItemModuleUnitCase 7 | { 8 | protected readonly Mock Repository; 9 | 10 | public ItemModuleUnitCase() 11 | { 12 | Repository = new Mock(); 13 | } 14 | 15 | protected void ShouldHaveSave(Item item) 16 | { 17 | Repository.Verify(x => x.Add(item), Times.AtLeastOnce()); 18 | } 19 | 20 | protected void ShouldSearch(Item response) 21 | { 22 | Repository.Setup(x => x.GetById(response.Id)).ReturnsAsync(response); 23 | } 24 | } -------------------------------------------------------------------------------- /Test/Src/CQRS.Test.Src.Todo/CQRS.Test.Src.Todo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | --------------------------------------------------------------------------------