Customization And Localization Of ASP.NET Core MVC Default Validation Error Messages

Editor's note: The following post was written by Visual Studio and Development Technologies MVP Francesco Abbruzzese   as part of our Technical Tuesday series. Ovais Mehboob of the MVP Award Blog Technical Committee served as the technical reviewer for this piece.  

The re-engineering job behind the new ASP.NET Core MVC sensibly improved the way validation messages are handled and customized. While default messages automatically displayed because of a user’s wrong inputs were stored and handled next to their sources in the previous ASP.NET MVC version, they have now all been moved to the same place -  the ASP.NET MVC option object.

Moreover, the new validation provider architecture made it easy to override the default messages built-in, in the .Net validation attributes with customized default messages defined in resource files. This way, the user can provide global custom messages once and for all, thus avoiding the burden of having to specify an error message in each occurrence, of each attribute.

This article goes into detail about how to customize both all ASP.NET MVC specific error messages, and all default messages coming from validation attributes.

Where do default error messages come from?

In general, default validation error messages are format strings with placeholders for various parameters. They come from 3 sources:

  1. Validation error messages are displayed when the model binder doesn’t find a value for an implicit “required” property, for a property whose value can’t be null (int, float, etc.). They also occur when the model binder is not able to transform an input into a .Net type, because the input is ill formatted - for instance, a numeric field containing letters, or an ill formatted date. Since the wrong format error is triggered by a failure of the model binder it is unique for all types. While the implicit required error has no parameters, the wrong format error has a placeholder for the field name and a placeholder for the ill formatted value. The model binder may produce other error messages, but since they should not be caused by user wrong inputs these won’t be discussed in this article.
  2. Client side Implicit “required” property, and wrong format error messages injected by client   validation providers into Html5 attributes of rendered input fields. These messages are used by  the client-side validation engine that performs the same verification of the model binder on the client side to return an immediate feedback to the user. The client side implicit required error message is actually produced by a “RequiredAttribute” instance  automatically associated to all non-nullable properties, so its features are covered in point 3 of this list. The wrong format error differs substantially from the analogous message of the model binder for the following reasons: Its placeholders are filled on the server side, so it can’t have a placeholder for the value. Secondly, it is not triggered by a conversion failure, since value conversion takes place on the server side by an explicit analysis of the string structure. Therefore, we have a different validation rule and a different error message for each different type. Currently in the 1.1 version, there is a unique built-in error message for ill formatted numbers. For more information on client validation, and on how to define more wrong format validation rules please refer to my previous article on client validation.
  3. Error messages are displayed when the user doesn’t provide an explicit error message in a validation attribute occurrence. For instance, when the user decorates a property simply with “[Required]” attribute and without specifying the error message inside that attribute, these messages are taken from resources contained inside .Net dlls, and can’t be changed. Therefore, if we want to provide a different default error message, we are forced to add automatically an explicit error message to all attribute instances that don’t have any error message already in place. As we will see later on in this article, this may be done by writing and installing an ad hoc validation provider.
    These error messages are used by both server side and client side validation engines. They have a placeholder for the field name, and sometimes also for some parameters specified in the validation attribute (for instance, minimum and maximum values in a “RangeAttribute”).

How to change wrong format and implicit required field validation messages

We need an Asp.net core application to describe and test all customization techniques:
pic_1

Let’s call it “CustomMessagesDemo”. Then select Web Application and no authentication. In order to test validation messages, we need a ViewModel, a test Controller and test View. Now, we add a “ViewModels” folder to our project. A ViewModel with a single decimal property is enough for our purposes; let’s call it “TestViewModel”:

 using System.ComponentModel.DataAnnotations;
 
namespace CustomMessagesDemo.ViewModels
{
    public class TestViewModel
    {
        [Range(1, 10)]
        public decimal ADecimal { get; set; }
    }
}

Now let’s open the “Controllers\HomeController.cs” file and substitute the “Index” action method with the code below:

 using CustomMessagesDemo.ViewModels;
using Microsoft.AspNetCore.Mvc;
 
namespace CustomMessagesDemo.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
        [HttpPost]
        public IActionResult Index(TestViewModel vm)
        {
            return View(vm);
        }

Finally, we’ll substitute the whole content of the “Views\Home\Index.cshtml” view with:

 @model CustomMessagesDemo.ViewModels.TestViewModel

@{
    ViewData["Title"] = "Custom default messages";

}

<h2>@ViewData["Title"]</h2>

<form asp-action="Index" asp-controller="Home">
    <div class="form-group">
        <label asp-for="ADecimal"/>
        <input type="text" asp-for="ADecimal" class="form-control">
        <span asp-validation-for="ADecimal" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-default">Submit</button>
</form>
@section Scripts {
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script 
 src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
}

We enforced a type text for our unique input in order to easily test ill formatted numbers, and send them to the server.

Now, let’s run the application, insert an ill formatted number, and try submitting the form. The message “The field ADecimal must be a number” appears. This is the client side default error message for ill formatted numbers. Note down this message, and try server side errors, too. We can do it by disabling client side validation in our test view as shown below:

 @model CustomMessagesDemo.ViewModels.TestViewModel
 
@{
    ViewData["Title"] = "Custom default messages";
    ViewContext.ClientValidationEnabled = true;
}

Submit the form - first with an ill formatted number, and then with an empty input field. Also note down the model binder ill formatted, and implicit required default error messages triggered by these submit actions.

In this first customization step, we’ll provide new default messages for the three errors analyzed above. Let’s define a new resource file in which we’ll store all our new error messages. We’ll place this in the root of our project and call it “SiteResources.resx.” Now let’s add all entries shown below:
pic_2

Now we must declare our new default message in the MVC option object. There are several ways to do this, but the simplest one is to pass a lambda function in the “services.AddMvc” instruction in the “startup.cs” file:

 public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);
 
    services.AddMvc(o =>
    {
        o.ModelBindingMessageProvider.ValueMustNotBeNullAccessor =
            value =>
            SiteResources.CustomImplicitRequired;
        o.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor =
            (value, fieldName) => 
            string.Format(SiteResources.CustomInvalidValue, value);
        o.ModelBindingMessageProvider.ValueMustBeANumberAccessor =
            fieldName => 
                string.Format(SiteResources.CustomNumeric, fieldName);
    });
}

The first two assignments redefine model-binder validation messages, while the third one redefines the client-side default error for ill formatted numbers. Here, we can’t change the client side implicit required error message, since it is the default error message of the “RequiredAttribute”. We will show how to change it, together with all validation attributes default error messages, in the next section.

We are ready to test our new messages. Let’s keep client validation disabled and trigger the two model binder validation errors again. Both of them changed! You may also test the new client side ill formatted number error message by enabling client side validation once again.

Customizing default error messages of validation attributes

First, define a name convention for the entries in the resource file. We can use the name of the validation attribute itself; so for instance, the name of the entry for the required attribute would be “RequiredAttribute.”.

Let’s add two more entries to our resource file, one for the required attribute, and the other one for the range attribute: pic_3

The plan is to loop through all validation attributes associated with each rendered property to add our custom messages. More specifically, whenever the attribute doesn’t have an error message already specified and there is an entry for that attribute in our resource file, we need to set its “ErrorMessageResourceType” property to the type associated to our resource file, and its “ErrorMessageResourceName” property to the name of the entry in the resource file.

In ASP. NET Core MVC, validation metadata are handled by validation metadata providers, which are implementations of “ IValidationMetadataProvider” and whose unique member is the method “CreateValidationMetadata”. The main purpose of validation metadata providers is to extract validation rules from property information, and metadata by other parts of the framework. However, we may also use them to modify and filter the validators added by other providers,  since the list of all validators added so far is contained in the context object passed to the “CreateValidationMetadata” provider method together with all input metadata.

So, the addition of our custom error messages to the validation attributes may be carried out in the “CreateValidationMetadata” method of a custom implementation of “IValidationMetadataProvider”:

 using System;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Resources;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
 
namespace CustomMessagesDemo.Validation
{
    public class CustomValidationMetadataProvider:IValidationMetadataProvider
    {
        private ResourceManager resourceManager; private Type resourceType;
        public CustomValidationMetadataProvider(string baseName, Type type)
        {
            resourceType = type;
            resourceManager = new ResourceManager(baseName, 
                type.GetTypeInfo().Assembly);
        }
        public void CreateValidationMetadata(
            ValidationMetadataProviderContext context)
        {
            if (context.Key.ModelType.GetTypeInfo().IsValueType &&
                context.ValidationMetadata.ValidatorMetadata
                    .Where(m => m.GetType() == typeof(RequiredAttribute)).Count()==0)
                context.ValidationMetadata.ValidatorMetadata.
                    Add(new RequiredAttribute());
            foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
            {
                ValidationAttribute tAttr = attribute as ValidationAttribute;
                if (tAttr != null && tAttr.ErrorMessage == null 
                        && tAttr.ErrorMessageResourceName == null)
                {
                    var name = tAttr.GetType().Name;
                    if (resourceManager.GetString(name) != null)
                    {
                        tAttr.ErrorMessageResourceType = resourceType;
                        tAttr.ErrorMessageResourceName = name;
                        tAttr.ErrorMessage = null;
                    }
                }
            }
        }
      }
}

Our implementation constructor receives all the information needed to create a resource manager for the resource file containing the custom messages. It then stores both the type associated to the resource file and the resource manager.

The first “if” adds a “RequiredAttribute” to the list of all validators - if not already in the list, and if the type is a value type, or one that is implicitly required.  Later on, other built-in providers that handle client validation would have performed the addition of this “RequiredAttribute” on value types, but we anticipate this operation here to have the possibility of adding our custom validation message.

The “foreach” loops through all validators added so far, and if they are validation attributes with no messages already specified, it tries to add both the type of the resource file passed in the constructor and proper entry.

According to the name convention previously established,  the entry name should match the type name of the attribute. We use the resource manager to verify if there was an entry for the current attribute, otherwise the addition of the not existing entry to the “ErrorMessageResourceName” property of the attribute would trigger a runtime exception.

Installing our provider is easy! We just need to add it to the list of all validation metadata providers contained in the MVC option object:

 services.AddMvc(o =>
    {
        o.ModelBindingMessageProvider.ValueMustNotBeNullAccessor =
            value =>
            SiteResources.CustomImplicitRequired;
        o.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor =
            (value, fieldName) => 
            string.Format(SiteResources.CustomInvalidValue, value);
        o.ModelBindingMessageProvider.ValueMustBeANumberAccessor =
            fieldName => 
                string.Format(SiteResources.CustomNumeric, fieldName);
        o.ModelMetadataDetailsProviders.Add(
            new CustomValidationMetadataProvider(
                "CustomMessagesDemo.SiteResources",
                typeof(SiteResources)));
 
    });
}

Now let verify that client validation is enabled. Run the project, and try submitting an empty input field: pic_4

Our custom message appears! Then try to submit an out of range number, say 11: pic_5

Again, our custom message appears!

Conclusion

To sum everything up, in the new core version of ASP.NET MVC, customizing default validation messages is quite easy. ASP.NET MVC specific messages are easily changed in the MVC options object, while all default messages coming from validation attributes may be customized by writing a few lines short implementation of “IValidationMetadataProvider”.


Francesco Abbruzzeseunnamed implements Asp.net MVC applications, and has offered consultancy services since the beginning of the 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