mrepoDev / EfFluentValidation

Adds FluentValidation support to EntityFramework

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

EfFluentValidation

Build status NuGet Status

Adds FluentValidation support to EntityFramework.

Support is available via a Tidelift Subscription.

Contents

NuGet package

Usage

Define Validators

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();
        }
    }
}

snippet source | anchor

See Creating a validator.

Context

Extra context is passed through FluentValidations CustomContext.

Data:

public class EfContext
{
    public DbContext DbContext { get; }
    public EntityEntry EntityEntry { get; }

snippet source | anchor

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");
                }
            });
    }
}

snippet source | anchor

ValidationFinder

ValidationFinder wraps FluentValidation.AssemblyScanner.FindValidatorsInAssembly to provide convenience methods for scanning Assemblies for validators.

var scanResults = ValidationFinder.FromAssemblyContaining<SampleDbContext>();

snippet source | anchor

DbContextValidator

DbContextValidator performs the validation a DbContext. It has two method:

TryValidate

/// <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)

snippet source | anchor

Validate

/// <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)

snippet source | anchor

ValidatorTypeCache

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));

snippet source | anchor

ValidatorFactory

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

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);
        }
    }
}

snippet source | anchor

DbContext

There are several approaches to adding validation to a DbContext

ValidatingDbContext

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>();
    }
}

snippet source | anchor

DbContext as a base

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);
    }
}

snippet source | anchor

Security contact information

To report a security vulnerability, use the Tidelift security contact. Tidelift will coordinate the fix and disclosure.

Icon

Database designed by Creative Stall from The Noun Project.

About

Adds FluentValidation support to EntityFramework

License:MIT License


Languages

Language:C# 100.0%