minutes to read

 Companion Repo: https://github.com/ajsaulsberry/BlipValidate

Author: A. J. Saulsberry


TechnologySkill Level
ASP.NET Core MVCBeginner
C#Beginner
HTMLBeginner

This article includes links to external websites. These links are covered by the Disclaimer.

Introduction

Data validation is an important aspect of an application's user experience, effectiveness, and security. Client data validation should happen at the front end, as data is collected, and at the back end as it is recorded to a permanent data store, such as a database.

Client data validation in stateless applications, such as web applications, should be implemented in the portion of the application that runs on the client and at the server in the components that handle client data exchange. This is essential for security and for maintaining a consistent user interface.

In web applications, performing client-side validation using HTML markup, JavaScript, and the browser's data entry features is the best way to ensure data conforms to validation constraints and the user experiences the most helpful interface. It also cuts down on round trips to the server, reducing server load and latency.

The server code should not presume the client has performed all validation tasks because there is always a possibility the form data in the HTTP POST request has not been completely validated. JavaScript libraries on the client may be missing or disabled, the POST request may be inadvertently or deliberately malformed, or transmission errors may have occurred.

The challenge for developers is to concisely define a consistent set of validation rules without duplicating code. Developers using Microsoft technologies can do this with the features of ASP.NET Core MVC (hereafter, "MVC") and the Model-View-ViewModel design pattern.

Objective

This article presents a technique for using the features of MVC with MVVM design to create validation rules that implement robust numeric fields. This is particularly important because some C# and .NET value types are converted to HTML text fields and back again when data is exchanged between the server and the client. This technique has wide application because the decimal data type is one of the value types that undergo conversion. The decimal type is used for currency values, one of the most frequently occurring types of data.1

Robust numeric fields are also relevant to the numeric value types that are not converted to text because they enable the server to differentiate between a non-response and a zero response for a required field without having to presume that client-side validation has been performed successfully.

Importantly, when correctly applied this technique provides consistency between client-side validation and server-side validation without having to write additional code. The definition for a robust numeric field works in both places and the server-side validation occurs regardless of whether client-side validation has been performed.

Robustness Defined

For the purpose of this article, robustness is achieved by constructing data models (viewmodels) and validation rules that can determine:

  1. if the user entered data for a numeric field and
  2. if the data received for the field meets the validation criteria.

A robust numeric field enables the server-side code to differentiate three types of responses based solely on the form data in the HTTP POST request:

  1. missing (non-response),
  2. invalid (out-of-range, wrong type), and
  3. zero.

A robust numeric field cannot determine if the form data was sent from a legitimate source. That is a separate concern provided for in other code through antiforgery protection, encryption, authentication, and authorization.

Sample code

Examples in this article are based on the same case study used in the previous two parts. A Visual Studio 2017 solution containing the complete code for each part of this series is available on GitHub.

Ellipsis (...) in the sample code shown below indicates portions of the code redacted for brevity. See the case study solution for the complete code.

Implementation

Implementation of robust numeric fields consists of two parts:

  1. Data annotations in the viewmodel associated with the Razor view that will be used by MVC to create the HTML for the data form and

  2. Code in the related HttpPost controller action method that looks at the values returned for the form and the form's ModelState and responds according to the business rules for missing and invalid data.

Relevant value types

Only some of the C# / .NET numeric value types are converted to HTML text fields and back again. The robust numeric fields technique is most relevant to these, but it is also useful for the other numeric value types when the field is a required input.

The table below lists all the value types and the HTML <input> element types to which they are converted. The numeric types converted to text are shown in bold.

C# Value TypeHTML Type Attribute
boolcheckbox *
bytenumber
chartext
decimaltext
doubletext
floattext
intnumber
longnumber
sbytenumber
shortnumber
uintnumber
ulongnumber
ushortnumber

ViewModel data annotations

Developers using ASP.NET MVC Core and the Model-View-ViewModel (MVVM) design pattern can build strongly-typed viewmodels for MVC to use with the associated Razor form in creating an HTML form. MVC leverages the information in the viewmodel in four ways relevant to data validation:

  1. building the form's HTML elements and setting their attributes;
  2. creating data- ("data dash") attributes used by jQuery unobtrusive validation in the client browser;
  3. performing model binding between the form data and an instance of the viewmodel class; and
  4. validating data returned to the server in an HTTP POST request.

Inherently required properties

Value types in C# require a value upon initialization. If a property of a class is declared as a value type, when an instance of the class is initialized a value must be assigned to the property.

  • For a complete explanation of value types, see Value Types in Microsoft's C# Language Reference

The DateTime type also requires a value on initialization. As the documentation shows, a DateTime can be initialized in a variety of ways.

  • For a complete explanation, see DateTime Struct in Microsoft's .NET API Reference.

In MVC applications, it is common to create an instance of a viewmodel class in the controller method that handles the HttpGet action for a view. The values of the object are then passed to the client by the action method.

If value type properties are not otherwise initialized with a value, such as with a repository method call, they must be initialized with a value of zero (or a value within the range specified by the Range data attribute. When MVC parses the data object and the Razor view to create the HTML for the HTML <form>, the value of the <input> field associated with the property will be set to zero.

Passing an initialized instance of a viewmodel class in the HttpGet action method will cause the initialized value to appear as the initial value on the rendered input form, which may not be desirable: if the user submits the form without entering data for the field, zero will be submitted.

Less commonly, MVC may create a page from a viewmodel that is associate with the Razor page with the @model directive, but the instance of the viewmodel class has not been initialized. In this case the <input> elements are created without initial values, so numeric fields do not have an initial value of zero.

  • It is possible to customize MVC's model binding behavior with additional model attributes, but it is not necessary to implement the functionality described in this article. See Customize model binding behavior with attributes in Microsoft's ASP.NET Core Guide for more information.

When MVC receives form data as part of an HttpPost request it attempts to bind the form data to a new instance of the viewmodel class. For non-nullable types, numeric types and DateTime, if the data is missing or invalid (out-of-range or the wrong type), MVC initializes the property with zero for numerics and 01/01/0001 12:00:00 AM for dates.2

The problem with this is that is is impossible to discern from the property value whether the user submitted zero as a value or MVC model binding set the value of the property to zero as a result of invalid data. The ModelState.IsValid will be false, and the validation state of the individual fields along with the submitted values can be determined from the ModelState object, but this requires an additional step.

Robust required fields

The first part of making a robust required value type is, paradoxically, to make it a nullable. Adding ? to the standard type declaration, as shown in the sample code below, enables the property to hold a value of null in addition to a value within the numeric range for the value.

  • For a complete explanation of nullable types, see Nullable Types in Microsoft's C# Programming Guide.

Done by itself, making the property nullable would indicate to MVC that the HTML code associated with the <input> field should not include the required attribute. This would produce the opposite of the desired behavior for a required field. It would also permit null values when MVC processes the form data for the field in response to the HttpPost request.

How can the property and the field be made required if the type is nullable? By adding the Required data attribute. A couple of examples are shown below:

(code snippet)
[Range(0.01, 100000)]
[Required]
public decimal? Price { get; set; }

[Range(0, 52)]
[Required]
public ushort? CardsInDeck { get; set; }

It probably seems contradictory to make a property nullable and at the same time require it. The reason for building the field this way is that doing so enables:

  1. MVC model binding to differentiate invalid responses (out-of-range, wrong data type) from zero responses and

  2. the application's code can identify invalid or missing responses at the server from the property values without having to check the ModelState information for the field.

Importantly, these capabilities work at the server regardless of whether client-side validation has occurred before the data has been submitted. This helps secure the application against man-in-the-middle and cross-site-scripting attacks.

Non-required fields

Declaring value types and DateTime as nullable permits instances of a viewmodel to be initialized without having the initial value set to zero. This is highly desirable from a user interface perspective for optional fields, particularly when the range of permissible values specified by a Range data attribute do not include zero.3 It also prevents an initialized value of zero from being submitted when the form is submitted.

(code snippet)
[Range(0, 100)]
public decimal? Discount { get; set; }

[Range(0, 52)]
public ushort? CardsInDeck { get; set; }

Client-side and server-side validation

One of the important characteristics of this technique is that it produces markup that implements the same validation rules for jQuery unobtrusive validation to use on the client side as MVC uses on the server side when parsing the form data and creating the ModelState object. Because this technique is implemented with the declaration of viewmodel properties and their data attributes, it follows the recommended pattern for defining data validation rules that are implemented by MVC for both client-side and server-side validation.

Client-side validation will reduce the volume of invalid data arriving at the server, but it cannot be counted on to eliminate it. Server-side validation must also address all the security considerations related to client data validation, so validation happening in the browser can never substitute for server-side validation.

Handling invalid data

Methods for handling invalid data that's closely related to using robust numeric fields. There are a few steps to handling invalid data when an HttpPost controller action method receives a model object created by MVC model binding:

  1. Does the model exist? If not, MVC may have been unable to bind the form data to the model.

  2. Is ModelState.IsValid equal to true. If not, MVC has found discrepancies between the form data it received and the model rules expressed in the property declarations and their data attributes.

  3. What is the validation state of each property in the model? If a property is flagged as invalid, the form data does not meet the validation criteria for the property (or one of its child properties in the case of complex property types).

  4. Optionally, what is the value of RawValue for the model state of the property? The form data originally submitted for the field can be found in RawValue, regardless of the value set (or not) by MVC model binding. Reviewing the contents may help understand and handle the error.

The follow sample code from the case study projects includes simple code to access the values for each of these steps. In a production project the controller action method code could raise errors, redirect to another route, or redisplay the view depending on the business logic that governs the type of error involved.

ValidationController.cs
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using BlipValidate.Data.ViewModels;

namespace BlipValidate.Web.Controllers
{
    public class ValidationController : Controller
    {
        ...
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult CustomerAdd(CustomerAddViewModel model)
        {
            if (model == null) // Step 1: does the model exist?
            {
                throw new ArgumentNullException(nameof(model));
            }
            if (ModelState.IsValid) // Step 2: does the model meet the validation requirements?
            {
                return RedirectToAction("Index");
            }
            else
            {
                foreach(var k in ModelState.Keys)
                {
                    var v = ModelState[k];
                    foreach (var e in v.Errors) // Step 3: find the properties with errors.
                    {
                        // Step 4: examine the submitted data for invalid properties.
                        Debug.WriteLine($"Field: {k}, v.ValidationState: {v.ValidationState}, v.RawValue: {v.RawValue}, e.ErrorMessage: {e.ErrorMessage}");
                    }
                }
                model.EarliestAudit = DateTime.Parse("2017-04-11");
                model.LatestAudit = DateTime.Parse("2017-04-29");
                return View(model);
            }
        }
        ...
    }
}

The ModelState object provides a wealth of data about what went wrong when form data could not be converted to a viewmodel. All of it can be accessed by iterating through the Errors collection for each element of the ModelState collection.

Conclusion

Is this technique overkill or duplication of effort? Like many topics in software development, that's a debatable point.

There is no duplication of code, so it is not a violation of the "Don't repeat yourself" principle. The viewmodel properties and their data attributes have a different scope of competence than the ModelState object, even if there is some overlap in the information that can be discerned from their values. Perhaps it's best to think of robust numeric values as a "belt and suspenders" approach to an important requirement: client data validation.

This is a concise technique which makes it easy to create validation rules in a single place, the viewmodel, and evaluate them in a single place on the server, the HttpPost controller action method. It also provides for a better user experience by making the user interface of forms more comprehensible.

Additional resources

Check out the resources below for sample code, in-depth explanations, reference documentation, video courses, and helpful tools relating to the material covered in this article.

Case study project

A Visual Studio 2017 solution containing the complete code from which the examples shown above are drawn is available at:

github.com/ajsaulsberry/BlipValidate

The author disclaims any liability for the accuracy and completeness of the sample code and make no warranty respecting its suitability for any purpose. The sample code is subject to revision.

Web resources

See the the Disclaimer for important information regarding external resources.

Microsoft's ASP.NET Core documentation offers two thoroughly helpful sections on controller validation:

Model Binding - includes instructions for customizing model binding behavior.

Introduction to model validation in ASP.NET Core MVC - includes instructions for setting up client-side validation with dynamic (Ajax) forms.

Notes

1 Microsoft recommends the decimal data type for currency values and financial calculations: decimal (C# Reference).

2 MVC initializes numeric fields to zero even if the Range set for the property does not include zero.

3 When creating a model in response to an HttpGet request MVC will initialize a numeric property to zero even if the range specified with the Range data attribute does not include zero. Setting the initial value of the field in the user interface this way is doubly bad, since it implies zero is a valid value and also enables that value to be submitted if client-side data validation is not working.

Disclaimer

The information presented in this article is offered "as-is" without any warranty. The author disclaims all liability for the reliability, accuracy, and completeness of information presented by third-parties.