| name | mediatorlite-core |
| description | Runtime implementation for MediatorLite -- AddMediatorLite() DI extension, the ThrowingMediator diagnostic fallback, the generated SourceGeneratedMediator (implements IMediator via ValueTask typed-switch dispatch), MediatorDiagnostics (ActivitySource + DiagnosticListener), and the Validation subsystem (ValidationBehavior + DataAnnotationsValidator). Use when touching dispatch, DI registration, diagnostics, or the validation runtime. |
| triggers | AddMediatorLite, dispatch, SourceGeneratedMediator, ThrowingMediator, ValueTask dispatch, ServiceCollectionExtensions, MediatorDiagnostics, MediatorActivitySource, ValidationBehavior, DataAnnotationsValidator, MediatorLite runtime |
MediatorLite (Core Runtime)
Purpose
MediatorLite (project name, not the solution) is the runtime library consumers reference. It does not contain a hand-written IMediator implementation — the real IMediator is the generated SourceGeneratedMediator emitted by MediatorLite.SourceGeneration. This project contains the DI extension (AddMediatorLite()), the ThrowingMediator diagnostic fallback, diagnostic sources, and the validation runtime. The dispatch path contains zero reflection at call time — the generated mediator dispatches via a compile-time C# type-pattern switch. Logging and tracing are emitted inline by the generator, not by this project.
When to use
- Adding or tweaking
AddMediatorLite() registrations (for example, registering additional runtime services).
- Understanding the
ThrowingMediator diagnostic fallback and how the generated mediator supersedes it.
- Adjusting
ValidationBehavior ordering semantics or DataAnnotationsValidator behavior.
- Renaming or adding OpenTelemetry tags / activity names in
MediatorDiagnostics.
Project location & entry points
- MediatorLite.csproj — targets
net10.0, references Microsoft.Extensions.DependencyInjection.Abstractions 9.0.0 and Microsoft.Extensions.Logging.Abstractions 9.0.0, and project-references MediatorLite.Abstractions.csproj.
- The
IMediator implementation is generated (SourceGeneratedMediator in the MediatorLite.Generated namespace) — it is not a file in this project. See the mediatorlite-source-generation skill.
- ServiceCollectionExtensions.cs —
AddMediatorLite() entry point.
- ThrowingMediator.cs — diagnostic fallback
IMediator that throws if no generator ran.
PipelineBehaviorTypeResolver.cs — deleted (v1 runtime behavior-type resolution; the generated mediator unrolls behaviors at compile time, so nothing needs it).
- MediatorDiagnostics.cs —
MediatorActivitySource (OpenTelemetry) + DiagnosticListener.
- ValidationBehavior.cs — generic pipeline behavior that runs registered
IValidator<T>s.
- DataAnnotationsValidator.cs — built-in validator using
System.ComponentModel.DataAnnotations.
Core types / API surface
The generated SourceGeneratedMediator — typed-switch dispatch
There is no hand-written Mediator.cs in v2. The generator emits SourceGeneratedMediator : global::MediatorLite.IMediator (namespace MediatorLite.Generated) which holds a single IServiceProvider _sp field and dispatches via a compile-time C# type-pattern switch:
public sealed class SourceGeneratedMediator : global::MediatorLite.IMediator
{
private readonly IServiceProvider _sp;
public SourceGeneratedMediator(IServiceProvider serviceProvider) => _sp = serviceProvider;
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> request, CancellationToken ct = default)
{
switch (request)
{
case MyQuery r:
{
var vt = Send_MyQuery(r, ct);
if (typeof(TResponse) == typeof(MyResult))
return Unsafe.As<ValueTask<MyResult>, ValueTask<TResponse>>(ref vt);
return SlowCast<MyResult, TResponse>(vt);
}
case null: throw new ArgumentNullException(nameof(request));
default: throw new InvalidOperationException();
}
}
}
Key invariants:
- No boxing. Each arm calls a fully typed
Send_<SafeType>(...) returning ValueTask<TConcrete>, converted to ValueTask<TResponse> via an identity-guarded System.Runtime.CompilerServices.Unsafe.As (the typeof guard JIT-folds to a constant). Value-type responses stay typed — there is no Task<object> and no (TResponse) unbox. v1 boxed; v2 eliminated it.
SlowCast is the only fallback, for covariant IRequest<out T> dispatch (reference cast, no value-type boxing).
Send_<SafeType> per-request methods are instance methods on _sp. A zero-behavior request with diagnostics disabled returns the handler's ValueTask directly — no async state machine.
PublishAsync has a matching switch over the notification's runtime type; Publish_<SafeType> methods return ValueTask. The default: arm returns default (no-op) when no handler is registered. Because it matches the runtime type, base/interface-typed publishes dispatch correctly (v1's typeof(TNotification) dictionary lookup silently no-oped for those).
- The
case null: arm throws ArgumentNullException before any handler resolution.
AddMediatorLite() — DI entry point
public static IServiceCollection AddMediatorLite(this IServiceCollection services)
{
// TryAdd keeps this order-independent with AddGeneratedHandlers(): the generated
// registration uses plain AddScoped, and the container resolves the last IMediator
// descriptor, so the generated mediator wins regardless of call order.
services.TryAddScoped<IMediator, ThrowingMediator>();
return services;
}
AddMediatorLite() is now an optional diagnostic fallback:
- It registers
ThrowingMediator via TryAddScoped<IMediator, ...>. The real mediator is registered by the generated AddGeneratedHandlers() with plain AddScoped<IMediator, SourceGeneratedMediator>().
- Because the generated registration is unconditional
AddScoped and the container resolves the last IMediator descriptor, the generated mediator always wins. The TryAdd only takes effect when AddGeneratedHandlers() never ran — turning a missing generator into a clear InvalidOperationException instead of a resolution failure.
- Call order of the two methods no longer matters. The mediator is Scoped (the generated mediator captures the resolving scope's
IServiceProvider; resolved from the root provider it behaves like a singleton). It is no longer Transient.
AddMediatorLite() takes no arguments. There is no MediatorOptions — v2 removed src/MediatorLite/Configuration/MediatorOptions.cs (see git status). All configuration is compile-time via attributes.
ThrowingMediator — diagnostic fallback
internal sealed class ThrowingMediator : IMediator
{
private const string Message =
"No source-generated mediator is registered. Reference the MediatorLite.SourceGeneration " +
"analyzer package from the assembly that contains your handlers and call " +
"services.AddGeneratedHandlers() so the generated mediator replaces this fallback.";
public ValueTask<TResponse> SendAsync<TResponse>(
IRequest<TResponse> request,
CancellationToken cancellationToken = default)
=> throw new InvalidOperationException(Message);
public ValueTask PublishAsync<TNotification>(
TNotification notification,
CancellationToken cancellationToken = default)
where TNotification : INotification
=> throw new InvalidOperationException(Message);
}
This type is registered only by AddMediatorLite() via TryAddScoped. When AddGeneratedHandlers() runs (the normal case), the generated SourceGeneratedMediator is registered after it and wins resolution, so ThrowingMediator never dispatches. If the generator never ran, every dispatch throws the guidance message above.
PipelineBehaviorTypeResolver — removed
This v1 helper (open- vs closed-behavior interface resolution for runtime registration) was deleted: the generated mediator unrolls behaviors itself at compile time, so nothing on the v2 dispatch or registration path needs runtime behavior-type resolution. Do not reintroduce it — behavior discovery/expansion belongs to the source generator (ExpandBehaviors in HandlerDiscoveryGenerator.cs).
MediatorActivitySource + MediatorDiagnostics
The generator emits Activity? starts with these constants; consumers subscribe via OpenTelemetry.
public static class MediatorActivitySource
{
/// <summary>
/// The name of the activity source.
/// </summary>
public const string SourceName = "MediatorLite";
/// <summary>
/// The version of the activity source.
/// </summary>
public const string Version = "1.0.0";
/// <summary>
/// The ActivitySource for MediatorLite tracing.
/// </summary>
public static readonly ActivitySource Source = new(SourceName, Version);
public static class ActivityNames
{
/// <summary>Send request activity name prefix.</summary>
public const string SendRequest = "MediatorLite.Send";
/// <summary>Publish notification activity name prefix.</summary>
public const string PublishNotification = "MediatorLite.Publish";
/// <summary>Pipeline behavior activity name prefix.</summary>
public const string PipelineBehavior = "MediatorLite.Behavior";
/// <summary>Notification handler activity name prefix.</summary>
public const string NotificationHandler = "MediatorLite.NotificationHandler";
}
Standard tag names are also centralized here:
public static class Tags
{
/// <summary>Request type tag.</summary>
public const string RequestType = "mediatorlite.request.type";
/// <summary>Response type tag.</summary>
public const string ResponseType = "mediatorlite.response.type";
/// <summary>Notification type tag.</summary>
public const string NotificationType = "mediatorlite.notification.type";
/// <summary>Handler type tag.</summary>
public const string HandlerType = "mediatorlite.handler.type";
/// <summary>Behavior type tag.</summary>
public const string BehaviorType = "mediatorlite.behavior.type";
/// <summary>Handler count tag.</summary>
public const string HandlerCount = "mediatorlite.handler.count";
/// <summary>Execution strategy tag.</summary>
public const string ExecutionStrategy = "mediatorlite.execution.strategy";
/// <summary>Error tag.</summary>
public const string Error = "error";
/// <summary>Error message tag.</summary>
public const string ErrorMessage = "error.message";
}
MediatorDiagnostics.Listener exposes a DiagnosticListener named "MediatorLite" with event constants under MediatorDiagnostics.Events for optional DiagnosticSource-based integration.
ValidationBehavior<TRequest, TResponse>
Registered automatically by the generator for any request type that has at least one IValidator<TRequest> (including DataAnnotationsValidator<TRequest> when the request class has any [ValidationAttribute]). It is inserted before other behaviors in the generated pipeline.
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private IReadOnlyList<IValidator<TRequest>> Validators { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ValidationBehavior{TRequest, TResponse}"/> class.
/// </summary>
/// <param name="validators">The validators for the request type.</param>
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
Validators = validators.ToList();
}
Execution runs every registered validator and aggregates their errors before throwing ValidationException — it does not stop at the first failed validator:
public async ValueTask<TResponse> HandleAsync(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken = default)
{
if (Validators.Count == 0)
{
return await next();
}
var allErrors = new List<ValidationError>();
foreach (var validator in Validators)
{
var result = await validator.ValidateAsync(request, cancellationToken);
if (!result.IsValid)
{
allErrors.AddRange(result.Errors);
}
}
if (allErrors.Count > 0)
{
throw new ValidationException(allErrors);
}
return await next();
}
DataAnnotationsValidator<TRequest>
Registered by the generator only for request types that carry any System.ComponentModel.DataAnnotations attribute. null requests short-circuit to a failure with a "Request" property name.
public class DataAnnotationsValidator<TRequest> : IValidator<TRequest>
{
/// <inheritdoc />
public ValueTask<ValidationResult> ValidateAsync(TRequest request, CancellationToken cancellationToken = default)
{
if (request is null)
{
return ValueTask.FromResult(ValidationResult.Failure(
new ValidationError("Request", "Request cannot be null")));
}
var context = new ValidationContext(request);
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
if (Validator.TryValidateObject(request, context, results, validateAllProperties: true))
{
return ValueTask.FromResult(ValidationResult.Success);
}
var errors = results.Select(r => new ValidationError(
string.Join(", ", r.MemberNames),
r.ErrorMessage ?? "Validation failed")).ToList();
return ValueTask.FromResult(ValidationResult.Failure(errors));
}
Patterns & invariants
Do:
- Call
services.AddGeneratedHandlers().AddMediatorLite(). AddGeneratedHandlers() is provided by the generator and registers IMediator (the generated SourceGeneratedMediator) + all handlers/behaviors/validators; AddMediatorLite() only adds the optional ThrowingMediator diagnostic fallback. Call order is interchangeable.
- Let the generator do all the heavy lifting (typed-switch dispatch, pipeline composition, logging, tracing). There is no hand-written mediator to keep small.
- Let
ValidationBehavior aggregate errors across validators — that is intentional.
- Use
MediatorActivitySource.Source as the subscription target in OpenTelemetry setup (builder.AddSource("MediatorLite")).
Don't:
- Don't add reflection-based dispatch. v2 dispatch is a compile-time typed switch in the generated mediator.
- Don't register
IMediator manually — AddGeneratedHandlers() registers it.
- Don't expect
ThrowingMediator to dispatch when the generator ran — the generated SourceGeneratedMediator always wins resolution (last AddScoped descriptor).
- Don't return an error from
PublishAsync when there are no handlers. The generated switch's default: arm returns default (a no-op) — a behavioral contract verified in tests.
- Don't introduce a
MediatorOptions class. The file was deliberately deleted.
Common tasks
-
Diagnose "No handler registered for request type X"
- Confirm the consumer calls
services.AddGeneratedHandlers() before or alongside AddMediatorLite().
- Look at the obj/generated folder (
obj/Debug/netX/generated/MediatorLite.SourceGeneration/.../MediatorLiteRegistration.g.cs) for the handler's dispatch entry.
- Check
MediatorLiteRegistration.RequestHandlerCount at startup — a 0 means the generator never discovered anything (likely because the handler is abstract, internal in a different assembly, or has [MediatorGeneration(Skip = true)]).
-
Wire OpenTelemetry tracing
- In the consumer:
builder.Services.AddOpenTelemetry().WithTracing(t => t.AddSource(MediatorActivitySource.SourceName)).
- Tags emitted by the generator use the
MediatorActivitySource.Tags.* constants — consumer dashboards can filter on mediatorlite.request.type, mediatorlite.handler.type, etc.
- Opt out per assembly with
[assembly: DisableMediatorTracing].
-
Adjust logger category / log level
- The generator always emits
LogDebug under the category MediatorLite.IMediator. To quiet it, add AddFilter("MediatorLite.IMediator", LogLevel.Information) (or higher) to the logging configuration.
- To remove the calls entirely, use
[assembly: DisableMediatorLogging].
-
Register a custom IValidator<T> without source generation
services.AddTransient<IValidator<MyCommand>, MyValidator>();
- Also register
ValidationBehavior<,> for the request — normally the generator does this, but if you're mixing manual DI it must live in IPipelineBehavior<MyCommand, TResponse>.
-
Verify the missing-generator diagnostic
- Register only
services.AddMediatorLite() (no AddGeneratedHandlers()), build the provider, and resolve IMediator — you get the ThrowingMediator fallback.
Pitfalls & gotchas
- Runtime-type dispatch: the generated switch matches the request/notification's runtime type, which matters if callers declare the parameter as
IRequest<T> / INotification. Notification dispatch by runtime type is why base/interface-typed publishes now dispatch (they silently no-oped in v1).
- Null requests on
PublishAsync throw ArgumentNullException from the switch's case null: arm. Tests in MediatorTests.cs assert this precisely.
PipelineBehaviorTypeResolver was deleted — do not reintroduce runtime behavior-type resolution; the generated mediator unrolls behaviors itself at compile time.
MediatorOptions.cs is deleted (see git status D src/MediatorLite/Configuration/MediatorOptions.cs). Do not re-introduce it.
ValidationBehavior.Validators is stored once in the constructor (validators.ToList()). Scoped handler/validator lifetimes still work because the generated mediator is Scoped and resolves them from the resolving scope's IServiceProvider.
ValidationException is thrown inside await validator.ValidateAsync(...); it does not wrap individual validator errors — all errors flatten into one exception.
DiagnosticListener Listener = new("MediatorLite") is publicly visible — do not rename its source without coordinating with downstream diagnostic observers.
Related skills & rules
- mediatorlite-abstractions — defines
IMediator (ValueTask), IRequest, IPipelineBehavior, and the IValidator<T> / validation model types consumed here.
- mediatorlite-source-generation — emits the
SourceGeneratedMediator (implementing IMediator) that owns all dispatch, plus AddGeneratedHandlers that registers it (AddScoped<IMediator, SourceGeneratedMediator>) and everything else; AddMediatorLite adds only the ThrowingMediator fallback.
- mediatorlite-tests —
tests/MediatorLite.Tests/SourceGeneration/MediatorTests.cs, ValidationTests.cs, PipelineBehaviorTests.cs, and NotificationTests.cs exercise the generated mediator, ValidationBehavior, and the DI wiring.
- AGENTS.md — the generated
SourceGeneratedMediator (Scoped) is the IMediator; v2 has no reflection fallback.
- Docs: docs/observability.md, docs/validation.md, docs/pipeline-behaviors.md.