Validate Nasze aplikacje ASP.NET CORE coraz częściej są tylko aplikacją REST. To oczywiście wymaga Walidacji po stronie klienta i po stronie serwera.

Jak taką walidację jak najszybciej zrobić. Może przecież sam napisać takie warunki, ale przy dużej ilości klas, które występują jako parametry mija się to z celem. 

Możesz też skorzystać z atrybutów i oznaczyć reguły do każdej właściwości.

Co, jeśli chcesz oddzielić logikę walidacyjną od samej klasy. Nie zastąpi to oczywiście dodawania adnotacji do encji w Entity Framework. Mówimy tutaj tylko przesyłaniu danych do serwera.

FluentValidation idzie Ci z pomocą. W tym wpisie też zobaczymy jak FluentValidation współpracuje z Swagger UI.

Na początku zainstaluj paczkę "FluentValidation.AspNetCore" będzie tam miał wszystko co potrzebne.

f-01.PNG

Aby dodać tą walidacje do kontrolerów,widoków oraz modeli musisz ją załączyć do opcji "AddControlers()".

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddFluentValidation(opt =>
    {
        opt.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
    });

Domyślnie chcemy zarejestrować wszystkie walidacje w naszej bibliotece

Pora stworzyć klasę, które nam przetestuje te mechanizmy. Oto łowca potworów. Student i pracownik był już w poprzednich wpisach o ASP.NET CORE.

public class MonsterHunter
{
    public string Title { get; set; }
    public int Strenght { get; set; }
    public int Magic { get; set; }
    public int Life { get; set; }

    public int Age { get; set; }
    public HeroType Type { get; set; }

    public IEnumerable<Monster> DefeatedMonsters { get; set; }
}

public enum HeroType
{
    Warrior = 1,
    Mage = 2,
    Barbarian = 3,
    Thief = 4,
    Priest = 5
}

Nasz łowca potworów będzie miał listę pokonanych potworów. 

public class Monster
{
    public string Name { get; set; }
    public int Power { get; set; }
    public FightType FightType { get; set; }
}

public enum FightType
{
    Magic = 1,
    Strenght = 2
}

Teraz jak utworzyć logikę walidacyjną.  Tworzysz na to oddzielną klasę, która musi dziedziczyć po AbstractValidator<T>.

public class MonsterHunterValidator : AbstractValidator<MonsterHunter>
{
    public MonsterHunterValidator()
    {
        RuleFor(c => c.Age).InclusiveBetween(0, 100);
        RuleFor(c => c.Title).MaximumLength(100)
            .NotNull();
        RuleFor(c => c.Type).IsInEnum();
        RuleForEach(c => c.DefeatedMonsters).NotEmpty();

        RuleFor(c => c.Strenght).GreaterThanOrEqualTo(1);
        RuleFor(c => c.Magic).GreaterThanOrEqualTo(1);
        RuleFor(c => c.Life).GreaterThanOrEqualTo(1);
    }
}

W konstruktorze opisujesz reguły do każdej właściwości. Możesz na przykład upewnić, że typ wyliczeniowy, który tak naprawdę będzie liczbą całkowitą będzie miał wartości z tego typu wyliczeniowego. Możesz ustawić maksymalne wartości lub ich zakresy.

Możesz też ustalić, że dana kolekcja nie może być pusta.

Dla potwora pokaże Ci jeszcze inne reguły. Mogę też dodać swoją własną metodę walidacyjną. W niej mógłbym na przykład sprawdzać bazę danych czy na przykład dane miasto istnieje w Polsce. Tutaj tylko robię prostą walidacje liczby całkowitej.

Użyłem też "WithMessage". Ta wiadomość powinna się pojawić, gdy dam złą wartości dla "Power".

public class MonsterValidator : AbstractValidator<Monster>
{
    public MonsterValidator()
    {
        RuleFor(c => c.Name).Length(3, 100)
            .NotNull().NotEmpty().NotEqual("-");
        RuleFor(c => c.Power).
            Must(BeAValidPostcode).
            WithMessage("Please specify a valid Power");

        RuleFor(c => c.FightType).IsInEnum();
    }

    private bool BeAValidPower(int power)
    {
        if (power < 0)
            return false;
        return true;
    }
}

Czy FluentValidation automatycznie będzie sprawdzał elementy zagnieżdżone? Odpowiedź brzmi :  nie.

Możemy to rozwiązać na dwa sposoby. Do kolekcji potworów w łowcy potworów możemy ustawić jawnie klasę do sprawdzania właściwości w tej kolekcji.

public class MonsterHunterValidator : AbstractValidator<MonsterHunter>
{
    public MonsterHunterValidator()
    {
        RuleForEach(c => c.DefeatedMonsters).NotEmpty()
            .SetValidator(new MonsterValidator());

Możemy też do ustawień FluentValidation dodać taką opcję, abyśmy nie robili tego za każdym razem.

.AddFluentValidation(configuration =>
    {
        ...
        configuration.ImplicitlyValidateChildProperties = true;
        ...
    })

Pozostało nam napisać kontroler, który sprawdzi czy to wszystko działa.

Tutaj Ciebie zaskoczę, ponieważ nie musisz pisać takiego kodu.

[ApiController]
[Route("[controller]")]
public class MonsterHunterController : Controller
{
    [HttpPost]
    public HttpResponseMessage Add(MonsterHunter hunter)
    {
        if (!ModelState.IsValid)
        {
            return new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.BadRequest,
                Content = new StringContent("Validation failed.")
            };
        }

        return new HttpResponseMessage
        {
            StatusCode = HttpStatusCode.OK,
            Content = new StringContent("OK")
        };
    }
}

Domyślnie FluentValidation automatycznie sprawdza czy wysłany parametr do metody jest poprawny. Nie musisz więc pisać "ModelState.IsValid".

[HttpPost]
[ProducesResponseType(400)]
[ProducesResponseType(200)]
public HttpResponseMessage Add(MonsterHunter hunter)
{
    //if (!ModelState.IsValid)
    //{

    //}

    return new HttpResponseMessage
    {
        StatusCode = HttpStatusCode.OK,
        Content = new StringContent("OK")
    };
}

Gdybyś jednak chciał ręcznie sprawdzać warunki poprzez "ModelState.IsValid" i zwracać wtedy odpowiednie wartości w swoim kontrolerze.

To programiści od FluentValidation zalecają abyś po prostu zarejestrował swoje klasy Walidacyjne w inny sposób tak aby ten automat nie działał.

Tak takie są oficjalne zalecenia.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    var assembliesToRegister = new List<Assembly>() { GetType().Assembly };
    AssemblyScanner.FindValidatorsInAssemblies(assembliesToRegister).ForEach(pair =>
    {
        services.Add(ServiceDescriptor.Transient(pair.InterfaceType, pair.ValidatorType));
    });

...

Dla ułatwienia dodałem też sobie akcję, która zwróci mi poprawnego Łowcę potworów.

[HttpGet]
public MonsterHunter Get()
{
    MonsterHunter mh = new MonsterHunter();

    mh.Title = "Aloha Katora";
    mh.Age = 100;
    mh.Magic = 5;
    mh.Type = HeroType.Mage;
    mh.Strenght = 1;
    mh.Life = 3;
    mh.DefeatedMonsters = new List<Monster>()
    {
        new Monster()
        { FightType = FightType.Magic , Name = "Ghost", Power = 4}
    };

    return mh;
}

Ten JSON za chwilę będzie mi potrzebny do testów.

{
  "title": "Aloha Katora",
  "strenght": 1,
  "magic": 5,
  "life": 3,
  "age": 100,
  "type": 2,
  "defeatedMonsters": [
    {
      "name": "Ghost",
      "power": 4,
      "fightType": 1
    }
  ]
}

W Swagger UI, który omówiłem tutaj zaraz przetestujemy działanie tego całego FluentValidation.

f-03.PNG

To są oczywiście poprawne wartości.

Zmienie teraz "Power" potwora na -4. To powinno zwrócić mi błąd 400 z moją treścią błędu.

f-04.PNG

Jak widać walidacja działa 

Integracja ze Swagger jest taka sobie

Swagger UI dokumentuje twoje REST API . Potrafi ono także pokazać jak poprawnie powinieneś wywołać daną metodę.

Swagger domyślnie nie widzi twojej logiki napisanej w FluentValidation

public class AddFluentValidationRules : ISchemaFilter
{
    private readonly IValidatorFactory _factory;

    /// <summary>
    ///     Default constructor with DI
    /// </summary>
    /// <param name="factory"></param>
    public AddFluentValidationRules(IValidatorFactory factory)
    {
        _factory = factory;
    }

    /// <summary>
    ///     To convert case as swagger may be using lower camel case
    /// </summary>
    /// <param name="inputString"></param>
    /// <returns></returns>
    private static string ToPascalCase(string inputString)
    {
        // If there are 0 or 1 characters, just return the string.
        if (inputString == null) return null;
        if (inputString.Length < 2) return inputString.ToUpper();
        return inputString.Substring(0, 1).ToUpper() + inputString.Substring(1);
    }

    public void Apply(OpenApiSchema model, SchemaFilterContext context)
    {
        // use IoC or FluentValidatorFactory to get AbstractValidator<T> instance
        var validator = _factory.GetValidator(context.Type);
        if (validator == null) return;
        if (model.Required == null)
            model.Required = new SortedSet<string>();

        var validatorDescriptor = validator.CreateDescriptor();
        foreach (var key in model.Properties.Keys)
        {
            foreach (var propertyValidator in validatorDescriptor
                .GetValidatorsForMember(ToPascalCase(key)))
            {
                if (propertyValidator is NotNullValidator
                  || propertyValidator is NotEmptyValidator)
                    model.Required.Add(key);

                if (propertyValidator is LengthValidator lengthValidator)
                {
                    if (lengthValidator.Max > 0)
                        model.Properties[key].MaxLength = lengthValidator.Max;

                    model.Properties[key].MinLength = lengthValidator.Min;
                }

                if (propertyValidator is RegularExpressionValidator expressionValidator)
                    model.Properties[key].Pattern = expressionValidator.Expression;

                // Add more validation properties here;
            }
        }
    }
}

Mógłbyś napisać sam taką wtyczkę pomiędzy tymi bibliotekami, ale po co, gdy masz na to gotową paczkę NuGet.

f-05.PNG

Pozostaje Ci przy rejestracji Swagger dodać "SchemaFilter".

services.AddSwaggerGen(c =>
{
    c.SchemaFilter<AddFluentValidationRules>();
    c.SwaggerDoc("v1",
        new OpenApiInfo
        {
            Title = "AddingStuffAndChecking",
            Version = "v1",
            Description = "An API to get students",
            TermsOfService = new Uri("https://mojafirmalichydziwg.com/terms"),
            Contact = new OpenApiContact
            {
                Name = "Cezary Walenciuk",
                Email = "cezary222@gmail.com",
                Url = new Uri("https://twitter.com/walenciukc"),
            },
            License = new OpenApiLicense
            {
                Name = "AddingStuffAndChecking API",
                Url = new Uri("https://mojafirmalichydziwg.com/license"),
            },
        });

    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
    
});

Niestety możesz też zobaczyć tutaj ograniczenia. O ile dla napisów wszystkie informacje o ograniczeniach się zachowały to dla typów liczbowych te informację nie zostały przekazane dalej.

f-06.PNG

Zakładam, że to wynika z samego ograniczenia Swagger UI. W taki wypadku musiałbyś w komentarzach XML poinformować użytkownika o ograniczeniach twojego REST API.

l