2

Adding client-side validation to ASP.NET Core, without jQuery or unobtrusive val...

 11 months ago
source link: https://andrewlock.net/adding-client-side-validation-to-aspnet-core-without-jquery-or-unobtrusive-validation/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Adding client-side validation to ASP.NET Core, without jQuery or unobtrusive validation

ASP.NET Core is heavily influenced by the legacy .NET Framework System.Web-based ASP.NET Framework, but it's fundamentally faster and more modern. Nevertheless, one feature made it's way essentially unchanged into ASP.NET Core: client-side validation.

In this post I look at an alternative to the default (supported) jQuery-based client-side validation. In this alternative approach we rely on a small JavaScript library called aspnet-client-validation instead.

Client-side validation in ASP.NET Core

I've been updating my book to .NET 7 recently, and one thing that still hasn't changed is how you do client-side validation in ASP.NET Core. The recommended/supported approach is all based around jQuery. In this section I'll show how client-side validation works currently. Feel free to skip ahead if you know all this!

So that we have a concrete example to work with, I'll create a very basic Razor Page, but this applies to "traditional" MVC in exactly the same way. The following PageModel contains a simple input model Person which we will bind to a form.

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

namespace MvcClientValidation.Pages;

public class TestModel : PageModel
{
    [BindProperty]
    public Person Input { get; set; }

    public ActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        TempData["Message"] = $"Hello {Input.Name} ({Input.Email})!";
        return RedirectToPage("Index");
    }

    public class Person
    {
        [Required]
        public string Name { get; set; }

        [Required]
        [EmailAddress]
        public string Email { get; set; }
    }
}

When submitted, the OnPost() handler verifies that validation of the Person model has passed successfully. If so, in this example we set some TempData with a message and redirect to another page (using the Post-Redirect-Get pattern).

If the model wasn't valid, we redisplay the form (which shows the validation error messages).

Image showing the validation form with errors for the Name and Email field

For completeness, the basic Razor form looks something like this:

@page
@model MvcClientValidation.Pages.TestModel

<div class="col-md-6 offset-md-3">
  <form method="post">
    <div class="form-floating mb-3">
      <input class="form-control" asp-for="Input.Name">
      <label asp-for="Input.Name"></label>
      <span class="text-danger field-validation-valid" asp-validation-for="Input.Name"></span>
    </div>
    <div class="form-floating mb-3">
      <input class="form-control" asp-for="Input.Email">
      <label asp-for="Input.Email"></label>
      <span class="text-danger field-validation-valid" asp-validation-for="Input.Email"></span>
    </div>
    <button type="submit" class="w-100 btn btn-lg btn-primary">Submit</button>
  </form>
</div>

One thing to note about the page above is that it currently doesn't use client-side validation. After completing the fields, the customer would hit submit, and it's only after the form is validated on the server and the response is sent that they get any feedback that something went wrong.

Using client-side validation improves this experience, giving immediate feedback. Instead of having to wait for the round-trip to the server, the customer is immediately notified about missing fields or errors. This is really table-stakes for any application on the web these days.

Note that you must always validate on the server, regardless of whether you have client-side validation. Client-side validation does not provide data integrity or security, only server-side validation can do that. Client-side validation is purely about user-experience.

All the default Razor Pages templates include a shared partial called _ValidationScriptsPartial.cshtml. This partial contains two script tags:

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

These work in conjunction with the jQuery script (typically added to all pages in _Layout.cshtml by default, but included above for demonstration purposes) to add client-side validation to your form. You can add the partial to the default "Scripts" Razor section by adding the following to the bottom of your Razor page:

@section Scripts{
    <partial name="_ValidationScriptsPartial" />
}

Now you magically have client-side validation! If the user fails to fill out the form correctly, they'll get instant feedback. If they click on Submit anyway, the form won't be sent to the server until they correct the errors.

Behind the scenes, this works because ASP.NET core emits a whole host of metadata onto the form elements it generates. For example, the form shown above looks something like this:

<div class="col-md-6 offset-md-3">
  <form method="post">
    <div class="form-floating mb-3">
      <input class="form-control" type="text" data-val="true" 
        data-val-required="The Name field is required." id="Input_Name" 
        name="Input.Name" value="">
      <label for="Input_Name">Name</label>
      <span class="text-danger field-validation-valid field-validation-valid"
        data-valmsg-for="Input.Name" data-valmsg-replace="true"></span>
    </div>
    <div class="form-floating mb-3">
      <input class="form-control" type="email" data-val="true" 
        data-val-email="The Email field is not a valid e-mail address." 
        data-val-required="The Email field is required." id="Input_Email" 
        name="Input.Email" value="">
      <label for="Input_Email">Email</label>
      <span class="text-danger field-validation-valid field-validation-valid" 
        data-valmsg-for="Input.Email" data-valmsg-replace="true"></span>
    </div>
    <button type="submit" class="w-100 btn btn-lg btn-primary">Submit</button>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8FJx1R5_kANFmBjoULQ...">
  </form>
</div>

In the HTML above you can see all the data-* attributes added by ASP.NET Core. The jQuery unobtrusive validation library looks for HTML elements with these attributes, and attaches event handlers. These handlers check the input values, and use the details in the attributes to generate the appropriate warnings and messages. And in that way, you get the same validation on the client side as you will ultimately get on the server side.

This all works out of the box with the built-in [DataAnnotations] attributes. If you're using FluentValidation or some other validation framework, then you may need extra steps to enable client-side validation. For FluentValidation, you must install FluentValidation.AspNetCore for example.

So if that all works, what's the problem? Does it matter that this solution uses jQuery?

So what's wrong with jQuery?

ASP.NET Core's default client-side validation experience uses a script from Microsoft called jQuery Unobtrusive Validation, which in turn uses the jQuery validation plugin. All of this (obviously) requires jQuery.

When you add all that up, you're looking at over 100kB (after GZIP) of JavaScript. That's… quite a lot. It's not necessarily being downloaded to the browser every time thanks to caching, but nevertheless, that's all JavaScript that the browser needs to parse and run for every single page.

jQuery is a historically important library, and excelled at smoothing out differences between browsers. However, these days, browsers are much better at following the same standards. That, coupled with the chromium monoculture mean that jQuery's main selling point just isn't as relevant these days. "You might not need jQuery" is a great site that shows the modern browser API equivalent of much of jQuery's functionality.

So if jQuery isn't as necessary these days, and takes up a lot of bandwidth, wouldn't it be nice if we could you remove it?

"But wait!" I hear you cry, "doesn't Bootstrap require jQuery?" Well, it depends. The current ASP.NET Core templates use Bootstrap 5 which is explicitly designed to not need jQuery. If you're still using Bootstrap 4, you'll need jQuery either way, but if you're using Bootstrap 5 or Tailwind (for example) then jQuery is just unnecessary extra bytes.

But nevertheless, ASP.NET Core's default client-side validation experience requires jQuery. So we're stuck with it, right?

But what's the alternative?

Well, funnily enough, there have been standard ways for performing client-side validation in browsers for years. HTML 5 introduced the "Constraints Validation API". This adds validation to form elements based on the type of the element, as well as other standard properties. For example, you may well have seen the following standard "invalid email" popup in your daily web browsing:

A form showing a standard validation popup about an invalid email

This validation message was generated by the Constraints Validation API. In this case it was triggered by the fact the input element has the attribute type="email".

If you're wondering why don't see on your Razor Pages applications that use jQuery unobtrusive validation, it's because that plugin adds the novalidate attribute to the <form> tag.

Unlike some browser APIs, the constraints validation API has excellent support. With the exception of Opera Mini, constraints are supported in basically every browser. Even IE 10 has (partial) support for it! 😮 Setting IE and Opera Mini aside, if you're using a browser updated in the last 5 years, you're going to be ok:

The caniuse table for the Constraint Validation API

So if the browser already has built in support for validation, can we use it in ASP.NET Core and get rid of jQuery?

Unfortunately, the answer is: not easily. The constraints validation relies on specific attributes being present, instead of the data-* attributes that ASP.NET Core adds by default. It's also not generally possible to style the errors produced by the browser. So using the Constraints API isn't an easy option unfortunately.

But all is not lost. What if we could leave the Constraints API, and instead purely replace the jQuery unobtrusive validation code.

An entirely different approach to client-side validation uses AJAX to execute your full server-side rules, using a library such as FormHelper. This avoids the need to write custom JavaScript rules, and may give a more consistent experience for users in some cases.

Introducing aspnet-client-validation

This post was inspired by a tweet from the legendary Phil Haack:

If you use aspnetcore or aspnetmvc without jquery, you should check out https://t.co/EMB8LZZWgg for client validation.

— https://hachyderm.io/@haacked (@haacked) March 21, 2023

The aspnet-client-validation library he's pointing to is a tiny (4kB GZIP) library is essentially a simple reimplementation of the jQuery unobtrusive validation script, simplified, and without any dependencies.

Note that aspnet-client-validation doesn't support IE.

You can easily try out the library by replacing the following <script> tags:

  • ~/lib/jquery/dist/jquery.min.js (from _Layout.cshtml)
  • ~/lib/jquery-validation/dist/jquery.validate.min.js (from _ValidationScriptsPartial.cshtml)
  • ~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js (from _ValidationScriptsPartial.cshtml)

Remember jQuery is required if you're using Bootstrap 4 (or earlier), but if you're using Bootstrap 5 (as in the default .NET 7 ASP.NET Core templates) then jQuery is entirely optional and is only required for adding client-side validation.

Once you've removed those files, you can add a reference to aspnet-validation.min.js inside of _ValidationScriptsPartial.cshtml. The example below also includes the required call to the bootstrap() method.

<script src="https://www.unpkg.com/aspnet-client-validation/dist/aspnet-validation.min.js" crossorigin="anonymous" integrity="sha384-4E0R5+D480pDlcgfpDw2NZDUAX0YsK6J4Zk6o4raasSXMaAlatx9tyNxkFBESu6C"></script>
<script>
    const v  = new aspnetValidation.ValidationService()
    v.bootstrap()
</script>

The above example references the JavaScript file directly from the unpkg CDN but you could alternatively reference the file locally in a <script> tag, or incorporate it into your bundles using ES Modules or Common JS, whichever approach you prefer.

The example shown above downloads the aspnet-client-validation script and then runs the bootstrap code. ValidationService.bootstrap() loads the default validation providers, and sets up a DOM listener that scans the HTML source looking for validation attributes.

The upshot is that everything just works. You get client-side validation that runs automatically, and essentially gives the same experience as you previously got with the jQuery-based experience.

aspnet-client-validation running automatically on a form

So with a simple change to _ValidationScriptsPartial.cshtml, you can ditch jQuery, and significantly reduce the payload size of your application. In the next section we'll extend the aspnet-client-validation library to show that it's as flexible as the jQuery equivalent

Creating a custom validation provider with aspnet-client-validation

ASP.NET Core comes with support for the default built-in validation attributes in .NET's DataAnnotations library, which gives support for things like Email, Required fields, minimum length, ranges, and Regex. These attributes cover a lot of common cases, but it's common to need to write additional custom validation rules. There are several ways to do this, but one approach is to create a custom validation attribute.

Creating a custom ValidationAttribute

To give a concrete example, lets imagine you want to restrict the allowed emails to only those that end with @mycompany.com. There's no built-in "endswith" validation attribute, but it's general enough that you might consider implementing it as a validation attribute. A simplistic implementation could look something like this:

public class EndsWithValidationAttribute : ValidationAttribute
{
    private readonly string _endsWith;
    public EndsWithValidationAttribute(string endsWith)
    {
        _endsWith = endsWith;
    }

    public override string FormatErrorMessage(string name)
        => $"The field {name} must end with '{_endsWith}'";

    public override bool IsValid(object? value)
    {
        if(value is null)
        {
            return true; // Allow null values so it works with Required attribute
        }

        return value is string s&& s.EndsWith(_endsWith);
    }
}

And would be added to the model as

public class Person
{
    [Required]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    [EndsWithValidation("@mycompany.com")] // 👈 Add this
    public string Email { get; set; }
}

When you submit the form, the validation runs on the server, and flags incorrect email addresses:

Running the validation on the server side.

By default, this validation only runs on the server after the data is posted, even if you have enabled client-side validation, so the feedback to the user is very slow. To enable client-side validation you need to:

  1. Emit data-* attributes that the client-side library can hook into.
  2. Add a custom provider to the client-side library.

Step 1. is the same whether you're using the default jQuery unobtrusive validation or aspnet-client-validation library. There's a couple of ways to do it, but for the purposes of this post, I'll implement IClientModelValidator on the validation attribute.

Adding data- attributes using IClientModelValidator

Implementing IClientModelValidator requires implementing a single method, AddValidation() which is responsible for adding the necessary data-* attributes. For our validation attribute, we need to add three attributes:

  • data-val="true". The client-side libraries use this attribute to understand that the field has validation requirements.
  • data-val-endswith. This attribute contains the error message that should be displayed when the validation fails.
  • data-val-endswith-value. This attribute contains the string that the client-side library should look for.

The -endswith portion is unique to our specific validator implementation, but it can ostensibly be anything. It just needs to be a known value that the client-side library can hook into. You can create a custom library for the jQuery unobtrusive validation library as described in the docs. We'll create a similar hook for the aspnet-client-validation library.

                                                             // 👇 Extra interface
public class EndsWithValidationAttribute : ValidationAttribute, IClientModelValidator
{
    private readonly string _endsWith;
    public EndsWithValidationAttribute(string endsWith)
    {
        _endsWith = endsWith;
    }

    // 👇 Add this method to add the attributes
    public void AddValidation(ClientModelValidationContext context)
    {
        var errorMessage = FormatErrorMessage(context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name);
        context.Attributes.TryAdd("data-val", "true");
        context.Attributes.TryAdd("data-val-endswith", errorMessage);
        context.Attributes.TryAdd("data-val-endswith-value", _endsWith);
    }

    public override string FormatErrorMessage(string name)
        => $"The field {name} must end with '{_endsWith}'";

    public override bool IsValid(object? value)
    {
        if(value is null)
        {
            return true; // Allow null values so it works with Required attribute
        }

        return value is string s&& s.EndsWith(_endsWith);
    }
}

You can see the result of the AddValidation call in the generated HTML, containing the two additional data-val-endswith* attributes:

<input class="form-control input-validation-error" type="email" 
    id="Input_Email" name="Input.Email" value="[email protected]"
    data-val="true" data-val-required="The Email field is required." 
    data-val-email="The Email field is not a valid e-mail address." 
    data-val-endswith="The field Email must end with '@mycompany.com'" 
    data-val-endswith-value="@mycompany.com">

Now we have the attributes, we need to hook up the aspnet-client-validation provider to use them.

Creating a custom provider for aspnet-client-validation

You can handle custom validation requirements in aspnet-client-validation by creating a new provider. To add a new provider, call ValidationService.addProvider(), passing in a provider name (endswith in our case) and pass a callback function that performs the validation. This callback has three parameters:

  • value: the value of the input element
  • elemet: the DOM element being validated
  • params: additional values passed via extra data-* attributes. So you can access the data-val-endswith-value by calling params.value. Alternatively, you could access data-val-endswith-somethingelse by calling params.somethingelse.

For our simple validation function, we can add a new provider as follows:

<script src="https://www.unpkg.com/aspnet-client-validation/dist/aspnet-validation.min.js"></script>
<script>
  const v  = new aspnetValidation.ValidationService()
  // 👇 Call addProvider() _before_ calling bootstrap()
  v.addProvider('endswith', (value, element, params) => {
    if (!value) {
        // Let [Required] handle validation error for empty input...
        return true;
    }

    return value.endsWith(params.value);
  });
  v.bootstrap();
</script>

And with that, we have custom client-side validation without jQuery!🎉

Running the validation on the client side.

The future of validation in ASP.NET Core

I haven't used aspnet-client-validation in a real life project, but from what I've seen, I don't really see a reason to stick with the jQuery unobtrusive validation scripts (unless you already have an investment there!) The aspnet-client-validation approach seems to fill the exact same hole, all with less complexity and with a much smaller download size.

The big question in my mind was why isn't this the default? With the jQuery dependency removed, there would be no good reason to keep it in the default templates. It feels like aspnet-client-validation would be a simpler and easier recommendation for users.

Well the good news is there's an issue in the ASP.NET Core repo covering exactly this. The bad news is that was created 4 years ago, and appears to be stuck in limbo 😢

https://github.com/dotnet/aspnetcore/issues/8573

So what's the hold up? Why hasn't it gone anywhere?

You can find what appears to be the root of the issue in a comment from 2021

The initial thought was that we could do something tactical building upon either browser features such as constraint validation or hand rolling a facsimile of jQuery.validation. But on further inspection, we realize that an alternative that simply removes jquery does not move the needle enough for us invest in.

There's a lot more detail in the comment, but the crux of the issue feels like they just don't want to ship a new JavaScript library. What started off as a simple "remove jQuery" request switched into a more grandiose "re-do how validation works in MVC", and subsequently stalled.

I can totally understand this sentiment; the constraints API seems like a potentially great, modern, option. If a solution using it was on the horizon, it would definitely be worth delaying and investing in it.

But in my opinion, this is short sighted. We've been waiting 4 years for that point. Is it every going to come? Wouldn't it be better to ship something now that goes part way there?

Remember, currently, the de facto, "blessed" way to do client-side validation is to rely on jQuery. In 2023. ASP.NET Core is a fast, modern web framework. Would you expect it to require jQuery? 😳 It's embarrassing.

Of course, I get it. Blazor is the focus of the team now, and it doesn't have the same issues. But MVC/Razor Pages are still very much supported ways to build applications.

So can we please just replace jQuery with aspnet-client-validation and be done with it😅

Summary

In this post I described how client validation currently works in ASP.NET Core, using jQuery and the unobtrusive validation plugin. I then described an alternative library by Phil Haack that performs the same function, called aspnet-client-validation. This library is just 4kB GZipped, and has no dependencies.

After showing how to use the library, I showcased it's compatibility with the existing unobtrusive validation library by creating a custom validation attribute, and showed how you can hook this up to the aspnet-client-validation library. Finally, I discussed the future of client-side-validation in ASP.NET Core.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK