Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core Model Binding Error Messages Localization

I'm using ASP.NET Core, and trying to localize the application. I managed to use new asp .net core resources to localize controllers and views, and old resources to localize error messages for model validation. However, when the error message is not linked to a model field annotation (like "Required") and the data for model binding is incorrect (like a text where a number is expected), I receive the error like below, which I'm unable to localize:

"The value 'abc' is not valid for ID."

When I enter abc for ID property in View, since the model binding can not be done to the field and it shows a validation message near the field, saying "The value 'abc' is not valid for ID.". Here is the class I'm using:

public class Country : IHasID
{
    public int ID { get; set; }

    [Required(ErrorMessageResourceType = typeof(L.Val),
    ErrorMessageResourceName = "NameR")]
    [MaxLength(100, ErrorMessageResourceType = typeof(L.Val), 
    ErrorMessageResourceName = "Max")]
    public string Name { get; set; }

    /*Some other properties*/
}

The similar issues I found on the internet were either targeted to older asp .net version, or else didn't help me solve the problem.

like image 350
Marko Avatar asked Nov 27 '16 11:11

Marko


2 Answers

To customize framework model binding error messages, you need to set custom accessors for different error message accessors of ModelBindingMessageProvider.

Example

Here you can download a full source code of what is described in this post. The repository contains example for ASP.NET Core 2.0 (VS 2017.3) and ASP.NET Core 1.1 (VS 2015):

  • r-aghaei/AspNetCoreLocalizationSample

Also here you can see the example, live:

  • aspnetcorelocalizationsample.azurewebsites.net

Default Error Messages

These are default error messages which the framework shows when model binding to a property fails:

MissingBindRequiredValueAccessor    A value for the '{0}' property was not provided.
MissingKeyOrValueAccessor           A value is required.
ValueMustNotBeNullAccessor          The value '{0}' is invalid. 
AttemptedValueIsInvalidAccessor     The value '{0}' is not valid for {1}.
UnknownValueIsInvalidAccessor       The supplied value is invalid for {0}.
ValueIsInvalidAccessor              The value '{0}' is invalid.
ValueMustBeANumberAccessor          The field {0} must be a number.

In addition to above messages, ASP.NET Core 2.0 have these messages as well:

MissingRequestBodyRequiredValueAccessor       A non-empty request body is required.
NonPropertyAttemptedValueIsInvalidAccessor    The value '{0}' is not valid.
NonPropertyUnknownValueIsInvalidAccessor      The supplied value is invalid.
NonPropertyValueMustBeANumberAccessor         The field must be a number.

Localize ASP.NET Core Model Binding Error Messages

To localize ASP.NET Core model binding error messages, follow these steps:

  1. Create Resource File - Create a resource file under Resources folder in your solution and name the file ModelBindingMessages.fa.resx. The name can be anything else but we will use it to create a localizer. In the example, I used fa (Persian) culture.

  2. Add Resource Keys - Open the resource file and add keys and values which you want to use for localizing error messages. I used keys and values like below image:

    enter image description here

    Keys which I used are like original messages, except the key for ValueMustNotBeNull which was the same as ValueIsInvalid, so I used Null value is invalid. for it.

  3. Configure Options - In ConfigureServices method, when adding Mvc, configure its options to set message accessors for ModelBindingMessageProvider:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization(options => { options.ResourcesPath = "Resources"; });
        services.AddMvc(options =>
        {
            var F = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
            var L = F.Create("ModelBindingMessages", "AspNetCoreLocalizationSample");
            options.ModelBindingMessageProvider.ValueIsInvalidAccessor =
                (x) => L["The value '{0}' is invalid.", x];
            options.ModelBindingMessageProvider.ValueMustBeANumberAccessor =
                (x) => L["The field {0} must be a number.", x];
            options.ModelBindingMessageProvider.MissingBindRequiredValueAccessor =
                (x) => L["A value for the '{0}' property was not provided.", x];
            options.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor =
                (x, y) => L["The value '{0}' is not valid for {1}.", x, y];
            options.ModelBindingMessageProvider.MissingKeyOrValueAccessor =
                () => L["A value is required."];
            options.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor =
                (x) => L["The supplied value is invalid for {0}.", x];
            options.ModelBindingMessageProvider.ValueMustNotBeNullAccessor =
                (x) => L["Null value is invalid.", x];
        })
        .AddDataAnnotationsLocalization()
        .AddViewLocalization();
        services.Configure<RequestLocalizationOptions>(options =>
        {
            var supportedCultures = new[]{new CultureInfo("en"), new CultureInfo("fa")};
            options.DefaultRequestCulture = new RequestCulture("en", "en");
            options.SupportedCultures = supportedCultures;
            options.SupportedUICultures = supportedCultures;
        });
    }
    

    Also add this code at beginning of Configure method:

    var supportedCultures = new[] { new CultureInfo("en"), new CultureInfo("fa") };
    app.UseRequestLocalization(new RequestLocalizationOptions()
    {
        DefaultRequestCulture = new RequestCulture(new CultureInfo("en")),
        SupportedCultures = supportedCultures,
        SupportedUICultures = supportedCultures
    });
    

Important Note for ASP.NET Core 2.0

In ASP.NET Core 2.0, model binding message provider properties has got read only, but a setter method for each property has been added.

For example, to set ValueIsInvalidAccessor, you should use SetValueIsInvalidAccessor() method this way:

options.ModelBindingMessageProvider.SetValueIsInvalidAccessor (
    (x) => L["The value '{0}' is invalid.", x]);
like image 162
Reza Aghaei Avatar answered Nov 02 '22 15:11

Reza Aghaei


With reference to this post that describes in detail the side effects for using BuildServiceProvider inside ConfigureServices, and to this answer regarding resolving services inside ConfigureServices, last but not least, considering the refered improved answer by Andrew Lock, the correct approach to localize model binding error messages should be by creating a custom configuration class that implements IConfigureOptions<T> then registering it in startup as below:

public class ConfigureModelBindingLocalization : IConfigureOptions<MvcOptions>
{
    private readonly IServiceScopeFactory _serviceFactory;
    public ConfigureModelBindingLocalization(IServiceScopeFactory serviceFactory)
    {
        _serviceFactory = serviceFactory;
    }

    public void Configure(MvcOptions options)
    {
        using(var scope = _serviceFactory.CreateScope())
        {
            var provider = scope.ServiceProvider;
            var localizer = provider.GetRequiredService<IStringLocalizer>();

            options.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor((x, y) => 
                localizer["The value '{0}' is not valid for {1}.", x, y]);

            options.ModelBindingMessageProvider.SetMissingBindRequiredValueAccessor((x) => 
                localizer["A value for the '{0}' parameter or property was not provided.", x]);

            options.ModelBindingMessageProvider.SetMissingKeyOrValueAccessor(() => 
                localizer["A value is required."]);

           options.ModelBindingMessageProvider.SetMissingRequestBodyRequiredValueAccessor(() =>
               localizer["A non-empty request body is required."]);

           options.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor((x) =>
               localizer["The value '{0}' is not valid.", x]);

           options.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor(() =>
               localizer["The supplied value is invalid."]);

           options.ModelBindingMessageProvider.SetNonPropertyValueMustBeANumberAccessor(() =>
               localizer["The field must be a number."]);

           options.ModelBindingMessageProvider.SetUnknownValueIsInvalidAccessor((x) =>
               localizer["The supplied value is invalid for {0}.", x]);

           options.ModelBindingMessageProvider.SetValueIsInvalidAccessor((x) =>
               localizer["The value '{0}' is invalid.", x]);

           options.ModelBindingMessageProvider.SetValueMustBeANumberAccessor((x) =>
               localizer["The field {0} must be a number.", x]);

           options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor((x) =>
               localizer["The value '{0}' is invalid.", x]);
        }
    }
}

Finally register the new configuration class in startup:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureModelBindingLocalization>();

    // ...
}
like image 35
LazZiya Avatar answered Nov 02 '22 16:11

LazZiya