Repository navigation
Releases: Adiakys/AnyMediator
Release list
2.2.1
Faster Publish
With the default sequential strategy, Publish now awaits handlers directly instead of building a delegate per handler, resolves its sinks once per IPublisher instance, and uses the built-in notification publishers as singletons (they hold no state; a custom publisher keeps cfg.Lifetime).
| 2.2.0 | 2.2.1 | MediatR 12.5 | |
|---|---|---|---|
Publish, 1 handler |
196 ns, 344 B | 90 ns, 80 B | 145 ns, 288 B |
Publish, 3 handlers |
302 ns, 632 B | 151 ns, 144 B | |
Send |
97 ns, 96 B | 86 ns, 96 B | 92 ns, 200 B |
Other strategies, tracing and handlers for base types behave as before. Details on the performance page.
Fixed
- Generic handlers are disposed. Open generic handlers (
AuditHandler<T> : INotificationHandler<EntityCreated<T>>) are now created by the container, so one that implementsIDisposableorIAsyncDisposableis disposed when its scope ends, like any other handler. Previously it was never disposed.
No API changes.
Full changelog: 2.2.0...2.2.1
2.2.0
Tracing and metrics
AnyMediator now emits OpenTelemetry-compatible spans and a duration histogram through .NET's own ActivitySource and Meter. No new dependency, no behavior change, and nothing is recorded until you subscribe:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(AnyMediatorDiagnostics.ActivitySourceName))
.WithMetrics(m => m.AddMeter(AnyMediatorDiagnostics.MeterName));- Spans:
send {Request},create_stream {Request}(open until the stream is read, fails or is abandoned),publish {Notification}with onehandle {Notification}child per handler. Attributesanymediator.operation,anymediator.message.type,anymediator.handler.type; failures setErrorstatus,error.typeand anexceptionevent. - Metric:
anymediator.operation.durationhistogram, in seconds, by operation, message type anderror.type. - New:
AnyMediatorDiagnostics.ActivitySourceNameandAnyMediatorDiagnostics.MeterName.
Cost
| Not listening | Listening | |
|---|---|---|
Send (1 span) |
87 ns, 96 B | 564 ns, 704 B |
Publish, 3 handlers (4 spans) |
307 ns, 632 B | 2,310 ns, 2,872 B |
Guide: Tracing and metrics
What's Changed
Full Changelog: 2.1.1...2.2.0
2.1.1
Faster dispatch, smaller library
No API or behavior change: upgrade from 2.1.0 by bumping the package version.
- Requests, streams and notifications are dispatched through generic wrappers built once per message type, without reflection on the hot path.
- About 20% less code in the runtime library.
| 2.1.0 | 2.1.1 | MediatR 12.5 | |
|---|---|---|---|
Send |
457 ns, 360 B | 97 ns, 96 B | 92 ns, 200 B |
Send, 3 behaviors |
992 ns, 1104 B | 225 ns, 632 B | — |
Publish, 1 handler |
379 ns, 656 B | 196 ns, 344 B | 145 ns, 288 B |
Measured with BenchmarkDotNet on .NET 10; details and the comparison with Mediator are on the Performance page.
What's Changed
- Dispatch without reflection, simpler runtime, benchmarks vs MediatR and Mediator by @Adiakys in #17
- Version 2.1.1 by @Adiakys in #18
Full Changelog: 2.1.0...2.1.1
2.1.0
MediatR compatibility
AnyMediator 2.1 closes the remaining gaps with MediatR: behaviors, handlers and calls written for MediatR now work unchanged once the namespace and AddMediatR are replaced.
Changes from 2.0.0 that need attention
2.0.0 was published the day before this release and had no downloads, so these changes ship as a minor version.
Publishre-throws failures, as MediatR does. Every sink still runs; one failure is re-thrown as is, several as anAggregateException. In 2.0.0 failures were logged and swallowed. The dependency onMicrosoft.Extensions.Logging.Abstractionsis removed.- Two handlers for the same request make
AddAnyMediatorthrow, naming both classes. In 2.0.0 the first one found was used silently. ISender.Send(object)andIPublisher.Publish(object)are new interface members: classes that implementISenderorIPublisherby hand must add them. Mocks are unaffected.
New
- Handlers for a notification's base class or interface also receive it:
INotificationHandler<IDomainEvent>runs for everyIDomainEvent. A handler class runs once perPublish. - Open generic handlers are found by scanning and closed when a message is dispatched, e.g.
GetByIdHandler<T> : IRequestHandler<GetById<T>, T>orIndexEntity<T> : INotificationHandler<EntityCreated<T>>. IPipelineBehaviorandIStreamPipelineBehaviorconstrainTRequesttonotnull, so MediatR behaviors compile unchanged.Send(object),CreateStream(object)andPublish(object)for messages whose type is only known at runtime;Publish(object)passes the runtime type to sinks.cfg.TypeEvaluatorfilters the types registered by scanning.
Docs: Upgrading from AnyMediator 2.0
What's Changed
- 2.1.0: MediatR-compatible Publish errors, object overloads, generic and polymorphic handlers by @Adiakys in #16
Full Changelog: 2.0.0...2.1.0
2.0.0
OpenMediatR is now AnyMediator
This release is OpenMediatR 1.5.0 under a new name. Behavior is unchanged; only names change, so this is a major version.
| OpenMediatR | AnyMediator |
|---|---|
OpenMediatR / OpenMediatR.Abstractions packages |
AnyMediator / AnyMediator.Abstractions |
using OpenMediatR; |
using AnyMediator; |
services.AddOpenMediatR(...) |
services.AddAnyMediator(...) |
OpenMediatRConfiguration |
AnyMediatorConfiguration |
dotnet remove package OpenMediatR
dotnet add package AnyMediatorWhy: "MediatR" is the name of a third-party product. AnyMediator stays compatible with the MediatR API, and is not affiliated with or endorsed by MediatR or Lucky Penny Software.
Documentation: https://adiakys.github.io/AnyMediator/ (see Upgrading from OpenMediatR).
What's Changed
Full Changelog: 1.5.0...2.0.0
1.5.0
Fixed
- Pipeline behaviors now run in registration order, as documented and as in MediatR: the first registered behavior is the outermost. Before 1.5.0 the last registered behavior ran first. Exception handler/action wrappers stay outermost and pre/post-processor wrappers stay innermost. If you register more than one behavior, check that their order is still the one you want.
Added
RequestHandlerDelegateandStreamHandlerDelegatetake an optionalCancellationToken, soawait next()compiles, as in MediatR code.- When no handler is registered,
SendandCreateStreamthrow anInvalidOperationExceptionthat names the request, the interface to implement and the registration call to add. - XML documentation for the whole public API, shipped in the package for IntelliSense and coding assistants.
- Documentation site: https://adiakys.github.io/OpenMediator/, with a guide for migrating from MediatR.
What's Changed
Full Changelog: 1.4.1...1.5.0
1.4.1
1.4.0
OpenMediatR 1.4.0
Closes the configuration API parity gap with MediatR. Every change is additive — existing configuration code continues to work unchanged.
Closed-generic AddBehavior overloads
Pipeline behaviors bound to a specific (TRequest, TResponse) pair can now be registered without going through the open generic IPipelineBehavior<,> route.
// Implementation type only — registers under every IPipelineBehavior<,> closed interface it implements
cfg.AddBehavior<MyClosedBehavior>();
// Explicit service + implementation types — useful when one type implements multiple closed interfaces
cfg.AddBehavior<IPipelineBehavior<MyRequest, MyResponse>, MyClosedBehavior>();
// Type-based equivalents
cfg.AddBehavior(typeof(MyClosedBehavior));
cfg.AddBehavior(typeof(IPipelineBehavior<MyRequest, MyResponse>), typeof(MyClosedBehavior));The same four overloads are available for stream pipeline behaviors via AddStreamBehavior / AddStreamBehavior<>.
Closed-generic processor two-type-arg overloads
Mirroring AddBehavior, processors now accept an explicit service type:
cfg.AddRequestPreProcessor<IRequestPreProcessor<MyRequest>, MyPreProcessor>();
cfg.AddRequestPreProcessor(typeof(IRequestPreProcessor<MyRequest>), typeof(MyPreProcessor));
cfg.AddRequestPostProcessor<IRequestPostProcessor<MyRequest, MyResponse>, MyPostProcessor>();
cfg.AddRequestPostProcessor(typeof(IRequestPostProcessor<MyRequest, MyResponse>), typeof(MyPostProcessor));Useful when one type implements multiple closed processor interfaces and you want to wire only one of them.
Bulk open behavior registration
Register multiple open generic behaviors in one call. Each entry can carry its own lifetime override via the new OpenBehavior struct.
cfg.AddOpenBehaviors(new[]
{
typeof(LoggingBehavior<,>),
typeof(ValidationBehavior<,>),
}, ServiceLifetime.Singleton);
// Or with per-item lifetime
cfg.AddOpenBehaviors(new[]
{
new OpenBehavior(typeof(LoggingBehavior<,>), ServiceLifetime.Singleton),
new OpenBehavior(typeof(ValidationBehavior<,>)), // null → uses cfg.Lifetime
});AddOpenStreamBehaviors follows the same shape for stream behaviors.
Open generic pre/post processors
Useful for cross-cutting processors that should run for every request type.
public sealed class LoggingPreProcessor<TRequest> : IRequestPreProcessor<TRequest>
where TRequest : notnull { /* ... */ }
cfg.AddOpenRequestPreProcessor(typeof(LoggingPreProcessor<>));
cfg.AddOpenRequestPostProcessor(typeof(AuditPostProcessor<,>));The pre/post-processor wrapper behaviors are wired automatically, just like with the closed-generic AddRequestPreProcessor<T>().
RequestExceptionActionProcessorStrategy
New enum + property controlling when IRequestExceptionAction<,> implementations fire relative to IRequestExceptionHandler<,,>.
public enum RequestExceptionActionProcessorStrategy
{
ApplyForUnhandledExceptions, // default — actions skip when a handler recovers via SetHandled
ApplyForAllExceptions, // actions always run; handler may then recover
}services.AddOpenMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<MyHandler>();
cfg.RequestExceptionActionProcessorStrategy = RequestExceptionActionProcessorStrategy.ApplyForAllExceptions;
});Use case: you want metrics/logging to fire on every failure, even when a recovery handler is going to suppress the exception downstream.
The default ApplyForUnhandledExceptions preserves the behavior introduced in 1.3.0 — actions are skipped when a handler calls SetHandled.
Internally implemented by reversing the order of the exception-handler vs exception-action wrapper behaviors in the pipeline. No reflection, no overhead.
cfg.NotificationPublisher instance overload
Until now, the notification publisher could only be configured by Type. 1.4.0 adds an instance-based overload:
services.AddOpenMediatR(cfg =>
{
cfg.NotificationPublisher = new MyConfiguredPublisher(threshold: 100);
});When set, the instance takes precedence over NotificationPublisherType. Useful for publishers that need pre-built state which can't easily be wired through DI alone.
RegisterServicesFromAssemblyContaining(Type)
Type-based overload of the assembly registration shortcut, for cases where the marker type is only known at runtime.
cfg.RegisterServicesFromAssemblyContaining(markerType);Internal cleanup
OpenMediatRConfigurationis now split into partial files by concern: core configuration + assembly registration in the main file, all behavior registration methods inOpenMediatRConfiguration.Behaviors.cs, all processor and exception handler/action methods inOpenMediatRConfiguration.Processors.cs.- Shared validation helpers (open vs closed generic checks, interface matching, assignability) extracted into private statics on the main partial — used by every Add* method to keep individual methods short and consistent.
- Stream behaviors now have their own
StreamBehaviorsToRegistercollection (matches MediatR), kept separate fromBehaviorsToRegisterwhich is reserved forIPipelineBehavior<,>.
Compatibility
- Purely additive. Existing configuration code is unaffected.
- All new properties default to values that preserve previous behaviour (no flag flipping required when upgrading).
- The new
OpenBehaviorstruct andRequestExceptionActionProcessorStrategyenum live in theOpenMediatRnamespace. - Targets
net8.0andnet10.0.
What's next
The remaining MediatR-only items are deliberately omitted:
TypeEvaluator(custom assembly-scan filter)MediatorImplementationType(customIMediatoroverride)- Public visibility of the registration collections
These are niche customization hooks that can be added on demand if real use cases surface.
1.3.0
OpenMediatR 1.3.0
Exception Handlers and Actions
OpenMediatR now supports IRequestExceptionHandler<TRequest, TResponse, TException> and IRequestExceptionAction<TRequest, TException>, completing the MediatR API parity for the request pipeline.
New types (in OpenMediatR.Abstractions)
IRequestExceptionHandler<TRequest, TResponse, TException>—Task Handle(TRequest, TException, RequestExceptionHandlerState<TResponse>, CancellationToken). May recover from the exception by callingstate.SetHandled(response).IRequestExceptionAction<TRequest, TException>—Task Execute(TRequest, TException, CancellationToken). Side-effects only; the exception is always re-thrown after all matching actions complete.RequestExceptionHandlerState<TResponse>— small DTO withHandledandResponseproperties plusSetHandled(TResponse).
Behavior
- Handler vs action ordering: when an exception bubbles up, the handler wrapper runs first. If any handler calls
SetHandled, the response is returned and the action wrapper never sees the exception. If no handler recovers, the action wrapper runs every matching action and then re-throws the original exception (stack trace preserved viaExceptionDispatchInfo). - Type-hierarchy matching: a handler/action declared for a base exception type catches derived ones.
IRequestExceptionAction<MyRequest, Exception>runs for every exception thrown duringMyRequestprocessing. The pipeline walks the actual exception's base-class chain to find every matching closed generic registration. - Multiple handlers per (request, exception) pair: run in registration order; iteration stops as soon as one handler marks the state as handled.
- Multiple actions: all matching actions run.
Registration
Exception handlers and actions are auto-scanned from assemblies unconditionally (no opt-in flag, mirroring MediatR — unlike pre/post processors which are opt-in via AutoRegisterRequestProcessors).
Explicit registration is also available:
services.AddOpenMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<MyRequestHandler>();
cfg.AddRequestExceptionHandler<FallbackOnConnectionLost>();
cfg.AddRequestExceptionAction<LogFailures>();
});Both registration paths trigger automatic wiring of the matching pipeline-behavior wrappers. No wrappers are added when no handlers or actions are present, so apps that don't use this feature pay no overhead.
Example
public sealed class FallbackOnConnectionLost : IRequestExceptionHandler<GetUser, User, ConnectionException>
{
public Task Handle(
GetUser request,
ConnectionException exception,
RequestExceptionHandlerState<User> state,
CancellationToken cancellationToken)
{
state.SetHandled(User.Anonymous);
return Task.CompletedTask;
}
}
public sealed class LogFailures : IRequestExceptionAction<GetUser, Exception>
{
private readonly ILogger<LogFailures> _logger;
public LogFailures(ILogger<LogFailures> logger) => _logger = logger;
public Task Execute(GetUser request, Exception exception, CancellationToken cancellationToken)
{
_logger.LogError(exception, "GetUser({Id}) failed", request.Id);
return Task.CompletedTask;
}
}Compatibility
- Purely additive: existing
Send,Publish,CreateStream, pipeline behaviors, and pre/post-processors are unaffected. - Apps with no exception handlers or actions in their assemblies pay zero pipeline overhead.
- Targets
net8.0andnet10.0.
What's next
This completes the original feature parity roadmap with MediatR. Future work focuses on hardening, performance, and reacting to real-world feedback.
1.2.0
OpenMediatR 1.2.0
Pre / Post Processors
OpenMediatR now supports IRequestPreProcessor<TRequest> and IRequestPostProcessor<TRequest, TResponse>, matching MediatR's API.
New types (in OpenMediatR.Abstractions)
IRequestPreProcessor<TRequest>—Task Process(TRequest, CancellationToken). Multiple pre-processors per request type are supported and run sequentially in registration order before the handler.IRequestPostProcessor<TRequest, TResponse>—Task Process(TRequest, TResponse, CancellationToken). Run after the handler returns successfully; skipped when the handler or any preceding step throws.
Two registration paths
Both paths trigger automatic wiring of the matching pipeline-behavior wrappers (RequestPreProcessorBehavior<,>, RequestPostProcessorBehavior<,>).
Explicit (recommended for full control):
services.AddOpenMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<MyRequestHandler>();
cfg.AddRequestPreProcessor<ValidateUser>();
cfg.AddRequestPostProcessor<AuditUserAccess>();
});AddRequestPreProcessor<T> / AddRequestPostProcessor<T> register T for every closed IRequestPreProcessor<TRequest> (or IRequestPostProcessor<TRequest, TResponse>) it implements — so a single processor class can cover multiple request types.
Opt-in assembly scan (mirrors MediatR's AutoRegisterRequestProcessors):
services.AddOpenMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<MyRequestHandler>();
cfg.AutoRegisterRequestProcessors = true;
});When enabled, the assembly scan picks up every processor implementation and wires the wrapper behaviors automatically. Default is false — without the flag, scanning ignores processor types.
Pipeline ordering
User pipeline behaviors registered via AddOpenBehavior wrap the processor wrappers (outer position):
- Outermost user behavior
beforelogic - … inner user behaviors
before - Pre-processors (in order)
- Handler
- Post-processors (in order)
- … inner user behaviors
after - Outermost user behavior
after
Example
public sealed class ValidateUser : IRequestPreProcessor<GetUser>
{
public Task Process(GetUser request, CancellationToken cancellationToken)
{
if (request.Id <= 0) throw new ArgumentException("Invalid user id.");
return Task.CompletedTask;
}
}
public sealed class AuditUserAccess : IRequestPostProcessor<GetUser, User>
{
public Task Process(GetUser request, User response, CancellationToken cancellationToken)
{
// record the access
return Task.CompletedTask;
}
}Behavior change: AddOpenMediatR no longer throws on missing assemblies
AddOpenMediatR previously threw ArgumentException when the configuration registered no assemblies. It now matches MediatR's behavior: completes silently, registers the core services, and lets you compose the rest yourself (explicit handler registrations, AddRequestPreProcessor<T>, etc.). Existing code that did supply assemblies is unaffected.
Fix: idempotent registration on repeated AddOpenMediatR calls
Calling AddOpenMediatR more than once (e.g. when composing modular DI setups) previously registered every scanned INotificationHandler<> implementation, every user pipeline behavior added via AddOpenBehavior / AddOpenStreamBehavior, and every pre/post-processor twice — causing each to execute twice on a single dispatch. Switched to TryAddEnumerable for all of them, which deduplicates by (ServiceType, ImplementationType). Different implementations of the same interface still coexist as expected.
IRequestHandler<,> and IStreamRequestHandler<,> were already idempotent via TryAddTransient and are unchanged.
Compatibility
- Purely additive: existing
Send,Publish,CreateStream, and pipeline-behavior code is unaffected. - Pre/post-processor scanning defaults to off; existing apps see no behavioral change unless they opt in or register processors explicitly.
- Targets
net8.0andnet10.0.