|
1 | 1 | # Foundatio.Mediator |
2 | 2 |
|
3 | | -**The fastest convention-based C# mediator library with source generators** |
4 | 3 |
|
5 | 4 | [](https://www.nuget.org/packages/Foundatio.Mediator/) |
6 | | -[](#performance-benchmarks) |
7 | | -[](#performance-benchmarks) |
8 | 5 |
|
9 | | -Foundatio.Mediator is a high-performance, convention-based mediator library that leverages C# source generators and cutting-edge interceptors to achieve near-direct method call performance. No interfaces, no base classes, no reflection at runtime—just clean, simple code that flies. |
| 6 | +Blazingly fast, convention-based C# mediator powered by source generators and interceptors. |
10 | 7 |
|
11 | | -## ✨ Why Choose Foundatio.Mediator? |
| 8 | +## High-Level Features |
12 | 9 |
|
13 | | -- **🚀 Blazing Fast** - Nearly as fast as direct method calls, 3x faster than MediatR |
14 | | -- **🎯 Convention-Based** - No interfaces or base classes required |
15 | | -- **⚡ Source Generated** - Compile-time code generation for optimal performance |
16 | | -- **🔧 Full DI Integration** - Works seamlessly with Microsoft.Extensions.DependencyInjection |
17 | | -- **🎪 Middleware Pipeline** - Elegant middleware support |
18 | | -- **� Cascading Messages** - Tuple returns automatically trigger follow-up events |
19 | | -- **�📦 Auto Registration** - Handlers discovered and registered automatically |
20 | | -- **🔒 Compile-Time Safety** - Rich diagnostics catch errors before runtime |
21 | | -- **🔧 C# Interceptors** - Direct method calls using cutting-edge C# interceptor technology |
22 | | -- **🎯 Built-in Result Type** - Comprehensive discriminated union for handling all operation outcomes |
| 10 | +- 🚀 Near-direct call performance, zero runtime reflection |
| 11 | +- ⚡ Convention-based handler discovery (no interfaces/base classes) |
| 12 | +- 🔧 Full DI support via Microsoft.Extensions.DependencyInjection |
| 13 | +- 🧩 Plain handler classes or static methods—just drop them in |
| 14 | +- 🎪 Middleware pipeline with Before/After/Finally hooks |
| 15 | +- 🎯 Built-in Result and Result\<T> types for rich status handling |
| 16 | +- 🔄 Automatic cascading messages via tuple returns |
| 17 | +- 🔒 Compile-time diagnostics and validation |
| 18 | +- 📦 Auto-registration with no boilerplate |
23 | 19 |
|
24 | | -## 🚀 Quick Start |
| 20 | +## 1. Simple Handler |
25 | 21 |
|
26 | | -### 1. Install the Package |
| 22 | +Just add any class ending with `Handler` or `Consumer`: |
27 | 23 |
|
28 | | -```bash |
29 | | -dotnet add package Foundatio.Mediator |
| 24 | +```csharp |
| 25 | +public record Ping(string Text); |
| 26 | + |
| 27 | +public static class PingHandler |
| 28 | +{ |
| 29 | + public static string Handle(Ping msg) => $"Pong: {msg.Text}"; |
| 30 | +} |
30 | 31 | ``` |
31 | 32 |
|
32 | | -### 2. Register the Mediator |
| 33 | +Call it: |
33 | 34 |
|
34 | 35 | ```csharp |
35 | | -services.AddMediator(); |
| 36 | +var reply = mediator.Invoke<string>(new Ping("Hello")); |
36 | 37 | ``` |
37 | 38 |
|
38 | | -### 3. Create Clean, Simple Handlers with Built-in Result Types |
| 39 | +## 2. Dependency Injection in Handlers |
| 40 | + |
| 41 | +Supports constructor and method injection: |
39 | 42 |
|
40 | 43 | ```csharp |
41 | | -// Messages (any class/record) |
42 | | -public record CreateUserCommand |
| 44 | +public class EmailHandler |
43 | 45 | { |
44 | | - [Required, StringLength(50, MinimumLength = 2)] |
45 | | - public string Name { get; set; } = string.Empty; |
46 | | - |
47 | | - [Required, EmailAddress] |
48 | | - public string Email { get; set; } = string.Empty; |
| 46 | + private readonly IEmailService _svc; |
| 47 | + public EmailHandler(IEmailService svc) => _svc = svc; |
49 | 48 |
|
50 | | - [Range(18, 120)] |
51 | | - public int Age { get; set; } |
| 49 | + public Task HandleAsync(SendEmail cmd, ILogger<EmailHandler> log, CancellationToken ct) |
| 50 | + { |
| 51 | + log.LogInformation("Sending to {To}", cmd.To); |
| 52 | + return _svc.SendAsync(cmd.To, cmd.Subject, cmd.Body, ct); |
| 53 | + } |
52 | 54 | } |
| 55 | +``` |
53 | 56 |
|
54 | | -public record User(int Id, string Name, string Email, int Age, DateTime CreatedAt); |
| 57 | +## 3. Simple Middleware |
55 | 58 |
|
56 | | -// Handlers return Result<T> for comprehensive status handling |
57 | | -public class CreateUserCommandHandler |
| 59 | +Discovered by convention; static or instance with DI: |
| 60 | + |
| 61 | +```csharp |
| 62 | +public static class ValidationMiddleware |
58 | 63 | { |
59 | | - public async Task<Result<User>> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken = default) |
60 | | - { |
61 | | - // Business logic validation |
62 | | - if (command.Email == "existing@example.com") |
63 | | - return Result.Conflict("A user with this email already exists"); |
| 64 | + public static HandlerResult Before(object msg) |
| 65 | + => MiniValidator.TryValidate(msg, out var errs) |
| 66 | + ? HandlerResult.Continue() |
| 67 | + : HandlerResult.ShortCircuit(Result.Invalid(errs)); |
| 68 | +} |
| 69 | +``` |
64 | 70 |
|
65 | | - // Create the user |
66 | | - var user = new User( |
67 | | - Id: Random.Shared.Next(1000, 9999), |
68 | | - Name: command.Name, |
69 | | - Email: command.Email, |
70 | | - Age: command.Age, |
71 | | - CreatedAt: DateTime.UtcNow |
72 | | - ); |
| 71 | +## 4. Logging Middleware Example |
73 | 72 |
|
74 | | - return user; // Implicit conversion to Result<User> |
| 73 | +```csharp |
| 74 | +public class LoggingMiddleware |
| 75 | +{ |
| 76 | + public Stopwatch Before(object msg) => Stopwatch.StartNew(); |
| 77 | + |
| 78 | + public void Finally(object msg, Stopwatch sw, Exception? ex) |
| 79 | + { |
| 80 | + sw.Stop(); |
| 81 | + if (ex != null) |
| 82 | + Console.WriteLine($"Error in {msg.GetType().Name}: {ex.Message}"); |
| 83 | + else |
| 84 | + Console.WriteLine($"Handled {msg.GetType().Name} in {sw.ElapsedMilliseconds}ms"); |
75 | 85 | } |
76 | 86 | } |
77 | 87 | ``` |
78 | 88 |
|
79 | | -### 4. Use the Mediator with Result Pattern |
| 89 | +## 5. Using Result<T> |
80 | 90 |
|
81 | 91 | ```csharp |
82 | | -var mediator = serviceProvider.GetRequiredService<IMediator>(); |
83 | | - |
84 | | -// Create a user with comprehensive result handling |
85 | | -var result = await mediator.InvokeAsync<Result<User>>(new CreateUserCommand |
86 | | -{ |
87 | | - Name = "John Doe", |
88 | | - Email = "john@example.com", |
89 | | - Age = 30 |
90 | | -}); |
91 | | - |
92 | | -// Handle different outcomes with pattern matching |
93 | | -var response = result.Status switch |
| 92 | +public class GetUserHandler |
94 | 93 | { |
95 | | - ResultStatus.Ok => $"User created successfully: {result.Value.Name}", |
96 | | - ResultStatus.Invalid => $"Validation failed: {string.Join(", ", result.Errors.Select(e => e.ErrorMessage))}", |
97 | | - ResultStatus.Conflict => $"Conflict: {result.ErrorMessage}", |
98 | | - _ => "Unexpected result" |
99 | | -}; |
100 | | - |
101 | | -Console.WriteLine(response); |
| 94 | + public Task<Result<User>> HandleAsync(GetUser cmd) |
| 95 | + => _repo.Find(cmd.Id) is { } user |
| 96 | + ? Task.FromResult(Result.Ok(user)) |
| 97 | + : Task.FromResult(Result.NotFound($"User {cmd.Id} not found")); |
| 98 | +} |
102 | 99 | ``` |
103 | 100 |
|
104 | | -## 🎯 Built-in Result Type - Essential for Message-Oriented Architecture |
| 101 | +## 6. Invocation API |
105 | 102 |
|
106 | | -Foundatio.Mediator includes a comprehensive `Result` and `Result<T>` type that acts as a discriminated union, allowing handlers to return different operation outcomes without exceptions. This is crucial for message-oriented architectures where you need to handle various scenarios gracefully. |
| 103 | +```csharp |
| 104 | +// With response |
| 105 | +var user = await mediator.InvokeAsync<User>(new GetUser(id)); |
107 | 106 |
|
108 | | -### Why Result Types Matter |
| 107 | +// Without response |
| 108 | +await mediator.InvokeAsync(new Ping("Hi")); |
| 109 | +``` |
109 | 110 |
|
110 | | -In message-oriented systems, operations can have many outcomes beyond just success/failure: |
| 111 | +## 7. Tuple Returns & Cascading Messages |
111 | 112 |
|
112 | | -- **Success** with data |
113 | | -- **Validation errors** with detailed field-level messages |
114 | | -- **Business rule violations** (conflicts, unauthorized access) |
115 | | -- **Not found** scenarios |
116 | | -- **Created** responses with location information |
| 113 | +Handlers can return tuples; one matches the response, the rest are published: |
117 | 114 |
|
118 | | -The Result type captures all these scenarios in a type-safe way without throwing exceptions. |
| 115 | +```csharp |
| 116 | +public async Task<(User user, UserCreated evt)> HandleAsync(CreateUser cmd) |
| 117 | +{ |
| 118 | + var user = await _repo.Add(cmd); |
| 119 | + return (user, new UserCreated(user.Id)); |
| 120 | +} |
119 | 121 |
|
120 | | -### Result Creation Methods |
| 122 | +// Usage |
| 123 | +var user = await mediator.InvokeAsync<User>(new CreateUser(...)); |
| 124 | +// UserCreated is auto-published |
| 125 | +``` |
| 126 | + |
| 127 | +## 8. Publish API |
121 | 128 |
|
122 | 129 | ```csharp |
123 | | -// Success results |
124 | | -var user = new User(1, "John", "john@example.com", 30, DateTime.UtcNow); |
125 | | -return user; // Implicit conversion to Result<User> |
126 | | -return Result.Ok(user); // Explicit success |
127 | | -return Result.Created(user, "/api/users/1"); // Created with location |
128 | | -
|
129 | | -// Error results |
130 | | -return Result.NotFound("User not found"); |
131 | | -return Result.Invalid(validationErrors); |
132 | | -return Result.Conflict("Email already exists"); |
133 | | -return Result.Unauthorized("Login required"); |
134 | | - |
135 | | -// Generic results (non-generic Result class) |
136 | | -return Result.Ok(); // Success with no return value |
137 | | -return Result.NoContent(); // Success with no content |
| 130 | +await mediator.PublishAsync(new OrderShipped(orderId)); |
138 | 131 | ``` |
139 | 132 |
|
| 133 | +All handlers run in parallel; if any fail, PublishAsync throws. |
| 134 | + |
140 | 135 | ### Example: Complete CRUD with Result Types |
141 | 136 |
|
142 | 137 | ```csharp |
@@ -273,107 +268,6 @@ public class ComplexOrderHandler |
273 | 268 | } |
274 | 269 | ``` |
275 | 270 |
|
276 | | -## 🎪 Beautiful Middleware Pipeline |
277 | | - |
278 | | -Create elegant middleware that runs before, after, and finally around your handlers. Middleware works seamlessly with the Result type for comprehensive error handling: |
279 | | - |
280 | | -```csharp |
281 | | -public class LoggingMiddleware |
282 | | -{ |
283 | | - private readonly ILogger<LoggingMiddleware> _logger; |
284 | | - |
285 | | - public LoggingMiddleware(ILogger<LoggingMiddleware> logger) |
286 | | - { |
287 | | - _logger = logger; |
288 | | - } |
289 | | - |
290 | | - public Stopwatch Before(object message) |
291 | | - { |
292 | | - _logger.LogInformation("Processing {MessageType}", message.GetType().Name); |
293 | | - return Stopwatch.StartNew(); |
294 | | - } |
295 | | - |
296 | | - public void Finally(object message, Stopwatch stopwatch, Exception? exception) |
297 | | - { |
298 | | - stopwatch.Stop(); |
299 | | - if (exception != null) |
300 | | - { |
301 | | - _logger.LogError(exception, "Error processing {MessageType}", message.GetType().Name); |
302 | | - } |
303 | | - else |
304 | | - { |
305 | | - _logger.LogInformation("Completed {MessageType} in {ElapsedMs}ms", |
306 | | - message.GetType().Name, stopwatch.ElapsedMilliseconds); |
307 | | - } |
308 | | - } |
309 | | -} |
310 | | - |
311 | | -public static class ValidationMiddleware |
312 | | -{ |
313 | | - public static HandlerResult Before(object message) |
314 | | - { |
315 | | - if (MiniValidator.TryValidate(message, out var errors)) |
316 | | - return HandlerResult.Continue(); |
317 | | - |
318 | | - var validationErrors = errors.SelectMany(kvp => |
319 | | - kvp.Value.Select(errorMessage => new ValidationError(kvp.Key, errorMessage))).ToList(); |
320 | | - |
321 | | - return HandlerResult.ShortCircuit(Result.Invalid(validationErrors)); |
322 | | - } |
323 | | -} |
324 | | -``` |
325 | | - |
326 | | -With this middleware, your handlers automatically get validation without any boilerplate: |
327 | | - |
328 | | -```csharp |
329 | | -// This command will be automatically validated by ValidationMiddleware |
330 | | -var result = await mediator.InvokeAsync<Result<User>>(new CreateUserCommand |
331 | | -{ |
332 | | - Name = "", // Invalid - too short |
333 | | - Email = "not-an-email", // Invalid - bad format |
334 | | - Age = 10 // Invalid - too young |
335 | | -}); |
336 | | - |
337 | | -if (result.Status == ResultStatus.Invalid) |
338 | | -{ |
339 | | - foreach (var error in result.Errors) |
340 | | - Console.WriteLine($"{error.PropertyName}: {error.ErrorMessage}"); |
341 | | -} |
342 | | -``` |
343 | | - |
344 | | -## 💉 Dependency Injection Made Simple |
345 | | - |
346 | | -Handlers support both constructor and method-level dependency injection: |
347 | | - |
348 | | -```csharp |
349 | | -public class SendWelcomeEmailHandler |
350 | | -{ |
351 | | - private readonly IEmailService _emailService; |
352 | | - private readonly IGreetingService _greetingService; |
353 | | - private readonly ILogger<SendWelcomeEmailHandler> _logger; |
354 | | - |
355 | | - public SendWelcomeEmailHandler( |
356 | | - IEmailService emailService, |
357 | | - IGreetingService greetingService, |
358 | | - ILogger<SendWelcomeEmailHandler> logger) |
359 | | - { |
360 | | - _emailService = emailService; |
361 | | - _greetingService = greetingService; |
362 | | - _logger = logger; |
363 | | - } |
364 | | - |
365 | | - public async Task HandleAsync( |
366 | | - SendWelcomeEmailCommand command, |
367 | | - CancellationToken cancellationToken = default) // Provided by mediator |
368 | | - { |
369 | | - _logger.LogInformation("Sending welcome email to {Email}", command.Email); |
370 | | - |
371 | | - var greeting = _greetingService.CreateGreeting(command.Name); |
372 | | - await _emailService.SendEmailAsync(command.Email, "Welcome!", greeting); |
373 | | - } |
374 | | -} |
375 | | -``` |
376 | | - |
377 | 271 | ## 📊 Performance Benchmarks |
378 | 272 |
|
379 | 273 | Foundatio.Mediator delivers exceptional performance, getting remarkably close to direct method calls while providing full mediator pattern benefits: |
@@ -454,30 +348,9 @@ public interface IMediator |
454 | 348 |
|
455 | 349 | // Publishing (multiple handlers) |
456 | 350 | Task PublishAsync(object message, CancellationToken cancellationToken = default); |
457 | | - void Publish(object message, CancellationToken cancellationToken = default); |
458 | 351 | } |
459 | 352 | ``` |
460 | 353 |
|
461 | | -## 🎬 Sample Applications |
462 | | - |
463 | | -### ConsoleSample - Comprehensive Demonstration |
464 | | - |
465 | | -The `samples/ConsoleSample` project provides a complete demonstration of all Foundatio.Mediator features: |
466 | | - |
467 | | -- **Simple Commands & Queries** - Basic fire-and-forget and request/response patterns |
468 | | -- **Dependency Injection** - Handler methods with injected services and logging |
469 | | -- **Publish/Subscribe** - Multiple handlers for the same event |
470 | | -- **Mixed Sync/Async** - Both synchronous and asynchronous handler examples |
471 | | -- **Middleware Pipeline** - Global and message-specific middleware examples |
472 | | -- **Service Integration** - Email, SMS, and audit service examples |
473 | | - |
474 | | -To run the comprehensive sample: |
475 | | - |
476 | | -```bash |
477 | | -cd samples/ConsoleSample |
478 | | -dotnet run |
479 | | -``` |
480 | | - |
481 | 354 | ## ⚙️ How It Works |
482 | 355 |
|
483 | 356 | The source generator: |
@@ -516,8 +389,6 @@ foreach (var handler in handlers) |
516 | 389 |
|
517 | 390 | - **Interceptors First** - Same-assembly calls use interceptors for maximum performance |
518 | 391 | - **DI Fallback** - Cross-assembly handlers and publish operations use DI registration |
519 | | -- **Automatic Selection** - The generator chooses the optimal strategy per call site |
520 | | -- **Keyed Services** - Handlers registered by fully qualified message type name |
521 | 392 | - **Zero Runtime Overhead** - Interceptors bypass all runtime lookup completely |
522 | 393 |
|
523 | 394 | **Benefits:** |
|
0 commit comments