Fluent Validation

“A popular .NET library for building strongly-typed validation rules.”

The example below could be used to validate a simple request object - FooRequest

1
2
3
4
5
6
7
8
namespace FooApi.Models
{
public class FooRequest
{
public Guid SomeId { get; set; }
public string SomeString { get; set; }
}
}

Setup (Web API)

Install the following from nuget, at the the time I updated this post the latest versions were 10.3.6

Use the built in extension method AddFluentValidation to resolve the rules you will write provided the inherit from AbstractValidators. This is probably using reflection to resolve from AbstractValidator. (This will resolve as Scoped) - read the docs for more info

1
2
3
4
5
6
7
8
public void ConfigureServices(IServiceCollection services)
{
services
.AddControllers()
.AddFluentValidation(fv => {
fv.RegisterValidatorsFromAssemblyContaining<Startup>();
});
...

Simple validation

Here FooRequestValidator is the validator and its constructor executes the validation(s)

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace FooApi.Validators
{
public class FooRequestValidator : AbstractValidator<FooRequest>
{
public FooRequestValidator()
{
RuleFor(x => x.SomeId)
.NotEmpty()
.WithMessage((_, someId) =>
$"SomeId must be set. Got {someId}.");
}
}
}

Inject and resolve

You only need to inject IValidator<FooRequest> and call _requestValidator.ValidateAsync is you need perform async validation. Else the FooRequestValidator will be automagically called for you \ :D /

The magic from RegisterValidatorsFromAssemblyContaining will now resolve the validators for you. Use constructor injection to resolve concrete implementations at runtime.

1
2
3
4
5
6
7
8
9
10
11
private readonly IValidator<FooRequest> _requestValidator;

public FooController(IValidator<FooRequest> requestValidator)
{
_requestValidator = requestValidator;
}

[HttpPost]
public async Task<IActionResult> SomePost([FromBody] FooRequest request)
{
await _requestValidator.ValidateAsync(request, options => options.ThrowOnFailures());

Should request.SomeId be a default guid (the 000000 one) then the response would be a 400.

1
2
3
4
5
6
7
8
9
10
11
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "xxxx",
"errors": {
"PartnerId": [
"SomeId must be set. Got 00000000-0000-0000-0000-000000000000."
]
}
}

Complex validations

You can inject services that do validations into the validator, it then pays to stop at that validation failure with .Cascade(CascadeMode.Stop). The services need to already be in the dependency injection container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace FooApi.Validators
{
public class FooRequestValidator : AbstractValidator<FooRequest>
{
public FooRequestValidator(IMyService myservice1, IMyService myservice2)
{
RuleFor(x => x.SomeId)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithMessage((_, someId) =>
$"SomeId must be set. Got {someId}.");
.MustAsync(async (_, someId, cancellationToken) =>
await myservice1.IsValid(someId, cancellationToken)) // here IsValid returns `Task<bool>`
.WithMessage((_, someId) => $"Some data for {someId} could not be found.")
.MustAsync(async (_, someId, cancellationToken) =>
{
var someCollection = await myservice2.GetStuff(someId, cancellationToken);
return !someCollection.Any(x => x.SomeProp);
})
.WithMessage((_, someId) => $"Some other data for {someId} could not be found.")

RuleFor(x => x.SomeString)
.NotEmpty()
.WithMessage((_, someString) =>
$"SomeString must be set. Got {someString}.");
}
...

Unit tests

Testing is done with FluentValidation.TestHelper. Simply new up the validator and await the extension method TestValidateAsync and assert on ShouldHaveValidationErrorFor.

The .Should().BeFalse() assertions are done using Fluent Assertions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using FluentValidation.TestHelper;

public class FooRequestValidatorTests
{
[Fact]
public async Task RuleForSomeId_GivenEmptyGuid_ShouldFailValidation()
{
// Arrange
var validator = new FooRequestValidator();
var request = new FooRequest()
{
SomeId = Guid.Empty // this will be the default but this is more verbose
};

// Act
var result = await validator.TestValidateAsync(request);

// Assert
result.IsValid.Should().BeFalse();

result
.ShouldHaveValidationErrorFor(x => x.SomeId)
.WithErrorMessage("SomeId must be set. Got 00000000-0000-0000-0000-000000000000.");
}
}