Skip to content

Generated Code

The dispatch generator produces plain .NET code — async/await, no runtime magic. This page covers the pipeline, generated methods, result types, and observability features.

Pipeline

The generated dispatch method composes pipeline steps as a single Eff expression using LINQ from/select. Each step is only included when the corresponding attribute is present:

authorize → validate → handler → audit → auto-commit
Step Triggered by Description
Authorize [Authorize] on handler Checks named policy before execution
Validate ValidatorType on handler attribute Calls Validate() and lifts errors into Eff
Handler Always Invokes the decorated handler method
Audit Audited = true on handler attribute Records an audit entry after execution
Auto-commit AutoCommit = true (default) Calls CommitAsync() on IAutoCommittable capabilities

Pipeline is compile-time

Steps are not checked at runtime — the generator includes only the steps you've configured. A handler with no [Authorize], no ValidatorType, and Audited = false generates a direct call to the handler method plus auto-commit.

Generated Dispatch Methods

For each handler, the generator emits a Dispatch overload and a convenience method on the dispatch module class:

Generated: dispatch methods
public static partial class AppDispatch
{
    private static readonly ActivitySource ActivitySource =
        new("MyApp.AppDispatch", "1.0.0");

    public static Eff<AppRuntime, OrderCreated> Dispatch(
        CreateOrder command) =>
        (from result in OrderCommands.Handle(command)
         from _commit in liftEff<AppRuntime, Unit>(async rt =>
         {
             if (rt is IAutoCommittable c)
                 await c.CommitAsync(default);
             return unit;
         })
         select result)
        .WithActivity("Dispatch CreateOrder", ActivitySource,
            destination: "OrderCommands");

    public static Eff<AppRuntime, OrderCreated> CreateOrder(
        string name, int quantity) =>
        Dispatch(new CreateOrder(name, quantity));
}

The runtime type parameter (AppRuntime above) is hardcoded to the concrete runtime used by the handler — dispatch is runtime-specific by design. The C# compiler resolves overloads at the call site, so dispatching is a direct method call with full type safety.

Convenience Overloads

For every command and query, the generator also emits a convenience method that constructs the record and dispatches in one call. Default values from the record constructor are preserved:

// Given:
public record CreateOrder(string Name, int Quantity = 10, bool Priority = false) : ICommand;

// Generated:
public static Eff<AppRuntime, OrderCreated> CreateOrder(
    string name, int quantity = 10, bool priority = false) =>
    Dispatch(new CreateOrder(name, quantity, priority));

Result Types

QueryResult<T>

Offset-based pagination result with computed metadata.

public sealed record QueryResult<T>(
    IReadOnlyList<T> Data,
    int TotalCount,
    int Page,
    int PageSize);
Property Type Description
Data IReadOnlyList<T> Items for the current page
TotalCount int Total items across all pages
Page int Current page number (1-based)
PageSize int Items per page
TotalPages int Computed: ⌈TotalCount / PageSize⌉
HasNextPage bool Computed: Page < TotalPages
HasPreviousPage bool Computed: Page > 1
[QueryHandler]
public static Eff<AppRuntime, QueryResult<OrderDto>> Handle(GetOrders query) =>
    from orders in OrderStore.GetAllAsync<AppRuntime>()
    let all = orders.ToList()
    let page = all
        .Skip((query.Page - 1) * query.PageSize)
        .Take(query.PageSize)
        .ToList()
    select new QueryResult<OrderDto>(page, all.Count, query.Page, query.PageSize);

CursorResult<T>

Cursor-based pagination — stable across inserts and deletes. Use for feeds, real-time apps, or large datasets.

public sealed record CursorResult<T>(
    IReadOnlyList<T> Data,
    string? NextCursor,
    bool HasMore,
    int Limit);
Property Type Description
Data IReadOnlyList<T> Items for the current page
NextCursor string? Opaque cursor for the next page, or null if no more
HasMore bool Whether more items exist
Limit int Requested page size
// Fetch limit + 1 items to detect if more exist
var items = await store.GetAfterCursorAsync(query.Cursor, query.Limit + 1);
return CursorResult<OrderDto>.From(items, query.Limit, o => o.Id.ToString());

OpenTelemetry Tracing

Every generated dispatch method is wrapped with .WithActivity(), creating an OpenTelemetry span:

  • Span nameDispatch {CommandName} (e.g., Dispatch CreateOrder)
  • Destination — the handler class name (e.g., OrderCommands)
  • ActivitySource{Namespace}.{DispatchModuleName} with version 1.0.0

This gives you end-to-end tracing through the dispatch pipeline — each command/query invocation appears as a distinct span in your traces.

Auditing

When Audited = true on a handler attribute, the generated pipeline records an AuditEntry after handler execution:

[CommandHandler(Audited = true)]
public static Eff<AppRuntime, OrderUpdated> Handle(UpdateOrder cmd) => ...

The generated audit step captures:

Field Value
Id A new Guid (v7)
Actor From IHasCorrelationContext.CorrelationContext.UserId, or "system"
Action The command/query type name (e.g., "UpdateOrder")
After JSON-serialized result
Timestamp DateTimeOffset.UtcNow

The entry is saved via rt.AuditStore.SaveAsync(...) on the runtime.

Direct Injection

The generated dispatch code is plain async/await under the hood — Eff<RT, T> is just a lazy async computation. You can run any dispatch call with .RunAsync(runtime):

var runtime = new AppRuntime(services);

var result = await AppDispatch.Dispatch(new CreateOrder("Widget", 5))
    .RunAsync(runtime);

result.Match(
    Succ: created => Console.WriteLine($"Created: {created.Id}"),
    Fail: error => Console.WriteLine($"Failed: {error.Message}"));

No dependency injection framework is required — pass the runtime directly. See Using Generated Code for more on effects composition and direct injection.