Creating handlers
Define a handler with the [Handler] attribute and a private Handle or HandleAsync method.
A handler is a partial class annotated with [Handler] that contains exactly one handle method. The
generator emits a nested Handler class next to it that wires up the pipeline and resolves your
dependencies.
using Immediate.Handlers.Shared;
namespace Application;
[Handler]
public static partial class GetUsersQuery
{
public sealed record Query;
private static ValueTask<IEnumerable<User>> HandleAsync(
Query query,
UsersService usersService,
CancellationToken token
)
{
return usersService.GetUsers(token);
}
}This generates GetUsersQuery.Handler, which:
- attaches every behavior that applies to this request and response pair
- receives
UsersServiceand any other dependencies from DI - implements
IHandler<GetUsersQuery.Query, IEnumerable<User>>
The rules
The generator and analyzers enforce a small, fixed shape.
The class must be partial, and must not be nested inside another type. A nested handler reports IHR0005 and no code is generated for it.
There must be exactly one handle method, named either Handle or HandleAsync — both names are
accepted and behave identically. Declaring both, or two overloads of either, reports IHR0010. Declaring neither reports IHR0001, which ships with an Add HandleAsync method code fix.
The handle method must be private (IHR0011). It is
an implementation detail; consumers go through the generated Handler.
The first parameter is the request (IHR0014 if it is missing).
The return type must be ValueTask, ValueTask<TResponse>, or IAsyncEnumerable<TResponse>.
Task and Task<T> are not accepted and report IHR0002. If you are calling Task-returning code, mark
the handle method async and return ValueTask<T>.
Commands and the implicit response
A handler that returns a bare ValueTask has no response type. The generator supplies System.ValueTuple in its place, so the generated handler implements IHandler<Command, ValueTuple>.
[Handler]
public static partial class CreateUserCommand
{
public sealed record Command(string Email);
private static async ValueTask HandleAsync(
Command command,
UsersService usersService,
CancellationToken token
)
{
await usersService.CreateUser(command.Email, token);
}
}public sealed class Consumer(CreateUserCommand.Handler handler)
{
public async ValueTask Run(CancellationToken token) =>
await handler.HandleAsync(new("user@example.com"), token);
} Cancellation tokens
A trailing CancellationToken parameter is optional, but omitting it reports the configurable warning IHR0012, because the request cannot then be cancelled.
[Handler]
public static partial class GetHelloResponse
{
public sealed record Query(string Name);
private static ValueTask<string> Handle(Query query) =>
ValueTask.FromResult($"Hello {query.Name}!");
}The token must be the last parameter. On an instance handler, anything after it reports IHR0015. On a static handler there is no such error —
the generator simply stops recognising the token as a token and treats every parameter after the request
as a dependency to resolve from DI, including the CancellationToken itself.
Request and response types
Nothing is enforced about the request and response types. They may be a record, record struct, class, sealed class or struct; they may be nested inside the handler or declared anywhere else;
and they need not derive from any base type or implement any interface.
The Query / Command / Response naming used throughout these docs is convention only. It does have
one mechanical effect: the IHR0001 code fix looks for
nested records whose names end in Query, Command or Response to infer the signature of the method
it generates.
Nesting the request and response inside the handler keeps a slice self-contained, but it produces types
named MyApp.DeleteUser+Command. If you also use Swashbuckle, see OpenAPI and Swashbuckle.
Consuming a handler
The concrete X.Handler is the direct route:
public sealed class Consumer(GetUsersQuery.Handler handler)
{
public async ValueTask<IEnumerable<User>> Run(CancellationToken token) =>
await handler.HandleAsync(new GetUsersQuery.Query(), token);
} When your project layout does not allow a reference from the consumer to the handler, depend on the IHandler<TRequest, TResponse> abstraction instead — the generated handler is registered against it:
public sealed class Consumer(IHandler<GetUsersQuery.Query, IEnumerable<User>> handler)
{
public async ValueTask<IEnumerable<User>> Run(CancellationToken token) =>
await handler.HandleAsync(new GetUsersQuery.Query(), token);
} If no concrete handler in the compilation implements the IHandler<,> you asked for, IHR0013 warns you at compile time rather than leaving
you with a DI resolution failure at startup.
Next: Handler dependencies covers the static versus sealed-instance choice and how services reach your handle method.