Blazor FluentValidation | C# Validation Best Practices

FluentValidation in Blazor: Validierungslogik mit Custom Rules und Feldern
Michael F. | Softwareentwickler
04/2025

Fluent Validation in Blazor

Eine relativ einfache Validierung von Eingabemasken kann bereits mit den Bordmitteln von .NET durch das Hinzufügen von ValidationAttribute-Dekorationen implementiert werden. Schnell stößt man jedoch an Grenzen, wodurch aufwändige Anpassungen notwendig werden, etwa die Implementierung eigener ValidationAttribute. Ein einfaches Beispiel hierfür wäre die Validierung zweier Datumsfelder, bei der der Wert im zweiten Feld stets größer als der im ersten sein muss. Solch eine Regel ließe sich noch relativ unkompliziert umsetzen.

Doch wie gestaltet sich die Validierung, wenn plötzlich fünf oder mehr Felder mit wechselnden Abhängigkeiten und Regeln überprüft werden müssen? Für solche Szenarien gibt es einige NuGet-Pakete, die diese Anforderungen bereits effizient lösen können. Vorausgesetzt, diese Pakete sind sowohl client- als auch serverseitig in Blazor anwendbar. Im Folgenden werden zwei dieser Pakete näher betrachtet und ihre Vor- und Nachteile gegenübergestellt. Ziel ist es, eine Entscheidungshilfe bereitzustellen, welches Paket sich für spezifische Problemstellungen am besten eignet.

Definition der Validierungsregeln

Es empfiehlt sich, die Validierungsregeln pro Datenklasse in einer separaten Klasse zu definieren, um die Struktur zu optimieren. Ein Beispiel für die Validierung eines Zahlenfelds RangeStart in Abhängigkeit von drei weiteren Feldern (Fixed, NumberLength und RangeEnd) wird nachfolgend erläutert. Das Feld NumberLength bestimmt dabei dynamisch den Zahlenbereich, in dem sich RangeStart bewegen darf. Beispielsweise gilt bei NumberLength = 4 ein Bereich von 1000 bis 9999. Zusätzlich muss RangeStart kleiner als RangeEnd sein. Diese Regeln gelten jedoch nur, wenn Fixed nicht gesetzt ist. Die Konfiguration ist dabei unkompliziert.

1public class DemoDTOValidator : AbstractValidator<DemoDTO>
2{
3    public DemoDTOValidator()
4    {
5        ...
6        RuleFor(dto => dto.RangeStart)
7            .NotEmpty()
8            .When(dto => !dto.Fixed, applyConditionTo: ApplyConditionTo.AllValidators)
9            .WithMessage("Zahlenbereich (von) muss angegeben werden")
10
11            .Must((dto, rangeStart) => IntWithVariableLength(rangeStart, dto.NumberLength))
12            .When(dto => !dto.Fixed, applyConditionTo: ApplyConditionTo.AllValidators)
13            .WithMessage(dto => $"Die eingegebene Zahl muss eine Länge von {dto.NumberLength} haben")
14
15            .LessThan(dto => dto.RangeEnd)
16            .When(dto => !dto.Fixed, applyConditionTo: ApplyConditionTo.AllValidators)
17            .WithMessage("Zahlenbereich (von) muss kleiner sein als 'bis'");
18        ...
19    }
20
21    private bool IntWithVariableLength(int? value, int? length)
22    {
23        return value.HasValue && length.HasValue && value.Value.ToString().Length == length.Value;
24    }
25}

Der Validator selbst kann eine beliebige Methode sein (hier: IntWithVariableLength). Diese Methode ist lediglich eine von vielen Regeln, die im Konstruktor der Validator-Klasse definiert werden können. Entsprechend müssen oder können auch Regeln für RangeEnd definiert werden.

Attribut-Validierung

Validation-Attribute müssen nicht zwingend in der Datenklasse definiert sein, können jedoch ergänzend zur FluentValidation genutzt werden, beispielsweise für grundlegende Überprüfungen wie Required oder Range.

1public class DemoDTO
2{
3    public int Id { get; set; }
4
5    [Required(ErrorMessage = "Bitte geben Sie einen Wert ein")]
6    [Range(4, 8, ErrorMessage = "{0} muss einen Wert zwischen {1} und {2} haben")]
7    [Display(Name = "Zahlenlänge")]
8    public int? NumberLength { get; set; }
9    ...
10    public bool Fixed { get; set; }
11    public string? FixedValue { get; set; }
12    public int? RangeStart { get; set; }
13    public int? RangeEnd { get; set; }
14}

Blazorise.FluentValidation

Versionen und Lizenz

  • Verwendete NuGet-Paket-Version: 1.6
  • FluentValidation-Version: 11.9.1
  • Lizenz: Non-Commercial License (im kommerziellen Umfeld kostenpflichtig).
    Link zur Lizenz

Program.cs

In der Program.cs muss beim App-Start zunächst Blazorise registriert und konfiguriert werden. Die Option Immediate ermöglicht eine Validierung bei jeder Tastatureingabe, anstatt erst beim Verlassen des Eingabefelds. Darüber hinaus muss die FluentValidation dem Blazorise-Service hinzugefügt werden.

1public class Program
2{
3    public static void Main(string[] args)
4    {
5        ...
6        builder.Services
7            .AddBlazorise(options => options.Immediate = true;)
8            .AddBlazoriseFluentValidation();
9        ...
10        builder.Services
11            .AddValidatorsFromAssemblyContaining<DemoDTOValidator>();
12        ...
13        var host = builder.Build();
14        ...
15        host.Run();
16    }
17}

Zusätzlich ist es notwendig, die Validierungsregeln, beispielsweise über ein Assembly, hinzuzufügen.

Angaben in einer Razor-Page

Die Definition der Razor-Page erfolgt wie folgt:

1<EditForm EditContext="EditContext">
2    <DataAnnotationsValidator />
3
4    <Validations @ref="@fluentValidations" Mode="ValidationMode.Auto" 
5                 EditContext="EditContext" HandlerType="typeof(FluentValidationHandler)">
6
7        <Validation>
8            <Field>
9                <Label>Zahlenlänge (DataAnnotation)</Label>
10                <NumericEdit Placeholder="Von" TValue="int?" @bind-Value="Item.NumberLength">
11                    <Feedback>
12                        <ValidationError />
13                    </Feedback>
14                </NumericEdit>
15            </Field>
16        </Validation>
17
18        <Validation>
19            <Field>
20                <Label>Zahlenbereich</Label>
21                <NumericEdit Placeholder="Von" TValue="int?" @bind-Value="Item.RangeStart">
22                    <Feedback>
23                        <ValidationError />
24                    </Feedback>
25                </NumericEdit>
26            </Field>
27        </Validation>
28
29        <Validation>
30            <Field>
31                <NumericEdit" Placeholder="Bis" TValue="int?" @bind-Value="Item.RangeEnd>
32                    <Feedback>
33                        <ValidationError />
34                    </Feedback>
35                </NumericEdit>
36            </Field>
37        </Validation>
38    </Validations>
39
40    <Button Color="Color.Primary" Clicked="@OnSave" Disabled="HasErrors">Save</Button>
41</EditForm>

Code-Behind:

1public partial class BlazoriseFluentValidation : ComponentBase
2{
3    public EditContext EditContext { get; set; }
4    public DemoDTO Item { get; set; }
5    public Validations fluentValidations;
6
7    bool HasErrors { get; set; } = true;
8
9    protected override async Task OnParametersSetAsync()
10    {
11        Item ??= new() { NumberLength = 5 };
12        EditContext = new EditContext(Item);
13        EditContext.OnFieldChanged += EditContext_OnFieldChanged;
14    }
15
16    private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
17    {
18        bool isValid1 = EditContext.Validate();                 // erfordert <DataAnnotationsValidator />
19        bool isValid = fluentValidations.ValidateAll().Result;  // erfordert <Validations @ref="@fluentValidations"
20        HasErrors = !(isValid && isValid1);
21    }
22
23    protected override async Task OnInitializedAsync()
24    {
25        await base.OnInitializedAsync();
26    }

Damit funktioniert die Validierung. Einziger Nachteil: Die Lizenz des Pakets ist nicht für kommerzielle Nutzung geeignet.

Blazored.FluentValidation

Versionen und Lizenz

  • Verwendete NuGet-Paket-Version: 2.2.0 für .NET 8
  • Lizenz: MIT License (kostenfrei).
    Link zur Lizenz

Dieses Paket basiert ebenfalls auf FluentValidation (Version 11.9.1) und erlaubt dadurch eine einheitliche Definition der Validierungsregeln.

Program.cs

In der Program.cs ist keine weitere Konfiguration notwendig.

Razor-Page

In der Razor-Page befinden sich mehrere Eingabefelder. Für die Validierung relevant sind jedoch nur drei Felder: NumberLength, RangeStart und RangeEnd. Diese Felder sind in der Validierung voneinander abhängig.

Hier das Resultat einer entsprechenden Validierung:

Die Definition der Razor-Page enthält eine EditForm mit den drei Controls, zugehörigen Labels und einer ValidationMessage. Neben dem bekannten DataAnnotationsValidator wird der FluentValidationValidator integriert.

1<EditForm EditContext="EditContext">
2    <DataAnnotationsValidator />
3    <FluentValidationValidator />
4
5    <div class="field">
6        <label for="@Item.NumberLength">Zahlenlänge (DataAnnotation)</label>
7        <InputNumber class="d-block" @bind-Value="Item.NumberLength" />
8        <ValidationMessage TValue="int?" For="() => Item.NumberLength"></ValidationMessage>
9    </div>
10
11    <div class="field">
12        <label for="@Item.RangeStart">Zahlenbereich</label>
13        <InputNumber class="d-block" @bind-Value="Item.RangeStart" placeholder="Von" />
14        <ValidationMessage For="() => Item.RangeStart"></ValidationMessage>
15    </div>
16
17    <div>
18        <InputNumber class="d-block" @bind-Value="Item.RangeEnd" placeholder="Bis" />
19        <ValidationMessage TValue="int?" For="() => Item.RangeEnd"></ValidationMessage>
20    </div>
21    
22    <button type="button" class="btn btn-primary" onclick="@OnSave" disabled="@HasErrors">Save</button>
23
24</EditForm>

Im Code-Behind wird der EditContext um einen EventHandler erweitert, der beim Verlassen eines Feldes den gesamten Kontext validiert.

1public partial class BlazoredFluentValidation
2{
3    public EditContext? EditContext { get; set; }
4    public DemoDTO Item { get; set; }
5
6    protected override async Task OnInitializedAsync()
7    {
8        Item ??= new() { NumberLength = 5 };
9        EditContext = new EditContext(Item);
10        EditContext.OnFieldChanged += EditContext_OnFieldChanged;
11
12        await base.OnInitializedAsync();
13    }
14
15    protected override async Task OnParametersSetAsync()
16    {
17        await base.OnParametersSetAsync();
18    }
19
20    private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
21    {
22        bool isValid1 = EditContext.Validate();    // erfordert <DataAnnotationsValidator />
23        HasErrors = !(isValid1);
24    }
25
26    protected void OnSave()
27    {
28    }
29
30    public void Dispose()
31    {
32        if (EditContext is not null)
33        {
34            EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
35        }
36    }
37}

Damit ist die Einrichtung abgeschlossen. Der Aufwand für dieses Paket ist deutlich geringer als für Blazorise.FluentValidation.

Mehr zum Thema

Pfeil nach rechts (Verlinkung)
Liskov Principle: Schnittstellenfehler & Sicherheitslücken vermeiden
12/2024

Solid Reihe - Liskov Substitution Principle

Pfeil nach rechts (Verlinkung)
Open/Closed Principle: Erweiterbare Software dank klarer Struktur
11/2024

Solid Reihe - Open Close Principle

Pfeil nach rechts (Verlinkung)
JavaScript & Blazor kombinieren: [JSInvokable] Funktionen in .NET-Apps nutzen
11/2024

Interoperabilität JavaScript und Blazor

Pfeil nach rechts (Verlinkung)
Clean Code in C#: Strukturierter, verständlicher und wartbarer Code
11/2024

Clean Code Prinzipien | Lesbarer C# Code Tutorial

Pfeil nach rechts (Verlinkung)

Gemeinsam Großes schaffen

Devware GmbH verpflichtet sich, Ihre Privatsphäre zu schützen. Wir benötigen Ihre Kontaktinformationen, um Sie bezüglich unserer Produkte und Dienstleistungen zu kontaktieren. Mit Klick auf Absenden geben Sie sich damit einverstanden. Weitere Informationen finden Sie unter Datenschutz.
Vielen Dank für Ihr Vertrauen.
Unser Team prüft Ihre Anfrage sorgfältig und meldet sich in der Regel innerhalb von 48 Stunden bei Ihnen zurück.
Falls es besonders eilig ist, erreichen Sie uns auch telefonisch:
+ 49 (0) 202 478 269 0.
Da ist etwas schief gegangen beim Absenden des Formulars.