Walidacja danych wejściowych jest ważnym procesem w naszych aplikacjach/usługach, umożliwia uniknięcie błędów wynikających z niepoprawnych wartości. Istnieje kilka sposób na implementacje walidacji w projektach. Najbrzydszym rozwiązaniem jest utworzenie długich litanii if-ów weryfikujących wartości uzyskane na wejściu akcji kontrolera. W ASP.NET najpowszechniejszą metodą walidacji danych wejściowych jest wykorzystanie wbudowanego mechanizmu walidacji za pomocą atrybutów (Data Annotations). Powyższe rozwiązanie dobrze sprawdza się przy prostych walidacjach, przy bardziej złożonych warto poszukać innej alternatywy. W tym artykule pokażę inne podejście do walidacji, czyli wykorzystam bibliotekę FluentValidation, która umożliwia definiowanie prostych oraz zaawansowanych reguł walidacji z wykorzystaniem wyrażeń lambda. Wykorzystanie FluentValidation pozwala wydzielić logikę walidacji z klasy modelu do klasy Validator. Definicja walidacji z wykorzystaniem interfejsu fluent poprawia czytelność kodu. Reguły walidacji są także łatwe do zweryfikowania testami jednostkowymi.

Integracja FluentValidation z ASP.NET Core Web API

W celu wykorzystania biblioteki w projekcie ASP.NET Core Web API należy zainstalować paczkę z NuGet.

Install-Package FluentValidation.AspNetCore

Po dodaniu biblioteki do projektu należy skonfigurować FluentValidation w klasie Startup, poprzez wywołanie metody AddFluentValidation z namespace FluentValidation.AspNetCore. W celu wykrycia przez API walidatorów,  należy wykonać rejestracje walidatorów w metodzie ConfigureServices. Rejestracje można wykonać poprzez wywołanie metody AddTransient dla każdego walidatora, lub poprzez automatyczną rejestrację wszystkich walidatorów z assembly.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
}

Przykład walidacji

Po wymaganej konfiguracji projektu przejdźmy do przykładu stworzonego na potrzeby artykułu. Wykorzystując bibliotekę FluentValidation zostanie sprawdzona poprawność obiektu, który jest przekazywany do akcji kontrolera. Dla prezentacji możliwości biblioteki załóżmy, że mamy API, które umożliwia dodanie obrazu do zasobu np. kolekcji, bazy danych. Na potrzeby wpisu nie interesuje nas implementacja interfejsu IPictureService, potraktujmy ją, jako czarną skrzynkę. Najpierw zdefiniujmy przykładowy model PictureForCreation.

public class PictureForCreation
{
    public string Title { get; set; }

    public string Artist { get; set; }

    public string Description { get; set; }

    public float Height { get; set; }

    public float Width { get; set; }
}

Kolejnym krokiem jest zdefiniowanie klasy Validator, która dziedziczy po abstrakcyjnej klasie AbstactValidator<T>, gdzie T jest typem modelu, dla którego definiowane są reguły walidacji. Reguły walidacji powinny zostać zdefiniowane w konstruktorze walidatora. W celu zdefiniowania reguły walidacji dla właściwości modelu wywoływana jest metoda RuleFor.

public class PictureForCreationValidator  : AbstractValidator<PictureForCreation>
{
    public PictureForCreationValidator ()
    {
        RuleFor(picture => picture.Title)
            .NotEmpty()
            .MaximumLength(100);
        RuleFor(picture => picture.Description)
                    .NotEmpty()
                    .MaximumLength(300)
                    .NotEqual(picture => picture.Title)
                    .WithMessage("The provided description should be different from the title");
        RuleFor(picture => picture.Artist).NotEmpty();
        RuleFor(picture => picture.Height).GreaterThan(0.0f);
        RuleFor(picture => picture.Width).GreaterThan(0.0f);
    }
}

PicturesController walidacja modelu zostanie wywołana dla żądania typu POST, którego zadaniem jest dodanie nowego obrazu do zasobów. Na wejście podajemy model typu PictureForCreation, po stronie PictureService operujemy modelem typu Picture, a z API zwracamy obiekt DTO (PictureDto). Gdy dane nie przejdą walidacji zostanie zwrócony kod odpowiedzi HTTP 400 (Bad Request), w przypadku powodzenia otrzymamy kod 201 (Created). Kontroler sprawdza czy model jest poprawny i umieszcza ewentualne błędy w obiekcie ModelState.  Poniżej zaprezentowano implementacje kontrolera PicturesController.

[Route("api/[controller]")]
public class PicturesController : Controller
{
    private IPictureService _pictureService;

    public PicturesController(IPictureService pictureService)
    {
        _pictureService = pictureService;
    }

    [HttpGet("{pictureId}", Name = "GetPicture")]
    public IActionResult Get(int pictureId)
    {
        var picture = _pictureService.GetPicture(pictureId);
        if (picture == null)
            return NotFound();
        var pictureResult = Mapper.Map<PictureDto>(picture);
        return Ok(pictureResult);
    }

    [HttpPost]
    public IActionResult Post([FromBody]PictureForCreation pictureForCreation)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);

        var picture = Mapper.Map<Picture>(pictureForCreation);
        _pictureService.AddPicture(picture);
        var createdPicture = Mapper.Map<PictureDto>(picture);

        return CreatedAtRoute("GetPicture", new { pictureId = createdPicture.Id }, createdPicture);
    }
}

W celu weryfikacji czy walidacji działa poprawnie zostało wykonane żądanie dodanie nowego obrazu, gdzie tytuł i opis zawierają ten sam ciąg znaków. W wyniku walidacji w narzędziu Postman otrzymałem kod odpowiedzi HTTP 400 z odpowiednim komunikatem.

Create picture

W przypadku poprawnego modelu otrzymamy kod odpowiedzi HTTP 201.

Created Picture

Podsumowanie

Artykuł przedstawił jak biblioteka FluentValidation zapewnia czytelną implementacje reguł walidacji po za klasą modelu. Mam nadzieje, że powyższym wpisem przekonałem was do spróbowania swoich sił z FluentValidation.