如何在 ASP.NET Core 中创建自定义验证器,它也会因无效输入而触发?

How to create a custom validator in ASP.NET Core that fires for invalid input too?

我在 ASP.NET Core 3.1 中为 DateTime 字段创建了自定义验证器,如下所示:

[CustomDate]
public DateTime DOB { get; set; }

public class CustomDate : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        //… some logic
    }
}

但是,我的问题是此自定义验证器仅在我将日期值放入文本框控件时触发。它不会触发无效输入,例如当我在文本框中输入字符串 'aaa' 时。

我的问题是如何让这个自定义验证器即使对于 'string' 等无效输入也能触发

原因是我想让这个自定义验证器替换 [Required][ReqularExpression] 等。一种 'One ring (validator) to rule them all'。我怎样才能做到这一点?

TL;DR: 当您提交无法转换为 DateTime 的值时,模型绑定失败。由于已经存在与 属性 关联的验证错误,因此不会触发后续验证(包括您的 CustomDate 验证器)。您的 属性 仍在接受验证,但是:如果您输入值 aaaModelState.IsValid 将 return false.


您最初发布的代码 应该 可以正常工作——但我怀疑它并没有像您 期望的 那样工作.最值得注意的是,您的困惑可能源于以下陈述:

"…this custom validator fires only when I put a date value in the text box control."

那是真的!让我来完成整个过程。

原码

为了帮助说明这一点,我希望你不介意我恢复你的原始代码示例,因为有具体的参考资料很有用。

[CustomDate]
public DateTime DOB { get; set; }

public class CustomDate : Attribute, IModelValidator
{
    public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
    {
        if (Convert.ToDateTime(context.Model) > DateTime.Now)
            return new List<ModelValidationResult> {
                new ModelValidationResult("", "Invalid - future date")
            };
        else if (Convert.ToDateTime(context.Model) < new DateTime(1970, 1, 1))
            return new List<ModelValidationResult> {
                new ModelValidationResult("", "Invalid - date is less than 1970 year")
            };
        else
            return Enumerable.Empty<ModelValidationResult>();
    }
}

验证过程

在我完成整个过程之前,有四个重要的基本注意事项需要注意:

  1. 模型绑定发生在之前模型验证
  2. 如果绑定失败,模型绑定将引发其自身的验证错误。
  3. 验证属性仅对保留 IsValid.
  4. 的属性进行评估
  5. ModelValidationContext.Model 属性 被输入到经过验证的 属性——因此,在这种情况下,DateTime 值。

用例 #1:无效值

考虑到这些因素,下面是当您提交一个值时发生的情况,例如aaa 在映射到您验证的字段中 DOB 属性:

  1. 模型绑定器尝试将值 aaa 绑定到 DateTime 属性。
  2. 模型绑定器失败,将 ModelError 添加到您的 ModelStateDictionary
  3. 您的CustomDate 验证器从未触发,因为该字段已经验证失败

用例 #2:缺失值

看看另一个测试用例很有启发性。不要输入 aaa,只要根本不输入任何值即可。在这种情况下,过程看起来有点不同:

  1. 模型绑定器找不到您的 DateTime 属性 的值,因此不会发生绑定。
  2. 您模型的 属性 初始化为 DateTime 的默认值 0001-01-01 00:00:00
  3. 你的 CustomDate 验证器触发,添加一个 ModelError 因为 "Invalid - date is less than 1970 year".

分析

正如您在上面看到的,true 当提交虚假日期时,您的 CustomDate 验证器没有触发。但是 并不 意味着没有进行验证。相反,验证已经已经发生并且失败了。如果您输入有效日期——或者根本不输入日期——则不会发生模型绑定错误,并且您的 CustomDate 验证器将按预期执行。

重温您的问题

"How to make this custom validator to fire even for invalid inputs like 'string' etc."

最终,我没有回答那个问题。但我认为我的回答会解释为什么会发生这种情况,以及为什么你的输入仍然得到验证那。请记住,即使你的 CustomDate 验证器 did 触发了,它的行为就像你根本没有提交值一样,因为 context.Model值将默认为 0001-01-01 00:00:00。主要区别在于您不会收到相同的错误消息,因为错误来自不同的来源。

强制验证

我不推荐这个,但是如果你真的想要你的 CustomDate 验证器触发,你可以将它应用到 string 属性 代替。在这种情况下,模型绑定不会失败,您的 CustomDate 验证器仍会被调用。如果你追求这个,你需要进行额外的验证以确保日期格式正确(例如,通过抢占或处理 InvalidFormatExceptions)。但是,当然,您的日期将存储为 string,这可能不是您想要的。

代码建议

这有点超出您最初问题的范围,但我在这里推荐以下内容:

  1. 您不需要在验证器中执行 Convert.ToDateTime();您的 context.Model 字段 已经 一个 DateTime。您只需要将其转换回 DateTime 对象(例如 (DateTime)context.Model),以便您的代码知道这一点。
  2. 至少,您应该考虑使用 <input type="date" /> (reference),在大多数浏览器上,它会限制输入正确的日期,同时还提供基本的日期选择器。
  3. 或者,如果您需要对演示文稿和客户端验证进行更多控制,可以考虑使用 JavaScript 编写的许多更复杂的 date/time 控件。