【问题标题】:Defer the selection of a child validator depending on the property type/value根据属性类型/值推迟选择子验证器
【发布时间】:2015-04-29 14:35:51
【问题描述】:

在 FluentValidation 中是否有扩展或其他方式来根据正在验证的属性的类型/值来推迟选择子验证器?

我的情况是我有一个要验证的通知类。这个类有一个 Payload 属性,它可以是许多 Payload 类型之一,例如SmsPayload、EmailPayload 等。这些 Payload 子类中的每一个都有自己关联的验证器,例如分别是 SmsPayloadValidator 和 EmailPayloadValidator。除了上述之外,核心库中没有对各个通知提供程序的引用。从本质上讲,这意味着我可以根据需要添加提供程序并使用 IoC 连接所有内容。

考虑以下类:

public class Notification
{
    public Payload Payload { get; set; }
    public IEnumerable<string> Details { get; set; }
}

public abstract class Payload
{
    public string Message { get; set; }
    public abstract string Type { get; }
}

public class SmsPayload : Payload
{
    public List<string> Numbers { get; set; }
    public string Region { get; set; }
    public string Provider { get; set; }
}

有一个Notification验证器和SmsPayloadValidator如下:

public class NotificationValidator : AbstractValidator<Notification>
{
    public NotificationValidator(IValidator<Payload> payloadValidator)
    {
        RuleFor(notification => notification.Payload).NotNull().WithMessage("Payload cannot be null.");
        RuleFor(notification => notification.Payload).SetValidator(payloadValidator);
    }
}

public class SmsPayloadValidator : AbstractValidator<SmsPayload>
{
    public SmsPayloadValidator()
    {
        RuleFor(payload => payload.Provider)
            .Must(s => !string.IsNullOrEmpty(s))
            .WithMessage("Provider is required.");
        RuleFor(payload => payload.Numbers)
            .Must(list => list != null && list.Any())
            .WithMessage("Sms has no phone numbers specified.");
        RuleFor(payload => payload.Region)
            .Must(s => !string.IsNullOrEmpty(s))
            .WithMessage("Region is required.");
    }
}

正如我提到的 NotificationValidator 所在的程序集不引用各个 Payload 验证器类所在的程序集。所有的接线都由 Ioc(本项目的 Simple-Injector)负责。

基本上我想做如下的事情 - 首先在 Simple Injector 中注册一个工厂回调:

container.Register<Func<Payload, IValidator<Payload>>>(() => (payload =>
{
    if (payload.GetType() == typeof(SmsPayload))
    {
        return container.GetInstance<ISmsPayloadValidator>();
    }
    else if (payload.GetType() == typeof(EmailPayload))
    {
        return container.GetInstance<IEmailPayloadValidator>();
    }
    else 
    {
        //something else;
    }
}));

这样我就可以选择合适的验证器如下:

public class NotificationValidator : AbstractValidator<Notification>
{
    public NotificationValidator(Func<Payload, IValidator<Payload>> factory)
    {
        RuleFor(notification => notification.Payload).NotNull().WithMessage("Payload cannot be null.");
        RuleFor(notification => notification.Payload).SetValidator(payload => factory.Invoke(payload));
    }
}

有什么建议吗?还是有更好的方法来做我提议的事情? 如果没有,我将 fork FluentValidation 存储库并提交 PR。

【问题讨论】:

    标签: .net validation expression-trees fluentvalidation simple-injector


    【解决方案1】:

    我同意 Taylor 关于使用 Composite 的回答(因此肯定是 +1),但他的实现并不那么实用。因此,我建议稍微不同的实现,同时仍然使用复合。

    如果我没记错的话,你的合成应该如下所示:

    public class CompositeValidator<T> : AbstractValidator<T> where T : class
    {
        private readonly Container container;
    
        public CompositeValidator(Container container)
        {
            this.container = container;
        }
    
        public override ValidationResult Validate(T instance)
        {
            var validators = this.container.GetAllInstances(instance.GetType());
    
            return new ValidationResult(
                from IValidator validator in validators
                from error in validator.Validate(instance).Errors
                select error);
        }
    }
    

    注册应如下:

    // Simple Injector v3.x
    container.RegisterCollection(typeof(IValidator<>),
        AppDomain.CurrentDomain.GetAssemblies());
    
    container.Register(typeof(IValidator<>), 
        typeof(CompositeValidator<>), 
        Lifestyle.Singleton);
    
    // Simple Injector v2.x
    container.RegisterManyForOpenGeneric(
        typeof(IValidator<>),
        container.RegisterAll,
        AppDomain.CurrentDomain.GetAssemblies());
    
    container.RegisterOpenGeneric(
        typeof(IValidator<>), 
        typeof(CompositeValidator<>), 
        Lifestyle.Singleton);
    

    这里发生的情况如下:

    • RegisterCollection 调用确保所有验证器都注册为集合。这意味着对于每个 T,其中可以有多个验证器。例如,如果您的系统有一个PayloadValidator 和一个SmsPayloadValidator,解析GetAllInstances&lt;IValidator&lt;SmsPayload&gt;&gt; 将返回两个验证器,因为IValidator&lt;in T&gt; 包含in 关键字(是逆变的)。
    • Register 注册将为每个请求的IValidator&lt;T&gt; 注册要返回的CompositeValidator&lt;T&gt;。由于 Simple Injector differentiates registrations of collections with one-to-one registrations,注入 IValidator&lt;T&gt; 将始终导致复合验证器被注入。由于复合验证器只依赖于容器,因此可以注册为单例。
    • 消费者被注入CompositeValidator&lt;T&gt;(当他们依赖于IValidator&lt;T&gt;时),复合验证器将根据确切类型请求验证器集合。因此,如果消费者使用 IValidator&lt;Payload&gt;,复合验证器将确定实际类型(例如 SmsPayload)并请求所有可分配的验证器以获取该确切类型,并将验证转发给这些类型。
    • 如果没有特定类型的验证器,复合验证器将自动返回有效的ValidationResult

    【讨论】:

    • +1 对于更通用的解决方案。由于您是该框架的开发人员,因此您可能有一点优势,而我在阅读此问题时才了解到它...大声笑。旁观者要注意的一件事是,虽然这个解决方案实施起来不那么冗长,但它也没有我的解决方案那么灵活。据推断,实现者总是希望注册和使用特定类型的所有验证器。如果这是您的意图,那么这绝对是更好的方法。
    • @TaylorBuchanan:非常值得注意的是,您在阅读文档时做了一些分析,只是为了回答这个问题。在 Stackoverflow 上没有多少人这样做。
    【解决方案2】:

    避开工厂可以让你的意图更加明确。虽然最终结果可能与这种方法相同,但您至少可以直接注入 IValidator&lt;Payload&gt; 而不是 Func&lt;Payload, IValidator&lt;Payload&gt;&gt;

    创建一个名为PolymorphicValidator 的类。这将允许您以一致的方式重复此模式,并且如果您愿意,还可以提供一个备用基础验证器。这本质上是 Simple Injector 文档中here 中描述的推荐“复合模式”。

    public class PolymorphicValidator<T> : AbstractValidator<T> where T : class
    {
        private readonly IValidator<T> _baseValidator;
        private readonly Dictionary<Type, IValidator> _validatorMap = new Dictionary<Type,IValidator>();
    
        public PolymorphicValidator() { }
    
        public PolymorphicValidator(IValidator<T> baseValidator)
        {
            _baseValidator = baseValidator;
        }
    
        public PolymorphicValidator<T> RegisterDerived<TDerived>(IValidator<TDerived> validator) where TDerived : T
        {
            _validatorMap.Add(typeof (TDerived), validator);
            return this;
        }
    
        public override ValidationResult Validate(ValidationContext<T> context)
        {
            var instance = context.InstanceToValidate;
            var actualType = instance == null ? typeof(T) : instance.GetType();
            IValidator validator;
            if (_validatorMap.TryGetValue(actualType, out validator))
                return validator.Validate(context);
            if (_baseValidator != null)
                return _baseValidator.Validate(context);
            throw new NotSupportedException(string.Format("Attempted to validate unsupported type '{0}'. " +
                "Provide a base class validator if you wish to catch additional types implicitly.", actualType));
        }
    }
    

    然后您可以像这样注册您的验证器(可选地提供基类回退和其他子类验证器):

    container.RegisterSingle<SmsPayloadValidator>();
    //container.RegisterSingle<EmailPayloadValidator>();
    container.RegisterSingle<IValidator<Payload>>(() =>
        new PolymorphicValidator<Payload>(/*container.GetInstance<PayloadValidator>()*/)
            .RegisterDerived(container.GetInstance<SmsPayloadValidator>())
          /*.RegisterDerived(container.GetInstance<EmailPayloadValidator>() */);
    

    这将创建一个包含单例子验证器的单例 PolymorphicValidator(FluentValidation 团队推荐单例)。您现在可以注入 IValidator&lt;Payload&gt;,如您的第一个 NotificationValidator 示例所示。

    public class NotificationValidator : AbstractValidator<Notification>
    {
        public NotificationValidator(IValidator<Payload> payloadValidator)
        {
            RuleFor(notification => notification.Payload)
                .NotNull().WithMessage("Payload cannot be null.")
                .SetValidator(payloadValidator);
        }
    }
    

    【讨论】:

    • +1 用于使用复合。但是,我认为您的实现比需要的更复杂。请参阅我的答案以了解替代方法。
    • 感谢您的支持。但是,我认为我的方法并不比你的更“复杂”。更乏味,是的。正如您在回答中提到的那样,可能不切实际,但仅取决于实施者的意图。
    • 虽然稍微复杂一点,但它更明确一点,而且我成功了。谢谢!
    猜你喜欢
    • 1970-01-01
    • 2018-09-21
    • 2023-04-09
    • 2020-12-02
    • 2012-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-29
    相关资源
    最近更新 更多