Skip to content

Commit 07036fc

Browse files
author
Anthony Sneed
committed
Add DevelopmentGuide.md.
1 parent ad8e5a7 commit 07036fc

8 files changed

+479
-189
lines changed

DevelopmentGuide.md

Lines changed: 448 additions & 0 deletions
Large diffs are not rendered by default.

EventDriven.ReferenceArchitecture.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AFFCBFA4-9D64-43AA-AC59-D4CC54BD9C72}"
77
ProjectSection(SolutionItems) = preProject
88
ReadMe.md = ReadMe.md
9+
DevelopmentGuide.md = DevelopmentGuide.md
910
EndProjectSection
1011
EndProject
1112
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{269CD137-4093-4100-B33E-808586D335F6}"

ReadMe.md

Lines changed: 28 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# EventDriven.ReferenceArchitecture
22

3-
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), [Command Query Responsibility Segregation](https://martinfowler.com/bliki/CQRS.html) (CQRS) and [Event Driven Architecture](https://en.wikipedia.org/wiki/Event-driven_architecture) (EDA).
44

55
## Prerequisites
66
- [.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
2121

2222
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.
2323

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+
2426
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.
2933
- **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.
3034

3135
> **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.
3236
3337
<p align="center">
34-
<img width="600" src="images/event-driven-cqrs-ref-arch.png">
38+
<img width="600" src="images/event-driven-ref-arch.png">
3539
</p>
3640

37-
### Run services with Tye and Dapr
41+
## Running Services with Tye and Dapr
3842

39-
> **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`:
4044
> `dapr run --app-id service-name --app-port #### --components-path ../dapr/components -- dotnet run`
4145
4246
1. Open a terminal at the **reference-architecture** directory and run Tye to launch all services simultaneously.
@@ -70,185 +74,22 @@ The **Reference Architecture** projects demonstrate how to apply these concepts
7074
- Note the address is also updated for the customer's orders.
7175
- Observe log messages in terminal when integration events are published and handled.
7276
73-
### Tests
74-
- **Unit Tests**: EventDriven.ReferenceArchitecture.Tests
75-
- **User Acceptance Tests**: EventDriven.ReferenceArchitecture.Specs
76-
- ReadMe: [Reference Architecture: User Acceptance Tests](test/EventDriven.ReferenceArchitecture.Specs/ReadMe.md)
77+
## Tests
7778
78-
### Development Guide
79+
### Unit Tests
7980
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.
8184
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.
93-
```csharp
94-
public class Customer :
95-
Entity,
96-
ICommandProcessor<CreateCustomer, CustomerCreated>,
97-
IEventApplier<CustomerCreated>
98-
{
99-
public string FirstName { get; set; }
100-
public string LastName { get; set; }
101-
public Address ShippingAddress { get; set; }
102-
103-
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`.
172-
```csharp
173-
// Registrations
174-
services.AddAutoMapper(typeof(Startup));
175-
services.AddSingleton<CustomerCommandHandler>();
176-
services.AddSingleton<ICustomerRepository, CustomerRepository>();
177-
services.AddMongoDbSettings<CustomerDatabaseSettings, Customer>(Configuration);
178-
179-
// Add Dapr event bus
180-
services.AddDaprEventBus(Configuration, true);
181-
182-
// Add Dapr Mongo event cache
183-
services.AddDaprMongoEventCache(Configuration);
184-
```
185-
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);
228-
await _orderRepository.UpdateOrderAddress(order.Id, shippingAddress);
229-
}
230-
}
231-
```
232-
- In `Startup.ConfigureServices` register `CustomerAddressUpdatedEventHandler` then add the Dapr Event Bus.
233-
```csharp
234-
services.AddSingleton<CustomerAddressUpdatedEventHandler>();
235-
services.AddDaprEventBus(Configuration, true);
236-
services.AddDaprMongoEventCache(Configuration);
237-
```
238-
- In `Startup.Configure` use Cloud Events, map subscribe handlers, and map Dapr Event Bus endpoints, subscribing with the event handler.
239-
```csharp
240-
// Use cloud events
241-
app.UseCloudEvents();
242-
app.UseEndpoints(endpoints =>
243-
{
244-
endpoints.MapControllers();
245-
246-
// Map subscribe handlers
247-
endpoints.MapSubscribeHandler();
248-
endpoints.MapDaprEventBus(eventBus =>
249-
{
250-
// Subscribe with event handler
251-
eventBus.Subscribe(customerAddressUpdatedEventHandler, null, "v1");
252-
});
253-
});
254-
```
85+
> **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).

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

Lines changed: 0 additions & 1 deletion
This file was deleted.

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

-105 KB
Binary file not shown.

0 commit comments

Comments
 (0)