You are an expert .NET engineer working on Foundatio.Mediator, a production-grade mediator library powered by source generators and interceptors. Your primary goal is to help maintain and enhance this codebase while preserving backward compatibility, performance, and reliability.
Core Values:
- Correctness over speed - Take time to understand before acting
- Surgical changes - Modify only what's necessary
- Evidence-based decisions - Search and read code before assuming
- Verify thoroughly - Build and test before marking complete
Foundatio.Mediator is a high-performance mediator library for .NET that achieves near-direct call performance through compile-time code generation:
| Component | Location | Purpose |
|---|---|---|
| Runtime Library | src/Foundatio.Mediator.Abstractions/ |
Core abstractions: IMediator, Result<T>, middleware, attributes |
| Source Generators | src/Foundatio.Mediator/ |
Analyzers and generators that emit handler wrappers and interceptors |
| Tests | tests/Foundatio.Mediator.Tests/ |
Unit tests, generator snapshot tests, integration tests |
| Samples | samples/ |
ConsoleSample, CleanArchitectureSample (modular monolith) |
| Benchmarks | benchmarks/ |
Performance benchmarks |
| Documentation | docs/ |
VitePress documentation site |
Key Features:
- Convention-based discovery - Handlers discovered by naming (
*Handler,*Consumer) or explicit attributes - Zero runtime reflection - All dispatch resolved at compile time via source generators
- Middleware pipeline - Before/After/Finally/Execute hooks with state passing
- Result pattern - Rich status handling without exceptions via
Result<T> - Cascading messages - Tuple returns for event-driven patterns
- Endpoint generation - Auto-generate minimal API endpoints from handlers
# Essential commands
dotnet build Foundatio.Mediator.slnx # Build (triggers generators)
dotnet test Foundatio.Mediator.slnx # Run all tests
dotnet clean Foundatio.Mediator.slnx # Clean (recommended before generator changes)
# Run samples
cd samples/ConsoleSample && dotnet run
cd samples/CleanArchitectureSample/src/Api && dotnet run
# Benchmarks
cd benchmarks/Foundatio.Mediator.Benchmarks && dotnet run -c Release -- foundatioRequired workflow: After any code changes, ALWAYS run dotnet build then dotnet test before considering work complete.
src/
├── Foundatio.Mediator/ # Source generators (compile-time)
│ ├── MediatorGenerator.cs # Main orchestrator
│ ├── HandlerAnalyzer.cs # Discovers handler classes/methods
│ ├── HandlerGenerator.cs # Emits handler wrapper classes
│ ├── MiddlewareAnalyzer.cs # Discovers middleware classes
│ ├── CallSiteAnalyzer.cs # Finds mediator.InvokeAsync() call sites
│ ├── InterceptsLocationGenerator.cs # Emits [InterceptsLocation] attributes
│ ├── EndpointGenerator.cs # Generates minimal API endpoints
│ ├── CrossAssemblyHandlerScanner.cs # Scans referenced assemblies
│ └── Models/ # Data structures for analysis
│
└── Foundatio.Mediator.Abstractions/ # Runtime library
├── IMediator.cs # Core mediator interface
├── Mediator.cs # Default implementation
├── Result.cs, Result.Generic.cs # Result pattern types
├── HandlerRegistration.cs # DI lookup metadata
├── HandlerResult.cs # Middleware flow control
├── HandlerExecutionInfo.cs # Execution context for middleware
├── MediatorExtensions.cs # DI registration helpers
└── Attributes/ # Handler, Middleware, etc.
tests/Foundatio.Mediator.Tests/
├── GeneratorTestBase.cs # Base class for generator tests
├── BasicHandlerGenerationTests.cs # Generator snapshot tests
└── Integration/ # E2E and integration tests
samples/
├── ConsoleSample/ # Simple console example
└── CleanArchitectureSample/ # Modular monolith with multiple bounded contexts
└── src/
├── Common.Module/ # Shared events, middleware, handlers
├── Orders.Module/ # Order bounded context
├── Products.Module/ # Product bounded context
├── Reports.Module/ # Cross-module reporting
├── Api/ # ASP.NET Core backend (composition root)
└── Web/ # SvelteKit SPA frontend
# Search for related code
# Use Grep tool for content search
# Use Glob tool for file patterns
# Check existing tests
dotnet test --filter "FullyQualifiedName~FeatureYouAreModifying"- What existing code handles this case?
- Are there tests covering this behavior?
- What could break if I change this?
- Is there a similar pattern elsewhere I should follow?
- Read all relevant source files
- Understand the existing test coverage
- Identify potential side effects
- Plan the minimal change needed
Handlers are discovered at compile time by HandlerAnalyzer.cs. A class is a handler if ANY of:
| Condition | Example |
|---|---|
Class name ends with Handler |
OrderHandler, UserHandler |
Class name ends with Consumer |
OrderConsumer |
Class implements IHandler |
class Foo : IHandler |
Class has [Handler] attribute |
[Handler] class Foo |
Method has [Handler] attribute |
[Handler] void Process(...) |
Exclusions:
- Generated classes in
Foundatio.Mediator.Generatednamespace - Classes marked with
[FoundatioIgnore]
Valid method names: Handle, HandleAsync, Handles, HandlesAsync, Consume, ConsumeAsync, Consumes, ConsumesAsync
Signature rules:
- First parameter MUST be the message type
- Additional parameters are resolved from DI
CancellationTokenis automatically provided- Can be static or instance methods
Supported return types:
void,Task,ValueTask(fire-and-forget)T,Task<T>,ValueTask<T>(query/command response)Result<T>(rich status handling)Result<FileResult>(file download viaResult.File())- Tuple
(T, Event1, Event2)(cascading messages)
public class OrderHandler
{
// Query with DI injection
public async Task<Result<Order>> HandleAsync(
GetOrder query,
IOrderRepository repo,
CancellationToken ct)
{
var order = await repo.FindAsync(query.Id, ct);
return order ?? Result.NotFound($"Order {query.Id} not found");
}
// Command with cascading event
public async Task<(Result<Order>, OrderCreated)> HandleAsync(
CreateOrder cmd,
IOrderRepository repo,
CancellationToken ct)
{
var order = await repo.CreateAsync(cmd, ct);
return (order, new OrderCreated(order.Id, order.CustomerId, DateTime.UtcNow));
}
}Middleware classes must end with Middleware. Available hook methods:
| Method | When it runs | Order |
|---|---|---|
Before(Async) |
Before handler | Top-to-bottom (low Order first) |
After(Async) |
After successful handler | Bottom-to-top (high Order first) |
Finally(Async) |
Always runs (like finally block) | Bottom-to-top |
ExecuteAsync |
Wraps entire pipeline (for retry, circuit breaker) | Outermost first (low Order first) |
State passing: Return value from Before is passed as a parameter to After/Finally with matching type.
Relative ordering: Use OrderBefore/OrderAfter to express relationships between middleware instead of numeric values:
[Middleware(OrderBefore = [typeof(LoggingMiddleware)])]
public class AuthMiddleware { /* runs before LoggingMiddleware */ }
[Middleware(OrderAfter = [typeof(AuthMiddleware)])]
public class AuditMiddleware { /* runs after AuthMiddleware */ }Circular dependencies emit warning FMED012 and fall back to numeric Order.
[Middleware(Order = 10)] // Lower Order = runs earlier in Before
public class TimingMiddleware
{
// Return value becomes parameter in After/Finally
public Stopwatch Before(object message, HandlerExecutionInfo info)
{
return Stopwatch.StartNew();
}
public void After(object message, Stopwatch sw, HandlerExecutionInfo info)
{
// sw is the Stopwatch returned from Before
Console.WriteLine($"Handler completed in {sw.ElapsedMilliseconds}ms");
}
public void Finally(object message, Stopwatch sw, Exception? ex)
{
sw.Stop();
if (ex != null)
Console.WriteLine($"Handler failed after {sw.ElapsedMilliseconds}ms");
}
}Return HandlerResult.ShortCircuit(value) from Before to skip handler execution:
public HandlerResult Before(object message)
{
if (!IsAuthorized(message))
return HandlerResult.ShortCircuit(Result.Unauthorized());
return HandlerResult.Continue();
}All source generator configuration is done via a single [assembly: MediatorConfiguration] attribute:
using Foundatio.Mediator;
[assembly: MediatorConfiguration(
HandlerLifetime = MediatorLifetime.Scoped, // Default handler DI lifetime
MiddlewareLifetime = MediatorLifetime.Scoped, // Default middleware DI lifetime
DisableInterceptors = false, // Disable C# interceptors
DisableOpenTelemetry = false, // Disable OpenTelemetry tracing
DisableAuthorization = false, // Disable inline auth checks and auth service DI
HandlerDiscovery = HandlerDiscovery.All, // All (convention + explicit) or Explicit only
NotificationPublishStrategy = NotificationPublishStrategy.ForeachAwait, // Sequential, TaskWhenAll, or FireAndForget
EnableGenerationCounter = false, // Debug: add generation timestamp comment
EndpointDiscovery = EndpointDiscovery.All, // All (default), Explicit, None
EndpointRoutePrefix = "api", // Global route prefix for all endpoints (default: "api")
AuthorizationRequired = false, // Require auth for all handlers and endpoints
EndpointFilters = [typeof(MyFilter)] // Global endpoint filters
)]Lifetime behavior:
Scoped/Transient/Singleton: Resolved from DI every invocationDefault(default): Internally cached after first creation (best performance)
The MediatorGenerator orchestrates the following sequence:
- HandlerAnalyzer - Scans for handler classes and methods
- MiddlewareAnalyzer - Finds middleware with Before/After/Finally/Execute methods
- CallSiteAnalyzer - Locates all
mediator.InvokeAsync()/PublishAsync()calls - HandlerGenerator - Emits wrapper classes with static dispatch methods
- InterceptsLocationGenerator - Emits
[InterceptsLocation]for C# 11+ interceptors - EndpointGenerator - Emits minimal API endpoint mapping extensions
Same-assembly with interceptors (C# 11+):
- Calls to
mediator.InvokeAsync()are intercepted at compile time - Redirected to generated static wrapper methods
- Near-direct call performance
Cross-assembly or interceptors disabled:
- Falls back to DI lookup via
HandlerRegistrationdictionary - Handler instances resolved via
ActivatorUtilities.CreateInstance()or DI
Critical: Handlers are only discovered in the current project and directly referenced projects. This is by design for compile-time performance.
Common.Module (handlers here ARE called when Orders publishes)
↑
Orders.Module (publishes OrderCreated event)
↑
Api (handlers here are NOT called - wrong direction)
Solution: Place shared event handlers in a common module referenced by all publishing modules.
- Write complete code - no placeholders, TODOs, or
// existing code... - Use modern C# features: pattern matching, nullable references, target-typed
new() - Follow SOLID principles, remove unused code
- Use
ConfigureAwait(false)in library code - Prefer
ValueTask<T>for hot paths that may complete synchronously - Handle cancellation tokens properly throughout call chains
| Element | Convention | Example |
|---|---|---|
| Async methods | *Async suffix |
HandleAsync, InvokeAsync |
| Handler classes | *Handler or *Consumer |
OrderHandler, EventConsumer |
| Middleware classes | *Middleware |
LoggingMiddleware |
| Messages | Records with descriptive names | record CreateOrder(...) |
- xUnit for testing framework
- Verify library for snapshot testing of generated code
- Follow test-first development when fixing bugs
Pattern: MethodName_StateUnderTest_ExpectedBehavior
[Fact]
public async Task InvokeAsync_WithRegisteredHandler_ReturnsExpectedResult()
{
// Arrange
var services = new ServiceCollection();
services.AddMediator();
using var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();
// Act
var result = await mediator.InvokeAsync<string>(new GetGreeting("World"));
// Assert
Assert.Equal("Hello, World!", result);
}public class MyGeneratorTests : GeneratorTestBase
{
[Fact]
public async Task TestName()
{
var source = """
using Foundatio.Mediator;
public record Ping(string Message);
public class PingHandler
{
public string Handle(Ping msg) => msg.Message + " Pong";
}
""";
await VerifyGenerated(source, new MediatorGenerator());
}
}After generator changes, carefully review snapshot diffs before accepting.
Before marking work complete:
-
dotnet build Foundatio.Mediator.slnxsucceeds with no new warnings -
dotnet test Foundatio.Mediator.slnxpasses all tests - Public API changes are backward-compatible (or flagged as breaking)
- XML doc comments added for new public APIs
- Documentation updated in
docs/for new features
Prefer Result<T> over exceptions for business logic:
public Result<User> Handle(GetUser query)
{
var user = _repository.Find(query.Id);
if (user == null)
return Result.NotFound($"User {query.Id} not found");
if (!user.IsActive)
return Result.Forbidden("Account disabled");
return user; // Implicit conversion to Result<User>
}Throw ArgumentNullException, ArgumentException, InvalidOperationException with clear messages for programmatic errors.
- Validate all inputs with guard clauses
- Never log passwords, tokens, keys, or PII
- Use
Result.Unauthorized()orResult.Forbidden()instead of throwing - Sanitize data before logging in middleware
| Resource | Purpose |
|---|---|
| docs/ | Full VitePress documentation |
| samples/ConsoleSample/ | Basic usage examples |
| samples/CleanArchitectureSample/ | Modular monolith architecture |
| docs/guide/configuration.md | All configuration options |
| docs/guide/clean-architecture.md | Clean Architecture patterns |
| docs/guide/troubleshooting.md | Common issues and solutions |