如何组织 FluentValidation 规则以便它们可以在多个验证器中重用?

How to organize FluentValidation rules so that they may be reused in multiple validators?

我有一个域 model/entity,根据它的填充方式需要进行不同的验证。假设我提出了 3 个如下所示的验证器:

public class Product1Validator : AbstractValidator<Ticket>
{
    public Product1Validator()
    {
        RuleFor(ticket => ticket.Policy.PolicyNumber)
         .NotEmpty()
         .WithMessage("Policy Number is missing.");

        RuleFor(ticket => ticket.Policy.ApplSignedInState)
         .NotEmpty()
         .WithMessage("Application Signed In State is missing or invalid.");
    }
}

public class Product2Validator : AbstractValidator<Ticket>
{
    public Product2Validator()
    {
        RuleFor(ticket => ticket.Policy.PolicyNumber)
         .NotEmpty()
         .WithMessage("Policy Number is missing.");

        RuleFor(ticket => ticket.Policy.ApplSignedInState)
         .NotEmpty()
         .WithMessage("Application Signed In State is missing or invalid.");
    }
}


public class Product3Validator : AbstractValidator<Ticket>
{
    public Product3Validator()
    {
        RuleFor(ticket => ticket.Policy.PolicyNumber)
         .NotEmpty()
         .WithMessage("Policy Number is missing.");

        RuleFor(ticket => ticket.Policy.ApplSignedInState)
         .NotEmpty()
         .WithMessage("Application Signed In State is missing or invalid.");

        RuleFor(ticket => ticket.Policy.DistributionChannel)
         .NotEmpty()
         .WithMessage("Distribution Channel is missing."); 
    }
}

如何重构重复的 RuleFor(s),使它们只有一个并由不同的验证器共享?

谢谢, 斯蒂芬

更新

我 运行 同意 Ouarzy 的想法,但是当我编写代码来验证它时,它无法编译。

[TestMethod]
public void CanChainRules()
{
    var ticket = new Ticket();
    ticket.Policy = new Policy();
    ticket.Policy.ApplSignedInState = "CA";
    ticket.Policy.PolicyNumber = "";
    ticket.Policy.DistributionChannel = null;

    var val = new Product1Validator();
    var result = val.Validate(ticket); //There is no Method 'Validate'
    Assert.IsTrue(!result.IsValid);
    Console.WriteLine(result.Errors.GetValidationText());
} 

更新 2

问题是新的复合验证器没有从 AbstractValidator 继承,一旦我更正它编译,但它们似乎不起作用。

public class Product1Validator : AbstractValidator<Ticket>
{
    public Product1Validator()
    {
        TicketValidator.Validate().Policy().ApplSignedState();
    }
} 

更新 3

在对最初的答案进行了严厉的思考并直接在 GitHub 上联系 Jeremy 之后,我想到了以下内容:

class Program{
    static void Main(string[] args){
        var p = new Person();
        var pv = new PersonValidator();
        var vr = pv.Validate(p);
        //Console.ReadKey();
    }
}

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
}

class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        CascadeMode = CascadeMode.Continue;
        this.FirstName();
        this.LastName();
    }
}

static class Extensions
{
    public static void FirstName(this AbstractValidator<Person> a)
    {
        a.RuleFor(b => b.FirstName).NotEmpty();
    }
    public static void LastName(this AbstractValidator<Person> a)
    {
        a.RuleFor(b => b.LastName).NotEmpty();
    }
}

对于您的情况,我可能会尝试使用所有规则为 Ticket 构建流畅的验证,然后为每个产品调用所需的验证。类似于:

public class TicketValidator : AbstractValidator<Ticket>
{
    public TicketValidator Policy()
    {
        RuleFor(ticket => ticket.Policy.PolicyNumber)
         .NotEmpty()
         .WithMessage("Policy Number is missing.");

        return this;
    }

    public TicketValidator ApplSignedState()
    {
        RuleFor(ticket => ticket.Policy.ApplSignedInState)
         .NotEmpty()
         .WithMessage("Application Signed In State is missing or invalid.");

        return this;
    }

    public TicketValidator DistributionChannel()
    {
        RuleFor(ticket => ticket.Policy.DistributionChannel)
        .NotEmpty()
        .WithMessage("Distribution Channel is missing.");

        return this;
    }

    public static TicketValidator Validate()
    {
        return new TicketValidator();
    }
}

然后每个产品一个验证器使用流畅的语法:

public class Product1Validator
{
    public Product1Validator()
    {
        TicketValidator.Validate().Policy().ApplSignedState();
    }
}  

public class Product2Validator
{
    public Product2Validator()
    {
        TicketValidator.Validate().Policy().ApplSignedState();
    }
}  

public class Product3Validator
{
    public Product3Validator()
    {
        TicketValidator.Validate().Policy().ApplSignedState().DistributionChannel();
    }
}  

我没有尝试编译这段代码,但我希望你明白这个想法。

希望对您有所帮助。

集中式扩展方法

我想在多种不同类型的对象中使用它们。

我通过创建集中式扩展方法做到了这一点。

一个简单的例子:

扩展方法

namespace FluentValidation
{
    public static class LengthValidator
    {
        public static IRuleBuilderOptions<T, string> 
           CustomerIdLength<T>(this IRuleBuilder<T, string> ruleBuilder)
        {
            return ruleBuilder.Length<T>(1, 0);
        }
    }
}

用法

public class CreateCustomerValidator : AbstractValidator<CreateCustomerCommand>
{
    public CreateCustomerValidator()
    {
        RuleFor(x => x.CustomerId).CustomerIdLength();
    }
}

由于类型化对象是通过泛型传递的,因此它可以跨多个对象使用,而不仅仅是一个对象,即

public class UpdateCustomerValidator : AbstractValidator<UpdateCustomerCommand>

就我而言,必须验证不同对象的简单(内置类型)属性,因此我需要另一种解决方案来消除重复。

我有一个 CreateTodoItemCommand 和一个 UpdateTodoItemCommand,两者都有一个 string Title 属性,它们应该以相同的方式验证。

我的选择:

  • 我不能使用 PropertyValidator,因为我没有(而且我不想创建)仅针对该字段(“IHaveTitle”接口)的通用合同
  • 使用 MustCustom 验证器代码需要我自己编写错误消息,我不想要,因为它是不必要的样板文件。

我想到的是以下扩展方法:

internal static IRuleBuilderOptions<T, string> ValidateTodoItemTitle<T>(
    this IRuleBuilder<T, string> ruleBuilder)
{
    return ruleBuilder
        .NotEmpty()
        .MaximumLength(5000);
}

并像 RuleFor(x => x.Title).ValidateTodoItemTitle(); 一样使用它。

我看到的唯一缺点是我在通用类型上创建了一个扩展方法可能会污染类型,但这应该不是问题,因为扩展 class 在它自己的命名空间中,它被使用仅在验证时。
这对于使用例如的人来说可能是不同的。 ReSharper 可能会显示来自未导入的命名空间的机会,从而造成真正的污染。出于这个原因,在 C# 中具有命名空间范围的可见性会很好。

对于使用扩展方法感到困扰的人,使用常规方法,用法变成这样:CommonTodoItemValidators.ValidateTodoItemTitle(RuleFor(x => x.Title));。它的可读性也可以通过例如提高。 using static.