Skip to content

Commit f12accf

Browse files
authored
Jsmith/commands (#1)
Incorporate mediator, command broker.
1 parent 4f49fc8 commit f12accf

File tree

111 files changed

+2955
-1161
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+2955
-1161
lines changed

EventDriven.CQRS.sln

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,31 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AF357BE9-9A6
1212
EndProject
1313
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventDriven.CQRS.Abstractions", "src\EventDriven.CQRS.Abstractions\EventDriven.CQRS.Abstractions.csproj", "{1F665C29-BF1C-45BA-8CB9-829E85C34999}"
1414
EndProject
15-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{269CD137-4093-4100-B33E-808586D335F6}"
16-
EndProject
1715
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "reference-architecture", "reference-architecture", "{C4FD0AF1-927A-4860-A634-7CE342807692}"
1816
EndProject
19-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventDriven.CQRS.Tests", "test\EventDriven.CQRS.Tests\EventDriven.CQRS.Tests.csproj", "{9809006C-2F6B-44A1-8AE2-BC449368D209}"
20-
EndProject
2117
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomerService", "reference-architecture\CustomerService\CustomerService.csproj", "{48983715-E6DF-462F-AF3C-769C1122794F}"
2218
EndProject
2319
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderService", "reference-architecture\OrderService\OrderService.csproj", "{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7}"
2420
EndProject
2521
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "json", "json", "{B11B21E0-7B89-4285-990A-D98793310B02}"
26-
ProjectSection(SolutionItems) = preProject
27-
reference-architecture\json\customers.json = reference-architecture\json\customers.json
28-
reference-architecture\json\orders.json = reference-architecture\json\orders.json
29-
EndProjectSection
22+
ProjectSection(SolutionItems) = preProject
23+
reference-architecture\json\customers.json = reference-architecture\json\customers.json
24+
reference-architecture\json\orders.json = reference-architecture\json\orders.json
25+
EndProjectSection
3026
EndProject
3127
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "reference-architecture\Common\Common.csproj", "{FC04D111-903D-49FF-84A6-8806C71E2168}"
3228
EndProject
3329
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dapr", "dapr", "{F0E48E00-7D72-4614-9C13-90A7B015B06F}"
3430
EndProject
3531
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "components", "components", "{00BA9501-787E-465C-97D0-F51295D97802}"
36-
ProjectSection(SolutionItems) = preProject
37-
reference-architecture\dapr\components\pubsub.yaml = reference-architecture\dapr\components\pubsub.yaml
38-
reference-architecture\dapr\components\snssqs-pubsub.yaml = reference-architecture\dapr\components\snssqs-pubsub.yaml
39-
EndProjectSection
32+
ProjectSection(SolutionItems) = preProject
33+
reference-architecture\dapr\components\pubsub.yaml = reference-architecture\dapr\components\pubsub.yaml
34+
reference-architecture\dapr\components\snssqs-pubsub.yaml = reference-architecture\dapr\components\snssqs-pubsub.yaml
35+
EndProjectSection
36+
EndProject
37+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomerService.Tests", "reference-architecture\CustomerService.Tests\CustomerService.Tests.csproj", "{A2B684E5-7DB5-4815-80E9-16F0EFBE15D8}"
38+
EndProject
39+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderService.Tests", "reference-architecture\OrderService.Tests\OrderService.Tests.csproj", "{A7D3F069-598D-435D-8B33-522767AACB76}"
4040
EndProject
4141
Global
4242
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -48,10 +48,6 @@ Global
4848
{1F665C29-BF1C-45BA-8CB9-829E85C34999}.Debug|Any CPU.Build.0 = Debug|Any CPU
4949
{1F665C29-BF1C-45BA-8CB9-829E85C34999}.Release|Any CPU.ActiveCfg = Release|Any CPU
5050
{1F665C29-BF1C-45BA-8CB9-829E85C34999}.Release|Any CPU.Build.0 = Release|Any CPU
51-
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
52-
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Debug|Any CPU.Build.0 = Debug|Any CPU
53-
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Release|Any CPU.ActiveCfg = Release|Any CPU
54-
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Release|Any CPU.Build.0 = Release|Any CPU
5551
{48983715-E6DF-462F-AF3C-769C1122794F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
5652
{48983715-E6DF-462F-AF3C-769C1122794F}.Debug|Any CPU.Build.0 = Debug|Any CPU
5753
{48983715-E6DF-462F-AF3C-769C1122794F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -64,19 +60,28 @@ Global
6460
{FC04D111-903D-49FF-84A6-8806C71E2168}.Debug|Any CPU.Build.0 = Debug|Any CPU
6561
{FC04D111-903D-49FF-84A6-8806C71E2168}.Release|Any CPU.ActiveCfg = Release|Any CPU
6662
{FC04D111-903D-49FF-84A6-8806C71E2168}.Release|Any CPU.Build.0 = Release|Any CPU
63+
{A2B684E5-7DB5-4815-80E9-16F0EFBE15D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
64+
{A2B684E5-7DB5-4815-80E9-16F0EFBE15D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
65+
{A2B684E5-7DB5-4815-80E9-16F0EFBE15D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
66+
{A2B684E5-7DB5-4815-80E9-16F0EFBE15D8}.Release|Any CPU.Build.0 = Release|Any CPU
67+
{A7D3F069-598D-435D-8B33-522767AACB76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
68+
{A7D3F069-598D-435D-8B33-522767AACB76}.Debug|Any CPU.Build.0 = Debug|Any CPU
69+
{A7D3F069-598D-435D-8B33-522767AACB76}.Release|Any CPU.ActiveCfg = Release|Any CPU
70+
{A7D3F069-598D-435D-8B33-522767AACB76}.Release|Any CPU.Build.0 = Release|Any CPU
6771
EndGlobalSection
6872
GlobalSection(SolutionProperties) = preSolution
6973
HideSolutionNode = FALSE
7074
EndGlobalSection
7175
GlobalSection(NestedProjects) = preSolution
7276
{1F665C29-BF1C-45BA-8CB9-829E85C34999} = {AF357BE9-9A6E-48A3-A995-E75F2147A43F}
73-
{9809006C-2F6B-44A1-8AE2-BC449368D209} = {269CD137-4093-4100-B33E-808586D335F6}
7477
{48983715-E6DF-462F-AF3C-769C1122794F} = {C4FD0AF1-927A-4860-A634-7CE342807692}
7578
{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7} = {C4FD0AF1-927A-4860-A634-7CE342807692}
7679
{B11B21E0-7B89-4285-990A-D98793310B02} = {C4FD0AF1-927A-4860-A634-7CE342807692}
7780
{FC04D111-903D-49FF-84A6-8806C71E2168} = {C4FD0AF1-927A-4860-A634-7CE342807692}
7881
{F0E48E00-7D72-4614-9C13-90A7B015B06F} = {AFFCBFA4-9D64-43AA-AC59-D4CC54BD9C72}
7982
{00BA9501-787E-465C-97D0-F51295D97802} = {F0E48E00-7D72-4614-9C13-90A7B015B06F}
83+
{A2B684E5-7DB5-4815-80E9-16F0EFBE15D8} = {C4FD0AF1-927A-4860-A634-7CE342807692}
84+
{A7D3F069-598D-435D-8B33-522767AACB76} = {C4FD0AF1-927A-4860-A634-7CE342807692}
8085
EndGlobalSection
8186
GlobalSection(ExtensibilityGlobals) = postSolution
8287
SolutionGuid = {427A0D03-63CA-48AE-AA95-D21800101398}

ReadMe.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The **EventDriven.CQRS.Abstractions** library contains interfaces and abstract b
2828
The **Reference Architecture** projects demonstrate how to apply these concepts to two microservices: `CustomerService` and `OrderService`. In addition, each service has *separate controllers for read and write operations*, thus segregating command and query responsibilities, with different sets of models, or Data Transfer Objects (DTO's).
2929
- **Query Controller**: Uses repository to retrieve entities and converts them to DTO's with AutoMapper.
3030
- **Command Controller**: Converts DTO's to domain entities using AutoMapper. Then hands control over to a command handler for executing business logic.
31+
- **Command Broker**: Dispatches the command object to the correct command handler.
3132
- **Command Handler**: Uses a domain entity to process commands which generate one or more domain events, then requests entity to apply the domain events in order to mutate entity state. Persists entity state to a state store and optionally publishes an integration event which is handled by another microservice.
3233
- **Repository**: Persists entity state to a database.
3334
- **Event Bus**: Used to publish integration events, as well as subscribe to events using an event handler. Dapr is used to abstract away the underlying pub/sub implementation. The default is Redis (for local development), but Dapr can be configured to use other components, such as AWS SNS+SQS.
@@ -87,9 +88,9 @@ The **Reference Architecture** projects demonstrate how to apply these concepts
8788
8889
1. Add **Domain** and **CustomerAggregate** folders to the project, then add a `Customer` class that extends `Entity`.
8990
- Add properties representing entity state.
90-
- Create commands that are C# records and extend a `Command` base class.
91+
- Create commands that are C# records and extend an `ICommand` interface with the result type as the generic.
9192
```csharp
92-
public record CreateCustomer(Customer Customer) : Command.Create(Customer.Id);
93+
public record CreateCustomer(Customer Customer) : ICommand<CommandResult<Customer>>;
9394
```
9495
- Create domain events that extend `DomainEvent`.
9596
```csharp
@@ -112,7 +113,7 @@ The **Reference Architecture** projects demonstrate how to apply these concepts
112113
- Inject `ICustomerRepository`, `IEventBus` and `IMapper` into the ctor.
113114
- In the handler for `CreateCustomer`, write code to process the command, apply events, and persist the entity.
114115
```csharp
115-
public async Task<CommandResult<Customer>> Handle(CreateCustomer command)
116+
public async Task<CommandResult<Customer>> Handle(CreateCustomer request, CancellationToken cancellationToken)
116117
{
117118
// Process command
118119
_logger.LogInformation("Handling command: {commandName}", nameof(CreateCustomer));
@@ -137,7 +138,7 @@ The **Reference Architecture** projects demonstrate how to apply these concepts
137138
```
138139
- In the `UpdateCustomer` handler, see if the shipping address has changed, and if so, publish a `CustomerAddressUpdated` integration event, so that the order service can update the shipping address in the customer's orders.
139140
```csharp
140-
public async Task<CommandResult<Customer>> Handle(UpdateCustomer command)
141+
public async Task<CommandResult<Customer>> Handle(UpdateCustomer request, CancellationToken cancellationToken)
141142
{
142143
// Compare shipping addresses
143144
_logger.LogInformation("Handling command: {commandName}", nameof(UpdateCustomer));
@@ -167,7 +168,7 @@ The **Reference Architecture** projects demonstrate how to apply these concepts
167168
}
168169
}
169170
```
170-
3. Add a `CustomerCommandController` to the project that injects `CustomerCommandHandler` into the ctor.
171+
3. Add a `CustomerCommandController` to the project that injects `ICommandBroker` into the ctor.
171172
- Add Post, Put and Delete actions which accept a `Customer` DTO, map it to a `Customer` entity and invoke the appropriate command handler.
172173
4. Add a `CustomerQueryController` to the project that injects a `ICustomerRepository` into the ctor.
173174
- Use the repository to retrieve entities, then map those to `Customer` DTO objects.

global.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"sdk": {
3+
"version": "5.0.100",
4+
"rollForward": "latestFeature"
5+
}
6+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using Common.Integration.Models;
34
using EventDriven.EventBus.Abstractions;
45

56
namespace Common.Integration.Events
67
{
8+
[ExcludeFromCodeCoverage]
79
public record CustomerAddressUpdated(Guid CustomerId, Address ShippingAddress) : IntegrationEvent;
810
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
13
namespace Common.Integration.Models
24
{
5+
[ExcludeFromCodeCoverage]
36
public record Address(string Street, string City, string State, string Country, string PostalCode);
47
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using AutoMapper;
4+
using CustomerService.Controllers;
5+
using CustomerService.Domain.CustomerAggregate;
6+
using CustomerService.Domain.CustomerAggregate.Commands;
7+
using CustomerService.Tests.Fakes;
8+
using CustomerService.Tests.Utils;
9+
using EventDriven.CQRS.Abstractions.Commands;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Moq;
12+
using Xunit;
13+
14+
namespace CustomerService.Tests.Controllers
15+
{
16+
public class CustomerCommandControllerTests
17+
{
18+
private readonly Mock<ICommandBroker> _commandBrokerMoq;
19+
private readonly IMapper _mapper;
20+
21+
public CustomerCommandControllerTests()
22+
{
23+
_commandBrokerMoq = new Mock<ICommandBroker>();
24+
_mapper = BaseUtils.GetMapper();
25+
}
26+
27+
[Fact]
28+
public void WhenInstantiated_ThenShouldBeOfCorrectType()
29+
{
30+
var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper);
31+
32+
Assert.IsAssignableFrom<ControllerBase>(controller);
33+
Assert.IsType<CustomerCommandController>(controller);
34+
}
35+
36+
[Fact]
37+
public async Task GivenWeAreCreatingACustomer_WhenSuccessful_ThenShouldProvideNewEntityWithPath()
38+
{
39+
var customerOut = _mapper.Map<Customer>(Customers.Customer1);
40+
41+
_commandBrokerMoq.Setup(x => x.InvokeAsync<CreateCustomer, CommandResult<Customer>>(It.IsAny<CreateCustomer>()))
42+
.ReturnsAsync(new CommandResult<Customer>(CommandOutcome.Accepted, customerOut));
43+
44+
var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper);
45+
46+
var actionResult = await controller.Create(Customers.Customer1);
47+
var createdResult = actionResult as CreatedResult;
48+
49+
Assert.NotNull(actionResult);
50+
Assert.NotNull(createdResult);
51+
Assert.Equal($"api/customer/{customerOut.Id}", createdResult.Location, true);
52+
}
53+
54+
[Fact]
55+
public async Task GivenWeAreCreatingACustomer_WhenFailure_ThenShouldReturnError()
56+
{
57+
_commandBrokerMoq.Setup(x => x.InvokeAsync<CreateCustomer, CommandResult<Customer>>(It.IsAny<CreateCustomer>()))
58+
.ReturnsAsync(new CommandResult<Customer>(CommandOutcome.NotHandled));
59+
60+
var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper);
61+
62+
var actionResult = await controller.Create(Customers.Customer1);
63+
var statusCodeResult = actionResult as StatusCodeResult;
64+
65+
Assert.NotNull(actionResult);
66+
Assert.NotNull(statusCodeResult);
67+
Assert.Equal(500, statusCodeResult.StatusCode);
68+
}
69+
70+
[Fact]
71+
public async Task GivenWeAreUpdatingACustomer_WhenSuccessful_ThenUpdatedEntityShouldBeReturned()
72+
{
73+
var customerIn = Customers.Customer2;
74+
var customerOut = _mapper.Map<Customer>(Customers.Customer2);
75+
76+
var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper);
77+
78+
_commandBrokerMoq.Setup(x => x.InvokeAsync<UpdateCustomer, CommandResult<Customer>>(It.IsAny<UpdateCustomer>()))
79+
.ReturnsAsync(new CommandResult<Customer>(CommandOutcome.Accepted, customerOut));
80+
81+
var actionResult = await controller.Update(customerIn);
82+
var objectResult = actionResult as OkObjectResult;
83+
84+
Assert.NotNull(actionResult);
85+
Assert.NotNull(objectResult);
86+
Assert.Equal(customerIn.Id, ((DTO.Write.Customer) objectResult.Value).Id);
87+
}
88+
89+
[Fact]
90+
public async Task GivenWeAreUpdatingACustomer_WhenCustomerDoesNotExist_ThenShouldReturnNotFound()
91+
{
92+
var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper);
93+
94+
_commandBrokerMoq.Setup(x => x.InvokeAsync<UpdateCustomer, CommandResult<Customer>>(It.IsAny<UpdateCustomer>()))
95+
.ReturnsAsync(new CommandResult<Customer>(CommandOutcome.NotFound));
96+
97+
var actionResult = await controller.Update(Customers.Customer2);
98+
var notFoundResult = actionResult as NotFoundResult;
99+
100+
Assert.NotNull(actionResult);
101+
Assert.NotNull(notFoundResult);
102+
}
103+
104+
[Fact]
105+
public async Task GivenWeAreUpdatingACustomer_WhenWeEncounterAConcurrencyIssue_ThenShouldReturnConflict()
106+
{
107+
var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper);
108+
109+
_commandBrokerMoq.Setup(x => x.InvokeAsync<UpdateCustomer, CommandResult<Customer>>(It.IsAny<UpdateCustomer>()))
110+
.ReturnsAsync(new CommandResult<Customer>(CommandOutcome.Conflict));
111+
112+
var actionResult = await controller.Update(Customers.Customer2);
113+
var conflictResult = actionResult as ConflictResult;
114+
115+
Assert.NotNull(actionResult);
116+
Assert.NotNull(conflictResult);
117+
}
118+
119+
[Fact]
120+
public async Task GivenWeAreRemovingACustomer_WhenSuccessful_ThenShouldReturnSuccess()
121+
{
122+
var customerId = Guid.NewGuid();
123+
var controller = new CustomerCommandController(_commandBrokerMoq.Object, _mapper);
124+
125+
_commandBrokerMoq.Setup(x => x.InvokeAsync<RemoveCustomer, CommandResult>(It.IsAny<RemoveCustomer>()))
126+
.ReturnsAsync(new CommandResult<Customer>(CommandOutcome.Accepted));
127+
128+
var actionResult = await controller.Remove(customerId);
129+
var noContentResult = actionResult as NoContentResult;
130+
131+
Assert.NotNull(actionResult);
132+
Assert.NotNull(noContentResult);
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)