Skip to content

Creating handlers

Define a handler with the [Handler] attribute and a private Handle or HandleAsync method.

3 min read

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.

GetUsersQuery.cs
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 UsersService and 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>.

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>.

CreateUserCommand.cs
[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.

GetHelloResponse.cs
[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.

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);
}

Next: Handler dependencies covers the static versus sealed-instance choice and how services reach your handle method.