Source Generator
UnambitiousFx.Synapse.Generator is a Roslyn incremental source generator that reads your handler class attributes and emits a registration class at compile time. This eliminates manual RegisterRequestHandler / RegisterEventHandler calls and makes the codebase NativeAOT-compatible by capturing dispatch delegates without reflection.
Install
dotnet add package UnambitiousFx.Synapse.Generator
No further setup is needed — the generator runs automatically during the build.
Mark your handlers
Annotate each handler class with the corresponding attribute:
[RequestHandler<TRequest, TResponse>] — for IRequest<TResponse>
using UnambitiousFx.Synapse.Abstractions;
[RequestHandler<CreateTaskCommand, Guid>]
public class CreateTaskHandler : IRequestHandler<CreateTaskCommand, Guid>
{
public async ValueTask<Result<Guid>> HandleAsync(CreateTaskCommand cmd, CancellationToken ct = default)
{
// ...
return Result.Success(Guid.NewGuid());
}
}
[RequestHandler<TRequest>] — for IRequest (no response)
[RequestHandler<DeleteTaskCommand>]
public class DeleteTaskHandler : IRequestHandler<DeleteTaskCommand>
{
public async ValueTask<Result> HandleAsync(DeleteTaskCommand cmd, CancellationToken ct = default)
{
// ...
return Result.Success();
}
}
[EventHandler<TEvent>] — for IEventHandler<TEvent>
[EventHandler<TaskCreatedEvent>]
public class AuditTaskCreated : IEventHandler<TaskCreatedEvent>
{
public ValueTask<Result> HandleAsync(TaskCreatedEvent @event, CancellationToken ct = default)
{
// ...
return ValueTask.FromResult(Result.Success());
}
}
[StreamRequestHandler<TRequest, TItem>] — for IStreamRequestHandler<TRequest, TItem>
[StreamRequestHandler<StreamTasksQuery, TaskDto>]
public class StreamTasksQueryHandler : IStreamRequestHandler<StreamTasksQuery, TaskDto>
{
public async IAsyncEnumerable<Result<TaskDto>> HandleAsync(
StreamTasksQuery query,
[EnumeratorCancellation] CancellationToken ct = default)
{
// ...
yield return Result.Success(new TaskDto());
}
}
Generated class
The generator produces one class in the root namespace of your assembly.
RegisterGroup
Implements both IRegisterGroup and IEventDispatcherRegistration. Contains one RegisterXxxHandler<...>() call per annotated handler, plus one AOT-safe dispatch delegate per IEvent type found in the assembly:
// Generated — do not edit
public sealed class RegisterGroup : IRegisterGroup, IEventDispatcherRegistration
{
public void Register(IDependencyInjectionBuilder builder)
{
builder.RegisterRequestHandler<CreateTaskHandler, CreateTaskCommand, Guid>();
builder.RegisterRequestHandler<DeleteTaskHandler, DeleteTaskCommand>();
builder.RegisterEventHandler<AuditTaskCreated, TaskCreatedEvent>();
builder.RegisterStreamRequestHandler<StreamTasksQueryHandler, StreamTasksQuery, TaskDto>();
}
public void RegisterDispatchers(Action<Type, DispatchEventDelegate> register)
{
register(
typeof(TaskCreatedEvent),
static (e, dispatcher, ct) => dispatcher.DispatchAsync((TaskCreatedEvent)e, ct));
}
}
Wire up the generated class
Pass the generated class to AddSynapse:
services.AddSynapse(cfg =>
{
cfg.AddRegisterGroup(new RegisterGroup());
});
AddRegisterGroup automatically detects that RegisterGroup implements IEventDispatcherRegistration and registers the AOT-safe dispatch delegates — no second call needed.
:::caution NativeAOT requirement
RegisterGroup implements IEventDispatcherRegistration, so the single AddRegisterGroup call is sufficient for NativeAOT. Without the source generator, event dispatch falls back to runtime typeof(T) resolution which the AOT compiler cannot analyze.
:::
Incremental generation
The generator is an IIncrementalGenerator — it only re-generates when a handler class is added, removed, or its attributes change. Build times are not affected significantly in large solutions.
See also
- Commands and Queries —
IRegisterGroupand modular registration. - Events —
IEventDispatcherRegistrationand polymorphic dispatch. - ASP.NET Core Integration — NativeAOT setup with
CreateSlimBuilder.