Skip to content

Commit 427cd2c

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents fdc11b9 + ada8d7a commit 427cd2c

22 files changed

Lines changed: 1130 additions & 172 deletions

.github/workflows/publish-nuget.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,4 @@ jobs:
177177
fi
178178
shell: bash
179179
env:
180-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
180+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

PIPELINE_GUIDE.md

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# Domain Event Pipelines
2+
3+
The CommandQuery.Framing framework now supports **before and after pipelines** for domain events using the `abes.GenericPipeline` library. This allows you to add cross-cutting concerns like logging, validation, authorization, and more around your domain event handlers.
4+
5+
## Features
6+
7+
- **Middleware Pattern**: Apply middleware before and after domain event execution
8+
- **Pipeline Composition**: Chain multiple middleware in a specific order
9+
- **DI Integration**: Middleware can use dependency injection
10+
- **Short-Circuit Support**: Stop pipeline execution based on conditions
11+
- **Context Sharing**: Share data between middleware via the context
12+
13+
## Quick Start
14+
15+
### 1. Add the Package
16+
17+
The `abes.GenericPipeline` package is already included in CommandQuery.Framing.
18+
19+
### 2. Create Middleware
20+
21+
Create middleware by implementing `IPipelineMiddleware<DomainEventContext<TMessage>>`:
22+
23+
```csharp
24+
using CommandQuery.Framing;
25+
using GenericPipeline;
26+
27+
public class LoggingMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
28+
{
29+
private readonly ILogger _logger;
30+
31+
public LoggingMiddleware(ILogger<LoggingMiddleware<TMessage>> logger)
32+
{
33+
_logger = logger;
34+
}
35+
36+
public async ValueTask InvokeAsync(
37+
DomainEventContext<TMessage> context,
38+
PipelineDelegate<DomainEventContext<TMessage>> next)
39+
{
40+
_logger.LogInformation("Before: {MessageType}", typeof(TMessage).Name);
41+
42+
await next(context); // Call next middleware or handler
43+
44+
_logger.LogInformation("After: {MessageType}, Success: {Success}",
45+
typeof(TMessage).Name,
46+
context.Success);
47+
}
48+
}
49+
```
50+
51+
### 3. Register Pipeline in Startup
52+
53+
Configure the pipeline for specific message types:
54+
55+
```csharp
56+
public void ConfigureServices(IServiceCollection services)
57+
{
58+
services.AddCommandQuery(typeof(Startup).Assembly);
59+
60+
// Register middleware
61+
services
62+
.AddDomainEventMiddleware<LoggingMiddleware<WidgetCreated>>()
63+
.AddDomainEventMiddleware<ValidationMiddleware<WidgetCreated>>();
64+
65+
// Configure pipeline for WidgetCreated events
66+
services.AddDomainEventPipeline<WidgetCreated>(builder =>
67+
{
68+
builder.Use<ValidationMiddleware<WidgetCreated>>();
69+
builder.Use<LoggingMiddleware<WidgetCreated>>();
70+
});
71+
}
72+
```
73+
74+
### 4. Publish Events
75+
76+
Domain events will now flow through the pipeline:
77+
78+
```csharp
79+
public class CreateWidget : IAsyncHandler<CreateWidgetMessage, CommandResponse<string>>
80+
{
81+
private readonly IDomainEventPublisher _publisher;
82+
83+
public CreateWidget(IDomainEventPublisher publisher)
84+
{
85+
_publisher = publisher;
86+
}
87+
88+
public async Task<CommandResponse<string>> Execute(
89+
CreateWidgetMessage message,
90+
CancellationToken cancellationToken = default)
91+
{
92+
var widgetId = Guid.NewGuid().ToString();
93+
94+
// This will execute through the configured pipeline
95+
await _publisher.Publish(
96+
new WidgetCreated { Id = widgetId, Name = message.Name },
97+
cancellationToken);
98+
99+
return Response.Ok(widgetId);
100+
}
101+
}
102+
```
103+
104+
## DomainEventContext<TMessage>
105+
106+
The pipeline context provides:
107+
108+
```csharp
109+
public class DomainEventContext<TMessage> : PipelineContext
110+
{
111+
// The message being published
112+
public TMessage Message { get; set; }
113+
114+
// Control flow - set to false to short-circuit
115+
public bool ShouldContinue { get; set; } = true;
116+
117+
// Result information
118+
public bool Success { get; set; } = true;
119+
public string ErrorMessage { get; set; }
120+
public Exception Exception { get; set; }
121+
122+
// From PipelineContext:
123+
public CancellationToken CancellationToken { get; init; }
124+
public IDictionary<string, object> Items { get; } // For sharing data
125+
}
126+
```
127+
128+
## Common Middleware Examples
129+
130+
### Validation Middleware
131+
132+
```csharp
133+
public class ValidationMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
134+
{
135+
public async ValueTask InvokeAsync(
136+
DomainEventContext<TMessage> context,
137+
PipelineDelegate<DomainEventContext<TMessage>> next)
138+
{
139+
if (context.Message == null)
140+
{
141+
context.Success = false;
142+
context.ErrorMessage = "Message cannot be null";
143+
context.ShouldContinue = false; // Stop the pipeline
144+
return;
145+
}
146+
147+
await next(context);
148+
}
149+
}
150+
```
151+
152+
### Authorization Middleware
153+
154+
```csharp
155+
public class AuthorizationMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
156+
{
157+
private readonly IHttpContextAccessor _httpContextAccessor;
158+
159+
public AuthorizationMiddleware(IHttpContextAccessor httpContextAccessor)
160+
{
161+
_httpContextAccessor = httpContextAccessor;
162+
}
163+
164+
public async ValueTask InvokeAsync(
165+
DomainEventContext<TMessage> context,
166+
PipelineDelegate<DomainEventContext<TMessage>> next)
167+
{
168+
var user = _httpContextAccessor.HttpContext?.User;
169+
170+
if (user == null || !user.Identity?.IsAuthenticated == true)
171+
{
172+
context.Success = false;
173+
context.ErrorMessage = "Unauthorized";
174+
context.ShouldContinue = false;
175+
return;
176+
}
177+
178+
await next(context);
179+
}
180+
}
181+
```
182+
183+
### Timing Middleware
184+
185+
```csharp
186+
public class TimingMiddleware<TMessage> : IPipelineMiddleware<DomainEventContext<TMessage>>
187+
{
188+
private readonly ILogger _logger;
189+
190+
public TimingMiddleware(ILogger<TimingMiddleware<TMessage>> logger)
191+
{
192+
_logger = logger;
193+
}
194+
195+
public async ValueTask InvokeAsync(
196+
DomainEventContext<TMessage> context,
197+
PipelineDelegate<DomainEventContext<TMessage>> next)
198+
{
199+
var sw = Stopwatch.StartNew();
200+
201+
try
202+
{
203+
await next(context);
204+
}
205+
finally
206+
{
207+
sw.Stop();
208+
_logger.LogInformation(
209+
"Domain event {MessageType} processed in {ElapsedMs}ms",
210+
typeof(TMessage).Name,
211+
sw.ElapsedMilliseconds);
212+
}
213+
}
214+
}
215+
```
216+
217+
## Middleware Ordering
218+
219+
You can control middleware execution order:
220+
221+
```csharp
222+
// Using IOrderedMiddleware for coarse ordering
223+
public class EarlyMiddleware : IPipelineMiddleware<MyContext>, IOrderedMiddleware
224+
{
225+
public int Order => -10; // Lower runs first
226+
227+
public async ValueTask InvokeAsync(MyContext context, PipelineDelegate<MyContext> next)
228+
{
229+
await next(context);
230+
}
231+
}
232+
233+
// Using IRunBefore<T> constraint
234+
public class ValidationMiddleware :
235+
IPipelineMiddleware<MyContext>,
236+
IRunBefore<LoggingMiddleware> // Runs before logging
237+
{
238+
public async ValueTask InvokeAsync(MyContext context, PipelineDelegate<MyContext> next)
239+
{
240+
await next(context);
241+
}
242+
}
243+
244+
// Using IRunAfter<T> constraint
245+
public class CleanupMiddleware :
246+
IPipelineMiddleware<MyContext>,
247+
IRunAfter<LoggingMiddleware> // Runs after logging
248+
{
249+
public async ValueTask InvokeAsync(MyContext context, PipelineDelegate<MyContext> next)
250+
{
251+
await next(context);
252+
}
253+
}
254+
```
255+
256+
## Short-Circuiting
257+
258+
Stop the pipeline early by setting `ShouldContinue = false`:
259+
260+
```csharp
261+
public async ValueTask InvokeAsync(
262+
DomainEventContext<TMessage> context,
263+
PipelineDelegate<TMessage> next)
264+
{
265+
if (SomeCondition())
266+
{
267+
context.ShouldContinue = false;
268+
context.ErrorMessage = "Condition not met";
269+
return; // Don't call next()
270+
}
271+
272+
await next(context);
273+
}
274+
```
275+
276+
## Benefits
277+
278+
**Separation of Concerns**: Keep cross-cutting logic separate from business logic
279+
**Reusability**: Write middleware once, use across multiple event types
280+
**Testability**: Test middleware independently
281+
**Flexibility**: Enable/disable middleware via configuration
282+
**Performance Monitoring**: Add timing and metrics easily
283+
**Error Handling**: Centralized exception handling
284+
285+
## See Also
286+
287+
- [abes.GenericPipeline Documentation](https://github.com/tomlazelle/pipeline)
288+
- [Sample Implementation](sample/Domain/Middleware/)
289+
- [DomainEventContext API](src/CommandQuery.Framing/DomainEventContext.cs)

0 commit comments

Comments
 (0)