Skip to content

Instantly share code, notes, and snippets.

@JoeGannon
Last active December 6, 2023 14:17
Show Gist options
  • Save JoeGannon/91e76605a2a7a7944a1a6ccde3abb3c3 to your computer and use it in GitHub Desktop.
Save JoeGannon/91e76605a2a7a7944a1a6ccde3abb3c3 to your computer and use it in GitHub Desktop.
namespace Demo
{
using FluentValidation;
using FluentValidation.Internal;
using FluentValidation.Validators;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using StructureMap;
using StructureMap.Graph;
using StructureMap.Graph.Scanning;
using System.Reflection;
using Expression = System.Linq.Expressions.Expression;
public class AttributeValidationConvention : IRegistrationConvention
{
public void ScanTypes(TypeSet types, Registry registry)
{
foreach (var type in types.AllTypes())
{
var validatedProperties = type.GetProperties()
.Where(x => Attribute.IsDefined(x, typeof(ValidationAttribute)))
.ToList();
if (validatedProperties.Any())
{
var validator = CreateValidator(type, validatedProperties);
registry.For(typeof(IValidator<>).MakeGenericType(type)).Add(validator).Singleton();
}
}
}
private IValidator CreateValidator(Type type, IEnumerable<PropertyInfo> validatedProperties)
{
var customValidator = typeof(CustomValidator<>).MakeGenericType(type);
var propertyRules = GetRules(type, validatedProperties);
var validator = (IValidator)Activator.CreateInstance(customValidator, propertyRules);
return validator;
}
private IEnumerable<PropertyRule> GetRules(Type instanceType, IEnumerable<PropertyInfo> validatedProperties)
{
foreach (var property in validatedProperties)
{
var propertyFunc = typeof(Func<,>).MakeGenericType(instanceType, property.PropertyType);
var instance = Expression.Parameter(instanceType, "x");
var memberExpr = Expression.Property(instance, property.Name);
var expr = Expression.Lambda(propertyFunc, memberExpr, instance);
var rule = (PropertyRule)typeof(ValidatorExtensions)
.GetMethod(nameof(ValidatorExtensions.CreateRule))
.MakeGenericMethod(instanceType, property.PropertyType)
.Invoke(null, new object[] { expr });
var validationAttributes = property
.GetCustomAttributes(typeof(ValidationAttribute), true)
.Cast<ValidationAttribute>()
.ToList();
validationAttributes.ForEach(attr => rule.AddValidator(property, attr));
yield return rule;
}
}
}
public static class ValidatorExtensions
{
private static readonly List<AttributeValidator> _attributeValidators = typeof(AttributeValidator)
.Assembly
.GetTypes()
.Where(x => typeof(AttributeValidator).IsAssignableFrom(x) && !x.IsAbstract)
.Select(Activator.CreateInstance)
.Cast<AttributeValidator>()
.ToList();
public static void AddValidator(this PropertyRule rule, PropertyInfo property, ValidationAttribute attribute)
{
var attributeValidator = _attributeValidators.SingleOrDefault(x => x.Matches(attribute)) ??
throw new Exception(
$"Could not find a validator for attribute {attribute.GetType()}. " +
$"Please add an implementation of AttributeValidator<{attribute.GetType()}>");
var propertyValidator = attributeValidator.GetValidator(property, attribute);
rule.AddValidator(propertyValidator);
}
//https://github.com/JeremySkinner/FluentValidation/blob/64b78d6bdc9595d221b4d56ce70a00e6de08aa4e/src/FluentValidation/Internal/PropertyRule.cs
public static PropertyRule CreateRule<TInstance, TProperty>(Expression<Func<TInstance, TProperty>> expression)
{
var member = expression.GetMember();
var compiled = AccessorCache<TInstance>.GetCachedAccessor(member, expression);
return new PropertyRule(member,
compiled.CoerceToNonGeneric(),
expression,
() => CascadeMode.Continue,
typeof(TProperty),
typeof(TInstance));
}
}
public class CustomValidator<T> : AbstractValidator<T>
{
public CustomValidator(IEnumerable<PropertyRule> rules)
{
foreach (var rule in rules)
AddRule(rule);
}
}
public class RequiredValidator : AttributeValidator<Required>
{
protected override IPropertyValidator GetValidator(PropertyInfo property, Required attribute)
{
var defaultValue = property.PropertyType.IsValueType
? Activator.CreateInstance(property.PropertyType)
: null;
return new NotEmptyValidator(defaultValue);
}
}
public class MaxLengthValidator : AttributeValidator<MaxLength>
{
protected override IPropertyValidator GetValidator(PropertyInfo property, MaxLength attr)
{
return new MaximumLengthValidator(attr.Max);
}
}
public abstract class AttributeValidator<T> : AttributeValidator where T : ValidationAttribute
{
public bool Matches(ValidationAttribute attr) => typeof(T) == attr.GetType();
public IPropertyValidator GetValidator(PropertyInfo property, ValidationAttribute attribute) => GetValidator(property, (T)attribute);
protected abstract IPropertyValidator GetValidator(PropertyInfo property, T attribute);
}
public interface AttributeValidator
{
bool Matches(ValidationAttribute attribute);
IPropertyValidator GetValidator(PropertyInfo property, ValidationAttribute attribute);
}
public class Required : ValidationAttribute
{
}
public class MaxLength : ValidationAttribute
{
public int Max { get; }
public MaxLength(int max)
{
Max = max;
}
}
[AttributeUsage(AttributeTargets.Property)]
public abstract class ValidationAttribute : Attribute
{
}
public class Person
{
[Required]
public string Name { get; set; }
[MaxLength(3)]
public string Age { get; set; }
}
}
@richardgavel
Copy link

richardgavel commented Sep 28, 2021

I almost wonder if you can now get the best of both worlds, using attributes to define the validation rules AND use native FluentValidation by making use of a Source Generator that will dynamically create the validator classes during the compile process as if they had been hand coded. You could also probably make use of partial classes like some source generators do to cover the 20% of validation logic that isn't covered by the attributes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment