This example demonstrates how to use Fluent Validation with Dependency Injection in a .NET application to validate objects before performing operations on them.
Why use Fluent Validation?
- Clear and maintainable validation logic
- Loose coupling between validators and consumer classes
- Isolated validation logic for each request (Transient scope)
Interface
The IClubCollectionValidators
interface defines two properties for validators
public interface IClubCollectionValidators
{
IValidator<ClubCollection> ClubCollectionNameValidator { get; }
IValidator<Header> HeaderValidator { get; }
}
ClubCollectionNameValidator
and HeaderValidator
are properties that return validators for ClubCollection
and Header
objects, respectively.
Implementation
The ClubCollectionValidators
class implements the IClubCollectionValidators
interface
public class ClubCollectionValidators : IClubCollectionValidators
{
public IValidator<ClubCollection> ClubCollectionNameValidator { get; }
public IValidator<Header> HeaderValidator { get; }
public ClubCollectionValidators()
{
ClubCollectionNameValidator = new InternalClubCollectionNameValidator();
HeaderValidator = new InternalHeaderValidator();
}
private class InternalClubCollectionNameValidator : AbstractValidator<ClubCollection>
{
public InternalClubCollectionNameValidator()
{
RuleFor(x => x.ClubCollectionName).NotEmpty().WithMessage("Club Collection Name is required.");
RuleFor(x => x.ClubSalesforceID).NotEmpty().WithMessage("Club Salesforce ID is required.");
RuleFor(x => x.CountryCode).NotEmpty().WithMessage("Country Code is required.");
}
}
private class InternalHeaderValidator : AbstractValidator<Header>
{
public InternalHeaderValidator()
{
RuleFor(x => x.HeaderName).NotEmpty().WithMessage("HeaderName is required.");
RuleFor(x => x.Order).NotEmpty().WithMessage("Order is required.");
RuleFor(x => x.Order).InclusiveBetween(0, 100).WithMessage("Order must be between 0 and 100.");
}
}
}
ClubCollectionValidators
contains two validators: InternalClubCollectionNameValidator
and InternalHeaderValidator
.
InternalClubCollectionNameValidator
validates that ClubCollectionName
, ClubSalesforceID
, and CountryCode
are not empty.
InternalHeaderValidator
validates that HeaderName
is not empty, and Order
is between 0 and 100.
Dependency Injection
To use these validators in the application, they are registered with the Dependency Injection (DI) container:
services.AddTransient<IClubCollectionValidators, ClubCollectionValidators>();
This registration adds ClubCollectionValidators
as a transient service, allowing it to be injected wherever IClubCollectionValidators
is required.
Why Transient? This creates a new instance of the validator for each request. This is the safest option as it ensures isolated validation logic for each operation. It’s particularly important if your validators have dependencies that shouldn’t be shared across requests.
While Transient is generally recommended, Scoped or Singleton could be considered in specific scenarios (e.g., if validators have state or expensive dependencies).
Using it
Constructor
An example service (CollectionsService
) demonstrates how to use the injected validators
public class CollectionsService : ICollectionsService
{
private readonly IClubCollectionValidators _clubCollectionValidators;
public CollectionsService(IClubCollectionValidators clubCollectionValidators)
{
_clubCollectionValidators = clubCollectionValidators;
}
Validation
public async Task AddHeaderToClubCollection(Header header)
{
var validation = await _clubCollectionValidators.HeaderValidator.ValidateAsync(header);
if (!validation.IsValid)
{
throw new ValidationException(validation.Errors);
}
try
{
await _collectionsAPI.AddHeader(header.ClubCollectionSalesforceID, header.HeaderName, header.Order);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error adding header to Club Collection with name {header.HeaderName}");
throw;
}
}
The CollectionsService
constructor injects IClubCollectionValidators
.
In AddHeaderToClubCollection
, the HeaderValidator
validates the header
object.
If validation fails, a ValidationException
is thrown with the validation errors.
If validation passes, the header is added using _collectionsAPI
.
This setup ensures that objects are validated consistently and thoroughly before any operations are performed, leveraging the power of Fluent Validation within a DI framework.
Further reading
For a more comprehensive understanding of Fluent Validation, refer to the official documentation or explore the GitHub repository mentioned in the sources.
In your application/ business layer, on top of validation, you can also use AutoMapper and MediatR