In the preceding post, I talked about how to handle errors by exception handling middleware globally. This article talks about two subjects:
- First, how to reuse error messages through Resource Files
- 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; }
}
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 class and follwing codes:
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:
<PropertyGroup>
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
</PropertyGroup>
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
andErrorMessageResource.resx
- Open resource file and change
Access Modifier
toPublic
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; }
}
- Add
FirstName
andLastName
toDisplayNameResource.resx
: - 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): - 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; }
}
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
:
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:
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 inConfigureServices
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;
});
- Add two more resource files
DisplayNameResource.fa.resx
andErrorMessageResource.fa.resx
and add the same translated name/values: - Open
Startup
class and add theLocalization
middleware inConfigure
method:
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:
- QueryStringRequestCultureProvider
- CookieRequestCultureProvider
- 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
toInfrastructure\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>(),
}
});
}
}
- Register filter in Swagger in
ConfigureService
method:
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
:
Let's test the post API with invalid inputs and selectingfa
from the language dropdown:
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; }
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}
isMaximumLength (32)
and parameter{2}
isMinimumLength (3)
You can find the source code for this walkthrough on Github.
Top comments (3)
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
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.
It's great,
Thank you Mohsen.