Author : HASSAN MD TAREQ | Updated : 2020/06/23

Model binding Github repository: https://github.com/hovermind/AspNetCoreModelBinding

Validation Types

  • Client side validation:
    • JS validates form data before sending it to server
    • Microsoft provides unobstrusive JavaScript for client side validation
  • Server side validation:
    • Annotations are used for Model Validation
    • If incoming data does not conform to the contraints (Data Annotations), model state becomes invalid (MVC WebApp Controller)
    • ASP.Net core (API Controller) sends http 400 (bad request) automatically

Client side validation

  • ASP.NET core includes unobtrusive client-side validation libraries, which makes it easier to add client side validation code, without writing a single line of code
  • Details: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation#client-side-validation
  • How Unobtrusive Validation work?
    • model properties have attributes/annotations
    • validation related tag helpers generates HTML5 data-* attributes based on model property attributes/annotations
    • Unobtrusive Validation script targets those HTML5 data-* attributes and performs actions when present (i.e. show error message, coloring error message etc.)

_ValidationScriptsPartial.cshtml includes validation related scripts

<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

The jQuery Unobtrusive Validation script is a custom Microsoft front-end library that builds on the popular jQuery Validate plugin. Without jQuery Unobtrusive Validation, you would have to code the same validation logic in two places: once in the server side validation attributes on model properties, and then again in client side scripts (the examples for jQuery Validate’s validate() method shows how complex this could become).

Instead, MVC’s Tag Helpers and HTML helpers are able to use the validation attributes and type metadata from model properties to render HTML 5 data- attributes in the form elements that need validation. MVC generates the data- attributes for both built-in and custom attributes. jQuery Unobtrusive Validation then parses the data- attributes and passes the logic to jQuery Validate, effectively “copying” the server side validation logic to the client. You can display validation errors on the client using the relevant tag helpers

Validation Message Tag Helper

There are two Validation Tag Helpers

  • asp-validation-for: Validation Message Tag Helper
  • asp-validation-summary: Validation Summary Tag Helper
    • ValidationSummary.All
    • ValidationSummary.ModelOnly
    • ValidationSummary.None
<span asp-validation-for="Email"></span>

<div asp-validation-summary="ModelOnly"></div>

asp-validation-for

<h1>Create Hover Info</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
		
		
			<!-- ... ... ... -->

            <div class="form-group">
                <label asp-for="Id" class="control-label"></label>
                <input asp-for="Id" class="form-control" />
                <span asp-validation-for="Id" class="text-danger"></span>
            </div>
			
			<!-- ... ... ... -->
			
			
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

sp-validation-summary

<h1>Create Hover Info</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
		
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
			
			<!-- ... ... ... -->
			
            <div class="form-group">
                <label asp-for="Id" class="control-label"></label>
                <input asp-for="Id" class="form-control" />
                <span asp-validation-for="Id" class="text-danger"></span>
            </div>
			
			<!-- ... ... ... -->
			
			
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<!-- ... ... ... -->

Showing validation error

  • Add annotations to model properties
  • Check model state in Controller Action
  • Add _ValidationScriptsPartial script to the view

HoverInputModel.cs

public class HoverInputModel
{

	[Required]
	[Display(Name = "First Name")]
	[Range(5, 50)]
	public int FirstName { get; set; }


	[Required]
	[Display(Name = "Last Name")]
	[StringLength(50, MinimumLength = 5, ErrorMessage = "Last name must have at least 5 characters")]
	public int LastName { get; set; }


	[Required(ErrorMessage = "{0} is required")]
	[StringLength(100, MinimumLength = 10, ErrorMessage = "Name must have at least 10 characters")]
	[RegularExpression(@"^[a-zA-Z\s]+$", ErrorMessage = "Please, use letters in the name. Digits are not allowed.")]
	[Display(Name = "Full Name")]
	public string FullName { get; set; }


	[Required(ErrorMessage = "{0} is required")]
	[StringLength(100, MinimumLength = 1, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.")]
	public string Email { get; set; }


	// will be null
	public string Bar { get; set; }
}

HoverController.cs

public class HoverController : Controller
{

    // ... ... ...

	// GET: Hover/Create
	public IActionResult Create()
	{
		return View();
	}
	

	// POST: Hover/Create
	// To protect from overposting attacks, enable the specific properties you want to bind to, for 
	// more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
	[HttpPost]
	[ValidateAntiForgeryToken]
	public async Task<IActionResult> Create([FromFrom]HoverInputModel hoverInputModel)
	{
		if (ModelState.IsValid)
		{
			// perform EF core task here i.e. _context.Add(hoverInputModel);await _context.SaveChangesAsync();
			
			return RedirectToAction(nameof(Index));
		}
		
		return View(hoverInputModel);
	}
	
	// ... ... ...
}

Create.cshtml

@model ModelBindingAndFormValidation.Models.HoverInputModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create Hover Info</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Id" class="control-label"></label>
                <input asp-for="Id" class="form-control" />
                <span asp-validation-for="Id" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FirstName" class="control-label"></label>
                <input asp-for="FirstName" class="form-control" />
                <span asp-validation-for="FirstName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="LastName" class="control-label"></label>
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FullName" class="control-label"></label>
                <input asp-for="FullName" class="form-control" />
                <span asp-validation-for="FullName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Email" class="control-label"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

When form is posted to server, depending on annotations applied to input model, error messages will be shown.

Scaffolding view with client side validation

Create input model and add annotations to properties

  • Right click on Models
  • Add > Class (FooInputModel.cs)

Models/FooInputModel.cs

using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;


[Bind(nameof(FirstName), nameof(LastName), nameof(FullName), nameof(Email))]
public class FooInputModel
{

	[Required]
	[Display(Name = "First Name")]
	[StringLength(50, MinimumLength = 5)]
	public string FirstName { get; set; }


	[Required]
	[Display(Name = "Last Name")]
	[StringLength(50, MinimumLength = 5, ErrorMessage = "{0} must have at least {2} characters")]
	public string LastName { get; set; }


	[Required(ErrorMessage = "{0} is required")]
	[StringLength(100, MinimumLength = 10, ErrorMessage = "{0} must have at least {2} characters")]
	[RegularExpression(@"^[a-zA-Z\s]+$", ErrorMessage = "Please, use letters in the name. Digits are not allowed.")]
	[Display(Name = "Full Name")]
	public string FullName { get; set; }


	[Required(ErrorMessage = "{0} is required")]
	[StringLength(100, MinimumLength = 1, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.")]
	public string Email { get; set; }


	// ignored (value will be null after model binding)
	public string Bar { get; set; }
}

Scaffold controller with read/write actions

  • Right click on Controllers folder > Add
  • New Scaffolded Item… > MVC Controller with read/write action > Add
  • Set controller class name: FooController > Add
  • For simplicity we are gonna keep index & create (GET & POST) actions and remove rest

Controllers/FooController.cs

public class FooController : Controller
{
	// GET: FooController
	public ActionResult Index()
	{
		return View();
	}


	// GET: FooController/Create
	public ActionResult Create()
	{
		return View();
	}

	// POST: FooController/Create
	[HttpPost]
	[ValidateAntiForgeryToken]
	public ActionResult Create(FooInputModel fooInputModel)
	{
		if (ModelState.IsValid)
		{
			// perform EF core task here i.e. _context.Add(hoverInputModel);await _context.SaveChangesAsync();

			return RedirectToAction(nameof(Index));
		}

		return View(fooInputModel);
	}
}

Scaffold view based on input model

  • Create a folder in Views according to MVC convention
    • MVC convention: if controller class name is XxxController then folder should be Views/Xxx
    • we created FooController, so folder should be Views/Foo
    • Right click on Views > Add > New folder > folder name: Foo
  • Scaffolding view
    • Build the project to make sure there is no error otherwise scaffolding would not work
    • Right click on Foo folder > Add
    • New Scaffolded Item.. > Razor View > Add
    • View name: Create
    • Template: Create
    • Model class: FooInputModel
    • Options:
      • check “reference script libraries”
      • check “use layout page”
      • input field below “use layout page”
        • leave empty since _viewstart will insert layout
        • to use custom layout: click ellipses button on rigt and set that custom layout
    • Add

Views/Foo/Create.cshtml

@model ModelBindingAndFormValidation.Models.FooInputModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>FooInputModel</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Id" class="control-label"></label>
                <input asp-for="Id" class="form-control" />
                <span asp-validation-for="Id" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FirstName" class="control-label"></label>
                <input asp-for="FirstName" class="form-control" />
                <span asp-validation-for="FirstName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="LastName" class="control-label"></label>
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="FullName" class="control-label"></label>
                <input asp-for="FullName" class="form-control" />
                <span asp-validation-for="FullName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Email" class="control-label"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Bar" class="control-label"></label>
                <input asp-for="Bar" class="form-control" />
                <span asp-validation-for="Bar" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Startup.Configure()

  • chnage default route pattern so that our foo/create is shown when application starts
  • app.UseEndpoints(…) => pattern: "{controller=Foo}/{action=Create}")

Add Index view

  • Right click Views/Foo > Add > View..
  • Razor view > Add
    • View name: Index
    • Template: Empty
  • Add

Now run the application

  • if form data is valid it will go to Index page
  • if form data is not valid, errors will be shown