非常离奇的Fluent验证行为

时间:2018-05-26 00:51:06

标签: asp.net asp.net-mvc asp.net-mvc-5 fluentvalidation

当前项目:

  • DotNet 4.7
  • MVC 5
  • 数据库优先并从非常古老的Web窗体数据库生成(nchar字段等...不要问为什么)

因此,我在Fluent Validation中遇到了非常奇怪的行为。

即便,

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

此行为似乎仅限于从下拉列表中填充的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验证只能在非常有限的情况下解雇。

请理解当 触发 - 所有其他字段的提交后贪婪验证,下拉菜单的服务器端验证 - 所有消息都完全符合预期。

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

的工作原理是通常的验证以及捕获.NotEmpty().NotNull()的任何尝试。

编辑:

我看不出这是怎么可能的(没有任何东西从数据库加载,只插入到它中),但是字符串字段验证可能被数据库字段的格式所扰乱?这可能是nchar的问题吗?

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

编辑2:

.Cascade(CascadeMode.StopOnFirstFailure)没有区别,在Validator类中有或没有这些问题。

编辑3:

让我更加准确地了解贪婪的验证。在所有情况下,任何.NotEmpty().NotNull()验证仍然无法触发,因此只需从一个字段导航到下一个字段(在点击提交后)将无法触发贪婪验证。只有当你把东西放入其中时,并不足以进行其他验证(太短,不是有效的电子邮件,而不是有效的电话号码等),贪婪的验证就会触发。这就是为什么我想出了我的第一个编辑(上图),因为系统可能没有看到那些字符串字段为空或null,即使它们是。

编辑4:

更多WTF奇异。当我将一个部分/不完整的字符串放入不仅仅是长度分析的东西时 - 例如只有eMail字段中的eMail的前半部分 - 然后点击提交,服务器端验证为所有字符串开启FIELDS,即使是空/空。

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

编辑5:

WTF x10:编辑4仅在 时发生,如果已选择三个下拉菜单 。如果仍然未选中三个下拉列表中的任何一个,则无法为任何文本字段触发服务器端验证。

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

神圣的女性。这变得奇怪了。

1 个答案:

答案 0 :(得分:-1)

这个问题源于我开始使用Fluent验证后的概念盲点。

在我开始使用Fluent验证之前,我使用GUID作为主键。我在整个数据生命周期中使用了GUID - 从ViewModel到View再到Mapper,数据被转储回数据模型以添加/更新数据库。

因此,当我使用下拉选择菜单时使用GUID s作为主键,并且那些选择菜单用于填充所需的外键,我能够将ViewModel字段设置为不可空的GUID并且仍然可以正确启用Fluent验证。当我想填充可选的外键时,我对该字段使用了可空的GUID,并且一切仍然有效。

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

男孩,我错了。

杰里米·斯金纳的流利验证成名took his Sunday out to assist me并向我展示了我需要做的事情。

基本上,任何整数驱动的下拉选择菜单,无论是针对所需的外键还是可选的外键,都需要ViewModel中的int?来保存所选的值。如果没有可以为空的int?,整个模型的验证将以意想不到的方式进行,甚至根本不以任何其他方式解雇(根据我的经验)。

当我将该字段的值更改为int?时,并向Mapper添加了一个空合并运算符(通过提供默认值将int?转换为int对于任何超过Fluent验证的空值,一切都突然开始按预期工作。