DEV Community

Cover image for What every ASP.NET Core Web API project needs - Part 4 - Error Message Reusability and Localization
Mohsen Esmailpour
Mohsen Esmailpour

Posted on • Edited on

What every ASP.NET Core Web API project needs - Part 4 - Error Message Reusability and Localization

In the preceding post, I talked about how to handle errors by exception handling middleware globally. This article talks about two subjects:

  1. First, how to reuse error messages through Resource Files
  2. How to localize error messages

For people like me that our first language is not English, the default error message of System.ComponentModel.DataAnnotations won't help since we have to show messages in our languages.

If you are a native English then you can skip this article, yet, this article comes in handy if you want to avoid hard-code error messages inside the codebase.

.NET developers normally use DataAnnotations attributes of FluentValidation to validate user inputs. Consider the following codes:

public class UserRegistrationViewModel
{
    [Required(ErrorMessage = "فیلد نام اجباری میباشد")]
    [MaxLength(32, ErrorMessage = "طول فیلد نام حداکثر 32 باید باشد")]
    [MinLength(2, ErrorMessage = "حداقل طول وارد شده برای فیلد نام بایستی 2 کاراکتر باشد")]
    public string FirstName { get; set; }

    [Required(ErrorMessage = "فیلد نام خانوادگی اجباری میباشد")]
    [MaxLength(32, ErrorMessage = "طول فیلد نام خانوادگی حداکثر 32 باید باشد")]
    [MinLength(2, ErrorMessage = "حداقل طول وارد شده برای فیلد نام خانوادگی بایستی 2 کاراکتر باشد")]
    public string LastName { get; set; }

    [DataType(DataType.EmailAddress, ErrorMessage = "ایمیل وارد شده معتبر نمیباشد")]
    [Required(ErrorMessage = "فیلد ایمیل اجباری میباشد")]
    [MaxLength(128, ErrorMessage = "طول فیلد ایمیل حداکثر 32 باید باشد")]
    [CustomEmailAddress(ErrorMessage = "فرمت ایمیل وارد شده معبتر نیست")]
    public string Email { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

The problem with the above code is that I have to copy/paste a message for all required properties which only the property names are different but require message is the same:

فیلد نام اجباری میباشد
فیلد نام خانوادگی اجباری میباشد
فیلد ایمیل اجباری میباشد
Enter fullscreen mode Exit fullscreen mode

فیلد ... اجباری می‌باشد means
... field is required. You've learned just a little bit Persian 😄

We will fix the above problems with help of the Resource files.
In .NET Core 3.0 and later, the new way is used for applying localization in terms of using resource files. I'm not going to use IStringLocalizer<Resource> or new solutions for localizing after .NET Core 3.0 has been released because they seem to be too complicated (at least for me).

Step 1 - Add localization required dependencies

services.AddControllers().AddDataAnnotationsLocalization();
services.AddLocalization(options => options.ResourcesPath = "Resources");
Enter fullscreen mode Exit fullscreen mode

ResourcesPath is the relative path under the application root where resource files are located.

  • Open CoolWebApi.csproj file:
<PropertyGroup>
  <EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
</PropertyGroup>
Enter fullscreen mode Exit fullscreen mode

The EmbeddedResourceUseDependentUponConvention property defines whether resource manifest file names are generated from type information in source files that are colocated with resource files. For example, if Form1.resx is in the same folder as Form1.cs, and EmbeddedResourceUseDependentUponConvention is set to true, the generated .resources file takes its name from the first type that's defined in Form1.cs. For example, if MyNamespace.Form1 is the first type defined in Form1.cs, the generated file name is MyNamespace.Form1.resources.

Step 2 - Add resource files

  • Add a new folder Resources to the root of the project
  • Add two Resource files to the Resources folder, DisplayNameResource.resx and ErrorMessageResource.resx
  • Open resource file and change Access Modifier to Public Alt Text

Step 3 - Add names and error messages

In the second part (Swagger I've created a DummyModel:

public class DummyModel
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    [JsonIgnore]
    public string FullName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
  • Add FirstName and LastName to DisplayNameResource.resx: Alt Text
  • Open ErrorMessageResource.rex file and add following values (you can error message in your default language for instance, if your website language is Persian add error messages in Persian): Alt Text
  • Add data annotation attributes to DummyModel:
public class DummyModel
{
    [Display(ResourceType = typeof(DisplayNameResource), Name = "FirstName")]
    [Required(ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "RequiredError")]
    [MaxLength(32, ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "MaxLengthError")]
    public string FirstName { get; set; }

    [Display(ResourceType = typeof(DisplayNameResource), Name = "FirstName")]
    [Required(ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "RequiredError")]
    [StringLength(32, MinimumLength = 3, ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "MaxLengthError")]
    public string LastName { get; set; }

    [JsonIgnore]
    public string FullName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

How to use error messages outside of data annotation attributes?
Let's add email property to DummyModel and forbid email containing dummy word:

  • Add the following name/value to the ErrorMessageResource.rex: name:DummyIsForbidenError value:{0} cannot contains dummy word.
  • Validate email property implementingIValidatableObject:
public class DummyModel : IValidatableObject
{
    [Display(ResourceType = typeof(DisplayNameResource), Name = "FirstName")]
    [Required(ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "RequiredError")]
    [MaxLength(32, ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "MaxLengthError")]
    public string FirstName { get; set; }

    [Display(ResourceType = typeof(DisplayNameResource), Name = "FirstName")]
    [Required(ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "RequiredError")]
    [MaxLength(32, MinimumLength = 3, ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "MaxLengthError")]
    public string LastName { get; set; }

    [Display(ResourceType = typeof(DisplayNameResource), Name = "Email")]
    [Required(ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "RequiredError")]
    [MaxLength(128, MinimumLength = 3, ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "MaxLengthError")]
    public string Email { get; set; }

    [JsonIgnore]
    public string FullName { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Email.Contains("dummy"))
            yield return new ValidationResult(string.Format(
                ErrorMessageResource.DummyIsForbidenError, 
                DisplayNameResource.Email));
    }
}
Enter fullscreen mode Exit fullscreen mode

Or throwing DomainException:

if (Email.Contains("dummy"))
    throw new DomainException(string.Format(
        ErrorMessageResource.DummyIsForbidenError, DisplayNameResource.Email));
Enter fullscreen mode Exit fullscreen mode

Step 4 - Localization

Let's add another language to localize display names and error messages.

  • Open Startup class and following culutres in ConfigureServices method:
var supportedCultures = new List<CultureInfo> { new CultureInfo("en"), new CultureInfo("fa") };
services.Configure<RequestLocalizationOptions>(options =>
{
    options.DefaultRequestCulture = new RequestCulture("fa");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
});
Enter fullscreen mode Exit fullscreen mode
  • Add two more resource files DisplayNameResource.fa.resx and ErrorMessageResource.fa.resx and add the same translated name/values: Alt Text Alt Text
  • Open Startup class and add the Localization middleware in Configure method:
app.UseRequestLocalization();
Enter fullscreen mode Exit fullscreen mode

Now it's time to test the DataAnnotations localization, however, preliminary to test we need to change the culture of application at the runtime. We can change the application culture through:

  1. QueryStringRequestCultureProvider
  2. CookieRequestCultureProvider
  3. AcceptLanguageHeaderRequestCultureProvider I'm going to show you how to change application culture by AcceptLanguageHeaderRequestCultureProvider when you are testing APIs with swagger (read official documentation for more information).
  • Add new class SwaggerLanguageHeader.cs to Infrastructure\Swagger folder and add following codes:
public class SwaggerLanguageHeader : IOperationFilter
{
    private readonly IServiceProvider _serviceProvider;

    public SwaggerLanguageHeader(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        operation.Parameters ??= new List<OpenApiParameter>();

        operation.Parameters.Add(new OpenApiParameter
        {
            Name = "Accept-Language",
            Description = "Supported languages",
            In = ParameterLocation.Header,
            Required = false,
            Schema = new OpenApiSchema
            {
                Type = "string",
                Enum = (_serviceProvider
                        .GetService(typeof(IOptions<RequestLocalizationOptions>)) as IOptions<RequestLocalizationOptions>)?
                    .Value?
                    .SupportedCultures?.Select(c => new OpenApiString(c.TwoLetterISOLanguageName)).ToList<IOpenApiAny>(),
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Register filter in Swagger in ConfigureService method:
services.AddSwaggerGen(options =>
{
    **options.OperationFilter<SwaggerLanguageHeader>();**
    ...
Enter fullscreen mode Exit fullscreen mode

Now run the application and you will see newly added dropdown input with two value of en and fa:
Alt Text
Let's test the post API with invalid inputs and selectingfa from the language dropdown:
Alt Text
That's it 😁.

  • And last but not least, how to pass parameters to the error message? for example, I want to write a message for StringLength attribute:
[StringLength(32, MinimumLength = 3, ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "MaxLengthError")]
public string FirstName { get; set; }
Enter fullscreen mode Exit fullscreen mode

The minimum length of {0} is {2} characters and maximum length is {1} characters.

  • First parameter {0} belongs to the property name
  • in {1},{2},... are related to the attribute parameters form left to the right. For StringLength parameter {1} is MaximumLength (32) and parameter {2} is MinimumLength (3) Alt Text

You can find the source code for this walkthrough on Github.

Top comments (3)

Collapse
 
jaimestuardo profile image
jaimestuardo

Hello.... interesting article, but, why there is a need to do this manually? I come from .NET Framework world, and with that, I have never worried about the translations. All messages appeared correctly in my language, Spanish in this case.

I am new to .NET Core and I have found that there are many things that should be done manually which makes the development slow, as in this case, localizations.

Regards
Jaime

Collapse
 
moesmp profile image
Mohsen Esmailpour

Looks like it only matters for people like me who speak Persian and there is no default translation. You can consider this implementation for your custom error messages and there is no default translation.

Collapse
 
mansourtarafdar profile image
Mansour Tarafdar

It's great,
Thank you Mohsen.