https://github.com/turgayturk/dualis
Fast, lightweight mediator for .NET with CQRS, pipelines, notifications, and a Roslyn source generator. Includes optional analyzers.
https://github.com/turgayturk/dualis
analyzers clean-architecture cqrs csharp ddd dotnet dotnet-9 incremental-generator mediator notifications nuget-package pipelines roslyn source-generator
Last synced: 4 months ago
JSON representation
Fast, lightweight mediator for .NET with CQRS, pipelines, notifications, and a Roslyn source generator. Includes optional analyzers.
- Host: GitHub
- URL: https://github.com/turgayturk/dualis
- Owner: TurgayTurk
- Created: 2025-09-29T19:31:01.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2025-10-08T09:18:58.000Z (8 months ago)
- Last Synced: 2025-10-08T09:23:57.544Z (8 months ago)
- Topics: analyzers, clean-architecture, cqrs, csharp, ddd, dotnet, dotnet-9, incremental-generator, mediator, notifications, nuget-package, pipelines, roslyn, source-generator
- Language: C#
- Homepage: htthttps://www.nuget.org/packages/Dualis
- Size: 229 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README

# Dualis
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` with `IRequestHandler<>`/`IRequestHandler<,>`
- Pipelines: request/response and void pipeline behaviors (plus unified behaviour option)
- Notifications: fan-out publish with failure strategies and alternative publishers
- Public `AddDualis` entry point for DI; source generator augments it in the host project
## What's new (0.2.5)
- Added: `Dualis.Analyzer` project. See installation and rule details in the analyzer README: [src/Dualis.Analyzer/README.md](https://github.com/TurgayTurk/Dualis/blob/main/src/Dualis.Analyzer/README.md).
- Changed: tiny performance refactoring in the source generator/dispatcher code paths.
See full details in [CHANGELOG.md](https://github.com/TurgayTurk/Dualis/blob/main/CHANGELOG.md).
## Install
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 generation (host-only)
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):
```
true
```
- 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 `EmitCompilerGeneratedFiles` to inspect generated files under `obj/generated`.
## Quick start
1) Register in DI. The public `AddDualis` is 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();
```
2) Define a query and handler:
```
public sealed record GetUserByNameQuery(string Name) : IRequest;
public sealed class GetUserByNameQueryHandler : IRequestHandler
{
public Task Handle(GetUserByNameQuery request, CancellationToken ct)
=> Task.FromResult(new UserDto(request.Name));
}
```
3) Use `ISender` in 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.
### Auto-registration, manual registration, and options
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>();
opts.Pipelines.Register>();
// Manually register notification handlers
opts.Notifications.Register();
// You can also register handlers via DI if preferred
// services.AddScoped, GetUserByNameQueryHandler>();
// Notifications: choose publisher and failure policy
opts.NotificationPublisherFactory = sp => sp.GetRequiredService();
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 include `ParallelWhenAllNotificationPublisher`).
- `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).
## Using Dualis without the generator (runtime path)
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.Dualizor` if 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.
## Multi-project (DDD/Clean Architecture) setup
- 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 via `InternalsVisibleTo` to the host).
- Other projects should NOT enable the generator.
## Pipelines
Two primary forms are supported:
- Request/response: `IPipelineBehavior`
- Void request: `IPipelineBehavior`
A unified form `IPipelineBehaviour` 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.
## Notifications
Define a notification and handlers:
```
public sealed record UserCreatedEvent(Guid Id) : INotification;
public sealed class UserCreatedEventHandler : INotificationHandler
{
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, throw `AggregateException` of failures
- `ContinueAndLog` � log and swallow failures
- `StopOnFirstException` � stop immediately on first failure (sequential)
Choose publisher implementation via `NotificationPublisherFactory` (default: `SequentialNotificationPublisher`; alternatives: `ParallelWhenAllNotificationPublisher`, `ChannelNotificationPublisher`).
## Source generator details
- Emits `Dualis.Dualizor`, the mediator/dispatcher used by `IDualizor`.
- Emits an internal, non-extension DI method in `Dualis.Generated`:
`ServiceCollectionExtensions.AddDualis(IServiceCollection, Action?)`.
- The public runtime extension `Dualis.ServiceCollectionExtensions.AddDualis(this IServiceCollection, Action?)` invokes the internal method reflectively when the generator runs in the host; otherwise it falls back to a runtime registration path.
### Gating
The generator runs when any of these is true in the host project:
- MSBuild property `DualisEnableGenerator` is visible to the compiler and set to `true`.
- A `.editorconfig`/`.globalconfig` sets `build_property.DualisEnableGenerator = true`.
- An assembly-level attribute `[assembly: Dualis.EnableDualisGeneration]` is present.
## Troubleshooting
- "AddDualis not found" or `IServiceCollection` missing extension:
- Ensure the host project references `Dualis` and has `using Dualis;` in scope.
- Ensure generation is enabled in the host (property, attribute, or `.editorconfig`).
- Clean bin/obj and rebuild to clear stale analyzer artifacts.
- Ambiguous `AddDualis` call (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 `public` or exposed via `InternalsVisibleTo` to the host.
- Ensure the host references the assemblies containing the handlers.
## Benchmarks
Basic microbenchmarks live under `tests/Dualis.Benchmarks`. Run in Release:
```
dotnet run -c Release --project tests/Dualis.Benchmarks/Dualis.Benchmarks.csproj
```
## Requirements
- Runtime library targets .NET 9
- Source generator targets .NET Standard 2.0 (works across SDKs/tooling)
## Contributing
Issues and PRs are welcome. Please run unit tests and benchmarks before submitting changes.
## License
MIT