Fast, lightweight mediator for .NET with unified requests, pipelines, and notifications. Dualis uses a Roslyn source generator to emit dispatcher and DI registration code at build time, keeping runtime overhead and allocations low while offering a clean, opinionated API.
- Requests:
IRequest/IRequest<T>withIRequestHandler<>/IRequestHandler<,> - Pipelines: request/response and void pipeline behaviors (plus unified behaviour option)
- Notifications: fan-out publish with failure strategies and alternative publishers
- Public
AddDualisentry point for DI; source generator augments it in the host project
- Added:
Dualis.Analyzerproject. See installation and rule details in the analyzer README: src/Dualis.Analyzer/README.md. - Changed: tiny performance refactoring in the source generator/dispatcher code paths.
See full details in CHANGELOG.md.
NuGet:
dotnet add package Dualis
The package includes the source generator as analyzer assets. You must opt-in to generation in your host project.
Enable the generator only in the project where you call services.AddDualis() (the composition root: API/Web/Worker). Do not enable it in Domain/Application/Infrastructure projects.
Options to enable in the host:
- MSBuild property (recommended):
<PropertyGroup>
<DualisEnableGenerator>true</DualisEnableGenerator>
</PropertyGroup>
<ItemGroup>
<CompilerVisibleProperty Include="DualisEnableGenerator" />
</ItemGroup>
- Assembly attribute:
using Dualis;
[assembly: EnableDualisGeneration]
- .editorconfig/.globalconfig:
is_global = true
build_property.DualisEnableGenerator = true
Notes:
- Only the host should enable generation.
- The generator output is internal and not an extension method, so it cannot collide with the public entry.
- You can optionally set
EmitCompilerGeneratedFilesto inspect generated files underobj/generated.
- Register in DI. The public
AddDualisis always available; when the generator runs in the host, it augments the registration.
var builder = WebApplication.CreateBuilder(args);
// Dualis auto-discovers and registers handlers, pipeline behaviors, and notifications
// from the host compilation and referenced assemblies. No manual DI registration required.
builder.Services.AddDualis();
var app = builder.Build();
- Define a query and handler:
public sealed record GetUserByNameQuery(string Name) : IRequest<UserDto?>;
public sealed class GetUserByNameQueryHandler : IRequestHandler<GetUserByNameQuery, UserDto?>
{
public Task<UserDto?> Handle(GetUserByNameQuery request, CancellationToken ct)
=> Task.FromResult<UserDto?>(new UserDto(request.Name));
}
- Use
ISenderin Minimal API to send the request:
app.MapGet("/users/{name}", async (string name, ISender sender, CancellationToken ct) =>
{
UserDto? result = await sender.Send(new GetUserByNameQuery(name), ct);
return result is null ? Results.NotFound() : Results.Ok(result);
});
await app.RunAsync();
Note: IDualizor implements both ISender and IPublisher. Inject either if you only need a subset.
By default, AddDualis will automatically register all discovered request handlers, pipeline behaviors, and notification handlers. You can disable any auto-registration and manually register specific pieces using DualizorOptions.
Disable auto-registration and register manually:
builder.Services.AddDualis(opts =>
{
// Disable auto-registration for any component type as needed
opts.RegisterDiscoveredBehaviors = false;
opts.RegisterDiscoveredCqrsHandlers = false;
opts.RegisterDiscoveredNotificationHandlers = false;
// Manually register pipeline behaviors (request/response or void)
opts.Pipelines.Register<LoggingBehavior<GetUserByNameQuery, UserDto?>>();
opts.Pipelines.Register<VoidBehavior<SomeCommand>>();
// Manually register notification handlers
opts.Notifications.Register<UserCreatedEventHandler>();
// You can also register handlers via DI if preferred
// services.AddScoped<IRequestHandler<GetUserByNameQuery, UserDto?>, GetUserByNameQueryHandler>();
// Notifications: choose publisher and failure policy
opts.NotificationPublisherFactory = sp => sp.GetRequiredService<ParallelWhenAllNotificationPublisher>();
opts.NotificationFailureBehavior = NotificationFailureBehavior.ContinueAndAggregate;
opts.MaxPublishDegreeOfParallelism = Environment.ProcessorCount;
});
Options overview (non-exhaustive):
RegisterDiscoveredBehaviors/RegisterDiscoveredCqrsHandlers/RegisterDiscoveredNotificationHandlers: toggles for auto-registration.Pipelines: registry for manual behavior registration and pipeline settings (also controls behavior auto-registration enablement).Notifications: registry for manual notification handler registration.NotificationPublisherFactory: selects the publisher implementation (sequential is default; alternatives includeParallelWhenAllNotificationPublisher).NotificationFailureBehavior:ContinueAndAggregate,ContinueAndLog,StopOnFirstException.MaxPublishDegreeOfParallelism: degree of parallelism when using parallel publisher.
Behavior ordering: behaviors run outer ? inner in registration order; annotate with PipelineOrderAttribute to control execution order when using auto-registration (lower runs earlier).
If you cannot or do not want to enable the generator, use:
services.AddDualisRuntime(opts =>
{
// Configure options, registries, and optional runtime discovery flags here.
});
- Uses the generated
Dualis.Dualizorif present; otherwise falls back to a reflection-based mediator. - Applies manual registries (
opts.Pipelines,opts.CQRS,opts.Notifications) and can perform basic runtime discovery when enabled by options flags.
- Reference Dualis abstractions wherever needed.
- Enable generation only in the host project and call
services.AddDualis()there. - The generator scans the host compilation (and referenced assemblies) to discover public
IRequestHandler<>/IRequestHandler<,>and pipeline behaviors.
Requirements:
- Handler and behavior types in referenced assemblies must be
public(or visible viaInternalsVisibleToto the host). - Other projects should NOT enable the generator.
Two primary forms are supported:
- Request/response:
IPipelineBehavior<TRequest, TResponse> - Void request:
IPipelineBehavior<TRequest>
A unified form IPipelineBehaviour<TMessage, TResponse> can apply to both requests and notifications (Unit for void).
Behaviors are executed in registration order (outer -> inner). You can annotate behaviors with PipelineOrderAttribute to control ordering when auto-registered. Lower values run earlier.
Define a notification and handlers:
public sealed record UserCreatedEvent(Guid Id) : INotification;
public sealed class UserCreatedEventHandler : INotificationHandler<UserCreatedEvent>
{
public Task HandleAsync(UserCreatedEvent n, CancellationToken ct)
=> Task.CompletedTask;
}
Publish from anywhere you have IPublisher/IDualizor:
await dualizor.Publish(new UserCreatedEvent(id));
Choose failure behavior:
ContinueAndAggregate– run all handlers, throwAggregateExceptionof failuresContinueAndLog– log and swallow failuresStopOnFirstException– stop immediately on first failure (sequential)
Choose publisher implementation via NotificationPublisherFactory (default: SequentialNotificationPublisher; alternatives: ParallelWhenAllNotificationPublisher, ChannelNotificationPublisher).
- Emits
Dualis.Dualizor, the mediator/dispatcher used byIDualizor. - Emits an internal, non-extension DI method in
Dualis.Generated:ServiceCollectionExtensions.AddDualis(IServiceCollection, Action<DualizorOptions>?). - The public runtime extension
Dualis.ServiceCollectionExtensions.AddDualis(this IServiceCollection, Action<DualizorOptions>?)invokes the internal method reflectively when the generator runs in the host; otherwise it falls back to a runtime registration path.
The generator runs when any of these is true in the host project:
- MSBuild property
DualisEnableGeneratoris visible to the compiler and set totrue. - A
.editorconfig/.globalconfigsetsbuild_property.DualisEnableGenerator = true. - An assembly-level attribute
[assembly: Dualis.EnableDualisGeneration]is present.
- "AddDualis not found" or
IServiceCollectionmissing extension:- Ensure the host project references
Dualisand hasusing Dualis;in scope. - Ensure generation is enabled in the host (property, attribute, or
.editorconfig). - Clean bin/obj and rebuild to clear stale analyzer artifacts.
- Ensure the host project references
- Ambiguous
AddDualiscall (CS0121):- Ensure only one Dualis analyzer is active (from the NuGet package you packed). Remove any old/stale analyzers and clean caches.
- Only the host should enable generation.
- Not all handlers are auto-registered:
- Ensure handler/behavior classes are
publicor exposed viaInternalsVisibleToto the host. - Ensure the host references the assemblies containing the handlers.
- Ensure handler/behavior classes are
Basic microbenchmarks live under tests/Dualis.Benchmarks. Run in Release:
dotnet run -c Release --project tests/Dualis.Benchmarks/Dualis.Benchmarks.csproj
- Runtime library targets .NET 9
- Source generator targets .NET Standard 2.0 (works across SDKs/tooling)
Issues and PRs are welcome. Please run unit tests and benchmarks before submitting changes.
MIT
