11

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?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Matze
  • 5,100
  • 6
  • 46
  • 69
  • Of course it's possible - just write your own attribute. You'll be able to generate any message you want... – Dawid Rutkowski Nov 24 '16 at 14:07
  • did you tried setting the threads culture to your language of preference? – Tseng Nov 24 '16 at 15:09
  • @Tseng Yes, but I would also like to change the wording... – Matze Nov 25 '16 at 10:31
  • @DawidRutkowski I would like to customize the functionality that transforms the annotations, rather than inventing custom annotations. – Matze Nov 25 '16 at 10:35
  • I think you can create a middleware to handle the errors and alter the error messages inside it. Not sure though. – alltej Nov 25 '16 at 14:28

5 Answers5

13

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());
})
jlchavez
  • 319
  • 4
  • 6
  • 1
    I'd like to point out that the RequiredAttribute check above also adds the attribute to nullable value types, which it generally should not. You can avoid that by adding the following to the if clause: && Nullable.GetUnderlyingType(context.Key.ModelType.GetTypeInfo()) == null – nforss Nov 06 '19 at 08:10
  • The condition `if (tAttr?.ErrorMessage == null && tAttr?.ErrorMessageResourceName == null)` is wrong, if `tAttr` is null, then it will be true. You can use `if (attribute is ValidationAttribute tAttr && tAttr.ErrorMessage == null && tAttr.ErrorMessageResourceName == null)` – Eric Mar 17 '21 at 22:57
  • Then you can also remove `tAttr.ErrorMessage = null;`. – Eric Mar 17 '21 at 23:47
  • Can anyone explain the `if` and the call to `ValidationMetadata.Add(new RequiredAttribute())` ? – drizin Mar 29 '23 at 22:33
  • 1
    @drizin If there is no RequiredAttribute applied to the property and this field is not a nullable value type, for example int?, it should have an implicit validation message. that would be localized. – jlchavez Apr 01 '23 at 06:16
4

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tseng
  • 61,549
  • 15
  • 193
  • 205
  • 2
    Okay, but what if I don´t want use resource files, but a database? Beside that, I would like to avoid additional arguments. – Matze Nov 25 '16 at 12:39
  • See the update,but still requires at least `ErrorMessage` and you lose the parameter name in the process. **MAYBE** it works if you leave the `{0}` or search for it,but I wouldn't bet on it or in the localized message in the DB. Only way you could avoid additional arguments is by using the original strings from https://github.com/dotnet/corefx/blob/v1.1.0/src/System.ComponentModel.Annotations/src/Resources/Strings.resx since it seems there are no localizations for it yet,but it may change in future and you'd have to parse all of the resx files to populate your DB keys with it for each language – Tseng Nov 25 '16 at 13:04
  • That being said, you can try using `RequiredAttribute_ValidationError` as key when configuring the database localization provider and return your desired string – Tseng Nov 25 '16 at 13:10
  • I dont think this works if the attribute is NotMapped. It seems to reconize its required but does not display the custom error message. aspnetcore 2.0 – Mike Nov 16 '18 at 16:03
  • @Mike: How does that relate? `NotMappedAttribute` is not a validation attribute (it inherits from `Attribute`, not `ValidationAttribute` – Tseng Nov 16 '18 at 17:19
  • adding this attribute causes the Validation Attrubute to not work properly. [NotMapped, Required(ErrorMessage = "Some cust error")] will show as the value '' is invalid. Just thought I would through it out there as a helpful note. – Mike Nov 18 '18 at 02:16
3

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;
            }
        }
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
T Brown
  • 1,385
  • 13
  • 9
1

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));
                })
kursat sonmez
  • 818
  • 1
  • 12
  • 22
0

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.

sjb-sjb
  • 1,112
  • 6
  • 14