Imported from behl1anmol/MediatorLite (
.claude/skills/mediatorlite-core/SKILL.md). Install upstream withnpx skills add behl1anmol/MediatorLite --skill mediatorlite-core. Copyright stays with the author.
MediatorLite (Core Runtime)
⚠️ Partially STALE. Validation moved to FluentValidation in the opt-in
MediatorLite.FluentValidationpackage. The in-houseValidationBehavior/DataAnnotationsValidatorreferenced here were removed from core — core has no validation subsystem. See mediatorlite-validation, .claude/rules/50-validation.md, docs/validation.md.
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
ThrowingMediatordiagnostic fallback and how the generated mediator supersedes it. - Adjusting
ValidationBehaviorordering semantics orDataAnnotationsValidatorbehavior. - Renaming or adding OpenTelemetry tags / activity names in
MediatorDiagnostics.
Project location & entry points
- MediatorLite.csproj — targets
net10.0, referencesMicrosoft.Extensions.DependencyInjection.Abstractions 9.0.0andMicrosoft.Extensions.Logging.Abstractions 9.0.0, and project-references MediatorLite.Abstractions.csproj. - The
IMediatorimplementation is generated (SourceGeneratedMediatorin theMediatorLite.Generatednamespace) — it is not a file in this project. See the mediatorlite-source-generation skill. - ServiceCollectionExtensions.cs —
AddMediatorLite()entry point. - ThrowingMediator.cs — diagnostic fallback
IMediatorthat throws if no generator ran. — deleted (v1 runtime behavior-type resolution; the generated mediator unrolls behaviors at compile time, so nothing needs it).PipelineBehaviorTypeResolver.cs- 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:
// Emitted shape (MediatorLite.Generated.SourceGeneratedMediator)
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); // ValueTask<MyResult>
if (typeof(TResponse) == typeof(MyResult))
return Unsafe.As<ValueTask<MyResult>, ValueTask<TResponse>>(ref vt);
return SlowCast<MyResult, TResponse>(vt); // covariant IRequest<out T> fallback
}
case null: throw new ArgumentNullException(nameof(request));
default: throw new InvalidOperationException(/* no handler */);
}
}
// ...PublishAsync switch + Send_<Type>/Publish_<Type> methods using _sp...
}
Key invariants:
- No boxing. Each arm calls a fully typed
Send_<SafeType>(...)returningValueTask<TConcrete>, converted toValueTask<TResponse>via an identity-guardedSystem.Runtime.CompilerServices.Unsafe.As(thetypeofguard JIT-folds to a constant). Value-type responses stay typed — there is noTask<object>and no(TResponse)unbox. v1 boxed; v2 eliminated it. SlowCastis the only fallback, for covariantIRequest<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'sValueTaskdirectly — no async state machine.PublishAsynchas a matching switch over the notification's runtime type;Publish_<SafeType>methods returnValueTask. Thedefault:arm returnsdefault(no-op) when no handler is registered. Because it matches the runtime type, base/interface-typed publishes dispatch correctly (v1'stypeof(TNotification)dictionary lookup silently no-oped for those).- The
case null:arm throwsArgumentNullExceptionbefore 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
ThrowingMediatorviaTryAddScoped<IMediator, ...>. The real mediator is registered by the generatedAddGeneratedHandlers()with plainAddScoped<IMediator, SourceGeneratedMediator>(). - Because the generated registration is unconditional
AddScopedand the container resolves the lastIMediatordescriptor, the generated mediator always wins. TheTryAddonly takes effect whenAddGeneratedHandlers()never ran — turning a missing generator into a clearInvalidOperationExceptioninstead 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 longerTransient.
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 registersIMediator(the generatedSourceGeneratedMediator) + all handlers/behaviors/validators;AddMediatorLite()only adds the optionalThrowingMediatordiagnostic 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
ValidationBehavioraggregate errors across validators — that is intentional. - Use
MediatorActivitySource.Sourceas 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
IMediatormanually —AddGeneratedHandlers()registers it. - Don't expect
ThrowingMediatorto dispatch when the generator ran — the generatedSourceGeneratedMediatoralways wins resolution (lastAddScopeddescriptor). - Don't return an error from
PublishAsyncwhen there are no handlers. The generated switch'sdefault:arm returnsdefault(a no-op) — a behavioral contract verified in tests. - Don't introduce a
MediatorOptionsclass. The file was deliberately deleted.
Common tasks
-
Diagnose "No handler registered for request type X"
- Confirm the consumer calls
services.AddGeneratedHandlers()before or alongsideAddMediatorLite(). - Look at the obj/generated folder (
obj/Debug/netX/generated/MediatorLite.SourceGeneration/.../MediatorLiteRegistration.g.cs) for the handler's dispatch entry. - Check
MediatorLiteRegistration.RequestHandlerCountat startup — a0means the generator never discovered anything (likely because the handler isabstract,internalin a different assembly, or has[MediatorGeneration(Skip = true)]).
- Confirm the consumer calls
-
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 onmediatorlite.request.type,mediatorlite.handler.type, etc. - Opt out per assembly with
[assembly: DisableMediatorTracing].
- In the consumer:
-
Adjust logger category / log level
- The generator always emits
LogDebugunder the categoryMediatorLite.IMediator. To quiet it, addAddFilter("MediatorLite.IMediator", LogLevel.Information)(or higher) to the logging configuration. - To remove the calls entirely, use
[assembly: DisableMediatorLogging].
- The generator always emits
-
Register a custom
IValidator<T>without source generationservices.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 inIPipelineBehavior<MyCommand, TResponse>.
-
Verify the missing-generator diagnostic
- Register only
services.AddMediatorLite()(noAddGeneratedHandlers()), build the provider, and resolveIMediator— you get theThrowingMediatorfallback. SendAsync/PublishAsyncthen throwInvalidOperationExceptionwith the "call services.AddGeneratedHandlers()" guidance — useful to verify error messaging.- For positive tests, always let the test project's source generator run (it is already wired — see tests/MediatorLite.Tests/MediatorLite.Tests.csproj). Resolved
IMediatorwill beMediatorLite.Generated.SourceGeneratedMediator.
- Register only
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
PublishAsyncthrowArgumentNullExceptionfrom the switch'scase null:arm. Tests in MediatorTests.cs assert this precisely. PipelineBehaviorTypeResolverwas deleted — do not reintroduce runtime behavior-type resolution; the generated mediator unrolls behaviors itself at compile time.MediatorOptions.csis deleted (see git statusD src/MediatorLite/Configuration/MediatorOptions.cs). Do not re-introduce it.ValidationBehavior.Validatorsis 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'sIServiceProvider.ValidationExceptionis thrown insideawait 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 theIValidator<T>/ validation model types consumed here. - mediatorlite-source-generation — emits the
SourceGeneratedMediator(implementingIMediator) that owns all dispatch, plusAddGeneratedHandlersthat registers it (AddScoped<IMediator, SourceGeneratedMediator>) and everything else;AddMediatorLiteadds only theThrowingMediatorfallback. - mediatorlite-tests —
tests/MediatorLite.Tests/SourceGeneration/MediatorTests.cs,ValidationTests.cs,PipelineBehaviorTests.cs, andNotificationTests.csexercise the generated mediator,ValidationBehavior, and the DI wiring. - AGENTS.md — the generated
SourceGeneratedMediator(Scoped) is theIMediator; v2 has no reflection fallback. - Docs: docs/observability.md, docs/validation.md, docs/pipeline-behaviors.md.