Automatyczna walidacja parametrów akcji

W internecie znaleźć można mnóstwo sposobów na zwalidowanie modelu. Moim zdaniem najlepszym sposobem jest zastosowanie atrybutów z przestrzeni System.ComponentModel.DataAnnotations, ponieważ walidacja odbywa się samoczynnie. Niestety kiedy te same atrybuty wykorzystamy do parametrów akcji, to już tak automagicznie nie jest. W tym poście zaprezentuję problem oraz pokażę jak można sobie z nim poradzić.

Walidacja modelu

Bez owijania w bawełnę, poniżej przykład implementacji, która działa poprawnie:

public class UserModel
{
    [Required]
    public string UserName { get; set; }

    [Required, RegularExpression(@"^\d{11}$")]
    public string Pesel { get; set; }
}

[Route("")]
public void Post(UserModel model)
{
    if (!ModelState.IsValid)
    {
        // return 400
    }
    // do sth
}

valid-model

Jeszcze lepszym rozwiązaniem jest utworzenie filtru akcji (ActionFilterAttribute). Sprawi to, iż walidacja modelu wykonywana będzie domyślnie przy każdej akcji.

public class ValidateModelStateAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = 
                actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }
}

Walidacja parametrów

Natomiast, nie wiedzieć czemu, kiedy zastosujemy atrybuty walidacyjne przy parametrach metody, to walidacja nie jest uruchamiana.

[Route("{userName}/{pesel}")]
public void Post([Required] string userName, [RegularExpression(@"^\d{11}$")] string pesel)
{
    if (!ModelState.IsValid)
    {
        // return 400
    }

    // do sth
}

not-valid-model

Niestety nie znalazłem w internetach rozwiązania mojego problemu toteż pokusiłem się o własną implementację, która wygląda następująco.

[AttributeUsage(AttributeTargets.Method)]
public class ValidateActionParametersAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var parameters = actionContext.ActionDescriptor.GetParameters();
        var parametersToValidate =
            parameters
                .Where(p => !p.IsOptional && p.DefaultValue == null)
                .Select(p => new {Parameter = p, Attributes = p.GetCustomAttributes<ValidationAttribute>()})
                .Where(p => p.Attributes.Count > 0)
                .ToList();

        parametersToValidate.ForEach(item => ValidateParameter(actionContext, item.Parameter, item.Attributes));

        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response =
                actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }

    private void ValidateParameter(HttpActionContext actionContext, HttpParameterDescriptor parameter, 
        IEnumerable<ValidationAttribute> attributes)
    {
        foreach (var attribute in attributes)
        {
            if (!attribute.IsValid(actionContext.ActionArguments[parameter.ParameterName]))
            {
                actionContext.ModelState.AddModelError(parameter.ParameterName,
                    attribute.FormatErrorMessage(parameter.ParameterName));
            }
        }
    }
}

Rezultaty są zadowalające.

valid-model-2

Podbij ↑

7 thoughts on “Automatyczna walidacja parametrów akcji

  1. Sam miałem na swojej liście rzeczy do zrobienia zapoznanie się dogłębniej z walidacją modeli i opisanie tego, także chętnie z tych informacji skorzystam. Przydałoby się trochę więcej wprowadzenia teoretycznego. Kod jest na wysokim poziomie i widać, że autor wie co robi, lecz tekst pisany jest trochę na zasadzie – ja wiem jak to działa, co trzeba zrobić ale nie mam pomysłu jak to opisać, więc napiszę pierwsze co mi przychodzi do głowy. Czepiając się dalej tekstu “Najlepszym sposobem jest zastosowanie atrybutów z przestrzeni” jeżeli już stosujesz takie stwierdzenie, to przydałoby się podać przykłady innych możliwości walidacji (FluentValidation, jQuery Validation Plugin, Foolproof Validation, Data Annotations Extensions) oraz stosować bardziej techniczną argumentacje zamiast “walidacja odbywa się automagicznie”, bo tak naprawdę nie wiem dlaczego jest najlepsze, może inne rozwiązania także pozwalają na walidacje automagiczną. Oczywiście nie traktuj tego jako obrazy bądź stwierdzenia, że napisałeś coś słabego, a jedynie próby wskazania Ci miejsc, gdzie jeszcze mógłbyś popracować. Na pewno jeszcze tu wrócę ;)

    • Krystian,
      Wielkie dzięki za konstruktywny komentarz.
      To prawda, pominąłem opis implementacji, lecz to dlatego, że kodu nie jest dużo oraz jest on czytelny. Wydaje mi się, że może być on niezrozumiały tylko dla programistów z niewielkim doświadczeniem.
      Moim zdaniem, to najlepsze rozwiązanie, ponieważ dostajemy je za darmo od Framework’a. Nie potrzebujemy zewnętrznych bibliotek. Wszystkie Twoje przykłady, to zewnętrzne projekty/biblioteki, które nie zawsze są pożądane.
      Jeszcze raz dziękuję. Obiecuję poprawę :)

  2. Jak zawsze w takich przypadkach działa na prostych przykładach, a teraz wyobraźmy sobie że przychodzi klient i mówi że chce np. żeby jedna data w modelu nie była starsza od drugiej, my rozbudowujemy infrastruktury itd. po kilku takich cyklach może się okazać że nasze proste rozwiązanie jest ciut za bardzo skomplikowane. Albo że czasami chcemy zrobić redirect zamiast zwrócić 400 a w innym przypadku to chcemy do klienta wysłać Json z listą błędów…

    Pomysł OK ale w większych projektach mogą być z nim kłopoty…

Leave a Reply

Your email address will not be published. Required fields are marked *