Adding validation
Validate a command with [Validate] and IValidationTarget, run through ValidationBehavior.
Immediate.Validations validates a request before the handler runs, as a behavior in the pipeline. You annotate properties with validator attributes; the generator writes the validation method.
A command that needs validating
using Immediate.Handlers.Shared;
using Immediate.Validations.Shared;
namespace Todo.Features;
[Handler]
public sealed partial class CreateTodoCommand(TodoRepository repository)
{
[Validate]
public sealed partial record Command : IValidationTarget<Command>
{
[NotEmpty]
[MaxLength(200)]
public required string Title { get; init; }
}
public sealed record Response(TodoItem Item);
private ValueTask<Response> HandleAsync(
Command command,
CancellationToken token
) => ValueTask.FromResult(new Response(repository.Add(command.Title)));
}Three pieces make this work:
[Validate]tells the generator to emit a validation method for this type.partial— the generated method is added to your type.IValidationTarget<Command>is what the behavior looks for at compile time. Leaving it off producesIV0013, with a code fix that adds it.
[NotEmpty] and [MaxLength(200)] are two of fifteen built-in validators — see the full list.
- The generator adds a null check to every non-nullable reference type property automatically,
so
Titleis checked even without[NotNull]. Use[AllowNull]to opt out of that check. - The generator applies
EnumValueto every enum-typed property.
Register the behavior
Validation runs because ValidationBehavior<,> is in the pipeline. Add it assembly-wide:
[assembly: Behaviors(typeof(ValidationBehavior<,>))]Put that at the top of Program.cs, above the top-level statements, or in an AssemblyInfo.cs.
Then register the behaviors:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<TodoRepository>();
builder.Services.AddTodoBehaviors();
builder.Services.AddTodoHandlers();[Behaviors] attribute on an individual handler replaces the
assembly-wide list for that handler rather than adding to it. If you put one on CreateTodoCommand, you must list ValidationBehavior<,> there too. IV0011 warns when the assembly list omits it entirely.What a failure looks like
ValidationBehavior<,> throws ValidationException when the request is invalid. The handler
method never runs.
app.MapPost("/", async (
string title,
CreateTodoCommand.Handler handler,
CancellationToken token) =>
{
try
{
var response = await handler.HandleAsync(new() { Title = title }, token);
return Results.Ok(response.Item);
}
catch (ValidationException ex)
{
return Results.BadRequest(ex.Errors);
}
});Each entry in ex.Errors is a ValidationError carrying the property name and the rendered
message:
[{ "PropertyName": "Title", "ErrorMessage": "'Title' must not be empty." }] Property names use a dotted path for nested types and an indexed suffix for collections — Items[0].Title. The exact rules matter if you’re rendering field-level errors in a UI; they’re
spelled out in Nested and collection validation.
Catching the exception per-endpoint like this is fine for now. The next page replaces it with
proper ProblemDetails wiring that covers every endpoint at once.
Validating without a handler
The generated method is also callable directly, which is handy in tests:
var errors = CreateTodoCommand.Command.Validate(command);
if (!errors.IsValid)
{
// inspect errors
} See Validating instances for the ThrowIfInvalid overloads.
What you have
Requests validated before your code runs, with the validation logic generated from attributes and no validator classes to maintain.
Up next
Exposing an HTTP endpoint — all four handlers become minimal-API endpoints, and validation failures turn into a proper 400.