« Fluent validation » : différence entre les versions
De Banane Atomic
Aller à la navigationAller à la recherche
(32 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
[[Category: | [[Category:.NET Application]] | ||
= Liens = | = Liens = | ||
* [https://fluentvalidation.net/ Fluent Validation] | * [https://fluentvalidation.net/ Fluent Validation] | ||
Ligne 33 : | Ligne 33 : | ||
.Must(name => name != null) | .Must(name => name != null) | ||
.Must(IsValid) | .Must(IsValid) | ||
.WithMessage(" | .WithMessage("{PropertyName} is invalid."); // change error message. Default is: 'Name' must not be empty. | ||
} | } | ||
Ligne 47 : | Ligne 47 : | ||
public string Name { get; set; } | public string Name { get; set; } | ||
} | } | ||
</kode> | |||
== Output == | |||
<kode lang='json'> | |||
{ | |||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", | |||
"title": "One or more validation errors occurred.", | |||
"status": 400, | |||
"traceId": "|84df05e2-41e0d4841bb61293.", | |||
"errors": { | |||
"Name": [ | |||
"Name is invalid." | |||
] | |||
} | |||
} | |||
</kode> | |||
= [https://docs.fluentvalidation.net/en/latest/built-in-validators.html Built-in Validators] = | |||
== [https://docs.fluentvalidation.net/en/latest/built-in-validators.html#enum-validator Enum Validator] == | |||
Checks whether a numeric value is valid to be in that enum. | |||
<kode lang='cs'> | |||
RuleFor(x => x.ErrorLevel).IsInEnum(); | |||
</kode> | </kode> | ||
Ligne 74 : | Ligne 96 : | ||
public string Name { get; set; } | public string Name { get; set; } | ||
} | } | ||
</kode> | |||
= [https://stackoverflow.com/questions/20529085/fluentvalidation-rule-for-multiple-properties Validation on multiple properties] = | |||
<kode lang='cs'> | |||
this.RuleFor(x => new { x.Property1, x.Property2 }) | |||
.Must(x => x.Property1.Count > 0 || x.Property2 .Count > 0) | |||
.OverridePropertyName(x => x.Property1); | |||
</kode> | </kode> | ||
= [https://fluentvalidation.net/start#collections Validate elements of a collection] = | = [https://fluentvalidation.net/start#collections Validate elements of a collection] = | ||
{{warn | If your root object is a collection, be aware that FV doesn't handle {{boxx|IReadOnlyCollection}}}} | |||
<kode lang='cs'> | <kode lang='cs'> | ||
public sealed class MyClassValidator : AbstractValidator<MyClass> { | public sealed class MyClassValidator : AbstractValidator<MyClass> { | ||
Ligne 116 : | Ligne 146 : | ||
</kode> | </kode> | ||
= [https://fluentvalidation.net/ | = [https://docs.fluentvalidation.net/en/latest/conditions.html Conditions] = | ||
<kode lang='cs'> | <kode lang='cs'> | ||
When(x => x.Property > 0, () => { | When(x => x.Property > 0, () => { | ||
Ligne 127 : | Ligne 157 : | ||
.NotNull() | .NotNull() | ||
.When(x => x.Property > 0); | .When(x => x.Property > 0); | ||
// code of length 4 or null | |||
RuleFor(x => x.Code) | |||
.Length(4) | |||
.Unless(x => string.IsNullOrEmpty(x.Code)); | |||
</kode> | </kode> | ||
Ligne 144 : | Ligne 179 : | ||
[https://fluentvalidation.net/built-in-validators#built-in-validators Built-in Validators and their Placeholders] | [https://fluentvalidation.net/built-in-validators#built-in-validators Built-in Validators and their Placeholders] | ||
= [https://fluentvalidation.net/ | == [https://docs.fluentvalidation.net/en/latest/custom-validators.html#custom-message-placeholders Custom message placeholders] == | ||
<filebox fn='ItemValidator.cs'> | |||
public ItemValidator(IItemService itemService) | |||
{ | |||
this.itemService = itemService; | |||
RuleFor(x => x).MustAsync(HasNoSimilarItemAsync) | |||
.OverridePropertyName("SimilarItem") | |||
.WithMessage("A similar item already exists. Name: {SimilarItemName} - Date: {SimilarItemDate:dd/MM/yyyy}"); | |||
} | |||
private async Task<bool> HasNoSimilarItemAsync( | |||
Item rootObject, | |||
Item item, | |||
PropertyValidatorContext context, | |||
CancellationToken token) | |||
{ | |||
var similarItems = await itemService.FindSimilarItemsAsync(item); | |||
if (similarItems.Any()) | |||
{ | |||
context.MessageFormatter.AppendArgument("SimilarItemName", similarItems[0].Name) | |||
.AppendArgument("SimilarItemDate", similarItems[0].Date); | |||
return false; | |||
} | |||
return true; | |||
} | |||
</filebox> | |||
= [https://docs.fluentvalidation.net/en/latest/custom-validators.html Custom validator] = | |||
<kode lang='cs'> | |||
RuleFor(x => x.Elements) | |||
.Custom((list, context) => | |||
{ | |||
if(list.Count > 10) | |||
context.AddFailure("The list must contain 10 items or fewer"); | |||
} | |||
); | |||
</kode> | |||
= [https://docs.fluentvalidation.net/en/latest/cascade.html Cascade mode] = | |||
<kode lang='cs'> | <kode lang='cs'> | ||
// test first if Name is not null, then even if Name is null test if lenght is >= 3 | // test first if Name is not null, then even if Name is null test if lenght is >= 3 | ||
RuleFor(x => x.Name).NotNull().MinimumLength(3); | RuleFor(x => x.Name) | ||
.NotNull() | |||
.MinimumLength(3); | |||
// test first if Name is not null, then only if Name is not null test if lenght is >= 3 | // test first if Name is not null, then only if Name is not null test if lenght is >= 3 | ||
RuleFor(x => x.Name).Cascade(CascadeMode. | RuleFor(x => x.Name) | ||
.Cascade(CascadeMode.Stop) | |||
.NotNull() | |||
.MinimumLength(3); | |||
// change the default cascade mode | // change the default cascade mode | ||
public MyClassValidator() { | public MyClassValidator() { | ||
// First set the cascade mode | // First set the cascade mode | ||
RuleLevelCascadeMode = CascadeMode.Stop; | |||
RuleFor(x => x.Name).NotNull().MinimumLength(3); | RuleFor(x => x.Name).NotNull().MinimumLength(3); | ||
Ligne 163 : | Ligne 242 : | ||
<kode lang='cs'> | <kode lang='cs'> | ||
var validator = new MyClassValidator(); | var validator = new MyClassValidator(); | ||
var | var validationResult = await validator.ValidateAsync(myClass); | ||
public sealed class MyClassValidator : AbstractValidator<MyClass> | public sealed class MyClassValidator : AbstractValidator<MyClass> | ||
Ligne 203 : | Ligne 282 : | ||
= [https://docs.fluentvalidation.net/en/latest/aspnet.html?highlight=ASP.NET%20Core ASP.NET Core integration] = | = [https://docs.fluentvalidation.net/en/latest/aspnet.html?highlight=ASP.NET%20Core ASP.NET Core integration] = | ||
{{warn | You should not use asynchronous rules when using ASP.NET automatic validation as ASP.NET’s validation pipeline is not asynchronous.}} | |||
<kode lang='bash'> | <kode lang='bash'> | ||
# install the nuget package | # install the nuget package | ||
dotnet add package FluentValidation.AspNetCore | dotnet add package FluentValidation.AspNetCore | ||
</kode> | </kode> | ||
<filebox fn='Startup.cs'> | |||
// automatic validation | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddControllers() | |||
.AddFluentValidation(fv => | |||
{ | |||
// automatic registration of the validators | |||
fv.RegisterValidatorsFromAssemblyContaining<PersonValidator>(); | |||
// disable other validator providers like DataAnnotations | |||
fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; | |||
}); | |||
// manual registration of the validators | |||
services.AddTransient<IValidator<Person>, PersonValidator>(); | |||
</filebox> | |||
<filebox fn='PersonController.cs'> | |||
public class PersonController : Controller { | |||
[HttpPost] | |||
public IActionResult Create(Person person) { | |||
if(!ModelState.IsValid) { | |||
return BadRequest(ModelState); | |||
} | |||
// manual async validation | |||
var personValidator = new PersonValidator(); | |||
var validationResult = await personValidator.ValidateAsync(person); | |||
validationResult.AddToModelState(ModelState, null); | |||
if (!ModelState.IsValid) | |||
{ | |||
return BadRequest(new ValidationProblemDetails(ModelState)); | |||
} | |||
</filebox> | |||
= [https://exceptionnotfound.net/custom-validation-in-asp-net-web-api-with-fluentvalidation/ Use filter to check the model validity] = | = [https://exceptionnotfound.net/custom-validation-in-asp-net-web-api-with-fluentvalidation/ Use filter to check the model validity] = | ||
Ligne 236 : | Ligne 352 : | ||
public void ValidationOfProp3_AskProp1AndProp2_Error() | public void ValidationOfProp3_AskProp1AndProp2_Error() | ||
{ | { | ||
// Arrange | |||
var query = new MyQuery | var query = new MyQuery | ||
{ | { | ||
Ligne 241 : | Ligne 358 : | ||
Prop2 = "value2" | Prop2 = "value2" | ||
}; | }; | ||
this.validator.ShouldHaveValidationErrorFor(x => x.Prop3, query) | |||
// Act | |||
var result = this.validator.TestValidate(query); | |||
// Assert | |||
result.ShouldHaveValidationErrorFor(x => x.Prop3, query) | |||
.WithErrorCode(nameof(ValidationMessageResources.MyErrorMessage)) | |||
.WithErrorMessage(ValidationMessageResources.MyErrorMessage); | |||
} | } | ||
</filebox> | </filebox> |
Dernière version du 10 septembre 2024 à 15:34
Liens
- Fluent Validation
- Built-in Validators
- Fluent Validation on git
- Dynamic Validation with FluentValidation in WPF/MVVM
Exemple
var mc = new MyClass(); var validator = new MyClassValidator(); ValidationResult results = validator.Validate(mc); if (!results.IsValid) { foreach (var failure in results.Errors) { Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " + failure.ErrorMessage); } string allMessages = results.ToString(); Console.WriteLine(allMessages); } public sealed class MyClassValidator : AbstractValidator<MyClass> { public MyClassValidator() { this.RuleFor(x => x.Name) .NotNull() .Must(name => name != null) .Must(IsValid) .WithMessage("{PropertyName} is invalid."); // change error message. Default is: 'Name' must not be empty. } private static bool IsValid(string value) { return value != null; } } public sealed class MyClass { public int Id { get; set; } public string Name { get; set; } } |
Output
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "|84df05e2-41e0d4841bb61293.", "errors": { "Name": [ "Name is invalid." ] } } |
Built-in Validators
Enum Validator
Checks whether a numeric value is valid to be in that enum.
RuleFor(x => x.ErrorLevel).IsInEnum(); |
Set validator for complex properties
public sealed class MyClassValidator : AbstractValidator<MyClass> { public MyClassValidator() { RuleFor(mc => mc.MySubClass).SetValidator(new MySubClassValidator()); } } public sealed class MySubClassValidator : AbstractValidator<MySubClass> { public MySubClassValidator() { RuleFor(msc => msc.Name).NotNull(); } } public sealed class MyClass { public int Id { get; set; } public MySubClass MySubClass { get; set; } } public sealed class MySubClass { public int Id { get; set; } public string Name { get; set; } } |
Validation on multiple properties
this.RuleFor(x => new { x.Property1, x.Property2 }) .Must(x => x.Property1.Count > 0 || x.Property2 .Count > 0) .OverridePropertyName(x => x.Property1); |
Validate elements of a collection
If your root object is a collection, be aware that FV doesn't handle IReadOnlyCollection |
public sealed class MyClassValidator : AbstractValidator<MyClass> { public MyClassValidator() { // use RuleForEach to apply rules on each element RuleForEach(x => x.Items) .NotEmpty() // use ChildRules for complex object .ChildRules(items => { items.RuleFor(x => x.Name).NotNull(); }); RuleForEach(mc => mc.Items) .SetValidator(new ItemValidator()); // use ForEach inside a RuleFor to apply rules on each element RuleFor(x => x.Ids) .NotEmpty() .ForEach(idRule => idRule.Must(x => x > 0)); RuleFor(x => x.Items) .NotEmpty() .ForEach(itemRule => itemRule.ChildRules(items => { items.RuleFor(x => x.Name).NotNull(); })); } } public sealed class MyClass { public List<int> Ids { get; } = new List<int>(); public List<Item> Items { get; } = new List<Item>(); } public sealed class Item { public string Name { get; set; } } |
Conditions
When(x => x.Property > 0, () => { RuleFor(x => x.Name).NotNull(); }).Otherwise(() => { RuleFor(x => x.Name).MinimumLength(3); }); RuleFor(x => x.Name) .NotNull() .When(x => x.Property > 0); // code of length 4 or null RuleFor(x => x.Code) .Length(4) .Unless(x => string.IsNullOrEmpty(x.Code)); |
Overriding the error message
this.RuleFor(x => x.Name) .WithMessage("Error message on {PropertyName}."); |
Placeholder | |
---|---|
{PropertyName} | The name of the property being validated |
{PropertyValue} | The value of the property being validated, these include the predicate validator (‘Must’ validator), the email and the regex validators. |
Built-in Validators and their Placeholders
Custom message placeholders
ItemValidator.cs |
public ItemValidator(IItemService itemService) { this.itemService = itemService; RuleFor(x => x).MustAsync(HasNoSimilarItemAsync) .OverridePropertyName("SimilarItem") .WithMessage("A similar item already exists. Name: {SimilarItemName} - Date: {SimilarItemDate:dd/MM/yyyy}"); } private async Task<bool> HasNoSimilarItemAsync( Item rootObject, Item item, PropertyValidatorContext context, CancellationToken token) { var similarItems = await itemService.FindSimilarItemsAsync(item); if (similarItems.Any()) { context.MessageFormatter.AppendArgument("SimilarItemName", similarItems[0].Name) .AppendArgument("SimilarItemDate", similarItems[0].Date); return false; } return true; } |
Custom validator
RuleFor(x => x.Elements) .Custom((list, context) => { if(list.Count > 10) context.AddFailure("The list must contain 10 items or fewer"); } ); |
Cascade mode
// test first if Name is not null, then even if Name is null test if lenght is >= 3 RuleFor(x => x.Name) .NotNull() .MinimumLength(3); // test first if Name is not null, then only if Name is not null test if lenght is >= 3 RuleFor(x => x.Name) .Cascade(CascadeMode.Stop) .NotNull() .MinimumLength(3); // change the default cascade mode public MyClassValidator() { // First set the cascade mode RuleLevelCascadeMode = CascadeMode.Stop; RuleFor(x => x.Name).NotNull().MinimumLength(3); |
Async Validation
var validator = new MyClassValidator(); var validationResult = await validator.ValidateAsync(myClass); public sealed class MyClassValidator : AbstractValidator<MyClass> { SomeExternalWebApiClient client; public MyClassValidator(SomeExternalWebApiClient client) { this.client = client; this.RuleFor(x => x.Name) .MustAsync(async (name, cancellation) => { bool nameExists = await client.NameExists(name); return !nameExists; }).WithMessage("Name is not valid."); } private static bool IsValid(string value) { return value != null; } } |
Throwing Exceptions
try { validator.ValidateAndThrow(mc); } catch (ValidationException e) { foreach (var failure in e.Errors) { Console.WriteLine(failure.ErrorMessage); } } |
ASP.NET Core integration
You should not use asynchronous rules when using ASP.NET automatic validation as ASP.NET’s validation pipeline is not asynchronous. |
# install the nuget package dotnet add package FluentValidation.AspNetCore |
Startup.cs |
// automatic validation public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddFluentValidation(fv => { // automatic registration of the validators fv.RegisterValidatorsFromAssemblyContaining<PersonValidator>(); // disable other validator providers like DataAnnotations fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; }); // manual registration of the validators services.AddTransient<IValidator<Person>, PersonValidator>(); |
PersonController.cs |
public class PersonController : Controller { [HttpPost] public IActionResult Create(Person person) { if(!ModelState.IsValid) { return BadRequest(ModelState); } // manual async validation var personValidator = new PersonValidator(); var validationResult = await personValidator.ValidateAsync(person); validationResult.AddToModelState(ModelState, null); if (!ModelState.IsValid) { return BadRequest(new ValidationProblemDetails(ModelState)); } |
Use filter to check the model validity
Ainsi il n'est plus nécessaire de tester ModelState.IsValid dans chaque Controller.
App_Start\WebApiConfig.cs |
public static void Register(HttpConfiguration config) { config.Filters.Add(new ValidateModelStateFilter()); |
ValidateModelStateFilter.cs |
public class ValidateModelStateFilter : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (!actionContext.ModelState.IsValid) { actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState); |
Unit tests
MyValidatorTest.cs |
public class MyValidatorTest { private readonly MyValidatorTest validator = new MyValidatorTest(); [Fact] public void ValidationOfProp3_AskProp1AndProp2_Error() { // Arrange var query = new MyQuery { Prop1 = "value1", Prop2 = "value2" }; // Act var result = this.validator.TestValidate(query); // Assert result.ShouldHaveValidationErrorFor(x => x.Prop3, query) .WithErrorCode(nameof(ValidationMessageResources.MyErrorMessage)) .WithErrorMessage(ValidationMessageResources.MyErrorMessage); } |
WebApi 2 integration
Integration with ASP.NET WebApi 2 is no longer supported as of FluentValidation 9. Use ASP.NET Core. link |
Installer le package NuGet FluentValidation.WebApi
Validator attribute
App_Start\WebApiConfig.cs |
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // ... FluentValidationModelValidatorProvider.Configure(config); |
ItemController.cs |
[HttpPost] [Route("")] [ResponseType(typeof(Item))] public IHttpActionResult Post(Item item) { // if the item is null, the validator is not executed if (item == null) { return this.BadRequest("Item cannot be null."); } // if there are validation errors if (!ModelState.IsValid) { return this.BadRequest(ModelState); } |
Item.cs |
// link the class with its validator [Validator(typeof(ItemValidator))] public sealed class Item { } |
Dependency injection with Autofac
App_Start\WebApiConfig.cs |
public static void Register(HttpConfiguration config) { var builder = new ContainerBuilder(); // register all the validator AssemblyScanner.FindValidatorsInAssembly(typeof(WebApiConfig).Assembly) .ForEach(result => { builder.RegisterType(result.ValidatorType).As(result.InterfaceType); }); // register the ValidatorFactory because its ctor needs an IComponentContext builder.RegisterType<AutofacValidatorFactory>(); IContainer container = builder.Build(); FluentValidationModelValidatorProvider.Configure(config, provider => { provider.ValidatorFactory = container.Resolve<AutofacValidatorFactory>(); }); |
AutofacValidatorFactory.cs |
internal class AutofacValidatorFactory : ValidatorFactoryBase { private readonly IComponentContext context; public AutofacValidatorFactory(IComponentContext context) { this.context = context; } public override IValidator CreateInstance(Type validatorType) { if (this.context.TryResolve(validatorType, out var instance)) { var validator = instance as IValidator; return validator; } return null; } } |