极其奇怪的 Fluent Validation 行为

Extremely bizarre Fluent Validation behaviour

当前项目:

所以我遇到了 Fluent Validation 中极其奇怪的行为。

也就是说,

Non-Nulled Nullable 字段仅在服务器端验证。取一个应该由下拉菜单的值填充的 int(而不是 int?)字段,虽然它会在服务器端验证,但只会在客户端。如果您再次选择不可接受的空值 ("Select a choice"),它不会在客户端重新验证。

此行为似乎仅限于 int 个从下拉列表中填写的字段。

在提交表单之前,所有字符串、日期和任何其他类型的字段都无法在客户端验证(一旦贪婪验证开始),并且根本不会在服务器端验证。此外,所有 .NotEmpty().NotNull() 声明似乎都被忽略了,即使它们是字符串字段验证中的唯一声明。

我的 Global.asax.cs 配置正确:

public class MvcApplication : System.Web.HttpApplication {
  protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    FluentValidationModelValidatorProvider.Configure();
  }
}

我有正确的 JS 文件进入页面:

<link href="/Content/bootstrap.css" rel="stylesheet"/>
<link href="/Content/bootstrap-datepicker3.css" rel="stylesheet"/>
<link href="/Content/fontawesome-all.css" rel="stylesheet"/>
<link href="/Content/style.css" rel="stylesheet"/>
<script src="/Scripts/modernizr-2.8.3.js"></script>
<script src="/Scripts/jquery-3.3.1.js"></script>
<script src="/Scripts/jquery.validate.js"></script>
<script src="/Scripts/jquery.validate.unobtrusive.js"></script>
<script src="/Scripts/bootstrap.js"></script>
<script src="/Scripts/bootstrap-datepicker.js"></script>
<script src="/Scripts/popper.js"></script>
<script src="/Scripts/jquery.mask.js"></script>
<script src="/Scripts/script.js"></script>

我的 ViewModel 已正确配置:

namespace Project.Models {
  using Controllers;
  using FluentValidation.Attributes;
  using System;
  using System.Collections.Generic;
  using System.ComponentModel;
  using System.ComponentModel.DataAnnotations;
  using System.Web.Mvc;
  using Validators;

  [Validator(typeof(MoreInfoValidator))]
  public class MoreInfoViewModel {
    [DisplayName(@"First Name")]
    public string FirstName { get; set; }
    [DisplayName(@"Last Name")]
    public string LastName { get; set; }
    [DisplayName(@"Phone Number")]
    [DataType(DataType.PhoneNumber)]
    public string Phone { get; set; }
    [DisplayName(@"eMail Address")]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
    [DisplayName(@"Date of Birth")]
    [DataType(DataType.DateTime)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
    public DateTime Dob { get; set; } = DateTime.Now.AddYears(-16);
    [DisplayName(@"Mailing Address")]
    public string Address { get; set; }
    [DisplayName(@"City")]
    public string City { get; set; }
    [DisplayName(@"Province or State")]
    public string ProvState { get; set; }
    [DisplayName(@"Postal Code")]
    [DataType(DataType.PostalCode)]
    public string Postal { get; set; }
    [DisplayName(@"Country")]
    public int CountryId { get; set; }
    [DisplayName(@"How did you hear about us?")]
    public int HowHeardId { get; set; }
    [DisplayName(@"Training Site")]
    public int TrainingSiteId { get; set; }
    [DisplayName(@"Comments")]
    public string Comments { get; set; }

    public IEnumerable<SelectListItem> HowHeardList = ListController.HowHeardList();
    public IEnumerable<SelectListItem> CountryList = ListController.CountryList();
    public IEnumerable<SelectListItem> TrainingSiteList = ListController.TrainingSiteList();
  }
}

我的验证器配置正确:

namespace Project.Validators {
  using FluentValidation;
  using Models;

  public class MoreInfoValidator : AbstractValidator<MoreInfoViewModel> {
    public MoreInfoValidator() {
      RuleFor(x => x.FirstName)
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty().WithMessage("You must provide a first name of some kind.")
        .MinimumLength(2).WithMessage(@"A first name must be at least two characters or longer.");
      RuleFor(x => x.LastName)
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty().WithMessage(@"You must provide a last name of some kind.")
        .MinimumLength(2).WithMessage(@"A last name must be at least two characters or longer.");
      RuleFor(x => x.Email.Trim())
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty().WithMessage(@"Please provide an eMail address to act as the login username.")
        .EmailAddress().WithMessage(@"Please provide a valid eMail address to act as the login username.");
      RuleFor(x => x.Phone)
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty().WithMessage("Please enter a valid 10-digit phone number.")
        .Length(12, 12).WithMessage("Phone number must be in the form of &#8220;123-456-7890&#8221;")
        .Matches(@"^\d{3}-\d{3}-\d{4}$").WithMessage("Phone number must be a valid 10-digit phone number with dashes, in the form of &#8220;123-456-7890&#8221;");
      RuleFor(x => x.Address)
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty().WithMessage("Please provide your street address.")
        .MinimumLength(6).WithMessage("Addresses should be at least 6 characters long.");
      RuleFor(x => x.City)
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty().WithMessage("Please provide your city.")
        .MinimumLength(2).WithMessage("City names should be at least 2 characters long.");
      RuleFor(x => x.ProvState)
        .Cascade(CascadeMode.StopOnFirstFailure)
        .NotEmpty().WithMessage("Please provide your province or state.")
        .Length(2).WithMessage("Please provide the 2-character code for your province or state.");
      RuleFor(x => x.CountryId)
        .NotEmpty().WithMessage("Please choose your country.");
      RuleFor(x => x.HowHeardId)
        .NotEmpty().WithMessage("How did you hear of us?");
      RuleFor(x => x.TrainingSiteId)
        .NotEmpty().WithMessage("Please choose a desired training site.");
    }
  }
}

我的表单构建正确:

@model Project.Models.MoreInfoViewModel
@{
  ViewBag.Title = "More Info";
}
<h1>@ViewBag.Title</h1>
<p><span class="requiredcolor">These fields</span> are required.</p>
@using(Html.BeginForm()) {
  @Html.AntiForgeryToken()
  @Html.ValidationMessage("", new { @class = "alert" })
  <div class="row">
    <div class="form-group col-md-6">
      @Html.LabelFor(x => x.FirstName, new { @class = "control-label required" })@Html.EditorFor(x => x.FirstName, new { htmlAttributes = new { @class = "form-control required", maxlength = 100 } })
      @Html.ValidationMessageFor(x => x.FirstName)
    </div>
    <div class="form-group col-md-6">
      @Html.LabelFor(x => x.LastName, new { @class = "control-label required" })@Html.EditorFor(x => x.LastName, new { htmlAttributes = new { @class = "form-control required", maxlength = 100 } })
      @Html.ValidationMessageFor(x => x.LastName)
    </div>
  </div>
  <div class="row">
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.Phone, new { @class = "control-label required" })@Html.EditorFor(x => x.Phone, new { htmlAttributes = new { @class = "form-control required phone", maxlength = 12 } })
      @Html.ValidationMessageFor(x => x.Phone)
    </div>
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.Email, new { @class = "control-label required" })@Html.EditorFor(x => x.Email, new { htmlAttributes = new { @class = "form-control required", maxlength = 75 } })
      @Html.ValidationMessageFor(x => x.Email)
    </div>
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.Dob, new { @class = "control-label required" })@Html.EditorFor(x => x.Dob, new { htmlAttributes = new { @class = "form-control required datepicker" } })
      @Html.ValidationMessageFor(x => x.Dob)
    </div>
  </div>
  <div class="row">
    <div class="form-group col-md-6">
      @Html.LabelFor(x => x.Address, new { @class = "control-label required" })@Html.EditorFor(x => x.Address, new { htmlAttributes = new { @class = "form-control required", maxlength = 150 } })
      @Html.ValidationMessageFor(x => x.Address)
    </div>
    <div class="form-group col-md-6">
      @Html.LabelFor(x => x.City, new { @class = "control-label required" })@Html.EditorFor(x => x.City, new { htmlAttributes = new { @class = "form-control required", maxlength = 50 } })
      @Html.ValidationMessageFor(x => x.City)
    </div>
  </div>
  <div class="row">
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.ProvState, new { @class = "control-label required" })@Html.EditorFor(x => x.ProvState, new { htmlAttributes = new { @class = "form-control required", maxlength = 2 } })
      @Html.ValidationMessageFor(x => x.ProvState)
    </div>
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.CountryId, new { @class = "control-label required" })@Html.DropDownListFor(x => x.CountryId, Model.CountryList, "« ‹ Select › »", new { @class = "form-control required" })
      @Html.ValidationMessageFor(x => x.CountryId)
    </div>
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.Postal, new { @class = "control-label" })@Html.EditorFor(x => x.Postal, new { htmlAttributes = new { @class = "form-control postalcode", maxlength = 7 } })
      @Html.ValidationMessageFor(x => x.Postal)
    </div>
  </div>
  <div class="row">
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.HowHeardId, new { @class = "control-label required" })@Html.DropDownListFor(x => x.HowHeardId, Model.HowHeardList, "« ‹ Select › »", new { @class = "form-control required" })
      @Html.ValidationMessageFor(x => x.HowHeardId)
    </div>
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.TrainingSiteId, new { @class = "control-label required" })@Html.DropDownListFor(x => x.TrainingSiteId, Model.TrainingSiteList, "« ‹ Select › »", new { @class = "form-control required" })
      @Html.ValidationMessageFor(x => x.TrainingSiteId)
    </div>
    <div class="form-group col-md-4">
      @Html.LabelFor(x => x.Comments, new { @class = "control-label" })@Html.TextAreaFor(x => x.Comments, new { @class = "form-control", rows = 3 })
      @Html.ValidationMessageFor(x => x.Comments)
    </div>
  </div>
  <div class="blank-divider clearfix" style="height:30px;"></div>
  <div class="row">
    <div class="form-group col-md-6">&nbsp;</div>
    <div class="form-group col-md-6"><label class="control-label">&nbsp;</label><input type="submit" value="Submit Request" alt="Submit Request" title="Submit Request" class="btn btn-default btn-success" /></div>
  </div>
}

我完全不明白为什么 Fluent Validation 只在极其有限的情况下才会触发。

请理解,当它确实触发时——post-对所有其他字段提交贪婪验证,对下拉菜单进行服务器端验证——所有消息都完全符合预期。

但是,对于 any 字段,客户端和服务器端都不会触发验证。如果它触发服务器端,则无法触发客户端。如果它触发客户端,它只会在有限的条件下(字符串长度不够等)这样做,然后拒绝触发服务器端。

什么是 not 工作是通常的验证和任何捕获 .NotEmpty().NotNull().

的尝试

编辑:

我看不出这是怎么可能的(没有从数据库中加载,只是插入其中),但是字符串字段验证是否会被数据库字段的格式? nchar 会成为这里的问题吗?

此外,我使用自定义模型收集数据,然后再将其添加到数据模型以插入数据库,因此我们与数据库结构的距离更远。从理论上讲,在将数据从 ViewModel 移动到 DataModel 之前,我看不出这种关系如何可能。


编辑 2:

.Cascade(CascadeMode.StopOnFirstFailure) 没有区别,在验证器 class 中有或没有这些问题都会出现。


编辑 3:

让我更准确地说明贪心验证。在所有情况下,任何 .NotEmpty().NotNull() 验证仍然无法触发,因此简单地从一个字段导航到下一个字段(点击提交后)将无法触发贪婪验证。只有当你输入一些东西,并且它不足以进行其他验证时(太短,不是有效的电子邮件,不是有效的 phone 号码等)贪婪验证才会触发。这就是为什么我想出我的第一个编辑(上图)的原因,因为也许系统没有将这些字符串字段视为空或空,即使它们是。


编辑 4:

WTF 更离奇。当我将 partial/incomplete 字符串放入不仅仅是长度分析的东西中时——例如只将电子邮件的前半部分放入电子邮件字段——然后点击提交,服务器端验证开始执行所有字符串字段,甚至 NULL/EMPTY 个字段。

喜欢,认真的威士忌。探戈。狐步舞。


编辑 5:

WTF x10:如果选择了三个下拉菜单,编辑 4 只会发生 。如果三个下拉列表中的任何一个仍未选中,服务器端验证将无法触发任何文本字段。

此外,如果选择了所有三个下拉菜单,使用 .NotEmpty().NotNull() 的完整验证会突然在所有文本字段上成功,包括服务器端 客户端贪婪验证。

神圣的玉米粉蒸肉。这越来越奇怪了。

这个问题源于我自从开始使用 Fluent Validation 以来的概念盲点。

自从我开始使用 Fluent Validation 之前,我就使用 GUIDs 作为主键。我在整个数据生命周期中使用了 GUIDs——从 ViewModel 到 View 再回到 Mapper,数据被转储回数据模型以添加 to/updating 数据库。

因此,当我使用下拉 select 菜单并将 GUID 用作主键时,那些 select 菜单旨在填充所需的外键,我能够将 ViewModel 字段设置为不可为 null 的 GUID 并仍然正确触发 Fluent Validation。当我想填充可选外键时,我为该字段使用了可为空的 GUID,一切仍然有效。

这个项目使用的数据库早于我的参与。因此,它使用 int 作为主键,因此所需的某些外键也是 int 而不是 int?。正因为如此,我做出了这样的假设——无论多么错误——我可以继续使用 int 来保存 所需外键 [=42] 的下拉菜单 select 的值=] 并能够成功验证其他所有内容,包括文本字段。

天哪,我错了。

Fluent Validation 的 Jeremy Skinner 名声大噪 took his Sunday out to assist me 并向我展示了我需要做什么。

本质上,任何整数驱动的下拉 select 菜单,无论是必需的外键还是可选的外键,都需要 ViewModel 中的 int? 来保存 selected值。如果没有可为 null 的 int?,整个模型的验证将以意想不到的方式进行,以至于根本不会触发任何其他内容(根据我的经验)。

当我将该字段的值更改为 int? 并向 Mapper 添加空合并运算符时(通过提供任何通过 Fluent Validation 的空值的默认值),一切突然开始按预期工作。