What every ASP.NET Core Web API project needs - Part 4 - Error Message Reusability and Localization
Tags: architecture, dotnet, aspnetcore, webapi
Published: 2021-03-16 21:35:22

In the [preceding post](https://dev.to/moesmp/what-every-asp-net-core-web-api-project-needs-part-3-exception-handling-middleware-3nif), 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:
```csharp
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; }
}
```
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:
```
فیلد نام اجباری میباشد
فیلد نام خانوادگی اجباری میباشد
فیلد ایمیل اجباری میباشد
```
`فیلد ... اجباری می‌باشد` 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
- Open [Startup.cs](https://github.com/mo-esmp/cool-webapi/blob/main/CoolWebApi/Startup.cs) class and follwing codes:
```csharp
services.AddControllers().AddDataAnnotationsLocalization();
services.AddLocalization(options => options.ResourcesPath = "Resources");
```
`ResourcesPath` is the relative path under the application root where resource files are located.
- Open `CoolWebApi.csproj` file:
```xml
<PropertyGroup>
  <EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
</PropertyGroup>
```
> The [EmbeddedResourceUseDependentUponConvention](https://docs.microsoft.com/en-us/dotnet/core/project-sdk/msbuild-props#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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j1o2gegfmweaqiqradq.jpg)

###Step 3 - Add names and error messages
In the second part ([Swagger](https://dev.to/moesmp/what-every-asp-net-core-web-api-project-needs-part-2-api-versioning-and-swagger-3nfm) I've created a `DummyModel`:
```csharp
public class DummyModel
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    [JsonIgnore]
    public string FullName { get; set; }
}
```
- Add `FirstName` and `LastName` to `DisplayNameResource.resx`:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cq5c717j72syy6ceokja.jpg)
- 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n7454j5uqm4dy4g0rmxb.jpg)
- Add data annotation attributes to `DummyModel`:
```csharp
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; }
}
```
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 implementing`IValidatableObject`:
```csharp
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));
    }
}
```
Or throwing [DomainException](https://github.com/mo-esmp/cool-webapi/blob/main/CoolWebApi/Domain/DomainException.cs):
```csharp
if (Email.Contains("dummy"))
    throw new DomainException(string.Format(
        ErrorMessageResource.DummyIsForbidenError, DisplayNameResource.Email));
```
###Step 4 - Localization
Let's add another language to localize display names and error messages.
- Open `Startup` class and following culutres in `ConfigureServices` method:
```csharp
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;
});
```
- Add two more resource files `DisplayNameResource.fa.resx` and `ErrorMessageResource.fa.resx` and add the same translated name/values:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xt10yyfv4v3awm0ajek.jpg)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u0ykpxw5sq0pubsvqqfa.jpg)
- Open `Startup` class and add the `Localization` middleware in `Configure` method:
```csharp
app.UseRequestLocalization();
```
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](AcceptLanguageHeaderRequestCultureProvider) for more information).

- Add new class `SwaggerLanguageHeader.cs` to `Infrastructure\Swagger` folder and add following codes:
```csharp
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>(),
			}
		});
	}
}
```
- Register filter in Swagger in `ConfigureService` method:
```csharp
services.AddSwaggerGen(options =>
{
    **options.OperationFilter<SwaggerLanguageHeader>();**
    ...
```
Now run the application and you will see newly added dropdown input with two value of `en` and `fa`:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1x3lo44wa4i3g2t3zlqn.jpg)
Let's test the post API with invalid inputs and selecting`fa` from the language dropdown:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8389k3mxwdfgj05vl82j.jpg)
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:
```csharp
[StringLength(32, MinimumLength = 3, ErrorMessageResourceType = typeof(ErrorMessageResource), ErrorMessageResourceName = "MaxLengthError")]
public string FirstName { get; set; }
```
```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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n4zpj2d26vvbjsgc12pd.jpg)

You can find the source code for this walkthrough on [Github](https://github.com/mo-esmp/cool-webapi).