├── .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