Skip to main content

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