Matze
Matze

Reputation: 5508

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?

Upvotes: 12

Views: 20793

Answers (6)

Lázár Zsolt
Lázár Zsolt

Reputation: 833

Although not officially supported and a bit hacky, the following solution will let you supply your own resource class for the built-in validation framework:

public static class ValidationLocalizationUtils
{
    public static void SetValidationLocalizationType<T>()
    {
        var assembly = typeof(ValidationContext).Assembly;
        var resourcesType = assembly.GetType("System.SR")!;
        var resourcesField = resourcesType.GetField("s_resourceManager", 
            BindingFlags.NonPublic | BindingFlags.Static)!;
        var resourceManagerField = typeof(T).GetProperty(nameof(ResourceManager), 
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!;
        
        resourcesField.SetValue(null, resourceManagerField.GetValue(null));
    }
}

This method will override the internal ResourceManager field used by the framework to resolve validation error messages. All you need to do is provide localized values for the keys found in the official resource file.

Upvotes: -1

kursat sonmez
kursat sonmez

Reputation: 908

Thanks for jlchavez's answer, his answer worked for me but I had to make a small correction. In jlchavez's reply there is a message for each validation attribute. But there can also be multiple messages for an attribute so I updated the code as follows:

    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 != null && tAttr.ErrorMessage == null && tAttr.ErrorMessageResourceName == null)
            {
                string defaultErrMessage = tAttr.GetType().BaseType
                    .GetProperty("ErrorMessageString", BindingFlags.NonPublic | BindingFlags.Instance)
                    ?.GetValue(tAttr)?.ToString();
                
                if (string.IsNullOrEmpty(defaultErrMessage))
                    continue;

                //var name = tAttr.GetType().Name;
                if (resourceManager.GetString(defaultErrMessage) != null)
                    tAttr.ErrorMessage = defaultErrMessage;
            }
        }
    }

With this change, the following setting should also be made:

.AddDataAnnotationsLocalization(options =>
                {
                    options.DataAnnotationLocalizerProvider = (type, factory) =>
                    factory.Create(typeof(AppLocales.Modules._Common.ValidationLocale));
                })

Upvotes: 2

T Brown
T Brown

Reputation: 1533

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;
            }
        }
    }
}

Upvotes: 3

Tseng
Tseng

Reputation: 64150

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).

Upvotes: 6

jlchavez
jlchavez

Reputation: 329

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());
})

Upvotes: 14

sjb-sjb
sjb-sjb

Reputation: 1187

I encountered the same problem and the solution I used was to create a subclass of the validation attribute to provide the localized error message.

To prevent programmers from accidentally using the non-localized version, I just left out the using statement for the non-localized library.

Upvotes: 0

Related Questions