Start Debugging

Minimal-API-Validierung vs FluentValidation in ASP.NET Core 11: Was sollten Sie wählen?

Verwenden Sie die integrierte, per Source Generator erzeugte Validierung für synchrone, per Attribut ausdrückbare Regeln in ASP.NET Core 11; greifen Sie zu FluentValidation, wenn Sie asynchrone Regeln, komplexe feldübergreifende Logik oder Validierung außerhalb Ihrer Domänenmodelle benötigen.

Wenn Sie eine neue Minimal API in ASP.NET Core 11 beginnen, verwenden Sie die integrierte Validierung: Rufen Sie AddValidation() auf, annotieren Sie Ihren Anfrage-Record mit DataAnnotations, und ein Source Generator liefert ein 400 ProblemDetails zurück, bevor Ihr Handler läuft, ohne Abhängigkeiten und mit Native-AOT-Unterstützung. Greifen Sie zu FluentValidation (aktuell 12.1.1, Apache 2.0) nur dann, wenn Sie etwas brauchen, das die integrierte Funktion wirklich nicht leisten kann: asynchrone Regeln, die eine Datenbank abfragen, reichhaltige feldübergreifende Logik oder Validierung, die vollständig außerhalb Ihrer Domänenmodelle gehalten wird. Die entscheidende Achse sind asynchrone und komplexe bedingte Regeln. Brauchen Sie diese, gewinnt FluentValidation; brauchen Sie sie nicht, ist die integrierte Funktion die reibungsärmere Wahl. Alles Folgende zielt auf .NET 11 mit Microsoft.NET.Sdk.Web und C# 14; die integrierte Funktion erschien erstmals in .NET 10 und ist in 11 unverändert.

Die Funktionsmatrix

Dies ist die Tabelle, derentwegen Sie hier sind. Jede Zeile spiegelt .NET 11 und FluentValidation 12.1.1 wider.

FunktionIntegriert (DataAnnotations)FluentValidation 12
Abhängigkeitkeine (eingebaut, .NET 10+)NuGet-Paket, Apache 2.0
MechanismusSource Generator zur Compile-ZeitReflexion zur Laufzeit + Expression Trees
Native AOT / Trimmingkompatibel (keine Laufzeitreflexion)erfordert Sorgfalt, teilweise Unterstützung
Asynchrone Regeln (DB-/HTTP-Abfragen)neinja (MustAsync, ValidateAsync)
Feldübergreifende RegelnIValidatableObject (umständlich)erstklassig (RuleFor(...).When(...))
Wo die Regeln lebenam Modell, als Attributein einer separaten Validator-Klasse
Bedingt / RegelsätzebegrenztWhen/Unless/WhenAsync, Regelsätze
Automatisches 400 in einer Minimal APIja, integrierter Endpunkt-Filtermanuell: Sie schreiben den Endpunkt-Filter
Form der FehlerantwortProblemDetails (RFC 9457) kostenlosSie bilden Fehler selbst auf eine Antwort ab
Wiederverwendbare zusammengesetzte Regelnbenutzerdefiniertes ValidationAttributeSetValidator, Regelverkettung, Vererbung

Die Kurzfassung: Die integrierte Funktion optimiert für “einschalten und vergessen” bei einfachen Modellen, FluentValidation optimiert für Ausdruckskraft bei komplexen. Keine ist generell besser. Die Zeilen, die die meisten Entscheidungen kippen, sind asynchrone Regeln und der Ort, an dem die Regeln leben.

Wann Sie die integrierte Validierung wählen sollten

Die integrierte Validierung, Schritt für Schritt erklärt in wie man Anfragekörper in Minimal APIs ohne Controller validiert, ist in diesen Fällen die richtige Standardwahl:

Wann Sie FluentValidation wählen sollten

FluentValidation 12 verdient sich seine Abhängigkeit, wenn die integrierte Funktion an eine Wand stößt:

Wie jede Variante im Code aussieht

Die integrierte Variante besteht aus den Annotationen, die Sie bereits kennen, plus einer Service-Registrierung:

// .NET 11, C# 14 -- Program.cs
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation();
var app = builder.Build();

app.MapPost("/products", (CreateProduct product) =>
    TypedResults.Created($"/products/{product.Sku}", product));

app.Run();

public record CreateProduct(
    [Required, Length(3, 20)] string Sku,
    [Required, MinLength(2)] string Name,
    [Range(1, 10_000)] int Quantity);

Ein ungültiger Körper erreicht den Handler nie. Er kommt automatisch als 400 mit einem ProblemDetails-Payload zurück, indexiert nach Sku, Name und Quantity.

Das FluentValidation-Äquivalent verschiebt die Regeln in eine Validator-Klasse und verdrahtet sie, da die integrierte Pipeline für automatische Validierung veraltet ist (mehr dazu unten), über einen Endpunkt-Filter, den Sie explizit aufrufen:

// .NET 11, C# 14 -- FluentValidation 12.1.1
using FluentValidation;
using FluentValidation.Results;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IValidator<CreateProduct>, CreateProductValidator>();
var app = builder.Build();

app.MapPost("/products", (CreateProduct product) =>
        TypedResults.Created($"/products/{product.Sku}", product))
   .AddEndpointFilter(async (ctx, next) =>
   {
       var product = ctx.GetArgument<CreateProduct>(0);
       var validator = ctx.HttpContext.RequestServices
           .GetRequiredService<IValidator<CreateProduct>>();

       ValidationResult result = await validator.ValidateAsync(product);
       if (!result.IsValid)
           return Results.ValidationProblem(result.ToDictionary());

       return await next(ctx);
   });

public record CreateProduct(string Sku, string Name, int Quantity);

public sealed class CreateProductValidator : AbstractValidator<CreateProduct>
{
    public CreateProductValidator()
    {
        RuleFor(x => x.Sku).NotEmpty().Length(3, 20);
        RuleFor(x => x.Name).NotEmpty().MinimumLength(2);
        RuleFor(x => x.Quantity).InclusiveBetween(1, 10_000);
    }
}

Das ist mehr Code, aber auch der Ort, an dem die Stärke liegt. Fügen Sie eine asynchrone Eindeutigkeitsprüfung hinzu, und die integrierte Funktion kann schlicht nicht folgen:

// .NET 11, C# 14 -- FluentValidation 12.1.1
public sealed class CreateProductValidator : AbstractValidator<CreateProduct>
{
    public CreateProductValidator(IProductRepository repo)
    {
        RuleFor(x => x.Sku)
            .NotEmpty()
            .Length(3, 20)
            .MustAsync(async (sku, ct) => !await repo.SkuExistsAsync(sku, ct))
            .WithMessage("A product with this SKU already exists.");
    }
}

MustAsync greift während der Validierung auf die Datenbank zu. Es gibt kein DataAnnotations-Attribut, das dies tut, ohne dass Sie einen DbContext innerhalb der Regel öffnen, und IValidatableObject.Validate ist synchron, kann also nichts awaiten. Das ist die Wand, die Teams zu FluentValidation schickt.

Sie können den Boilerplate-Code des Endpunkt-Filters in eine kleine generische Erweiterung auslagern, sodass jeder Endpunkt nur .WithValidation<CreateProduct>() anhängt. Die Mechanik, Filter über eine Routengruppe zu komponieren, ist dieselbe wie in Minimal-API-Endpunkte mit MapGroup organisieren beschrieben, sodass ein einzelnes MapGroup(...).AddEndpointFilter(...) eine ganze Gruppe validieren kann.

Der Benchmark: Start, nicht Dauerbetrieb

Die ehrliche Performance-Geschichte ist hier nicht der Anfragedurchsatz. Bei einer Handvoll Eigenschaften validieren beide Ansätze in Mikrosekunden, und die Kosten werden von der JSON-Deserialisierung und dem, was Ihr Handler tut, in den Schatten gestellt. Der messbare Unterschied liegt an den Rändern: Kaltstart und AOT.

Da die integrierte Funktion zur Compile-Zeit erzeugt wird, fügt sie beim Start keine Reflexion hinzu. FluentValidation baut seine Regelketten mit Expression Trees, die bei der ersten Nutzung kompiliert werden, sodass die erste Validierung jedes Typs einmalige Kosten für JIT und Expression-Kompilierung trägt. Auf einem warmen Server amortisieren sich diese Kosten auf null. Bei einer kaltstartenden Serverless-Funktion, die eine Anfrage bedient und dann einfriert, zahlen Sie sie bei einem erheblichen Anteil der Aufrufe.

Die schärfere Unterscheidung ist die Veröffentlichung. Eine Minimal API mit Native AOT und dem integrierten Validator wird ohne Trimming-Warnungen veröffentlicht und ausgeführt, weil es keine Reflexion zu erhalten gibt. Dieselbe Anwendung mit FluentValidation wird Trimming-Warnungen erzeugen, sofern Sie die validierten Typen und ihre Member nicht verankern, da die Bibliothek über Ihr Modell und über Member-Zugriffsausdrücke reflektiert. Das heißt nicht “FluentValidation ist langsam”, sondern “FluentValidation kostet Sie Trimming-Sicherheitsarbeit, die die integrierte Funktion nicht kostet”. Ist Ihr Bereitstellungsziel AOT, behandeln Sie das als entscheidenden Faktor statt einer Nanosekundenzählung. Zahlen, die älter als dieser .NET-11-Zyklus sind, sollten Sie neu messen, bevor Sie ihnen vertrauen; die Validierungsinterna änderten sich zwischen .NET 8 und .NET 10.

Der Stolperstein, der für Sie entscheidet: das veraltete AspNetCore-Paket

Wenn Sie nach FluentValidations ASP.NET-Core-Integration suchen, finden Sie das Paket FluentValidation.AspNetCore und seine alte automatische Validierungs-Pipeline. Fangen Sie dort nicht an. Die Maintainer haben sie als veraltet markiert, und die Dokumentation ist explizit: “We no longer recommend using this approach for new projects but it is still available for legacy implementations.” Sie führt keine asynchronen Validatoren aus (sie wirft), hat Minimal APIs oder Blazor nie unterstützt, und ihr implizites Verhalten ist schwer zu debuggen. Der 2026 empfohlene Weg ist der oben gezeigte manuelle: Registrieren Sie IValidator<T> im DI und rufen Sie ValidateAsync selbst auf, aus einem Endpunkt-Filter für Minimal APIs oder aus dem Handler. Wenn ein Tutorial Sie AddFluentValidationAutoValidation() aufrufen lässt, stammt es aus der Zeit vor dieser Empfehlung.

Ein zweiter Stolperstein schneidet in die andere Richtung, zugunsten von FluentValidation: Die Lizenzangst ist hier fehl am Platz. FluentValidation bleibt Apache 2.0 und kostenlos, auch für kommerzielle Nutzung. Die Bibliothek, die im Januar 2025 auf eine restriktive, kostenpflichtige Lizenz wechselte, war Fluent Assertions, ein anderes Projekt mit verwirrend ähnlichem Namen. Sie sind nicht verwandt, und die Wahl von FluentValidation setzt Sie keiner Gebühr pro Platz aus. Wenn ein Richtlinienprüfer “Fluent*” auf Ihrer Abhängigkeitsliste markiert, ist das die zu treffende Unterscheidung.

Der letzte ist eine Korrektheitsfalle, die nur FluentValidation betrifft: Ist irgendeine Regel eines Validators asynchron, muss jede Aufrufstelle ValidateAsync verwenden. Ein versehentliches synchrones Validate() an einem Validator mit einer MustAsync-Regel wirft zur Laufzeit, nicht zur Compile-Zeit, kann also Tests bestehen, die diesen Pfad nie ausführen, und in der Produktion scheitern. Standardisieren Sie überall auf ValidateAsync, um die Falle ganz zu vermeiden.

Die Empfehlung, erneut

Wählen Sie in ASP.NET Core 11 standardmäßig die integrierte Validierung. Sie ist kostenlos, AOT-kompatibel, liefert Ihnen einen standardisierten ProblemDetails-Vertrag, und für die synchronen, attributförmigen Regeln, die den Großteil der Anfragevalidierung ausmachen, ist sie strikt weniger Arbeit als das Hinzufügen einer Abhängigkeit. Wählen Sie FluentValidation bewusst, wenn Sie an eine konkrete Grenze gestoßen sind: asynchrone Regeln, die E/A brauchen, bedingte feldübergreifende Logik, die IValidatableObject unschön macht, oder eine architektonische Regel, dass die Validierung Ihre Domänenmodelle nicht berühren darf. Viele reale Systeme enden mit beidem, und das ist in Ordnung: Lassen Sie die integrierte Funktion die einfachen DTOs bewachen und FluentValidation die Handvoll Anfragetypen mit wirklich komplexen Regeln übernehmen. Der Fehler ist, in einem neuen .NET-11-Dienst aus Gewohnheit zu einer Validierungsbibliothek zu greifen, bevor Sie eine Regel haben, die das Framework nicht ohnehin schon kostenlos ausdrücken kann.

Verwandt

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück