cd ../

Validating Minimal APIs under Native AOT: how Validify was born

#dotnet #native-aot #minimal-apis #fluentvalidation

Some libraries start as a grand plan. This one started as a constraint I couldn’t work around.

The constraint

I was building a Minimal API for a client, running it on AWS Lambda. I wanted the smallest cold start and the lowest memory footprint I could get, so I compiled it with Native AOT. .NET 8 had just shipped, and that combination came with a catch: there was no built-in validation for Minimal APIs yet.

So I went shopping. I tried library after library, looking for a way to validate the request inside the endpoint pipeline — before it reached my handler — instead of writing guard clauses at the top of every method. They all worked fine on a normal build and fell apart under AOT. The reason was always the same: they leaned on reflection, and reflection is exactly what the trimmer strips away.

The insight

Then it clicked. FluentValidation validators are just plain classes. No reflection, no runtime magic — which means the trimmer leaves them alone. If the validators survive AOT, I only needed a thin layer to run them at the right moment: an endpoint filter that validates the model before the handler ever runs, and returns an RFC 9457 HttpValidationProblemDetails when it doesn’t pass.

public sealed class CreateUserValidator : AbstractValidator<CreateUser>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Username)
            .NotEmpty().WithMessage("Username is required")
            .MaximumLength(255);
    }
}

app.MapPost("/users", CreateUser)
   .WithValidation<CreateUser>();

That was the whole idea. Invalid requests get a clean 400 before they touch business logic, and the entire thing stays AOT-friendly.

v1, and then limbo

I shipped version 1, and it worked — but you had to register every validator by hand. It did the job, yet there wasn’t a real differentiator: it was a convenience wrapper, nothing more. So it sat there.

Then .NET 10 arrived with native validation for Minimal APIs, and the library felt even more buried. For a while I assumed that was the end of it.

The revival: v2

The idea that brought it back came suddenly: what if I didn’t have to register validators at all? A source generator could discover every validator in the assembly and wire them up at compile time. That solved two problems at once — it killed the manual registration from v1, and it kept everything reflection-free, which is the part Native AOT actually needs.

builder.Services.AddValidify();
builder.Services.AddValidifyValidators(); // source-generated, zero reflection

One call now finds and registers everything, at build time. That was the differentiator the library was missing, and it shipped as 2.0.0.

A bug on launch day

Launch day wasn’t clean. The generator was reading the assembly and emitting two registration methods with the exact same content — duplicate output that broke the build. I tracked it down, fixed how it scanned the assembly, and pushed 2.1.0.

And honestly, that’s worth dwelling on for a second. Shipping a bug on release day isn’t failure — it’s the job. Engineering isn’t being right the first time; it’s not letting the mistake knock you down, and iterating until it’s right. A bug doesn’t define the developer you are. How you respond to it does.

Where it stands

Validify is now a small, focused library: declarative validation for Minimal APIs, built on FluentValidation, auto-wired by a source generator, and fully Native AOT compatible.

dotnet add package Kopticx.Validify

Does it replace .NET 10’s native validation? Not always — if you’re on .NET 10 and the built-in support covers your needs, use it. But if you’re targeting .NET 8, leaning on FluentValidation’s rule set, or want validators wired up automatically without reflection, Validify still earns its place.

Sometimes the most useful thing you can build is the workaround you needed and couldn’t find. The code lives on GitHub.