Skip to content

Commit ff0548a

Browse files
committed
Update readme and other changes
1 parent 0a0a6d7 commit ff0548a

File tree

6 files changed

+314
-150
lines changed

6 files changed

+314
-150
lines changed

README.md

Lines changed: 218 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Foundatio.Mediator is a high-performance, convention-based mediator library that
1818
- **📦 Auto Registration** - Handlers discovered and registered automatically
1919
- **🔒 Compile-Time Safety** - Rich diagnostics catch errors before runtime
2020
- **🔧 C# Interceptors** - Direct method calls using cutting-edge C# interceptor technology
21+
- **🎯 Built-in Result Type** - Comprehensive discriminated union for handling all operation outcomes
2122

2223
## 🚀 Quick Start
2324

@@ -33,48 +34,218 @@ dotnet add package Foundatio.Mediator
3334
services.AddMediator();
3435
```
3536

36-
### 3. Create Clean, Simple Handlers
37+
### 3. Create Clean, Simple Handlers with Built-in Result Types
3738

3839
```csharp
3940
// Messages (any class/record)
40-
public record PingCommand(string Id);
41-
public record GreetingQuery(string Name);
42-
43-
// Handlers - just classes ending with "Handler" or "Consumer"
44-
public class PingHandler
41+
public record CreateUserCommand
4542
{
46-
public async Task HandleAsync(PingCommand command, CancellationToken cancellationToken = default)
47-
{
48-
Console.WriteLine($"Ping {command.Id} received!");
49-
}
43+
[Required, StringLength(50, MinimumLength = 2)]
44+
public string Name { get; set; } = string.Empty;
45+
46+
[Required, EmailAddress]
47+
public string Email { get; set; } = string.Empty;
48+
49+
[Range(18, 120)]
50+
public int Age { get; set; }
5051
}
5152

52-
public class GreetingHandler
53+
public record User(int Id, string Name, string Email, int Age, DateTime CreatedAt);
54+
55+
// Handlers return Result<T> for comprehensive status handling
56+
public class CreateUserCommandHandler
5357
{
54-
public string Handle(GreetingQuery query)
58+
public async Task<Result<User>> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken = default)
5559
{
56-
return $"Hello, {query.Name}!";
60+
// Business logic validation
61+
if (command.Email == "existing@example.com")
62+
return Result.Conflict("A user with this email already exists");
63+
64+
// Create the user
65+
var user = new User(
66+
Id: Random.Shared.Next(1000, 9999),
67+
Name: command.Name,
68+
Email: command.Email,
69+
Age: command.Age,
70+
CreatedAt: DateTime.UtcNow
71+
);
72+
73+
return user; // Implicit conversion to Result<User>
5774
}
5875
}
5976
```
6077

61-
### 4. Use the Mediator
78+
### 4. Use the Mediator with Result Pattern
6279

6380
```csharp
6481
var mediator = serviceProvider.GetRequiredService<IMediator>();
6582

66-
// Fire and forget
67-
await mediator.InvokeAsync(new PingCommand("123"));
83+
// Create a user with comprehensive result handling
84+
var result = await mediator.InvokeAsync<Result<User>>(new CreateUserCommand
85+
{
86+
Name = "John Doe",
87+
Email = "john@example.com",
88+
Age = 30
89+
});
90+
91+
// Handle different outcomes with pattern matching
92+
var response = result.Status switch
93+
{
94+
ResultStatus.Ok => $"User created successfully: {result.Value.Name}",
95+
ResultStatus.Invalid => $"Validation failed: {string.Join(", ", result.Errors.Select(e => e.ErrorMessage))}",
96+
ResultStatus.Conflict => $"Conflict: {result.ErrorMessage}",
97+
_ => "Unexpected result"
98+
};
6899

69-
// Request/response
70-
var greeting = mediator.Invoke<string>(new GreetingQuery("World"));
71-
Console.WriteLine(greeting); // "Hello, World!"
100+
Console.WriteLine(response);
101+
```
102+
103+
## 🎯 Result Types: The Foundation of Robust Message-Oriented Architecture
104+
105+
The built-in `Result` and `Result<T>` types are fundamental to Foundatio.Mediator's design, providing a discriminated union pattern that's essential for message-oriented architectures. Instead of relying on exceptions for control flow, Result types enable explicit, type-safe handling of all operation outcomes.
106+
107+
### Why Result Types Are Critical
108+
109+
**Message-oriented architectures benefit from Result types because:**
110+
111+
- **🎯 Explicit Error Handling** - All possible outcomes are represented in the type system
112+
- **🚫 No Hidden Exceptions** - Errors are data, not exceptional control flow
113+
- **📊 Rich Status Information** - Beyond success/failure: validation, conflicts, authorization, etc.
114+
- **🔄 Composable Operations** - Chain operations with confidence about what can happen
115+
- **📈 Better Observability** - Track success rates, error patterns, and business metrics
116+
- **🛡️ Defensive Programming** - Force consumers to handle all possible scenarios
117+
118+
**Traditional Exception-Based Approach:**
119+
120+
```csharp
121+
try
122+
{
123+
var user = await mediator.InvokeAsync<User>(new GetUserQuery(id));
124+
// What could go wrong? NotFound? Unauthorized? Validation? Who knows!
125+
}
126+
catch (NotFoundException ex) { /* Handle */ }
127+
catch (UnauthorizedException ex) { /* Handle */ }
128+
catch (ValidationException ex) { /* Handle */ }
129+
// Did we catch everything? Are we sure?
130+
```
131+
132+
**Result-Based Approach:**
133+
134+
```csharp
135+
var result = await mediator.InvokeAsync<Result<User>>(new GetUserQuery(id));
136+
137+
var response = result.Status switch
138+
{
139+
ResultStatus.Ok => $"Found user: {result.Value.Name}",
140+
ResultStatus.NotFound => "User not found",
141+
ResultStatus.Unauthorized => "Access denied",
142+
ResultStatus.Invalid => $"Validation failed: {string.Join(", ", result.Errors.Select(e => e.ErrorMessage))}",
143+
_ => "Unexpected status"
144+
};
145+
146+
// Compiler ensures all scenarios are handled!
147+
```
148+
149+
## 🎯 Built-in Result Type - Essential for Message-Oriented Architecture
150+
151+
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.
152+
153+
### Why Result Types Matter
154+
155+
In message-oriented systems, operations can have many outcomes beyond just success/failure:
156+
157+
- **Success** with data
158+
- **Validation errors** with detailed field-level messages
159+
- **Business rule violations** (conflicts, unauthorized access)
160+
- **Not found** scenarios
161+
- **Created** responses with location information
162+
163+
The Result type captures all these scenarios in a type-safe way without throwing exceptions.
164+
165+
### Available Result Statuses
166+
167+
```csharp
168+
public enum ResultStatus
169+
{
170+
Ok, // Operation successful
171+
Created, // Resource created successfully
172+
NoContent, // Success with no data to return
173+
Error, // General error occurred
174+
Invalid, // Validation failed
175+
NotFound, // Resource not found
176+
Unauthorized, // Authentication required
177+
Forbidden, // Authorization failed
178+
Conflict, // Business rule conflict
179+
CriticalError // Critical system error
180+
}
181+
```
182+
183+
### Result Creation Methods
184+
185+
```csharp
186+
// Success results
187+
var user = new User(1, "John", "john@example.com", 30, DateTime.UtcNow);
188+
return user; // Implicit conversion to Result<User>
189+
return Result.Ok(user); // Explicit success
190+
return Result.Created(user, "/api/users/1"); // Created with location
191+
192+
// Error results
193+
return Result.NotFound("User not found");
194+
return Result.Invalid(validationErrors);
195+
return Result.Conflict("Email already exists");
196+
return Result.Unauthorized("Login required");
197+
198+
// Generic results (non-generic Result class)
199+
return Result.Ok(); // Success with no return value
200+
return Result.NoContent(); // Success with no content
201+
```
202+
203+
### Example: Complete CRUD with Result Types
204+
205+
```csharp
206+
public record GetUserQuery(int Id);
207+
public record UpdateUserCommand(int Id, string Name, string Email);
208+
public record DeleteUserCommand(int Id);
209+
210+
public class UserHandler
211+
{
212+
public async Task<Result<User>> HandleAsync(GetUserQuery query)
213+
{
214+
var user = await _repository.GetByIdAsync(query.Id);
215+
return user != null
216+
? Result.Ok(user)
217+
: Result.NotFound($"User with ID {query.Id} not found");
218+
}
219+
220+
public async Task<Result<User>> HandleAsync(UpdateUserCommand command)
221+
{
222+
var existingUser = await _repository.GetByIdAsync(command.Id);
223+
if (existingUser == null)
224+
return Result.NotFound($"User with ID {command.Id} not found");
225+
226+
if (await _repository.EmailExistsAsync(command.Email, command.Id))
227+
return Result.Conflict("Another user already has this email address");
228+
229+
var updatedUser = existingUser with { Name = command.Name, Email = command.Email };
230+
await _repository.UpdateAsync(updatedUser);
231+
232+
return Result.Ok(updatedUser);
233+
}
234+
235+
public async Task<Result> HandleAsync(DeleteUserCommand command)
236+
{
237+
var deleted = await _repository.DeleteAsync(command.Id);
238+
return deleted
239+
? Result.NoContent()
240+
: Result.NotFound($"User with ID {command.Id} not found");
241+
}
242+
}
72243
```
73244

74245

75246
## 🎪 Beautiful Middleware Pipeline
76247

77-
Create elegant middleware that runs before, after, and finally around your handlers:
248+
Create elegant middleware that runs before, after, and finally around your handlers. Middleware works seamlessly with the Result type for comprehensive error handling:
78249

79250
```csharp
80251
public class LoggingMiddleware
@@ -111,17 +282,38 @@ public class ValidationMiddleware
111282
{
112283
public HandlerResult Before(object message)
113284
{
114-
if (!TryValidate(message, out var errors))
285+
if (!MiniValidator.TryValidate(message, out var errors))
115286
{
116-
// If validation fails, short-circuit the handler execution
117-
return HandlerResult.ShortCircuit(Result.Invalid(errors));
287+
// Convert validation errors to Result format
288+
var validationErrors = errors.SelectMany(kvp =>
289+
kvp.Value.Select(errorMessage =>
290+
new ValidationError(kvp.Key, errorMessage))).ToList();
291+
292+
// Short-circuit handler execution and return validation result
293+
return HandlerResult.ShortCircuit(Result.Invalid(validationErrors));
118294
}
119295

120296
return HandlerResult.Continue();
121297
}
122298
}
299+
```
300+
301+
With this middleware, your handlers automatically get validation without any boilerplate:
302+
303+
```csharp
304+
// This command will be automatically validated by ValidationMiddleware
305+
var result = await mediator.InvokeAsync<Result<User>>(new CreateUserCommand
306+
{
307+
Name = "", // Invalid - too short
308+
Email = "not-an-email", // Invalid - bad format
309+
Age = 10 // Invalid - too young
310+
});
123311

124-
var user = await mediator.InvokeAsync<Result<User>>(new GetUserQuery(userId), cancellationToken);
312+
if (result.Status == ResultStatus.Invalid)
313+
{
314+
foreach (var error in result.Errors)
315+
Console.WriteLine($"{error.PropertyName}: {error.ErrorMessage}");
316+
}
125317
```
126318

127319
## 💉 Dependency Injection Made Simple
@@ -268,8 +460,8 @@ The source generator:
268460
1. **Discovers handlers** at compile time by scanning for classes ending with `Handler` or `Consumer`
269461
2. **Discovers handler methods** looks for methods with names like `Handle`, `HandleAsync`, `Consume`, `ConsumeAsync`
270462
3. **Parameters** first parameter is the message, remaining parameters are injected via DI
271-
3. **Generates C# interceptors** for blazing fast same-assembly dispatch using direct method calls
272-
4. **Middleware** with can run `Before`, `After`, and `Finally` around handler execution and can be sync or async
463+
4. **Generates C# interceptors** for blazing fast same-assembly dispatch using direct method calls
464+
5. **Middleware** can run `Before`, `After`, and `Finally` around handler execution and can be sync or async
273465

274466
### 🔧 C# Interceptors - The Secret Sauce
275467

src/Foundatio.Mediator.SourceGenerator/MediatorImplementationGenerator.cs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -180,25 +180,6 @@ public static string GenerateMediatorImplementation(List<HandlerInfo> handlers)
180180
source.AppendLine(" }");
181181
source.AppendLine();
182182

183-
// Generate Publish method (sync)
184-
source.AppendLine(" public void Publish(object message, CancellationToken cancellationToken = default)");
185-
source.AppendLine(" {");
186-
source.AppendLine(" var handlersList = GetAllApplicableHandlers(message).ToList();");
187-
source.AppendLine();
188-
source.AppendLine(" // Check if any handlers require async execution");
189-
source.AppendLine(" if (handlersList.Any(h => h.IsAsync))");
190-
source.AppendLine(" {");
191-
source.AppendLine(" var messageTypeName = message.GetType().FullName;");
192-
source.AppendLine(" throw new InvalidOperationException($\"Cannot use synchronous Publish with async-only handlers for message type {messageTypeName}. Use PublishAsync instead.\");");
193-
source.AppendLine(" }");
194-
source.AppendLine();
195-
source.AppendLine(" // Execute all handlers synchronously");
196-
source.AppendLine(" foreach (var handler in handlersList)");
197-
source.AppendLine(" {");
198-
source.AppendLine(" handler.Handle!(this, message, cancellationToken, null);");
199-
source.AppendLine(" }");
200-
source.AppendLine(" }");
201-
source.AppendLine();
202183
source.AppendLine(" private static readonly System.Collections.Concurrent.ConcurrentDictionary<System.Type, object> _middlewareCache = new();");
203184
source.AppendLine(" [DebuggerStepThrough]");
204185
source.AppendLine(" internal static T GetOrCreateMiddleware<T>(IServiceProvider serviceProvider) where T : class");

src/Foundatio.Mediator/IMediator.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,4 @@ public interface IMediator
7979
/// and the first exception encountered will be thrown after all handlers complete.
8080
/// </remarks>
8181
ValueTask PublishAsync(object message, CancellationToken cancellationToken = default);
82-
83-
/// <summary>
84-
/// Synchronously publishes a message to zero or more handlers.
85-
/// </summary>
86-
/// <param name="message">The message to publish to handlers.</param>
87-
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
88-
/// <remarks>
89-
/// All handlers for the message type will be executed sequentially. If any handler throws an exception,
90-
/// all other handlers will still execute, and the first exception encountered will be thrown after all
91-
/// handlers complete. This method can only be used with handlers that have synchronous implementations.
92-
/// </remarks>
93-
void Publish(object message, CancellationToken cancellationToken = default);
9482
}

tests/Foundatio.Mediator.Tests/FixedPublishTest.cs

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,6 @@ public FixedPublishTest(ITestOutputHelper output) : base(output)
1212
{
1313
}
1414

15-
[Fact]
16-
public void Publish_WithTwoSyncHandlers_CallsAllHandlers()
17-
{
18-
// Arrange
19-
var services = new ServiceCollection();
20-
services.AddLogging(builder => builder.AddTestLogger());
21-
services.AddMediator();
22-
services.AddSingleton<FixedTestService>();
23-
var serviceProvider = services.BuildServiceProvider();
24-
var mediator = serviceProvider.GetRequiredService<IMediator>();
25-
var testService = serviceProvider.GetRequiredService<FixedTestService>();
26-
27-
var command = new FixedSyncCommand("Fixed Sync Test");
28-
29-
_logger.LogInformation("Starting synchronous Publish test with message: {Message}", command.Message);
30-
31-
// Act
32-
mediator.Publish(command);
33-
34-
// Assert
35-
_logger.LogInformation("Sync Publish completed. CallCount: {CallCount}, Messages: {Messages}",
36-
testService.CallCount, String.Join(", ", testService.Messages));
37-
38-
Assert.Equal(2, testService.CallCount); // Two handlers should be called for FixedSyncCommand
39-
Assert.Contains("FixedSyncCommand1Handler: Fixed Sync Test", testService.Messages);
40-
Assert.Contains("FixedSyncCommand2Handler: Fixed Sync Test", testService.Messages);
41-
}
42-
4315
[Fact]
4416
public async Task PublishAsync_WithTwoAsyncHandlers_CallsAllHandlers()
4517
{

0 commit comments

Comments
 (0)