Custom Validation Attributes mit ASP.NET – Türsteher für deine C# Web-API

Jan-Hendrik Precht

Jan-Hendrik Precht begeistert sich für die Themen "lebenslanges Lernen" und "systemische Unterstützung in Personalentwicklung und Vertrieb". Zum Einen, weil Ihn sowohl die Vielfalt an möglichen Themen als auch innovative Steuerungsmöglichkeiten faszinieren. Zum Anderen, weil letztendlich alle Beteiligten etwas davon haben.

Die Validierung eingehender Daten ist eine Standardanforderung bei der Implementierung deiner Web-API. Denn bevor deine Datenverarbeitung stattfinden kann, musst du die Integrität der eingehenden Daten sicherstellen. ASP.NET bietet dir die Möglichkeit dieser Anforderung leicht nachzukommen, denn du kannst benutzereigene Validatoren komfortabel integrieren.

Vorteile

Folgende Vorteile bieten sich dir:

  • Du erstellst modulare Validatoren nach dem Single-Responsibility-Prinzip.
  • Deine Validatoren lassen sich über Attribute auf Datenmodell-Ebene integrieren.
  • Dein Business-Code legt den Fokus auf Funktionalität und ist frei von Validierung.
  • Die Unit-Tests deiner Validatoren werden stark vereinfacht.

Einstieg

Du implementierst im Folgenden einen Validator für ein konkretes Anwendungsbeispiel. Dein Validator soll sicherstellen, dass eine Zeichenkette lediglich aus Groß- und Kleinbuchstaben besteht. Der fertige Validator wird dann zur Überprüfung eines Nutzernamens verwendet. Der Nutzername wird über einen HTTP Request an eine Web-API übermittelt.

Schritt eins – Klasse erzeugen

Erstelle zu Beginn eine neue Klasse inkl. zugehöriger Datei und benenne beide dann nach folgendem Muster {UseCase}Attribute. Der Name soll für dieses Beispiel LettersOnlyAttribute lauten.

// File: LettersOnlyAttribute.cs

namespace Validation.Attributes;

public class LettersOnlyAttribute
{
}

Schritt zwei – Referenzen hinzufügen

Füge dann die folgenden Referenzen zu Beginn der Klassendatei ein.

using System;
using System.Globalization;
using System.ComponentModel.DataAnnotations;

Schritt drei – Klasse ableiten und dekorieren

Leite anschließend von der Basisklasse ValidationAttribute ab und setze den sealed Modifizierer, um weitere Ableitungen zu verhindern. Dekoriere deine Klasse dann mit dem AttributeUsage Attribut, um dessen Verwendbarkeit zu definieren. Du kannst die folgenden Einstellungen wählen.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field |
    AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class LettersOnlyAttribute : ValidationAttribute
{
}

Die Wahl der AttributeTargets definiert die Anwendbarkeit des Validators auf die gewünschten Elemente in deinem Code. Durch AllowMultiple definierst du, ob mehr als eine Instanz des Validators auf jeweils ein Code Element angewendet werden kann. Hier ist der Standardwert false.

Schritt vier – Validierung durchführen

Überschreibe nun die IsValid Methode der Basisklasse ValidationAttribute. Zunächst wandelst du auf den Zieldatentypen um und implementierst dann deine Validierungslogik.

public override bool IsValid(object? value)
{
    if (value is null)
        return false;

    var text = (string)value;

    return IsLettersOnly(text);
}

Der Rückgabewert der Validierungslogik muss dabei vom Datentyp bool sein. Hier kannst du z.B. die Klasse Regex zur Überprüfung regulärer Ausdrücke verwenden.

// using System.Text.RegularExpressions;

private static bool IsLettersOnly(string text)
{
    var regex = new Regex("^[a-zA-Z]*$");

    return regex.IsMatch(text);
}

Schritt fünf – Fehlermeldung formatieren (optional)

Optional überschreibst du noch die FormatErrorMessage Methode. So kannst du die Fehlermeldung ein wenig sprechender machen. Die Fehlermeldung wird automatisch erzeugt, wenn die Validierung nicht bestanden wird.

public override string FormatErrorMessage(string name)
{
    return string.Format(CultureInfo.CurrentCulture,
        $"The property, field or parameter '{name}' is invalid, " +
         "because only letters are allowed.");
}

Unit-Test

Für den Unit-Test reicht es jetzt aus, die IsValid Methode der Klasse LettersOnlyAttribute zu testen.

using FluentAssertions;
using Validation.Attributes;
using Xunit;

namespace Validation.Test.Attributes;

public class LettersOnlyAttributeTest
{
    private readonly LettersOnlyAttribute _uut;

    public LettersOnlyAttributeTest()
    {
        _uut = new LettersOnlyAttribute();
    }

    [Theory]
    [InlineData("ValidInput", true)]
    [InlineData("Invalid_Input", false)]
    public void Test_IsValid(string input, bool result)
    {
        var actual = _uut.IsValid(input);

        actual.Should().Be(result);
    }
}

Anwendungsbeispiel

Im folgenden Anwendungsbeispiel nutzt du das erstellte Validation Attribute, um den Datentransfer in deine Web-API abzusichern. Dein Validator soll hier den Namen eines Nutzers validieren. Der Nutzername darf sich also nur aus Groß- und Kleinbuchstaben zusammensetzen.

Variante A – im Datenmodell (Property)

Du platzierst den Validator direkt im Datenmodell User des Nutzers.

public class User
{
    [LettersOnly]
    public string Name { get; set; } = "";

    public int Age { get; set; } = 0;
}

Die Daten werden über den Controller TestController in die Web-API gereicht und dort verarbeitet.

namespace WebApi.Controllers;

[ApiController]
public class TestController : ControllerBase
{
    public TestController()
    {
    }

    [HttpPost("api/test-user")]
    public IActionResult TestUser([FromBody] User user)
    {
        /* Place your business code here. */

        return Ok(user);
    }
}

Die Methode TestUser entspricht einem HTTP POST Request mit Datentransfer über den Request Body.

curl -X POST "https://localhost:5001/api/test-user" \
     -H "accept: */*" \
     -H "Content-Type: application/json" \
     -d "{\"name\":\"ArthurDent\",\"age\":42}"

Variante B – an der Controller-Methode (Parameter)

Du hast ebenfalls die Möglichkeit den Validator direkt auf einem Parameter deiner Controller-Methode zu integrieren.

[HttpPost("api/test-letters-only")]
public IActionResult TestLettersOnly([FromQuery] [LettersOnly] string text)
{
    /* Place your business code here. */

    return Ok(text);
}

Die Methode TestLettersOnly entspricht einem HTTP POST Request mit Datentransfer über den Request URL.

curl -X POST "https://localhost:5001/api/test-letters-only?text=ArthurDent" \
     -H "accept: */*" \
     -d ""

Integrationstest

Starte die Web-API Anwendung und führe die gezeigten HTTP Requests über deine Console aus. Wenn der Validator deine Dateneingabe akzeptiert, dann wird dein Business-Code ausgeführt. Im gezeigten Beispiel schickt die Web-API die Eingabe im HTTP Response Body einfach wieder zurück.

// Status code 200 (Ok) - Response body:
{
  "name": "ArthurDent",
  "age": 42
}

Sollte der Validator deine Dateneingabe allerdings ablehnen, weil du gegen das Prüfkriterium aus der Methode IsLettersOnly verstößt, dann wird der Controller deinen HTTP Request ablehnen. Der Controller wird dann mit dem Status Code 400 (Bad Request) antworten und folgenden Response Body zurückgeben. Dein Business-Code wird nicht ausgeführt.

// Status code 400 (Bad Request) - Response body:
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-48f9cc895b0ce04e89a4e48cf1046e5e-f81c15186260824d-00",
  "errors": {
    "Name": [
      "The property, field or parameter 'Name' is invalid, because only letters are allowed. The value of 'Name' is 'ArthurDent_42', but must match regex pattern '^[a-zA-Z]*$'."
    ]
  }
}

Fazit

Das besprochene Beispiel zeigt dir hoffentlich, wie du eigene Validation Attributes unter ASP.NET implementierst und in deine Web-API integrierst. Wenn du in Zukunft Validation Attributes verwenden möchtest, dann berücksichtige sie schon frühzeitig bei der Planung deiner Web-API.

Den Code zum Blog findest du auch auf GitHub.

Für einen schnellen Einstieg findest du im GitHub Repository eine Web-API (inkl. Swagger) startklar für deine eigenen Tests. Außerdem kannst du dir die Implementierung eines weiteren Validators OfLegalAgeAttribute anschauen, der das Alter des Nutzers Age auf Volljährigkeit prüft.

Happy Coding!