Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to provide localized validation messages for validation attributes

I am working on an ASP.NET Core application and I would like to override the default validation error messages for data-annotations, like Required, MinLength, MaxLength, etc. I read the documentation at Globalization and localization in ASP.NET Core, and it seems that it does not cover what I was looking for...

For instance, a validation error message for the Required attribute can always be the same for any model property. The default text just states: The {0} field is required, whereby the {0} placeholder will be filled up with the property’s display name.

In my view models, I use the Required attribute without any named arguments, like this...

class ViewModel
{
    [Required, MinLength(10)]
    public string RequiredProperty { get; set; }
}

Setting an ErrorMessage or ErrorMessageResourceName (and ErrorMessageResourceType) is unnecessary overhead, in my opinion. I thought I could implement something similar to IDisplayMetadataProvider allowing me to return error messages for applied attributes, in case the validation has failed. Is this possible?

like image 335
Matze Avatar asked Nov 24 '16 13:11

Matze


People also ask

How do I create a custom validation attribute?

To create a custom validation attributeUnder Add New Item, click Class. In the Name box, enter the name of the custom validation attribute class. You can use any name that is not already being used. For example, you can enter the name CustomAttribute.

What are validation attributes?

Validation attributes let you specify validation rules for model properties. The following example from the sample app shows a model class that is annotated with validation attributes. The [ClassicMovie] attribute is a custom validation attribute and the others are built in.

How to add validation in ASP net Core mvc?

Add validation rules to the movie model DataAnnotations also contains formatting attributes like DataType that help with formatting and don't provide any validation. Update the Movie class to take advantage of the built-in Required , StringLength , RegularExpression , and Range validation attributes.


3 Answers

For those that end up here, in search of a general solution, the best way to solve it is using a Validation Metadata Provider. I based my solution on this article: AspNetCore MVC Error Message, I usted the .net framework style localization, and simplified it to use the designed provider.

  1. Add a Resource file for example ValidationsMessages.resx to your project, and set the Access Modifier as Internal or Public, so that the code behind is generated, that will provide you with the ResourceManager static instance.
  2. Add a custom localization for each language ValidationsMessages.es.resx. Remember NOT to set Access Modifier for this files, the code is created on step 1.
  3. Add an implementation of IValidationMetadataProvider
  4. Add the localizations based on the Attributes Type Name like "RequiredAtrribute".
  5. Setup your app on the Startup file.

Sample ValidationsMessages.es.resx

enter image description here

Sample for IValidatioMetadaProvider:

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

public class LocalizedValidationMetadataProvider : IValidationMetadataProvider
{
    public LocalizedValidationMetadataProvider()
    {
    }

    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        if (context.Key.ModelType.GetTypeInfo().IsValueType && Nullable.GetUnderlyingType(context.Key.ModelType.GetTypeInfo()) == null && context.ValidationMetadata.ValidatorMetadata.Where(m => m.GetType() == typeof(RequiredAttribute)).Count() == 0)
            context.ValidationMetadata.ValidatorMetadata.Add(new RequiredAttribute());
        foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
        {
            var tAttr = attribute as ValidationAttribute;
            if (tAttr?.ErrorMessage == null && tAttr?.ErrorMessageResourceName == null)
            {
                var name = tAttr.GetType().Name;
                if (Resources.ValidationsMessages.ResourceManager.GetString(name) != null)
                {
                    tAttr.ErrorMessageResourceType = typeof(Resources.ValidationsMessages);
                    tAttr.ErrorMessageResourceName = name;
                    tAttr.ErrorMessage = null;
                }
            }
        }
    }
}

Add the provider to the ConfigureServices method on the Startup class:

services.AddMvc(options =>
{
     options.ModelMetadataDetailsProviders.Add(new LocalizedValidationMetadataProvider());
})
like image 68
jlchavez Avatar answered Sep 30 '22 13:09

jlchavez


If you want to change the complete text, you should use resource files to localize it.

Every ValidationAttribute has properties for ErrorMessageResourceType and ErrorMessageResourceName (see source here).

[Required(ErrorMessageResourceName = "BoxLengthRequired", ErrorMessageResourceType = typeof(SharedResource))]

Update

Okay, there seems to be a way to use the localization provider to localize it, but it's still a bit hacky and requires at least one property on the attribute (from this blog post - Word of warning though, it was initially for an old RC1 or RC2 version. It should work, but some of the API in that article may not work):

In startup:

services.AddMvc()
   .AddViewLocalization()
   .AddDataAnnotationsLocalization();

On your model:

[Required(ErrorMessage = "ViewModelPropertyRequired"), MinLength(10, ErrorMessage = "ViewModelMinLength")]
public string RequiredProperty { get; set; }

and implement/use an localization provider that uses DB (i.e. https://github.com/damienbod/AspNet5Localization).

like image 37
Tseng Avatar answered Sep 30 '22 12:09

Tseng


So, I landed here because of creating my own custom IStringLocalizer and wanted to share my solution because @jlchavez helped me out.

I created a MongoDB IStringLocalizer and wanted to use the resources via the DataAnnotations. Problem is that DataAnnotations Attributes expect localizations via a static class exposing the resources.

One enhancement over jlchavez's answer is that this will fix the resource messages for all ValidationAttribute(s)

services.AddTransient<IValidationMetadataProvider, Models.LocalizedValidationMetadataProvider>();
services.AddOptions<MvcOptions>()
    .Configure<IValidationMetadataProvider>((options, provider) =>
    {
        options.ModelMetadataDetailsProviders.Add(provider);
    });


public class Resource
{
    public string Id => Culture + "." + Name;
    public string Culture { get; set; }
    public string Name { get; set; }
    public string Text { get; set; }
}

public class MongoLocalizerFactory : IStringLocalizerFactory
{
    private readonly IMongoCollection<Resource> _resources;

    public MongoLocalizerFactory(IMongoCollection<Resource> resources)
    {
        _resources = resources;
    }

    public IStringLocalizer Create(Type resourceSource)
    {
        return new MongoLocalizer(_resources);
    }

    public IStringLocalizer Create(string baseName, string location)
    {
        return new MongoLocalizer(_resources);
    }
}

public class MongoLocalizer : IStringLocalizer
{
    private readonly IMongoCollection<Resource> _resources;

    public MongoLocalizer(IMongoCollection<Resource> resources)
    {
        _resources = resources;
    }

    public LocalizedString this[string name]
    {
        get
        {
            var value = GetString(name);
            return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
        }
    }

    public LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            var format = GetString(name);
            var value = string.Format(format ?? name, arguments);
            return new LocalizedString(name, value, resourceNotFound: format == null);
        }
    }

    public IStringLocalizer WithCulture(CultureInfo culture)
    {
        CultureInfo.DefaultThreadCurrentCulture = culture;

        return new MongoLocalizer(_resources);
    }

    public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
    {
        var resources = _resources.Find(r => r.Culture == CultureInfo.CurrentCulture.Parent.Name).ToList();
        return resources.Select(r => new LocalizedString(r.Name, r.Text, false));
    }

    private string GetString(string name)
    {
        var resource = _resources.Find(r => r.Culture == CultureInfo.CurrentCulture.Parent.Name && r.Name == name).SingleOrDefault();
        if (resource != null)
        {
            return new LocalizedString(resource.Name, resource.Text, false);
        }
        return new LocalizedString(name, name, true);
    }
}

public class LocalizedValidationMetadataProvider : IValidationMetadataProvider
{
    private IStringLocalizer _localizer;

    public LocalizedValidationMetadataProvider(IStringLocalizer localizer)
    {
        _localizer = localizer;
    }

    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        foreach(var metadata in context.ValidationMetadata.ValidatorMetadata)
        {
            if (metadata is ValidationAttribute attribute)
            {
                attribute.ErrorMessage = _localizer[attribute.ErrorMessage].Value;
            }
        }
    }
}
like image 36
T Brown Avatar answered Sep 30 '22 11:09

T Brown