Freigeben über


Defining Custom Client Validation Rules in Asp.net Core MVC

Editor’s note: The following post was written by Visual Studio and Development Technologies MVP Francesco Abbruzzese as part of our Technical Tuesday series with support from his technical editor Michele Aponte, who is an MVP in the same category.   

Asp.net core MVC is not only a multi-platform porting of classic Asp.net MVC. But it’s a complete rewrite and reengineering of the product, meant to increase performance and modularity. Several areas that evolved incrementally from MVC 1 to MVC 5 have been cleaned up and simplified into a homogeneous design, which is founded on a few fundamental concepts -  like Dependency Injection, Middleware Pipeline, and the new options framework. In this article, I’ll focus on client side validation and on how the developer may define his or her custom client validation rules.

The way Asp.net MVC handles client side validation has relied on unobtrusive validation since Asp.net MVc 3. All client validation rules for each input field are extracted from various sources, including validation attributes, and the type of property to be rendered and encoded in the content of Html5 data.

On the client side, input fields are parsed to extract these rules, with the help of rule specific JavaScript adapters that take care of instantiating adequate jQuery Validation Plugin validation rules. While in Asp.net Core MVc the key concepts and all JavaScript handling of client validation remained the same, its server side handling changed substantially to take advantage of the new Dependency Injection and Option frameworks. In this article, I will show in detail two procedures for providing YouTube URL client validation.

Where do client validation rules come from?

Client validation rules are rendered by implementing the IClientModelValidator interface. This exposes the single method AddValidation to create all Html5 data - attributes needed by the client validation rule. IClientModelValidator implementations are created by providers, which extract the validation rules from various sources. I will analyze two important sources: validation attributes, and property metadata.

We need an asp.net core application: pic1

Let’s call it CustomValidationdemo. Then select WebApplication and Individual User Accounts for the authentication.

Client validation with validation attributes

In order to test validation attributes, we need a YouTubeAttribute:

 using System;
using System.ComponentModel.DataAnnotations;

namespace CustomValidationDemo.Validation
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false,
        Inherited = true)]
    public class YouTubeAttribute: ValidationAttribute
{

        protected override ValidationResult IsValid(object value,
            ValidationContext validationContext)
        {
            return ValidationResult.Success;
         }
      }
}

Let’s add it to a newly created Validation folder. Since this article is about client validation, I’ve written no server side validation logic.

Now we need an IClientModelValidator implementation to associate with this attribute. Since validation, attributes are first class citizens. In Asp.net core we have the AttributeAdapterBase<T> class to inherit from:

 using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
namespace CustomValidationDemo.Validation
{
    public class YouTubeAttributeAdapter : AttributeAdapterBase
    {
       public YouTubeAttributeAdapter(YouTubeAttribute attribute,
           IStringLocalizer stringLocalizer)
           : base(attribute, stringLocalizer)
       {
       }
       public override void AddValidation(ClientModelValidationContext context)
       {
          MergeAttribute(context.Attributes, "data-val", "true");
          MergeAttribute(context.Attributes, "data-val-youtube",
                                GetErrorMessage(context));
       }
       public override string GetErrorMessage(ModelValidationContextBase validationContext)
       {
          return GetErrorMessage(validationContext.ModelMetadata,
           validationContext.ModelMetadata.GetDisplayName());
       }
    }
}

It is enough to merge the data- attributes into the dictionary contained in the context object passed to the AddValidation method. “data-val=true” selects all input fields that need some client validation, so it is rendered once for each input field, having at least once validation rule. The second data-val-* attribute specifies the name of the validation rule (in our case “youtube”) and provides the error message.

We don’t need more data- attributes since our validation rule has no parameters. Validation rules with parameters (as for instance the one associated with the RangeAttribute) add further attributes named like data-val-rulename-par1, data-val-rulename-par2, etc., that define both the parameter names and their values.

In the GetErrorMessage method, we extract the display name of the property to render in the input field from the property metadata, and pass it to the inner GetErrorMessage method inherited from AttributeAdapterBase<T>. This, in turn,  extracts the error message contained in our YouTubeAttribute (“{0} is not a valid YouTube Url”), localizes it with stringLocalizer (see https://docs.asp.net/en/latest/fundamentals/localization.html), and finally uses it as a format string whose unique “hole” is filled with the property display name.

A unique provider scans all ValidationAttributes and for each of them instantiates the appropriate IClientModelValidator. It implements the IValidationAttributeAdapterProvider interface. We need to substitute its default implementation with a custom provider that processes also our YouTubeAttribute.

 using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.Extensions.Localization;

namespace CustomValidationDemo.Validation
{
    public class CustomValidatiomAttributeAdapterProvider:
        IValidationAttributeAdapterProvider
    {
        IValidationAttributeAdapterProvider baseProvider = 
            new ValidationAttributeAdapterProvider();
        public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, 
            IStringLocalizer stringLocalizer)
        {
            if (attribute is YouTubeAttribute) return
                    new YouTubeAttributeAdapter(attribute as YouTubeAttribute, 
         stringLocalizer);
            else return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
}

Our implementation checks for the YouTubeAttribute and then calls the default implementation. Since the IValidationAttributeAdapterProvider is injected, we may install our provider easily by declaring it in the ConfigureServices section of the web site Startup class.

 services.AddMvc();

services.AddSingleton<IValidationAttributeAdapterProvider, 

    CustomValidatiomAttributeAdapterProvider>();

That’s all! We have completed the configuration on the server side.

Defining client validation rules on the client side

On the JavaScript side, we need a function that actually performs the validation and an adapter that, once invoked by the unobtrusive parser, adds this function to the validation list for the input field being processed.

  (function ($) {
    var youtubeUrl =
        new RegExp("(?:http(?:s?)\\:\\/\\/)?(?:www\\.)?youtu(?:be\\.com\\/(?:watch\\?v="
        + "|embed\\/|v\\/)|\\.be\\/)([\\w\\-\\_]*)(&(amp;)?‌[\\w\\?‌=]*)?");
    var $jQval = $.validator;

    $jQval.addMethod("youtube", function (value, element, params) {
        if (!value) return true;
        value = $.trim(value);
        if (!value) return true;
        var match = youtubeUrl.exec(value);
        return match && match.index === 0 && match.length > 1 && match[1];
    });
    
    var adapters = $jQval.unobtrusive.adapters;
    adapters.addBool("youtube");
})(jQuery);

addMethod defines our new validation rule called “youtube”. Our function receives both the value typed by the user and the input field. Our rule has no parameters, but in general, params contains rule parameters. Value is trimmed and then matched with a regular expression. We just check that the match starts at the beginning of the URL typed by the user, but we don’t pretend that the match covers the whole URL, thus allowing for extra query string parameters. Instead, we pretend match[1] has a value, since it represents the group containing the YouTube video Id that for sure we need on the server side to play somehow the video.

Since our rule has no parameter (boolean rule), we may create the adapter with no further coding effort, by simply calling addBool. For more information on how to create adapters for rules with parameters, please refer to the JavaScript part of this famous Brad Wilson article:

Testing the YouTube URL validation rule

To test the rule, define this simple ViewModel.

 using System.ComponentModel.DataAnnotations;
using CustomValidationDemo.Validation;
namespace CustomValidationDemo.Models
{
    public class YoutubeViewModel
    {
        //[DataType("YouTubeUrl")]
        [YouTube(ErrorMessage = "{0} is not a valid yotube url")]
        public string MyYoutube { get; set; }
    }
}

Then open the “Home\Index.cshtml” view, delete its content and substitute it with:

 @model YoutubeViewModel

@{ViewData["Title"] = "Custom validation demo";}
<h2>@ViewData["Title"]</h2>

<form method="post" class="form-horizontal">
    <div asp-validation-summary="All" class="text-danger"></div>
    <div class="form-group">
        <label asp-for="MyYoutube" class="col-md-2 control-label"></label>
        <div class="col-md-10">
            <input asp-for="MyYoutube" class="form-control" />
            <span asp-validation-for="MyYoutube" class="text-danger"></span>
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <button type="submit" class="btn btn-default">Submit</button>
        </div>
    </div>
</form>
@section Scripts {
    @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
    <script src="~/js/YoutubeValidation.js"></script>
}

Run and enjoy!

An alternative implementation based on type metadata

Client validation rules that check the right format for numbers don’t rely on validation attributes, but are inferred directly from the property type. We might do something similar with our YouTube validation rule, either by defining a YouTube data type, or by using simply the DataType attribute to declare that a string represents a YouTube URL.

This is nice, since once we decorate a property with [DataType("YouTubeUrl")] we can also provide adequate Display and Edit templates. For instance, a display template might simply play the video, and an Edit template might accept suggestions from one or more video sources. To carry out this plan we must implement the IClientModelValidator interface from the scratch.

  using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace CustomValidationDemo.Validation
{
    public class YouTubeModelValidator : IClientModelValidator
    {
        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-youtube",
                GetErrorMessage(context));
        }
        private static bool MergeAttribute(IDictionary
            attributes, string key, string value)
        {
            if (attributes.ContainsKey(key)) return false;
            attributes.Add(key, value);return true;
        }
        private string GetErrorMessage(ClientModelValidationContext context)
        {
            return string.Format("{0} is not a valid youtube url", 
                context.ModelMetadata.GetDisplayName());
        }
    }
}

This takes a little bit of extra work, since we must implement the MergeAttribute method. Moreover, we have no place to take the error message from. I wrote this in the code, but you may put it in a resource file in order to localize it. Then, we may implement the basic IClientModelValidator provider:

  using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace CustomValidationDemo.Validation
{
    public class YouTubeModelValidatorProvider : IClientModelValidatorProvider
    {
        public void CreateValidators(ClientValidatorProviderContext context)
        {
            if(context.ModelMetadata.ModelType == typeof(string) &&
                context.ModelMetadata.DataTypeName == "YouTubeUrl" && 
                !context.Results.Any(m => m.Validator is YouTubeModelValidator))
                context.Results.Add(new ClientValidatorItem
                {
                    Validator = new YouTubeModelValidator(),
                    IsReusable = true
                });

        }
    }

 

Our provider adds the validator to the validators list only if this list doesn’t contain another instance of the same type. This time, our validator will not substitute any other validator, but we will add it to a list of validators contained in the MVcViewOptions. Options are added/modified/configured using asp.net core options framework.

 services.AddMvc();

services.AddSingleton();

services.Configure(o =>
        o.ClientModelValidatorProviders.Add(new YouTubeModelValidatorProvider()));

For more information on the Asp.net core options framework, see https://docs.asp.net/en/latest/fundamentals/configuration.html. To test the new implementation go to the YouTubeViewModel, comment out the YouTubeAttribute and substitute it with [DataType("YouTubeUrl")].

Conclusion

In summary, client validation rules require the implementation of the IClientModelValidator interface. If the client rule comes from a validation attribute we may inherit from AttributeAdapterBase<T>, otherwise we have to implement it from the scratch. Client rules based on validation attributes are installed by substituting the default IValidationAttributeAdapterProvider and configuring the custom implementation in the ConfigureServices section of our application. If not, we need to implement the basic IClientModelValidatorProvider interface, and add it to the ClientModelValidatorProviders list contained in the MVcViewOptions. On the JavaScript side, we must provide a function that implements the actual validation logic and an adapter that takes care of attaching properly this validation logic to the input field, when it is invoked by the unobtrusive attributes parser.


screen-shot-2016-12-22-at-8-47-12-amFrancesco Abbruzzese implements Asp.net MVC applications, and has offered consultancy services since the beginning of this technology. He is the author of the famous MVC Controls Toolkit, and his company offers tools, UI controls, and services for Asp.net MVC. He's moved from decision support systems for banks and financial institutions, to the Video Games arena, and finally started his .Net adventure with the first .Net release.

Follow him on Twitter @F_Abbruzzese

Comments

  • Anonymous
    January 05, 2017
    Ottimo articolo!!!
  • Anonymous
    May 12, 2017
    It would be very handy for the code to be in a block so that it could be copy/pasted
    • Anonymous
      May 17, 2017
      Hi Oliver,We just switched out the code. Thanks!
  • Anonymous
    August 04, 2017
    Took me 1 hour to find merge attribute: https://github.com/aspnet/Mvc/issues/5392