Skip to content

Commit f426f3a

Browse files
tonysneedAnthony Sneed
authored andcommitted
Initial commit.
1 parent ac51c29 commit f426f3a

File tree

103 files changed

+3198
-1
lines changed

Some content is hidden

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

103 files changed

+3198
-1
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,6 @@ MigrationBackup/
360360
.ionide/
361361

362362
# Fody - auto-generated XML schema
363-
FodyWeavers.xsd
363+
FodyWeavers.xsd
364+
.idea
365+
.DS_Store

EventDriven.CQRS.sln

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.31129.286
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AFFCBFA4-9D64-43AA-AC59-D4CC54BD9C72}"
7+
ProjectSection(SolutionItems) = preProject
8+
ReadMe.md = ReadMe.md
9+
EndProjectSection
10+
EndProject
11+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AF357BE9-9A6E-48A3-A995-E75F2147A43F}"
12+
EndProject
13+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventDriven.CQRS.Abstractions", "src\EventDriven.CQRS.Abstractions\EventDriven.CQRS.Abstractions.csproj", "{1F665C29-BF1C-45BA-8CB9-829E85C34999}"
14+
EndProject
15+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{269CD137-4093-4100-B33E-808586D335F6}"
16+
EndProject
17+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "reference-architecture", "reference-architecture", "{C4FD0AF1-927A-4860-A634-7CE342807692}"
18+
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
21+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomerService", "reference-architecture\CustomerService\CustomerService.csproj", "{48983715-E6DF-462F-AF3C-769C1122794F}"
22+
EndProject
23+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderService", "reference-architecture\OrderService\OrderService.csproj", "{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7}"
24+
EndProject
25+
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
30+
EndProject
31+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "reference-architecture\Common\Common.csproj", "{FC04D111-903D-49FF-84A6-8806C71E2168}"
32+
EndProject
33+
Global
34+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
35+
Debug|Any CPU = Debug|Any CPU
36+
Release|Any CPU = Release|Any CPU
37+
EndGlobalSection
38+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
39+
{1F665C29-BF1C-45BA-8CB9-829E85C34999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40+
{1F665C29-BF1C-45BA-8CB9-829E85C34999}.Debug|Any CPU.Build.0 = Debug|Any CPU
41+
{1F665C29-BF1C-45BA-8CB9-829E85C34999}.Release|Any CPU.ActiveCfg = Release|Any CPU
42+
{1F665C29-BF1C-45BA-8CB9-829E85C34999}.Release|Any CPU.Build.0 = Release|Any CPU
43+
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44+
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Debug|Any CPU.Build.0 = Debug|Any CPU
45+
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Release|Any CPU.ActiveCfg = Release|Any CPU
46+
{9809006C-2F6B-44A1-8AE2-BC449368D209}.Release|Any CPU.Build.0 = Release|Any CPU
47+
{48983715-E6DF-462F-AF3C-769C1122794F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
48+
{48983715-E6DF-462F-AF3C-769C1122794F}.Debug|Any CPU.Build.0 = Debug|Any CPU
49+
{48983715-E6DF-462F-AF3C-769C1122794F}.Release|Any CPU.ActiveCfg = Release|Any CPU
50+
{48983715-E6DF-462F-AF3C-769C1122794F}.Release|Any CPU.Build.0 = Release|Any CPU
51+
{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
52+
{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
53+
{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
54+
{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7}.Release|Any CPU.Build.0 = Release|Any CPU
55+
{FC04D111-903D-49FF-84A6-8806C71E2168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
56+
{FC04D111-903D-49FF-84A6-8806C71E2168}.Debug|Any CPU.Build.0 = Debug|Any CPU
57+
{FC04D111-903D-49FF-84A6-8806C71E2168}.Release|Any CPU.ActiveCfg = Release|Any CPU
58+
{FC04D111-903D-49FF-84A6-8806C71E2168}.Release|Any CPU.Build.0 = Release|Any CPU
59+
EndGlobalSection
60+
GlobalSection(SolutionProperties) = preSolution
61+
HideSolutionNode = FALSE
62+
EndGlobalSection
63+
GlobalSection(NestedProjects) = preSolution
64+
{1F665C29-BF1C-45BA-8CB9-829E85C34999} = {AF357BE9-9A6E-48A3-A995-E75F2147A43F}
65+
{9809006C-2F6B-44A1-8AE2-BC449368D209} = {269CD137-4093-4100-B33E-808586D335F6}
66+
{48983715-E6DF-462F-AF3C-769C1122794F} = {C4FD0AF1-927A-4860-A634-7CE342807692}
67+
{16A5B2CB-8C46-4F3E-B7A1-97C47D9F66E7} = {C4FD0AF1-927A-4860-A634-7CE342807692}
68+
{B11B21E0-7B89-4285-990A-D98793310B02} = {C4FD0AF1-927A-4860-A634-7CE342807692}
69+
{FC04D111-903D-49FF-84A6-8806C71E2168} = {C4FD0AF1-927A-4860-A634-7CE342807692}
70+
EndGlobalSection
71+
GlobalSection(ExtensibilityGlobals) = postSolution
72+
SolutionGuid = {427A0D03-63CA-48AE-AA95-D21800101398}
73+
EndGlobalSection
74+
EndGlobal

ReadMe.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# EventDriven.CQRS
2+
3+
An event-driven approach to Command Query Responsibility Segregation.
4+
5+
### Prerequisites
6+
- [.NET Core SDK](https://dotnet.microsoft.com/download) (5.0 or greater)
7+
- [Docker Desktop](https://www.docker.com/products/docker-desktop)
8+
- MongoDB Docker: `docker run --name mongo -d -p 27017:27017 -v /tmp/mongo/data:/data/db mongo`
9+
- [MongoDB Client](https://robomongo.org/download):
10+
- Download Robo 3T only.
11+
- Add connection to localhost on port 27017.
12+
- [Dapr](https://dapr.io/) (Distributed Application Runtime)
13+
- [Install Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
14+
- [Initialize Dapr](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
15+
16+
## Introduction
17+
18+
This project builds on the principles of [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) to provide a set of abstractions and reference architecture for implementing the [Command Query Responsibility Segregation](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs) pattern, also known as as CQRS. Because entities process commands by emitting domain events, adding [event sourcing](https://microservices.io/patterns/data/event-sourcing.html) at a later time will be relatively straightforward.
19+
20+
The **EventDriven.CQRS.Abstractions** library contains interfaces and abstract base classes to support these concepts:
21+
- **Entity**: A type that has an identity with behavior and state that can change over time.
22+
- **Command**: An object that is sent to the domain for a state change which is handled by a command handler.
23+
- **Event**: A statement of fact about what change has been made to the domain state.
24+
25+
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).
26+
- **Query Controller**: Uses repository to retrieve entities and converts them to DTO's with AutoMapper.
27+
- **Command Controller**: Converts DTO's to domain entities using AutoMapper. Then hands control over to a command handler for executing business logic.
28+
- **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.
29+
- **Repository**: Persists entity state to a database.
30+
- **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.
31+
32+
> **Note**: This example illustrates a *simple* CQRS implementation with a **shared database** and **single service** for both read and write operations. A more sophisticated implementation might entail **separate services and databases** for read and write operations, using integration events to communicate between them. This simple example only uses integration events to communicate between the customer and order services.
33+
34+
<p align="center">
35+
<img width="600" src="images/event-driven-cqrs-ref-arch.png">
36+
</p>
37+
38+
### Usage: Reference Architecture Projects
39+
40+
1. Run Dapr Dashboard.
41+
- Then open http://localhost:8080 to view containers after executing `dapr run` commands.
42+
```
43+
dapr dashboard
44+
```
45+
2. Use Dapr to run the customer service.
46+
```
47+
dapr run --app-id customer-service --app-port 5000 -- dotnet run
48+
```
49+
3. Use Dapr to run the order service.
50+
```
51+
dapr run --app-id order-service --app-port 5050 -- dotnet run
52+
```
53+
4. Create some customers.
54+
- Open http://localhost:5000/swagger
55+
- Execute posts using contents of **customers.json**.
56+
- Copy post response, modify fields, then execute puts.
57+
- Make sure to copy `etag` value from last response, or you will get a concurrency error.
58+
- Copy `id` and `etag` values to execute deletes.
59+
- Execute gets to retrieve customers.
60+
- View customers database collections using Robo 3T.
61+
5. Create some orders.
62+
- Execute posts using contents of **orders.json**.
63+
- Copy post response, modify fields, then execute puts.
64+
- Make sure to copy `etag` value from last response, or you will get a concurrency error.
65+
- Copy `id` and `etag` values to execute deletes.
66+
- Execute gets to retrieve orders.
67+
- View orders database collections using Robo 3T.
68+
6. Update the address of a customer who has order.
69+
- Note the address is also updated for the customer's orders.
70+
- Observe log messages in terminal when integration events are published and handled.
71+
7. To **debug** services, you will need to use **Visual Studio Code** with the *Dapr extension*.
72+
- Open instances of VS Code at both CustomerService and OrderService.
73+
- If .vscode folder not present:
74+
- First create build and debug artifacts.
75+
- Then from the task palette run `Dapr: Scaffold Dapr Tasks`.
76+
- Enter values for launch, app id (customer-service) and port (5000).
77+
- Enter values for launch, app id (order-service) and port (5050).
78+
- Switch to the Debug tab and select "with Dapr" configuration.
79+
- Set breakpoints as needed and press F5 to start debugging.
80+
81+
### Usage: EventDriven.CQRS.Abstractions
82+
83+
> This section describes how to build the Customer and Order services from scratch using the **EventDriven.CQRS.Abstractions** package. For your own project substitute `Customer` and `Order` for your own aggregate entites and related classes.
84+
85+
1. Add **Domain** and **CustomerAggregate** folders to the project, then add a `Customer` class that extends `Entity`.
86+
- Add properties representing entity state.
87+
- Create commands that are C# records and extend a `Command` base class.
88+
```csharp
89+
public record CreateCustomer(Customer Customer) : Command.Create(Customer.Id);
90+
```
91+
- Create domain events that extend `DomainEvent`.
92+
```csharp
93+
public record CustomerCreated(Customer Customer) : DomainEvent(Customer.Id);
94+
```
95+
- Where you need to execute business logic, implement `ICommandProcessor` and `IEventApplier` interfaces to process commands by emitting domain events and to apply those events to mutate entity state.
96+
```csharp
97+
public IEnumerable<IDomainEvent> Process(CreateCustomer command)
98+
// To process command, return one or more domain events
99+
=> new List<IDomainEvent>
100+
{
101+
new CustomerCreated(command.Customer)
102+
};
103+
104+
public void Apply(CustomerCreated domainEvent) =>
105+
// Set Id
106+
Id = domainEvent.EntityId != default(Guid) ? domainEvent.EntityId : Guid.NewGuid();
107+
```
108+
2. Add a `CustomerCommandHandler` class that implements `ICommandHandler` for create, update and remove commands.
109+
- Inject `ICustomerRepository`, `IEventBus` and `IMapper` into the ctor.
110+
- In the handler for `CreateCustomer`, write code to process the command, apply events, and persist the entity.
111+
```csharp
112+
public async Task<CommandResult<Customer>> Handle(CreateCustomer command)
113+
{
114+
// Process command
115+
_logger.LogInformation("Handling command: {commandName}", nameof(CreateCustomer));
116+
var events = command.Customer.Process(command);
117+
118+
// Apply events
119+
var domainEvent = events.OfType<CustomerCreated>().SingleOrDefault();
120+
if (domainEvent == null) return new CommandResult<Customer>(CommandOutcome.NotHandled);
121+
command.Customer.Apply(domainEvent);
122+
123+
// Persist entity
124+
var entity = await _repository.Add(command.Customer);
125+
if (entity == null) return new CommandResult<Customer>(CommandOutcome.InvalidCommand);
126+
return new CommandResult<Customer>(CommandOutcome.Accepted, entity);
127+
}
128+
```
129+
- Create a **Common** class library project and add the package **EventDriven.CQRS.Abstractions**.
130+
- Reference the Common project from the CustomerService project.
131+
- Create a `CustomerAddressUpdated` record that extends `IntegrationEvent`.
132+
```csharp
133+
public record CustomerAddressUpdated(Guid CustomerId, Address ShippingAddress) : IntegrationEvent;
134+
```
135+
- 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.
136+
```csharp
137+
public async Task<CommandResult<Customer>> Handle(UpdateCustomer command)
138+
{
139+
// Compare shipping addresses
140+
_logger.LogInformation("Handling command: {commandName}", nameof(UpdateCustomer));
141+
var existing = await _repository.Get(command.EntityId);
142+
var addressChanged = command.Customer.ShippingAddress != existing.ShippingAddress;
143+
144+
try
145+
{
146+
// Persist entity
147+
var entity = await _repository.Update(command.Customer);
148+
if (entity == null) return new CommandResult<Customer>(CommandOutcome.NotFound);
149+
150+
// Publish events
151+
if (addressChanged)
152+
{
153+
var shippingAddress = _mapper.Map<Integration.Models.Address>(entity.ShippingAddress);
154+
_logger.LogInformation("Publishing event: {eventName}", $"v1.{nameof(CustomerAddressUpdated)}");
155+
await _eventBus.PublishAsync(
156+
new CustomerAddressUpdated(entity.Id, shippingAddress),
157+
null, "v1");
158+
}
159+
return new CommandResult<Customer>(CommandOutcome.Accepted, entity);
160+
}
161+
catch (ConcurrencyException)
162+
{
163+
return new CommandResult<Customer>(CommandOutcome.Conflict);
164+
}
165+
}
166+
```
167+
3. Add a `CustomerCommandController` to the project that injects `CustomerCommandHandler` into the ctor.
168+
- Add Post, Put and Delete actions which accept a `Customer` DTO, map it to a `Customer` entity and invoke the appropriate command handler.
169+
4. Add a `CustomerQueryController` to the project that injects a `ICustomerRepository` into the ctor.
170+
- Use the repository to retrieve entities, then map those to `Customer` DTO objects.
171+
5. Repeat these steps for the Order service.
172+
- Reference the Common project.
173+
- Add **Integration/EventHandlers** folders with a `CustomerAddressUpdatedEventHandler` class that extends `IntegrationEventHandler<CustomerAddressUpdated>`.
174+
- Override `HandleAsync` to update the order addresses for the customer.
175+
```csharp
176+
public override async Task HandleAsync(CustomerAddressUpdated @event)
177+
{
178+
_logger.LogInformation("Handling CustomerAddressUpdated event.");
179+
var orders = await _orderRepository.GetCustomerOrders(@event.CustomerId);
180+
foreach (var order in orders)
181+
{
182+
var shippingAddress = _mapper.Map<Address>(@event.ShippingAddress);
183+
await _orderRepository.UpdateOrderAddress(order.Id, shippingAddress);
184+
}
185+
}
186+
```
187+
- In `Startup.ConfigureServices` register `CustomerAddressUpdatedEventHandler` then add the Dapr Event Bus.
188+
```csharp
189+
services.AddSingleton<CustomerAddressUpdatedEventHandler>();
190+
services.AddDaprEventBus(Constants.DaprPubSubName);
191+
```
192+
- In `Startup.Configure` use Cloud Events, map subscribe handlers, and map Dapr Event Bus endpoints, subscribing with the event handler.
193+
```csharp
194+
// Use cloud events
195+
app.UseCloudEvents();
196+
app.UseEndpoints(endpoints =>
197+
{
198+
endpoints.MapControllers();
199+
200+
// Map subscribe handlers
201+
endpoints.MapSubscribeHandler();
202+
endpoints.MapDaprEventBus(eventBus =>
203+
{
204+
// Subscribe with event handler
205+
eventBus.Subscribe(customerAddressUpdatedEventHandler, null, "v1");
206+
});
207+
});
208+
```

images/eda-logo.jpeg

23.3 KB
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<mxfile host="app.diagrams.net" modified="2021-04-24T21:13:00.498Z" agent="5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36" etag="jcSRvpNV_wRMNOIdpFs0" version="14.6.1" type="device"><diagram id="H1tdGHeVPZ8NpCb7zYOw" name="Page-1">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</diagram></mxfile>

images/event-driven-cqrs-ref-arch.png

106 KB
Loading

0 commit comments

Comments
 (0)