Validace - verisoftCZ/verisoft-framework GitHub Wiki
#Validace Na frontendu validovat můžeš, ale na backendu musíš. Při validování se nelze spoléhat na to, že nám FE pošle správná data a zároveň všechny validace na frontendu nelze udělat. Proto je potřeba vstupy pro POST a PUT endpointy a navázané metody validovat.
Většinu validací provádíme v controlleru pomocí obecné ValidationService : IValidationService. Potřeba validace však může vyvstat i uvnitř business logiky v serivce, proto Metoda dané service vrací BusinessActionResult (blíže popsáno v Návratové kódy a typy), čímž je zajištěno validní přeložení do IActionResult na controlleru.
BusinessActionResult využívá implicit operatory pro jednoduchou inicializaci objektu.
#Validace kontraktů
Registrace
Verisoft.Core.Validation.ServiceCollectionExtensions.AddVerisoftFluentValidation<TValidator>(this IServiceCollection serviceCollection)
- extension metoda nad IServiceCollection, kterou lze zavolat z InstallExtensions.cs nebo Program.cs
- type parametr je jeden z fluent validátorů (vysvětleno níže), ostatní jsou pak zaregistrovány z jeho Assembly
- tato metoda zaregistruje i ValidationService : IValidationService
Pro validaci používáme FluentValidation NuGet
using FluentValidation;
, přes jehož validátory drtivou většinu validací provádíme.
using FluentValidation;
namespace DemoApi.Application.Validators;
public class CustomerValidator: AbstractValidator<CustomerValidatedObject>
{
public CustomerValidator()
{
RuleFor(customer => customer.Name)
.NotEmpty()
.WithMessage(ErrorFactory.CustomerNameEmptyErrorMessage)
.WithCode(ErrorFactory.CustomerNameEmptyErrorCode)
}
}
Pro každý objekt, který validuji a jeho podřízené objekty (v kardinalitě 1:1 i 1:n), je potřeba vytvořit samostatnou třídu validátoru, která dědí z AbstractValidatoru s type parametrem kontraktu : IValidatedObject, který vracíme z POST/PUT endpointu v 422.
Prerekvizity pro validaci pomocí IValidationService
- vytvoření třídy, která dědí od kontraktu, který přišel jako param endpointu a zároveň implementuje Verisoft.Core.Contracts.Validation.IValidatedObject interface -- na této třídě je potřeba vydefinovat zanořené kolekce nebo objekty, které chci rovněž validovat a skrýt původní definici
public class ClientEditModel
{
public string Name { get; set; }
public string TradeId { get; set; }
public HashSet<DocumentCreateModel> Documents { get; set; }
}
public class ClientValidatedObject : ClientEditModel, IValidatedObject
{
public List<ValidationProblem> ValidationProblems { get; set; }
public new HashSet<DocumentValidatedObject> Documents { get; set; }
}
- mapování všech "edit modelů" na "validated objecty" přes AutoMapper
cfg.CreateMap<ClientEditModel, ClientValidatedObject>();
cfg.CreateMap<DocumentCreateModel, DocumentValidatedObject>();
- inject IValidationService do controlleru
public class ClientController(IClientService clientService, IValidationService validationService)
V controlleru pak vypadá validace takto
[HttpPut]
[Route("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Client))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(BusinessActionErrors))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(BusinessActionErrors))]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(BusinessActionErrors<ClientValidatedObject>))]
[AllowAnonymous]
public async Task<IActionResult> UpdateClientsAsync(ClientEditModel clientEditModel, int id)
{
var validatedObject = await validationService.ValidateAsync<ClientValidatedObject, ClientEditModel>(clientEditModel, id);
if (validatedObject.HasValidationErrors())
{
return validatedObject.ToActionResult();
}
var result = await clientService.UpdateClientAsync(clientEditModel, id);
return result.ToActionResult();
}
Příklad validátoru
- validátor pro hlavní kontrakt má property
public int? Id { get; set; }
kdy int je PK entity kontraktu -- slouží pro validaci existence entity, kdy lze využítVerisoft.Core.Validation.Validators.ExistingEntityValidator
RuleFor(x => Id)
.SetAsyncValidator(new ExistingEntityValidator<ClientValidatedObject, ClientEntity, int>(clientRepository))
.When(x => Id.HasValue)
.DependentRules(() =>
{
// ... ostatní validace
}
- lze využít i validátor pro kontrolu null inputu
RuleFor(x => x)
.SetValidator(new NullInputValidator<ClientValidatedObject>())
.DependentRules(() =>
{
// ... ostatní validace
}
@<1EAAC8F1-E175-6326-A70E-86A045983945> - příklad celého validátoru vč těch výše popsaných
Pokud failne rule na nullinput controller vrátí 400 Pokud failne rule na existenci controller vrátí 404 Pokud failnou jiné rules, controller vrátí 422
#Přímé validace V krajních případech můžeme dělat i validace přímo v business service, ale měli bychom se snažit vše odchytat přes fluent validace v IValidationService.
Často potřebujeme validovat, dostupnost resource. Například, pokud chceme dělat update entity, potřebujeme zajistit, že updatovaná entita v databázi existuje.
public async Task<BusinessActionResult<Customer>> UpdateCustomerAsync(int id, CustomerEditModel customerEditModel)
{
var customerEntity = await customerRepository.GetAsync(id);
if (customerEntity is null)
{
return ErrorFactory.NotFound(nameof(CustomerEntity), "Id", id.ToString());
}
// ... rest of update method code
}
Statické metody třídy Core.Common.BusinessResult.ErrorFactory umožňují vrátit BusinessActionResult naplněný objektem BusinessActionErrors a tím poskytnout potřebné informace z controlleru ven. Stejný kód, jako je výše se použije i pro vrácení 404 do GET endpointu.
Pro přímé validace lze využít metody NotFound(), BadRequest(), Forbidden() a protected metodu Error(), která je jediným způsobem, jak vytvořit objekt Core.Common.BusinessResult.BusinessActionErrors, který je v kolekci v objektu Errors. Pro užití metody Error je potřeba vytvořit novou třídu, která od třídy ErrorFactory dědí. V této třídě by pak měly vzniknout nové metody, které Error vrací tak, aby bylo zajištěno, že veškeré validační hlášky řeší jedna třída.
Příklad:
public class ErrorFactory : Core.Common.BusinessResult.ErrorFactory
{
private const string PayrollItemTypeInvoicingConfigurationDataCorruptionErrorCode = "PayrollItemTypeInvoicingConfigurationDataCorruption";
private const string PayrollItemTypeInvoicingConfigurationDataCorruptionErrorMessage = "Payroll Item Type Invoicing Configuration for {payrollItemTypeCode} has been corrupted. Contact application support.";
public static Error PayrollItemTypeInvoicingConfigurationDataCorruption(string payrollItemTypeCode)
{
return Error(PayrollItemTypeInvoicingConfigurationDataCorruptionErrorCode, PayrollItemTypeInvoicingConfigurationDataCorruptionErrorMessage, new { payrollItemTypeCode });
}
}
Jak je popsáno v Návratové kódy a typy, tak veškeré Errory, které neobsahují kód, který je v jedné ze tří veřejných statických metod ErrorFactory se projeví návratovou hodnotou 422 na controlleru.