You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reference architecture for using **EventDriven** abstractions and libraries for Domain Driven Design (**DDD**), Command-Query Responsibility Segregation (**CQRS**) and Event Driven Architecture (**EDA**).
3
+
Reference architecture for using **Event Driven .NET** abstractions and libraries for [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) (DDD), [CommandQuery Responsibility Segregation](https://martinfowler.com/bliki/CQRS.html) (CQRS) and [Event Driven Architecture](https://en.wikipedia.org/wiki/Event-driven_architecture) (EDA).
4
4
5
5
## Prerequisites
6
6
-[.NET Core SDK](https://dotnet.microsoft.com/download) (6.0 or greater)
@@ -21,22 +21,26 @@ Reference architecture for using **EventDriven** abstractions and libraries for
21
21
22
22
This project builds on the principles of [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design) (DDD) 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) (CQRS) pattern. By providing an event bus abstraction over [Dapr](https://dapr.io/) (Distributed Application Runtime), the reference architecture demonstrates how to apply principles of [Event Driven Architecture](https://en.wikipedia.org/wiki/Event-driven_architecture) (EDA). 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.
23
23
24
+
> **Note**: [EventDriven.CQRS.Abstractions](https://github.com/event-driven-dotnet/EventDriven.CQRS.Abstractions) version 2.0 or later uses [MediatR](https://github.com/jbogard/MediatR) to enable a handler per command pattern with behaviors for cross-cutting concerns.
25
+
24
26
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).
25
-
-**Query Controller**: Uses repository to retrieve entities and converts them to DTO's with AutoMapper.
26
-
-**Command Controller**: Converts DTO's to domain entities using AutoMapper. Then hands control over to a command handler for executing business logic.
27
-
-**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.
28
-
-**Repository**: Persists entity state to a database.
27
+
-**Command Controller**: Converts DTO's to domain entities using AutoMapper. Passes commands to a command broker, which selects the appropriate command handler for executing business logic.
28
+
-**Command Handlers**: 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 using a repository abstraction and optionally publishes an integration event which is handled by another microservice.
29
+
-**Query Controller**: Passes queries to a query broker, which selects the appropriate query handler to retrieve entities. Converts entities to DTO's with AutoMapper.
30
+
-**Query Handlers**: Processes queries to retrieve entities using a repository abstraction.
31
+
-**Behaviors**: Used to implement cross-cutting concerns such as logging or validation.
32
+
-**Repository**: Used to persist or retrieve entities from a database.
29
33
-**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.
30
34
31
35
> **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.
> **Note**: As an alternative to Tye, you can run services directly usng the Dapr CLI. This may be useful for troubleshooting Dapr issues after setting `Microsoft.AspNetCore` logging level to `Debug`.
43
+
> **Note**: As an alternative to Tye, you can run services directly usng the [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/). This may be useful for troubleshooting Dapr issues after setting `Microsoft.AspNetCore` logging level to `Debug`:
- ReadMe: [Reference Architecture: User Acceptance Tests](test/EventDriven.ReferenceArchitecture.Specs/ReadMe.md)
77
+
## Tests
77
78
78
-
### Development Guide
79
+
### Unit Tests
79
80
80
-
> This section describes how to build the Customer and Order services from scratch using the **EventDriven.DDD.Abstractions** package. For your own project substitute `Customer` and `Order` for your own aggregate entites and related classes.
81
+
In the **test** folder you'll find unit tests for both **CustomerService** and **OrderService** projects.
82
+
- [xUnit](https://xunit.net/) is used as the unit testing framework.
83
+
- [Moq](https://github.com/moq/moq4) is used as the mocking framework.
81
84
82
-
1. Add **Domain** and **CustomerAggregate** folders to the project, then add a `Customer` class that extends `Entity`.
83
-
- Add properties representing entity state.
84
-
- Create commands that are C# records and extend a `Command` base class.
85
-
```csharp
86
-
public record CreateCustomer(Customer Customer) : Command.Create(Customer.Id);
87
-
```
88
-
- Create domain events that extend `DomainEvent`.
89
-
```csharp
90
-
public record CustomerCreated(Customer Customer) : DomainEvent(Customer.Id);
91
-
```
92
-
- 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.
public CustomerCreated Process(CreateCustomer command)
104
-
// To process command, return one or more domain events
105
-
=> new(command.Entity);
106
-
107
-
public void Apply(CustomerCreated domainEvent) =>
108
-
// Set Id
109
-
Id = domainEvent.EntityId != default ? domainEvent.EntityId : Guid.NewGuid();
110
-
}
111
-
```
112
-
2. Add a `CustomerCommandHandler` class that implements `ICommandHandler` for create, update and remove commands.
113
-
- Inject `ICustomerRepository`, `IEventBus` and `IMapper` into the ctor.
114
-
- In the handler for `CreateCustomer`, write code to process the command, apply events, and persist the entity.
115
-
```csharp
116
-
public async Task<CommandResult<Customer>> Handle(CreateCustomer command)
117
-
{
118
-
// Process command
119
-
var domainEvent = command.Entity.Process(command);
120
-
121
-
// Apply events
122
-
command.Entity.Apply(domainEvent);
123
-
124
-
// Persist entity
125
-
var entity = await _repository.AddAsync(command.Entity);
126
-
if (entity == null) return new CommandResult<Customer>(CommandOutcome.InvalidCommand);
127
-
return new CommandResult<Customer>(CommandOutcome.Accepted, entity);
128
-
}
129
-
```
130
-
- Create a **Common** class library project and add the package **EventDriven.DDD.Abstractions**.
131
-
- Reference the Common project from the CustomerService project.
132
-
- Create a `CustomerAddressUpdated` record that extends `IntegrationEvent`.
133
-
```csharp
134
-
public record CustomerAddressUpdated(Guid CustomerId, Address ShippingAddress) : IntegrationEvent;
135
-
```
136
-
- 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.
137
-
```csharp
138
-
public async Task<CommandResult<Customer>> Handle(UpdateCustomer command)
139
-
{
140
-
// Compare shipping addresses
141
-
var existing = await _repository.GetAsync(command.EntityId);
142
-
if (existing == null) return new CommandResult<Customer>(CommandOutcome.NotHandled);
143
-
var addressChanged = command.Entity.ShippingAddress != existing.ShippingAddress;
144
-
145
-
try
146
-
{
147
-
// Persist entity
148
-
var entity = await _repository.UpdateAsync(command.Entity);
149
-
if (entity == null) return new CommandResult<Customer>(CommandOutcome.NotFound);
150
-
151
-
// Publish events
152
-
if (addressChanged)
153
-
{
154
-
var shippingAddress = _mapper.Map<Integration.Models.Address>(entity.ShippingAddress);
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. Register dependencies in `Startup.ConfigureServices`.
6. Add configuration entries to **appsettings.json**.
186
-
```json
187
-
"CustomerDatabaseSettings": {
188
-
"ConnectionString": "mongodb://localhost:27017",
189
-
"DatabaseName": "CustomersDb",
190
-
"CollectionName": "Customers"
191
-
},
192
-
"DaprEventBusOptions": {
193
-
"PubSubName": "pubsub"
194
-
},
195
-
"DaprEventCacheOptions": {
196
-
"DaprStateStoreOptions": {
197
-
"StateStoreName": "statestore-mongodb"
198
-
}
199
-
},
200
-
"DaprStoreDatabaseSettings": {
201
-
"ConnectionString": "mongodb://localhost:27017",
202
-
"DatabaseName": "daprStore",
203
-
"CollectionName": "daprCollection"
204
-
},
205
-
"DaprEventBusSchemaOptions": {
206
-
"UseSchemaRegistry": true,
207
-
"SchemaValidatorType": "Json",
208
-
"SchemaRegistryType": "Mongo",
209
-
"AddSchemaOnPublish": true,
210
-
"MongoStateStoreOptions": {
211
-
"ConnectionString": "mongodb://localhost:27017",
212
-
"DatabaseName": "schema-registry",
213
-
"SchemasCollectionName": "schemas"
214
-
}
215
-
}
216
-
```
217
-
7. Repeat these steps for the **Order** service.
218
-
- Reference the Common project.
219
-
- Add **Integration/EventHandlers** folders with a `CustomerAddressUpdatedEventHandler` class that extends `IntegrationEventHandler<CustomerAddressUpdated>`.
220
-
- Override `HandleAsync` to update the order addresses for the customer.
221
-
```csharp
222
-
public override async Task HandleAsync(CustomerAddressUpdated @event)
223
-
{
224
-
var orders = await _orderRepository.GetCustomerOrders(@event.CustomerId);
225
-
foreach (var order in orders)
226
-
{
227
-
var shippingAddress = _mapper.Map<Address>(@event.ShippingAddress);
> **Note**: Because database API's are notoriously [difficult to mock](https://jimmybogard.com/avoid-in-memory-databases-for-tests/), **repositories** are deliberately *excluded* from unit testing. Instead, repositories attain code coverage with **integration / acceptance tests**.
86
+
87
+
### Integration / Acceptance Tests
88
+
89
+
In the **tests** folder you'll find an **EventDriven.ReferenceArchitecture.Specs** project with automated integration / acceptance tests.
90
+
- [SpecFlow](https://specflow.org/) is used as the acceptance testing framework.
91
+
- Feature files use [Gherkin](https://specflow.org/learn/gherkin/) syntax to enable [Behavior Driven Development](https://en.wikipedia.org/wiki/Behavior-driven_development) with scenarios that match **acceptance criteria** in user stories.
92
+
93
+
## Development Guide
94
+
95
+
For step-by-step instructions on how to build microservices with [Event Driven .NET](https://github.com/event-driven-dotnet/Home) using this reference architecture, please see the **Event Driven .NET** [Development Guide](DevelopmentGuide.md).
0 commit comments