Skip to content

Attributes and interfaces

The complete public surface of Immediate.Handlers.Shared, with signatures and examples.

3 min read

Everything you write against lives in the Immediate.Handlers.Shared namespace, shipped inside the Immediate.Handlers package.

TypeKindApplies to
HandlerAttributeAttributeclass
BehaviorsAttributeAttributeclass, assembly
ImmediateAssemblyIdentifierAttributeAttributeassembly
IHandler<TRequest, TResponse>Interfaceimplemented by generated code
IStreamingHandler<TRequest, TResponse>Interfaceimplemented by generated code
Behavior<TRequest, TResponse>Abstract classyou derive from it
StreamingBehavior<TRequest, TResponse>Abstract classyou derive from it

[Handler]

[AttributeUsage(AttributeTargets.Class)]
public sealed class HandlerAttribute : Attribute
{
	public HandlerAttribute();
	public HandlerAttribute(ServiceLifetime serviceLifetime);

	public ServiceLifetime? ServiceLifetime { get; }
	public string[]? Tags { get; init; }
}

Marks a class for handler generation. The class must be partial and not nested. Only classes are valid targets — a record or struct cannot be a handler container, though the request and response types may be anything.

ServiceLifetime overrides the app-wide lifetime for this handler only; when null the handler is registered with whatever was passed to AddXxxHandlers, which defaults to Scoped. Tags filters the handler out of registration calls that pass a non-overlapping tag list.

[Handler]
[Handler(ServiceLifetime.Singleton)]
[Handler(Tags = ["worker"])]
[Handler(ServiceLifetime.Transient, Tags = ["worker", "background"])]

See Registering with IServiceCollection and Tagged registration.

[Behaviors]

[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
public sealed class BehaviorsAttribute(params Type[] types) : Attribute
{
	public Type[] Types { get; }
}

Declares the behavior pipeline. Three placements:

// 1. assembly-wide — every handler in the assembly
[assembly: Behaviors(typeof(LoggingBehavior<,>))]

// 2. on a handler — replaces the assembly-wide list for that handler
[Handler]
[Behaviors(typeof(LoggingBehavior<,>))]
public static partial class GetUsersQuery
{
	// ..
}

// 3. on an attribute of your own — a reusable bundle
[Behaviors(typeof(ValidationBehavior<,>), typeof(TransactionBehavior<,>))]
public sealed class DefaultBehaviorsAttribute : Attribute;

Generic behaviors must be listed unbound. The first type listed is the outermost in the pipeline. See Creating behaviors.

[assembly: ImmediateAssemblyIdentifier]

[AttributeUsage(AttributeTargets.Assembly)]
public sealed class ImmediateAssemblyIdentifierAttribute(string identifier) : Attribute
{
	public string Identifier { get; }
}

Replaces the assembly name in the generated AddXxxHandlers and AddXxxBehaviors method names.

AssemblyAttributes.cs
[assembly: ImmediateAssemblyIdentifier("Application")]
// generates AddApplicationHandlers / AddApplicationBehaviors

The value must be a valid C# identifier and must not start with @; anything else reports IHR0023 and the assembly name is used instead. The same attribute drives the generated method names in Immediate.Apis, Immediate.Cache and Immediate.Injections — see The assembly identifier, which also covers the trap for projects that use Immediate.Injections without referencing Immediate.Handlers.

IHandler<TRequest, TResponse>

public interface IHandler<TRequest, TResponse>
{
	ValueTask<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken = default);
}

Implemented by every generated non-streaming Handler, and registered alongside the concrete class. Use it when the consumer cannot reference the handler’s assembly. For a command handler whose method returns a bare ValueTask, TResponse is System.ValueTuple.

public sealed class Consumer(IHandler<GetUsersQuery.Query, IEnumerable<User>> handler);

IStreamingHandler<TRequest, TResponse>

public interface IStreamingHandler<TRequest, TResponse>
{
	IAsyncEnumerable<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken = default);
}

The streaming counterpart, implemented by handlers whose handle method returns IAsyncEnumerable<TResponse>. TResponse is the element type, not the enumerable. See Streaming handlers.

Behavior<TRequest, TResponse>

public abstract class Behavior<TRequest, TResponse>
{
	public Type HandlerType { get; [EditorBrowsable(EditorBrowsableState.Never)] set; }

	public abstract ValueTask<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken);

	protected ValueTask<TResponse> Next(TRequest request, CancellationToken cancellationToken);

	[EditorBrowsable(EditorBrowsableState.Never)]
	public void SetInnerHandler(Behavior<TRequest, TResponse> handler);
}
  • HandleAsync — override this. Do your work, await Next(...), do more work, return.
  • Next — invokes the remainder of the pipeline. Throws InvalidOperationException if the inner handler has not been set, which only happens if you construct the behavior yourself and skip the wiring.
  • HandlerType — set by the generated handler to the concrete handler container’s type before the request runs. Its setter is hidden from IntelliSense and is for the generator’s use.
  • SetInnerHandler — called by generated code only; it throws if called twice.

StreamingBehavior<TRequest, TResponse>

public abstract class StreamingBehavior<TRequest, TResponse>
{
	public Type HandlerType { get; [EditorBrowsable(EditorBrowsableState.Never)] set; }

	public abstract IAsyncEnumerable<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken);

	protected IAsyncEnumerable<TResponse> Next(TRequest request, CancellationToken cancellationToken);

	[EditorBrowsable(EditorBrowsableState.Never)]
	public void SetInnerHandler(StreamingBehavior<TRequest, TResponse> handler);
}

Identical in shape, with IAsyncEnumerable<TResponse> in place of ValueTask<TResponse>. Override HandleAsync, mark its token parameter [EnumeratorCancellation], and re-yield the items from Next.

Streaming and non-streaming behaviors are matched to handlers separately, so listing both kinds in one [Behaviors] attribute is safe.

Generated members

The generator adds Handler, HandleBehavior and AddHandlers to each handler class, and a HandlerServiceCollectionExtensions class per assembly. Those are described on How it works.