Current project:
- DotNet 4.7
- MVC 5
- Database-first and generated from a very old Web Forms database (nchar fields and the like… don’t ask why)
So I am experiencing extremely bizarre behaviour from Fluent Validation.
To wit,
Non-Nulled Nullable fields validate only on the server-side. Take an int
(and NOT an int?
) field that is supposed to be populated by a drop-down menu's value, and while it will validate on the server-side, it will only de-validate on the client side. It will not re-validate on the client side if you choose a non-acceptable null value ("Select a choice") again.
This behaviour appears to be limited to int
fields that are filled from drop-down lists.
All strings, dates and any other types of fields fail to validate on the client side until the form is submitted (once greedy validation kicks in), and will not validate at all on the server side. Plus, all .NotEmpty()
and .NotNull()
declarations appear to be ignored, even when they are the only one in the validation for a string field.
I have my Global.asax.cs
properly configured:
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();
}
}
I have the correct JS files coming into the page:
<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>
My ViewModel is appropriately configured:
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();
}
}
I have my validators properly configured:
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 “123-456-7890”")
.Matches(@"^\d{3}-\d{3}-\d{4}$").WithMessage("Phone number must be a valid 10-digit phone number with dashes, in the form of “123-456-7890”");
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.");
}
}
}
My forms are correctly built:
@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"> </div>
<div class="form-group col-md-6"><label class="control-label"> </label><input type="submit" value="Submit Request" alt="Submit Request" title="Submit Request" class="btn btn-default btn-success" /></div>
</div>
}
I am utterly stumped as to why Fluent Validation is only firing under extremely limited circumstances.
Please understand that when it does fire -- post-submission greedy validation for all other fields, server-side validation for the drop-down menus -- all messages are perfectly as expected.
However, validation is not firing for both client-side and server-side for any field. If it fires server-side, it fails to fire client-side. If it fires client side, it does so only under limited conditions (string length not sufficient, etc.) and it then refuses to fire server-side.
What is not working is the usual validation and any attempt to catch .NotEmpty()
and .NotNull()
.
EDIT:
I can’t see how this is possible (nothing is being loaded from the DB, only inserted into it), but could string-field validation be buggered up by the format of the database’s fields? Could it being an nchar
be an issue here?
Plus, I am using a custom model to gather the data, before adding it to a Data Model for insertion into the DB, so we have even more distance from the DB structure. Theoretically I couldn’t see how such a relationship is possible prior to moving the data from the ViewModel to the DataModel.
EDIT 2:
The .Cascade(CascadeMode.StopOnFirstFailure)
made no difference, the issues occur with or without these in the Validator class.
EDIT 3:
Let me be more precise about the greedy validation. In all cases, any .NotEmpty()
and .NotNull()
validations still fail to fire, so simply navigating from one field to the next (after hitting Submit) will fail to trigger greedy validation. It’s only when you put something in, and it’s not sufficient for other validation (too short, not a valid eMail, not a valid phone number etc.) that the greedy validation fires. That is why I came up with my first edit (above), because maybe the system is not seeing those string fields as either empty or null, even when they are.
EDIT 4:
More WTF bizarreness. When I put in a partial/incomplete string into something that does more than just length analysis -- such as only the first half of an eMail into the eMail field -- and then hitting Submit, THE SERVER-SIDE VALIDATION KICKS IN FOR ALL STRING FIELDS, EVEN THE NULL/EMPTY ONES.
Like, seriously Whisky. Tango. Foxtrot.
EDIT 5:
WTF x10: Edit 4 only happens if the three drop-downs have been selected. If any one of the three drop-downs are still un-selected, server-side validation fails to fire for any text field.
Plus, if all three drop-downs have been selected, full validation using .NotEmpty()
and .NotNull()
suddenly succeeds on all text fields, including both server-side and client-side greedy validation.
Holy tamale. This is getting bizarre.