18.09.17
Creating Complex Validation Rules Using Fluent Validation with ASP.NET Core
By John Ciliberti
Many ecommerce web sites are driven by user input and the choices they make. As a developer, you want to help them make the right choices and have a positive experience with your site, so they will complete their purchase, and return to make another one!
In this example, imagine that you are building a web site that allows users to build their own electric guitars. They pick the woods, body, pickups, strings, tremolo, and other parts, and then a robot will put the guitar together and ship it to them. Before the guitar is built, you need to ensure that the parts they selected are compatible. You want to be able to give the users feedback as they are filling out the form when they select a part that is not compatible.
A recipe for complex validation requirements
This recipe will demonstrate how to use the Fluent Validation package to solve the above problem. Fluent Validation is a popular open source library for solving complex validation requirements written by Jeremy Skinner. You can find the source code and documentation for the library at https://github.com/JeremySkinner/fluentvalidation.
The Fluent Validation library uses a fluent interface and lambda expressions to allow you to write very readable and expressive validation rules. If you are interested in reading more of the theory behind fluent interface design, I recommend reading the article by Martin Fowler at https://www.martinfowler.com/bliki/FluentInterface.html.
How It Works
To create this example, you will create a new ASP.NET Core web application. You will then add models to define a guitar and parts you will add to it. Next you will install the Fluent Validation for ASP.NET Core NuGet package and use it to create validation rules. Finally, you will add a view model, the controller, and the views that make up the new guitar form.
Creating the Project
Create a new ASP.NET Core Web Application (.NET Core 1.1) project using the Web Application template. Name the project and solution Recipe05. Ensure that No Authentication is selected and Docker support is not enabled.
Creating the Models
The models used for this example will be made up of four classes:
- Guitar.cs: Represents the guitar that will be built
- GuitarBody: Represents the main body of the guitar that houses all of the electronics including the pickups, input jacks, and volume controls
- GuitarPickUp: Represents the magnets that must be installed on a guitar to allow it to convert the vibrations of the guitar strings into electronic signals
- GuitarString: Represents the metal strings that will be installed on the guitar
To create the model, add a folder called Models under the root of the Recipe05 project. Add classes to the folder named Guitar, GuitarBody, GuitarPickup, and GuitarString. Listings 1 through 4 show the completed classes.
Listing 1. Guitar.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Recipe05.Models
{
public class Guitar
{
[Display(Description ="Name your custom guitar.")]
public string Name { get; set; }
public GuitarBody Body { get; set; }
[Display(Name = "Bridge Pickup.")]
public GuitarPickup BridgePickup { get; set; }
[Display(Name = "Middle Pickup.")]
public GuitarPickup MiddlePickup { get; set; }
[Display(Name = "Neck Pickup.")]
public GuitarPickup NeckPickup { get; set; }
public IList<GuitarString> Strings { get; set; }
}
}
Note that, in Listing 1, even though you will be using the Fluent Validation library for your validation rules, data annotations can still be applied to the model for display information.
Listing 2. GuitarBody.cs
using System;
namespace Recipe05.Models
{
public class GuitarBody
{
public string Name { get; set; }
public string ToneWood { get; set; }
public int NumberOfStringsSupported { get; set; }
public bool AllowBridgePickup { get; set; }
public bool AllowMiddlePickup { get; set; }
public bool AllowNeckPickup { get; set; }
public BodyType BodyType { get; set; }
public BodyStyle Style { get; set; }
public String Color { get; set; }
}
}
// BodyType.cs
namespace Recipe05.Models
{
public enum BodyType { HollowBody, Chambered, SolidBody}
}
// BodyStyle.cs
namespace Recipe05.Models
{
public enum BodyStyle { LesPaul, SG, Strat,Telecaster, FlyingV, Jazzmaster, Explorer, Gem }
}
The GuitarBody class shown in Listing 2 makes use of two enums. Using enums helps to keep your code readable. This will be especially important when you start defining the validation rules using the Fluent interface.
Listing 3. GuitarPickup.cs
namespace Recipe05.Models
{
public class GuitarPickup
{
public string Name { get; set; }
public PickUpType PickUpType { get; set; }
public PickUpPosition RecommendedPosition { get; set; }
public int NumberOfStringsSupported { get; set; }
public int NumberOfConductorsRequired { get; set; }
}
}
// PickUpTypes.cs
namespace Recipe05.Models
{
public enum PickUpType { Humbucker, SingleCoil, Piezo}
}
// PickUpPosition.cs
namespace Recipe05.Models
{
public enum PickUpPosition { Piezo, Bridge, Middle, Neck}
}
Listing 4. GuitarString.cs
namespace Recipe05.Models
{
public class GuitarString
{
public string Name
{
get
{
return string.Format("{0} : {1}", NoteAtStandardTuning, Gage);
}
}
public string NoteAtStandardTuning { get; set; }
public int Gage { get; set; }
public string Material { get; set; }
}
}
The GuitarString class shown in Listing 4 uses a computed property to derive the name. The Name property will be used for display purposes.
Installing the Fluent Validation NuGet Package
There are two steps required to integrate the Fluent Validation NuGet package with ASP.NET Core. First you must install the NuGet package using the Package Manager, and then you must configure Fluent Validation in Startup.cs.
To install Fluent Validation, open the Package Manager Console window in Visual Studio and enter the following command:
Install-Package FluentValidation.AspNetCore
After the installation has completed, modify the ConfigureServices method of Startup.cs so that it matches Listing 5 below. RegisterValidatorsFromAssemblyContaining<Startup>()) will use reflection to find all the classes in the current assembly that are derived from AbstractValidator<T> and then register all the validation rules defined inside them.
Listing 5. Startup.cs Modified to Configure Fluent Validation
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc().AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
}
Creating the Validation Rules
To create validation rules for a class using Fluent Validation, create a separate class that extends the FluentValidation.AbstractValidator<T> class, where T is the class where you want to apply the validation rules.
Each rule in the validation class’s constructor uses calls to the RuleFor method. The RuleFor method takes a lambda expression as an argument that allows you to specify the property the rule is to be applied to. RuleFor returns an IRuleBuilderInitial object instance. The IRuleBuilderInitial interface exposes a set of built-in rules that can be applied to the model. The following is a list of built-in validators:
- NotNull: Invalidates the model when the property is null.
- NotEmpty: Equivalent to the Required data annotation; invalidates the model when the property is null, empty, or whitespace.
- NotEqual: Invalidates the model when the property does not match the comparison value.
- Equal: Inverse of the NotEqual rule. Invalidates the model when the property matches the comparison value.
- Length: Like the StringLength data annotation, invalidates the model when the property length is not in the specified range.
- LessThan: Invalidates the model when the property value is less than the comparison value.
- LessThanOrEqual: Invalidates the model when the property value is less than or equal to the comparison value.
- GreaterThan: Invalidates the model when the property value is greater than the comparison value.
- GreaterThanOrEqual: Invalidates the model when the property value is greater than or equal to the comparison value.
- Must: Allows you to create custom validators inline using lambda expressions. Invalidates the model when the property does not meet the criteria specified in the expression.
- Matches: Equivalent to the RegularExpression data annotation. Invalidates the model when the property value does not match the specified regular expression.
- Email: Invalidates the model when the property value is not a valid e-mail address.
Each of the built-in validation methods returns an instance of the IRuleBuilder interface. The IRuleBuilder interface exposes a secondary set of rules that either can add additional rules to the property specified in RuleFor or can add restrictions to when the first rule can be applied. For example, if you wanted to create a rule for the Guitar class that ensured that if that guitar body requires a neck pickup, the neck pickup cannot be empty, then you would write the following:
RuleFor(guitar => guitar.NeckPickup).NotEmpty().When(guitar => guitar.Body.AllowNeckPickup);
Listing 6 shows a set of rules defined for the Guitar class in a validation class named GuitarValidator. To create this class, add a folder to the root of Recipe05 named Validation and then add a class named GuitarValidator.
Listing 6. GuitarValidator
using FluentValidation;
using Recipe05.Models;
namespace Recipe05.Validation
{
public class GuitarValidator : AbstractValidator<Guitar>
{
public GuitarValidator()
{
// guitar name cannot be null, empty, or whitespace and
// must be at least 3 but no more than 40 characters long
RuleFor(guitar => guitar.Name).NotEmpty().Length(3,40);
// guitar must have a body
RuleFor(guitar => guitar.Body).NotEmpty();
// guitar must have a pickup installed in each slot available in the selected body
RuleFor(guitar => guitar.NeckPickup).NotEmpty().When(guitar => guitar.Body.AllowNeckPickup);
RuleFor(guitar => guitar.BridgePickup).NotEmpty().When(guitar => guitar.Body.AllowBridePickup);
RuleFor(guitar => guitar.MiddlePickup).NotEmpty().When(guitar => guitar.Body.AllowMiddlePickup);
// can't select more strings then guitar body supports
RuleFor(guitar => guitar.Strings)
.NotNull()
.Must((guitar, strings) => strings?.Count == guitar?.Body?.NumberOfStringsSupported)
.WithMessage(@"The number of strings selected {0}
does not match the number supported by the guitar body {1}",
guitar => guitar?.Strings?.Count,
guitar => guitar?.Body?.NumberOfStringsSupported);
// can't add a middle pickup if the guitar body does not support it
RuleFor(guitar => guitar.MiddlePickup)
.Null()
.When(guitar => guitar.Body.AllowMiddlePickup = false);
}
}
}
Creating the View Model
The guitar builder form will contain a set of drop-down lists that list the available parts for each of the required guitar components. The drop-down lists will be created using the Select Tag Helper. The Select Tag Helper takes two attributes, asp-for and asp-items. The asp-for attribute wires the generated HTML form field with a model property. The asp-items attribute contains the data used to generate the Options tags that make up the drop-down list. The items passed to the asp-items attribute must be of the type IEnumerable<SelectListItem>.
Creating a Generic SelectListItem Adapter
Since SelectListItems objects are a concern of the view and do not naturally appear in the model, a view model is required to allow the select elements to be generated and to provide a translation between the properties of the model and items selected in the form.
To simplify the code in the view model and to reduce the need to duplicate code, you will build a utility class called SelectListAdapter. The SelectListAdapter class will contain a method called ConvertToSelectListItemCollection. ConvertToSelectListItemCollection will allow you to convert a collection from the model into the IEnumerable<SelectListItem> required by the view. ConvertToSelectListItemCollection also allows you to specify the properties from the model’s collection to be used for the text and value properties of each SelectListItem.
To create the SelectListAdapter class, first create a folder under the root of the Recipe05 project called Util. Next add a new class file called SelectListItemAdapter.
Since the SelectListItemAdapter class will be stateless and does not use any instance properties, it will be defined as static. Inside SelectListItemAdapter, create a generic method named ConvertToSelectListItemCollection. Generic methods allow the method to be strongly typed while supporting many types. The ConvertToSelectListItemCollection takes four arguments.
- source: This is the source list from the model that will be converted to an IEnumerable<SelectListItem>.
- text: This is a lambda expression that takes an instance of the collection type used in the source and returns a string. This will allow developers to select a property to use for the Text property of the SelectListItem list items.
- value: This is a lambda expression that takes an instance of the collection type used in source and returns a string. This will allow developers to select a property to use for the Value property of the SelectListItem list items.
- createEmpty: If true, which is the default value, an empty SelectListItem will be added to the list that contains the text Please Select.
A second version of the ConvertToSelectListItemCollection is also added that allows a single property to be used for both the Text and Value properties of SelectListItem.
Listing 7. SelectListItemAdapter
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Recipe05.Util
{
public static class SelectListItemAdapter
{
public static IEnumerable<SelectListItem> ConvertToSelectListItemCollection<T>
(IEnumerable<T> source, Func<T, string> text, Func<T, string> value, bool createEmpty = true) where T : class
{
var selectListItems = new List<SelectListItem>();
if (createEmpty)
{
selectListItems.Add(new SelectListItem { Text = "Please Select", Value = "" , Selected= true });
}
foreach (var item in source)
{
selectListItems.Add(new SelectListItem { Text = text(item), Value = value(item)});
}
return selectListItems;
}
public static IEnumerable<SelectListItem> ConvertToSelectListItemCollection<T>
(IEnumerable<T> source, Func<T, string> textAndValue, bool createEmpty = true) where T : class
{
return ConvertToSelectListItemCollection(source, textAndValue, textAndValue, createEmpty);
}
}
}
Simulating an Inventory Module
In a real system, you may have an inventory module that would call out to a back-end database to pull the list of available parts. For this exercise, you will simulate the database call with the Inventory class. The Inventory class will consist of a set of properties containing lists of the various parts that make up the guitar. To create the Inventory class, create a new folder called Data and within it create a new class called Inventory. Listing 8 shows the Inventory class.
Listing 8. Simulated Inventory Module
using Recipe05.Models;
using System.Collections.Generic;
namespace Recipe05.Data
{
public class Inventory
{
public IList<GuitarBody> GuitarBodies = new List<GuitarBody>
{
new GuitarBody {
Name = "Red Les Paul",
AllowBridePickup = true,
AllowMiddlePickup = false,
AllowNeckPickup = true,
BodyType = BodyType.SolidBody,
ToneWood = "mahogany",
Color = "Red",
NumberOfStringsSupported =6,
Style = BodyStyle.LesPaul
}
// additional guitar body choices go here
};
public IList<GuitarPickup> GuitarPickups = new List<GuitarPickup>
{
new GuitarPickup{
Name = "Imperium 7™ Neck",
NumberOfStringsSupported = 7,
PickUpType =PickUpType.Humbucker,
RecommendedPosition = PickUpPosition.Bridge,
NumberOfConductorsRequired = 4
}
// additional guitar pickup choices go here
};
public IList<GuitarString> GuitarStrings = new List<GuitarString>
{
new GuitarString { Gage=9, Material = "Steel", NoteAtStandardTuning = "E"},
new GuitarString {Gage=10, Material = "Nickel", NoteAtStandardTuning = "E"}
// additional guitar string choices go here
};
}
}
Creating the GuitarBuilderViewModel Class
To simplify the view logic used inside the Razor views, you will create a view model named GuitarBuilderViewModel. To help you differentiate models that are purely entities from models that are bound to views, you will create a new folder called ViewModels and name each view model class in the folder with the ViewModel suffix. The GuitarBuilderViewModel class will contain an IEnumerable<SelectListItem> for each of the drop-down lists required on the form. It will also have a string value to represent the selected value from each of the drop-down lists. In addition, since you want to use the display metadata from the Guitar class to create the labels and descriptions used on the form, the view model class will also have a Guitar property.
In addition to the properties, the GuitarBuilderViewModel class contains a helper method that uses SelectListItemAdapter to create IEnumerable<SelectListItem>. Listing 9 shows GuitarBuilderViewModel.
Listing 9. The GuitarBuilderViewModel Class
using Microsoft.AspNetCore.Mvc.Rendering;
using Recipe05.Data;
using Recipe05.Models;
using Recipe05.Util;
using System.Collections.Generic;
namespace Recipe05.ViewModels
{
public class GuitarBuilderViewModel
{
public GuitarBuilderViewModel()
{
// in a real app we would get the data via constructor injection
PopulateFromInventory();
}
public Guitar Guitar { get; set; } = new Guitar();
public IEnumerable<SelectListItem> BridgePickupList { get; set; }
public string SelectedBridgePickup { get; set; }
public IEnumerable<SelectListItem> MiddlePickupList { get; set; }
public string SelectedMiddlePickup { get; set; }
public IEnumerable<SelectListItem> NeckPickupList { get; set; }
public string SelectedNeckPickup { get; set; }
public IEnumerable<SelectListItem> BodyList { get; set; }
public string SelectedBody { get; set; }
public IEnumerable<SelectListItem> StringsList { get; set; }
public IEnumerable<string> SelectedStrings { get; set; }
private void PopulateFromInventory()
{
Inventory = new Inventory();
BodyList = SelectListItemAdapter.ConvertToSelectListItemCollection
(Inventory.GuitarBodies, s => s.Name);
BridePickupList = SelectListItemAdapter.ConvertToSelectListItemCollection
(Inventory.GuitarPickups, s => s.Name);
MiddlePickupList = SelectListItemAdapter.ConvertToSelectListItemCollection
(Inventory.GuitarPickups, s => s.Name);
NeckPickupList = SelectListItemAdapter.ConvertToSelectListItemCollection
(Inventory.GuitarPickups, s => s.Name);
StringsList = SelectListItemAdapter.ConvertToSelectListItemCollection
(Inventory.GuitarStrings, s => s.Name);
}
// used by the GuitarBuilderToGuitarAdapter class shown in next section
internal Inventory Inventory { get; private set; }
}
}
Creating the GuitarBuilderToGuitarAdapter Class
The GuitarBuilderViewModel class does not contain enough information to be used to perform the rules processing required to meet the business requirements. The only purpose of the view model is to collect the data submitted by the user back to the controller. The complex rules that were defined in Listing 6 were defined on the Guitar class. Even though a Guitar class was included in the view model, not enough information was passed to it to invoke the rules. For the rules to be processed, data from the view model needs to be used to create a fully populated Guitar object.
The GuitarBuilderToGuitarAdapter class performs the function of creating the Guitar object from the form data collected in the view model. It does this by querying the Inventory property of the view model and finding the matching item using the key saved in the view model for each select list. Listing 10 shows the GuitarBuilderToGuitarAdapter class.
Listing 10. The GuitarBuilderToGuitarAdapter Class
using Recipe05.Data;
using Recipe05.Models;
using System.Linq;
namespace Recipe05.ViewModels
{
public class GuitarBuilderToGuitarAdapter
{
public Guitar BuildGuitar(GuitarBuilderViewModel viewModel)
{
if (viewModel == null) return null;
var guitar = new Guitar()
{
Name = viewModel.Guitar.Name,
BridgePickup = SelectPickUp(viewModel.Inventory, viewModel.SelectedBridgePickup),
MiddlePickup = SelectPickUp(viewModel.Inventory, viewModel.SelectedMiddlePickup),
NeckPickup = SelectPickUp(viewModel.Inventory, viewModel.SelectedNeckPickup),
Body = viewModel.Inventory?.GuitarBodies?.FirstOrDefault(a => a.Name == viewModel.SelectedBody),
Strings = (from gs in viewModel.Inventory.GuitarStrings
where viewModel.SelectedStrings!=null && viewModel.SelectedStrings.Contains(gs.Name)
select gs).ToList()
};
return guitar;
}
private GuitarPickup SelectPickUp(Inventory inventory, string pickupName)
{
if (string.IsNullOrEmpty(pickupName)) return null;
return inventory?.GuitarPickups?.FirstOrDefault(a => a.Name == pickupName);
}
}
}
Creating the Controller
For this recipe, the Home controller that is created by Visual Studio will be modified to support the guitar builder form. First, modify the Index action so that it is passed a GuitarBuilderViewModel class to the view. Next, create an HttpPost version of the Index action that accepts GuitarBuilderViewModel as a parameter. Change the signature of the action to be asynchronous.
Inside the action, create an instance of GuitarBuilderToGuitarAdapter and then use the BuildGuitar method to create an instance of Guitar from the GuitarBuilderViewModel class passed into the action.
In the next line, call TryUpdateModelAsync with the Guitar object created by the adapter. TryUpdateModelAsync will cause the validation rules attached to the Guitar class from GuitarValidator to be run against the current state of the object. TryUpdateModelAsync will update the ModelState value of the controller with results of the validation and add any validation errors to the controller’s model error list. This will allow validation errors to appear on the view when the Validation-Summary and Validation-For Tag Helpers are used. Listing 11 shows the updated Home controller.
Listing 11. Home Controller Modified to Support Guitar Builder Form
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Recipe05.ViewModels;
using Recipe05.Models;
namespace Recipe05.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
var model = new GuitarBuilderViewModel { Guitar = new Guitar { Name = "My New Guitar" } };
return View("Index", model);
}
[HttpPost]
public async Task<IActionResult> Index(GuitarBuilderViewModel model)
{
var adapter = new GuitarBuilderToGuitarAdapter();
model.Guitar = adapter.BuildGuitar(model);
await TryUpdateModelAsync(model.Guitar);
if (ModelState.IsValid)
{
return RedirectToAction("OrderRecieved");
}
return View("Index", model);
}
public IActionResult OrderRecieved()
{
return View("OrderRecieved");
}
public IActionResult Error()
{
return View();
}
}
}
Creating the Views and Testing the Application
The final step is to modify the Index view of the Home controller to host the guitar builder form. First, remove all the boilerplate code that was added by Visual Studio when the project was created. Next, use the @model directive to make the view strongly typed to Recipe05.ViewModels.GuitarBuilderViewModel.
Then create a FORM element with an asp-action attribute set to index. The asp-action attribute will render the FORM attributes necessary to post the form back to the Index action. Finally, add all the required form elements needed for the guitar builder form, as shown in Listing 12. Don’t forget to include the _ValidationScriptsPartial view at the bottom of the page.
Listing 12. The Guitar Builder Form
@model Recipe05.ViewModels.GuitarBuilderViewModel
@{
ViewData["Title"] = "Home Page";
}
<h1>Chapter 9 - Recipe 05</h1>
Use this form to build the guitar of your dreams and our robots will build it for you.
<form asp-action="index">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Guitar.Name"></label>
<input class="form-control" asp-for="Guitar.Name" placeholder="Name for your custom guitar">
<span asp-validation-for="Guitar.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Guitar.Body"></label>
<select class="form-control"
asp-for="SelectedBody"
asp-items="Model.BodyList">
</select>
<span asp-validation-for="Guitar.Body" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Guitar.BridgePickup"></label>
<select class="form-control"
asp-for="SelectedBridgePickup"
asp-items="Model.BridgePickupList"></select>
<span asp-validation-for="Guitar.BridgePickup" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Guitar.MiddlePickup"></label>
<select class="form-control"
asp-for="SelectedMiddlePickup"
asp-items="Model.MiddlePickupList"></select>
</div>
<div class="form-group">
<label asp-for="Guitar.NeckPickup"></label>
<select class="form-control"
asp-for="SelectedNeckPickup"
asp-items="Model.NeckPickupList"></select>
</div>
<div class="form-group">
<label asp-for="Guitar.Strings"></label>
<select class="form-control"
asp-for="SelectedStrings"
asp-items="Model.StringsList"></select>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
@section scripts{
@Html.Partial("_ValidationScriptsPartial")
}
Run the application and try submitting the form using different combinations of the input. You will see that for most of the validation errors, the validation summary will be updated, but no validation error will appear on the form field. The exceptions to this rule are validation errors, such as when the guitar name field is left blank.
About the Author
John Ciliberti is an enterprise architect with over 14 years of professional experience in software engineering and architecture. After almost seven years with KPMG's Enterprise Architecture practice and five years of solutions architecture consulting, John has acquired strong business and communications skills backed up by a broad range of technical knowledge. He specializes in enterprise architecture, web application development technologies, and mobile device development.
Want more? This article is excerpted from the book ASP.NET Core Recipes. Get your copy today and learn more straightforward solutions to common web development problems using proven methods based on best practices. You can also download the source code files for the listings in this article from the Apress GitHub repository.