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?
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.
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.
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.
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.
Sample ValidationsMessages.es.resx
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());
})
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))]
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).
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;
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With