Skip to content
Merged
90 changes: 71 additions & 19 deletions contents/CQRSWithBrighterAndDarker.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,38 @@ In this guide, we'll explore how Brighter and Darker work together, common archi

**Example violations of CQS:**
```csharp
using System;

// ❌ Bad: Query with side effects
public Customer GetCustomer(int id)
public class CustomerService(ICustomerRepository repository)
{
var customer = _repository.GetById(id);
customer.LastAccessed = DateTime.UtcNow; // Side effect!
_repository.Update(customer);
return customer;
public Customer GetCustomer(int id)
{
var customer = repository.GetById(id);
customer.LastAccessed = DateTime.UtcNow; // Side effect!
repository.Update(customer);
return customer;
}
}
```

```csharp
using System;

// ✅ Good: Separate command and query
public void UpdateLastAccessed(int customerId) // Command
public class CustomerService(ICustomerRepository repository)
{
var customer = _repository.GetById(customerId);
customer.LastAccessed = DateTime.UtcNow;
_repository.Update(customer);
}
public void UpdateLastAccessed(int customerId) // Command
{
var customer = repository.GetById(customerId);
customer.LastAccessed = DateTime.UtcNow;
repository.Update(customer);
}

public Customer GetCustomer(int id) // Query
{
return _repository.GetById(id); // No side effects
public Customer GetCustomer(int id) // Query
{
return repository.GetById(id); // No side effects
}
}
```

Expand Down Expand Up @@ -229,14 +241,16 @@ For more information on Brighter, see [Brighter Basic Configuration](/contents/B

### Query Handler Example

Here's a brief example of a Darker query handler. For complete details, see [Implementing a Query Handler](/contents/ImplementAQueryHandler.md):
Here's a brief example of a Darker query handler. It projects onto the `OrderItemDto` that the [e-commerce example](#example-e-commerce-order-system) below declares. For complete details, see [Implementing a Query Handler](/contents/ImplementAQueryHandler.md):

```csharp
using Microsoft.EntityFrameworkCore;
using Paramore.Darker;
using Paramore.Darker.Policies;
using Paramore.Darker.QueryLogging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -364,10 +378,12 @@ Here's how Brighter and Darker fit together in a typical ASP.NET Core applicatio

### Dependency Injection Setup

Configure both Brighter and Darker in your `Program.cs` or `Startup.cs`:
Configure both Brighter and Darker in your `Program.cs` or `Startup.cs`. This registers the handlers the examples on this page declare, `PlaceOrderCommandHandler` and `GetOrderDetailsQueryHandler`:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Paramore.Brighter;
using Paramore.Brighter.Extensions;
Expand Down Expand Up @@ -415,7 +431,7 @@ app.Run();

### Using Both in Controllers

Inject both `IAmACommandProcessor` and `IQueryProcessor` where needed:
Inject both `IAmACommandProcessor` and `IQueryProcessor` where needed. The controller sends the `PlaceOrderCommand` and executes the `GetOrderDetailsQuery` declared above:

```csharp
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -726,6 +742,9 @@ public class OrderPlacedEvent : Event
```

**Command Handler:**

It handles the `PlaceOrderCommand` against the write model declared above:

```csharp
using Paramore.Brighter;
using Paramore.Brighter.Logging.Attributes;
Expand Down Expand Up @@ -815,6 +834,8 @@ public class PlaceOrderCommandHandler : RequestHandlerAsync<PlaceOrderCommand>

**Query:**
```csharp
using System;
using System.Collections.Generic;
using Paramore.Darker;

public sealed class GetOrderDetailsQuery : IQuery<OrderDetailsDto>
Expand Down Expand Up @@ -849,6 +870,9 @@ public class OrderItemDetailsDto
```

**Query Handler:**

It handles the query above and projects onto its DTOs:

```csharp
using Microsoft.EntityFrameworkCore;
using Paramore.Darker;
Expand Down Expand Up @@ -957,34 +981,62 @@ Design your UI to gracefully handle scenarios where the read model hasn't caught

## CQRS Common Pitfalls

The examples below are methods of the handlers in the [e-commerce example](#example-e-commerce-order-system), using its command and query.

**1. Querying in command handlers**

❌ Bad:
```csharp
public override async Task<PlaceOrderCommand> HandleAsync(PlaceOrderCommand command, ...)
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Paramore.Brighter;

public override async Task<PlaceOrderCommand> HandleAsync(
PlaceOrderCommand command,
CancellationToken cancellationToken = default)
{
// Don't query to display data to user in a command handler
var customer = await _customerRepository.GetByIdAsync(command.CustomerId);
_logger.LogInformation($"Order placed by {customer.Name}"); // Avoid!

return await base.HandleAsync(command, cancellationToken);
}
```

✅ Good:
```csharp
public override async Task<PlaceOrderCommand> HandleAsync(PlaceOrderCommand command, ...)
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Paramore.Brighter;

public override async Task<PlaceOrderCommand> HandleAsync(
PlaceOrderCommand command,
CancellationToken cancellationToken = default)
{
// Only query what's necessary for business rules
var customer = await _customerRepository.GetByIdAsync(command.CustomerId);
if (!customer.IsActive)
throw new InvalidOperationException("Customer is inactive");

return await base.HandleAsync(command, cancellationToken);
}
```

**2. Updating in query handlers**

❌ Bad:
```csharp
public override async Task<OrderDetailsDto> ExecuteAsync(GetOrderDetailsQuery query, ...)
using System;
using System.Threading;
using System.Threading.Tasks;

public override async Task<OrderDetailsDto> ExecuteAsync(
GetOrderDetailsQuery query,
CancellationToken cancellationToken = default)
{
var order = await _repository.GetByIdAsync(query.OrderId);
order.LastViewed = DateTime.UtcNow; // Don't modify state in queries!
Expand Down
17 changes: 9 additions & 8 deletions contents/DarkerBasicConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Install-Package Paramore.Darker.Policies
To configure Darker in an ASP.NET Core application, use the `AddDarker` extension method in your `Program.cs` or `Startup.cs` file. Darker integrates with the ASP.NET Core dependency injection container, making the `IQueryProcessor` available for injection into your controllers and endpoints.

```csharp
using Microsoft.AspNetCore.Builder;
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using Paramore.Darker.Policies;
Expand Down Expand Up @@ -82,6 +83,8 @@ The `AddHandlersFromAssemblies` method scans the specified assembly for query ha
Here's a complete example of using Darker with ASP.NET Core Minimal APIs:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Paramore.Darker;
using Paramore.Darker.AspNetCore;

Expand Down Expand Up @@ -271,6 +274,7 @@ For more control, you can create a custom policy registry with specific retry st
```csharp
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using System;
using Paramore.Darker.Policies;
using Polly;
using Polly.Registry;
Expand Down Expand Up @@ -313,6 +317,8 @@ Once policies are configured, you can apply them to query handlers using attribu
This is the most common setup for an ASP.NET Core Web API using Darker:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
using Paramore.Darker.Policies;
Expand All @@ -328,17 +334,9 @@ builder.Services.AddDarker()

// Add other services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
Expand All @@ -351,7 +349,9 @@ app.Run();
When using Entity Framework Core, you must configure the Query Processor with scoped lifetime to match the DbContext:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Paramore.Darker;
using Paramore.Darker.AspNetCore;
Expand Down Expand Up @@ -381,6 +381,7 @@ Without the scoped configuration, the query processor is a singleton that resolv
When your query handlers are spread across multiple assemblies, register all of them:

```csharp
using Microsoft.AspNetCore.Builder;
using Paramore.Darker;
using Paramore.Darker.AspNetCore;

Expand Down
44 changes: 37 additions & 7 deletions contents/EFCoreQueryIntegration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,35 @@ How to run Darker queries against Entity Framework Core: tracking, eager loading
**Always use `AsNoTracking()` for query handlers**. Since queries don't modify data, change tracking is unnecessary overhead.

```csharp
// ...
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

// ✅ Good: AsNoTracking for read-only queries
public override async Task<List<ProductDto>> ExecuteAsync(...)
public override async Task<List<ProductDto>> ExecuteAsync(
GetProductsQuery query,
CancellationToken cancellationToken = default)
{
return await _dbContext.Products
.AsNoTracking() // Disables change tracking
.Select(p => new ProductDto { /* ... */ })
.Select(p => new ProductDto { Id = p.Id, Name = p.Name })
.ToListAsync(cancellationToken);
}
```

```csharp
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

// ❌ Bad: Change tracking enabled (default)
public override async Task<List<Product>> ExecuteAsync(...)
public override async Task<List<Product>> ExecuteAsync(
GetProductEntitiesQuery query,
CancellationToken cancellationToken = default)
{
// Change tracking is enabled by default - unnecessary for queries!
return await _dbContext.Products
Expand All @@ -45,9 +62,15 @@ public override async Task<List<Product>> ExecuteAsync(...)
**Use `Include()` and `ThenInclude()` to load related entities** in a single query, avoiding N+1 query problems.

```csharp
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

public override async Task<OrderDto> ExecuteAsync(...)
// Load each Order with its Customer, and each OrderItem with its Product
public override async Task<OrderDto?> ExecuteAsync(
GetOrderQuery query,
CancellationToken cancellationToken = default)
{
return await _dbContext.Orders
.Include(o => o.Customer) // Load related Customer
Expand All @@ -72,9 +95,15 @@ public override async Task<OrderDto> ExecuteAsync(...)

**Alternative: Projection without Include:**
```csharp
// ...
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

// You don't need Include if you're projecting with Select
public override async Task<OrderDto> ExecuteAsync(...)
public override async Task<OrderDto?> ExecuteAsync(
GetOrderQuery query,
CancellationToken cancellationToken = default)
{
return await _dbContext.Orders
.Where(o => o.Id == query.OrderId)
Expand Down Expand Up @@ -114,6 +143,7 @@ For more details, see [Darker Configuration Reference](/contents/DarkerConfigura

```csharp
using Microsoft.EntityFrameworkCore;
using Paramore.Darker;
using System;
using System.Linq;
using System.Threading;
Expand Down
Loading
Loading