ASP.NET Core Model Binding Error Messages Localization
Asked Answered
P

3

35

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.

Platinumblond answered 27/11, 2016 at 11:46 Comment(2)
I wonder why you get the validation message on Test property as it do not contain any validation attributes at allAn
User might insert text in the corresponding HTML field, which would generate a model error (message od which I'm trying to change).Platinumblond
T
72

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

Also here you can see the example, live:

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]);
Trimolecular answered 16/1, 2017 at 4:40 Comment(41)
I've tested it using Microsoft.AspNetCore.Mvc 1.1.0. Let me know if yo uhave any problem applying the solution.Trimolecular
This looks really cool, however I'm not able to get it to apply in Core 1.1 - the resource file is there, and the localisation for the rest of the project is working, but using the above I'm still getting the default error messages... That is "The {0} Field is required" on the ValueMustNotBeNullAccessor - so it doesn't even take the English values... Any ideas?Tumultuous
@Tumultuous Make sure you are doing all checks and if the problem is still there send me an email (r.aghaei at outlook.com). Then I'll create a test project using VS2015, Microsoft.AspNetCore.Mvc 1.1.0 and .NET 4.6.1 and will share with you.Trimolecular
1) If you use ...ValueMustNotBeNullAccessor = (x) => L["xxx", x]; then even if there is no resource key for xxx, the same message should be shown. 2) If you create a ModelBindingMessages.xx.resx and add a key The {0} Field is require, then the value of this key will be used as error message. 3) Don't forget var F= ... and var L = .... 4) The localization service should be added before mvc: services.AddLocalization...Trimolecular
If you can make the test project public, that would be nice. I tried to apply everything that you wrote (except the language code, I used a different one) and for me it didn't work. I rechecked everything, but without success.Platinumblond
@Platinumblond Send me an email (r.aghaei at outlook.com). Then I'll create a test project using VS2015, Microsoft.AspNetCore.Mvc 1.1.0 and .NET 4.6.1 and will share with you.Trimolecular
Could we setup a project on Github? I'm using .NET Core framework, rather than .NET 4.6.1 - could that be the cause?Tumultuous
@Tumultuous I checked all options again, it works properly, just updated the answer. I added a piece of code which I used in my project which is related to the answer but was forgotten to be added in answer. I'll create a github repository for it when I have time :)Trimolecular
@Tumultuous github repository added. github.com/r-aghaei/AspNetCoreLocalizationSampleTrimolecular
@Platinumblond github repository added. github.com/r-aghaei/AspNetCoreLocalizationSampleTrimolecular
Great example. Thank you!Platinumblond
@RezaAghaei Thanks a lot for your answer, the translation works wel! The issue is that the parameter is not set in case of "Null value is invalid." I tried to add '{0}' to it, but no success so far. What do you think? Thanks in advance!Inerrant
@Inerrant 1) Error message can be set using ValueMustNotBeNullAccessor and the default message is "The value '{0}' is invalid." which means it can have parameter {0}. 2) To add {0} you should change the message in 2 location, in startup, where we are setting SetValueMustNotBeNullAccessor and also in your localized resource file. This is a common mistake to forget one of those locations. 3) After correcting those locations, keep in mind that value of {0} which will be passed to the message is Null and displays nothing instead of {0}! So there is no point in showing it.Trimolecular
@RezaAghaei Thanks a lot for the quick reply, I appreciate it! Cheers!Inerrant
I'm sorry, but in Core 2.0 this approach does not work for validation messages generated by [Required] annotations. It seems that the error string The XYZ field is required. cannot be translated this way.Tempe
@Tempe Read my comments here.Trimolecular
Yes, @RezaAghaei, you are correct. I recommend that you add to your answer here, that certain annotations cannot be translated using the ModelBindingMessageProvider approach, but need a static ErrorMessage string in the annotations that will trigger the translation. The [Required] case is one of them and it might be the case that brings most people here. I took me hours to get to this point of understanding.Tempe
Thanks for your comment @Jpsy. The question is about Model Binding Error Messages Localization. It's not about Validation Attribute Error Messages Localization. So I believe most users who reached to this question/answer are aware of difference :)Trimolecular
@Tempe If you download the sample project, you will see it contains localization of model binding error messages and also localization of validation attribute error messages. Maybe I add a note about it in the question. Again, thank you for your comment :)Trimolecular
Somehow I cannot make it work in 2.1... If I name the file ModelBindingMessages.resx, it works, but of course it is not going to help if more than one language is supported by the app... Any thoughts here? I am using Core 2.1Sf
In Core 2.2 I get Property or indexer 'DefaultModelBindingMessageProvider.ValueIsInvalidAccessor' cannot be assigned to -- it is read only. I guess, we have to implement custom ModelBindingMessageProvider and also IValidationAttributeAdapterProvider for attributes such as [EmailAddress].Zita
@Zita You probably missed Important Note for ASP.NET Core 2.0options.ModelBindingMessageProvider.SetValueIsInvalidAccessor ((x) => L["The value '{0}' is invalid."]);Trimolecular
Maybe I update the main part of the answer to ASP.NET CORE 2 rather than just keeping as a note.Trimolecular
One more gotcha that breaks these default binding translations - if your web API receives JSON, Json.Net formatter will take over the ModelBindingMessageProvider and will throw out their own messages such as Input string '111a' is not a valid number. Path 'countryId', line 3, position 23. I'll have to look further how to solve this...Zita
Maybe it works, but I can't got known it: I don't use any error message from this list. For example, how to set accessors for these messages in ASP.NET Core 2.2: The {0} field is not a valid e-mail address. and The {0} field is required.?Essary
And these: The {0} must be at least {1} and at max {2} characters long., The password and confirmation password do not match.Essary
@Essary I haven't updated the example for 2.2 but you can download a working example of 2.0Trimolecular
@RezaAghaei I've already downloaded it. According to 2.0 example I've implemented ModelBindingMessageProvider for 2.2. There is the same list of options.ModelBindingMessageProvider.Set... methods in Startup.cs file. [Required] and [Email] attributes aren't novelty in ASP.NET Core 2.2. The most important question is How to implement localization for standard validation attributes?Essary
@Essary Look at SampleModel class and StringLength attribute. I've put the persian localized error message of this validation attribute in Models.SampleModel.fa.resx.Trimolecular
@Essary see the example at runtime. Enter a long string in the Name field and see the validation message. Then change the language to Persian and enter long name again and see the validation. In fact the answer shows how to localize Validation messages and also Model binding messages.Trimolecular
@RezaAghaei it works good, but only for specific message inside 1 specific model. How to make this localization rule workable for all models for standard attribute message? For example, I have [Required] attribute in my models 100 times, which produce a message "The {0} field is required.", where {0} is the field display name. How to override this message once for all models?Essary
@Essary I got your question. It's a different topic and a good question. If you couldn't find an answer for that, I think it's better to ask a new question about it. Feel free to let me know if you asked a new question. I'll take a look at that and try to share my ideas :)Trimolecular
@RezaAghaei I've created a question #59284538Essary
@Learning I haven't tested it using that version, but I expect no major change in the solution, If I find time, I'll add the project for 3.1 to the repository.Trimolecular
It does not work if I add [FromBody] before SampleModel ``` c# HttpPost] public IActionResult Index([FromBody] SampleModel model) { return View(model); }```Drucill
@TrongVo Thanks for the feedback, I need to update the answer for .NET 5 as soon as I can.Trimolecular
i still using your example. .net 2.0Drucill
For [FromBody] attribute, we can disable default message in services.AddMvc().AddJsonOptions(options => options.AllowInputFormatterExceptionMessages = false;). This will add an empty string for the error message, which we can detect and then just display a generic message to user. I have not found a better way of doing this but this method will suffice for now.Drucill
Apparently this is not longer valid in .NET 6. In this case var F = services.BuildServiceProvider().GetService<IStringLocalizerFactory>(); shows a singleton warning. I have done all other steps but the messages still appear in English. How to do that in ,NET 6 and EF 7?Dyestuff
@Dyestuff Then it's time for me to update the answer as soon as I can.Trimolecular
BuildServiceProvider() is a red flag for me, this should never be executed at ASP.NET Core because it will create an entire DI container.Infundibuliform
B
10

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

    // ...
}
Buran answered 4/11, 2020 at 14:15 Comment(4)
This is the valid answer for asp.net core 5Bosley
Would like to add that if you're using Orchestra PO localization, you can declare an empty class and call IStringLocalizer and inject it and use it instead of the localizer variableBrutify
I get this error No service for type 'Microsoft.Extensions.Localization.IStringLocalizer' has been registered. .... using asp.net core 6 and added builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); to program.cs ... also added .AddViewLocalization()Gnostic
I got the same error as DoubleVoid. I was able to fix it by adding the type of my resource file to the call to get the service instead of changing program or startup.cs var localizer = provider.GetRequiredService<IStringLocalizer<SharedResources>>();Hydrargyrum
N
-1

Tested in .NET Core 3.1 and .NET 5. Create a private method that determine the UICulture

private string GetStringValidationError()
    {
        CultureInfo uiCultureInfo = Thread.CurrentThread.CurrentUICulture;

        string errorMessaeg = string.Empty;

        errorMessaeg = uiCultureInfo.ToString() == "ar" ? "هذا الحقل مطلوب" : "This field is required";
        return errorMessaeg;
    }

After that you can append this Method to the Func delegate as a first parameter to SetValueMustNotBeNullAccessor method like so:

options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(value => GetStringValidationError());
Nellie answered 16/7, 2021 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.