Adds FluentValidation support to EntityFramework.
Support is available via a Tidelift Subscription.
using System.ComponentModel.DataAnnotations.Schema;
using FluentValidation;
public class Employee :
IProvideId
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public int CompanyId { get; set; }
public Company Company { get; set; } = null!;
public string? Content { get; set; }
public int Age { get; set; }
public class Validator :
AbstractValidator<Employee>
{
public Validator()
{
RuleFor(_ => _.Content)
.NotEmpty();
}
}
}
See Creating a validator.
Extra context is passed through FluentValidations CustomContext.
Data:
public class EfContext
{
public DbContext DbContext { get; }
public EntityEntry EntityEntry { get; }
Usage:
using System.Diagnostics;
using EfFluentValidation;
using FluentValidation;
public class ValidatorWithContext :
AbstractValidator<Employee>
{
public ValidatorWithContext()
{
RuleFor(_ => _.Content)
.Custom((propertyValue, validationContext) =>
{
var efContext = validationContext.EfContext();
Debug.Assert(efContext.DbContext != null);
Debug.Assert(efContext.EntityEntry != null);
if (propertyValue == "BadValue")
{
validationContext.AddFailure("BadValue");
}
});
}
}
ValidationFinder wraps FluentValidation.AssemblyScanner.FindValidatorsInAssembly
to provide convenience methods for scanning Assemblies for validators.
var scanResults = ValidationFinder.FromAssemblyContaining<SampleDbContext>();
DbContextValidator
performs the validation a DbContext. It has two method:
/// <summary>
/// Validates a <see cref="DbContext"/> an relies on the caller to handle those results.
/// </summary>
/// <param name="dbContext">
/// The <see cref="DbContext"/> to validate.
/// </param>
/// <param name="validatorFactory">
/// A factory that accepts a entity type and returns
/// a list of corresponding <see cref="IValidator"/>.
/// </param>
public static async Task<(bool isValid, IReadOnlyList<EntityValidationFailure> failures)> TryValidate(
DbContext dbContext,
Func<Type, IEnumerable<IValidator>> validatorFactory)
/// <summary>
/// Validates a <see cref="DbContext"/> and throws a <see cref="MessageValidationException"/>
/// if any changed entity is not valid.
/// </summary>
/// <param name="dbContext">
/// The <see cref="DbContext"/> to validate.
/// </param>
/// <param name="validatorFactory">
/// A factory that accepts a entity type and returns a
/// list of corresponding <see cref="IValidator"/>.
/// </param>
public static async Task Validate(
DbContext dbContext,
Func<Type, IEnumerable<IValidator>> validatorFactory)
ValidatorTypeCache
creates and caches IValidator
instances against their corresponding entity type.
It can only be used against validators that have a public default constructor (i.e. no parameters).
var scanResults = ValidationFinder.FromAssemblyContaining<SampleDbContext>();
ValidatorTypeCache typeCache = new(scanResults);
var validators = typeCache.GetValidators(typeof(Employee));
Many APIs take a validation factory with the signature Func<Type, IEnumerable<IValidator>>
where Type
is the entity type and IEnumerable<IValidator>
is all validators for that entity type.
This approach allows a flexible approach on how Validators can be instantiated.
DefaultValidatorFactory
combines ValidatorTypeCache and ValidationFinder.
It assumes that all validators for a DbContext exist in the same assembly as the DbContext and have public default constructors.
Implementation:
using System;
using System.Collections.Generic;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
namespace EfFluentValidation
{
public static class DefaultValidatorFactory<T>
where T : DbContext
{
public static Func<Type, IEnumerable<IValidator>> Factory { get; }
static DefaultValidatorFactory()
{
var validators = ValidationFinder.FromAssemblyContaining<T>();
ValidatorTypeCache typeCache = new(validators);
Factory = type => typeCache.GetValidators(type);
}
}
}
There are several approaches to adding validation to a DbContext
ValidatingDbContext
provides a base class with validation already implemented in SaveChnages
and SaveChangesAsync
using System;
using System.Collections.Generic;
using EfFluentValidation;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
public class SampleDbContext :
ValidatingDbContext
{
public DbSet<Employee> Employees { get; set; } = null!;
public DbSet<Company> Companies { get; set; } = null!;
public SampleDbContext(
DbContextOptions options,
Func<Type, IEnumerable<IValidator>> validatorFactory) :
base(options, validatorFactory)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Company>()
.HasMany(c => c.Employees)
.WithOne(e => e.Company)
.IsRequired();
modelBuilder.Entity<Employee>();
}
}
In some scenarios it may not be possible to use a custom base class, I thise case SaveChnages
and SaveChangesAsync
can be overridden.
public class SampleDbContext :
DbContext
{
Func<Type, IEnumerable<IValidator>> validatorFactory;
public DbSet<Employee> Employees { get; set; } = null!;
public DbSet<Company> Companies { get; set; } = null!;
public SampleDbContext(
DbContextOptions options,
Func<Type, IEnumerable<IValidator>> validatorFactory) :
base(options)
{
this.validatorFactory = validatorFactory;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Company>()
.HasMany(c => c.Employees)
.WithOne(e => e.Company)
.IsRequired();
modelBuilder.Entity<Employee>();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
DbContextValidator.Validate(this, validatorFactory).GetAwaiter().GetResult();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override async Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
await DbContextValidator.Validate(this, validatorFactory);
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
}
To report a security vulnerability, use the Tidelift security contact. Tidelift will coordinate the fix and disclosure.
Database designed by Creative Stall from The Noun Project.