書接上回,在 ASP.NET Core Web API 多語系(i18n) 共用資源檔 這篇文章中有介紹到在單一檔案中使用 i18n 多語系設定,但是只有介紹到存取自訂的字串,在 ASP.NET Core 中提供的 DataAnnotations 資料驗證屬性 i18n 多語系不小心被忽略了,本篇就來介紹 DataAnnotations(RequiredAttribute 和 DisplayAttribute) 的 i18n 使用方式。
假設有一個 API ,需要傳入 UserCreateDto 參數,我們可以使用 RequiredAttribute 來標記此欄位為必填,但是預設顯示的內容為「The field is required.」,並沒有做多語系。
我們可以使用 RequiredAttribute 的 ErrorMessageResourceType 屬性標記多語系資源檔,使用 ErrorMessageResourceName 屬性來標記尋找多語系的字段:
在 SharedResources.resx 檔案中需要增加以下多語系字串:
API 回應結果:
ㄜ...成功了一半,但是對於 UserCreateDto 類別的 Code 屬性名稱還是英文,有沒有什麼方式可以讓 DTO 的屬性(範例中的 Code)也有多語系?
當然!我們可以使用 DisplayAttribute 達成。
在 SharedResources.resx 檔案中需要增加以下多語系字串:
筆者在啟動程式時會出現以下錯誤:
這個原因是因為在多語系自動產生的 SharedResources.Designer.cs 檔案中,SharedResources 類別和剛剛 DisplayAttribute 用到的 UserCreateDto_Code 字串都是 internal,在 DisplayAttribute 內部無法存取。
需要手動將 internal 改為 public 才可以正常執行:
不過 SharedResources.Designer.cs 是自動產生的程式碼檔案,所以有調整多語系資料很可能需要再次手動將 internal 修改為 public ,有點麻煩。
並且很可惜的是 DisplayAttribute 是密封(sealed)類別,我們無法透過繼承來自訂,如果有網友有更好的解決方式,請務必留言指導。提前致謝!
這樣以後我們就可以直接使用 CustomRequiredAttribute 達成多語系設定了:
經過筆者一翻 好幾翻的研究,終於找到筆者認為最好的解決方式,就是透過自訂 ModelValidatorProvider 來修改全部 RequiredAttribute 的多語系設定。
建立一個名為 CustomValidationProvider 的物件,實作 IModelValidatorProvider 介面,將所有的 RequiredAttribute 都指定 ErrorMessageResourceType 和 ErrorMessageResourceName
在 Program.cs 中註冊服務:
這樣在 API 的 DTO 就只要使用內建的 RequiredAttribute 就可以自動達成多語系了!
參考資料:
Microsoft.Learn - RequiredAttribute Class
Microsoft.Learn - DisplayAttribute Class
Microsoft.Learn - Microsoft.AspNetCore.Mvc.ModelBinding.Validation/IModelValidatorProvider Interface
假設有一個 API ,需要傳入 UserCreateDto 參數,我們可以使用 RequiredAttribute 來標記此欄位為必填,但是預設顯示的內容為「The field is required.」,並沒有做多語系。
我們可以使用 RequiredAttribute 的 ErrorMessageResourceType 屬性標記多語系資源檔,使用 ErrorMessageResourceName 屬性來標記尋找多語系的字段:
public class UserCreateDto
{
[Required(
ErrorMessageResourceType = typeof(Resources.SharedResources),
ErrorMessageResourceName = "RequiredAttribute_ValidationError"
)]
public string Code { get; set; } = null!;
}
在 SharedResources.resx 檔案中需要增加以下多語系字串:
<data name="RequiredAttribute_ValidationError" xml:space="preserve">
<value>此 {0} 欄位為必填</value>
</data>
API 回應結果:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Code": [
"此 Code 欄位為必填"
]
}
}
ㄜ...成功了一半,但是對於 UserCreateDto 類別的 Code 屬性名稱還是英文,有沒有什麼方式可以讓 DTO 的屬性(範例中的 Code)也有多語系?
當然!我們可以使用 DisplayAttribute 達成。
使用 DisplayAttribute 自訂屬性多語系顯示名稱
public class UserCreateDto
{
[Required(
ErrorMessageResourceType = typeof(Resources.SharedResources),
ErrorMessageResourceName = "RequiredAttribute_ValidationError"
)]
[Display(
ResourceType = typeof(Resources.SharedResources),
Name = nameof(Resources.SharedResources.UserCreateDto_Code)
)]
public string Code { get; set; } = null!;
}
在 SharedResources.resx 檔案中需要增加以下多語系字串:
<data name="UserCreateDto_Code" xml:space="preserve">
<value>代號</value>
</data>
筆者在啟動程式時會出現以下錯誤:
Unhandled exception. System.InvalidOperationException: Cannot retrieve property 'Name' because localization failed. Type 'SoicApi.Resources.SharedResources' is not public or does not contain a public static string property with the name 'UserCreateDto_Code'.
這個原因是因為在多語系自動產生的 SharedResources.Designer.cs 檔案中,SharedResources 類別和剛剛 DisplayAttribute 用到的 UserCreateDto_Code 字串都是 internal,在 DisplayAttribute 內部無法存取。
internal class SharedResources {
internal static string RequiredAttribute_ValidationError {
get {
return ResourceManager.GetString("RequiredAttribute_ValidationError", resourceCulture);
}
}
internal static string UserCreateDto_Code {
get {
return ResourceManager.GetString("UserCreateDto_Code", resourceCulture);
}
}
}
需要手動將 internal 改為 public 才可以正常執行:
public class SharedResources {
internal static string RequiredAttribute_ValidationError {
get {
return ResourceManager.GetString("RequiredAttribute_ValidationError", resourceCulture);
}
}
public static string UserCreateDto_Code {
get {
return ResourceManager.GetString("UserCreateDto_Code", resourceCulture);
}
}
}
不過 SharedResources.Designer.cs 是自動產生的程式碼檔案,所以有調整多語系資料很可能需要再次手動將 internal 修改為 public ,有點麻煩。
並且很可惜的是 DisplayAttribute 是密封(sealed)類別,我們無法透過繼承來自訂,如果有網友有更好的解決方式,請務必留言指導。提前致謝!
自訂 RequiredAttribute 類別,避免每個 RequiredAttribute 都需要重複標記多語系資源設定
RequiredAttribute 這個用來標記必填的 Attribute ,每個的多語系設定都一樣,每次都要設定 ErrorMessageResourceType 和 ErrorMessageResourceName ,非常麻煩。我們可以建立一個自訂的 Attribute,取名為 CustomRequiredAttribute ,繼承 RequiredAttribute 來複寫多語系設定:
public class CustomRequiredAttribute : RequiredAttribute
{
public CustomRequiredAttribute()
{
ErrorMessageResourceName = "RequiredAttribute_ValidationError";
ErrorMessageResourceType = typeof(Resources.SharedResources);
}
}
這樣以後我們就可以直接使用 CustomRequiredAttribute 達成多語系設定了:
public class UserCreateDto
{
[CustomRequired]
[Display(
ResourceType = typeof(Resources.SharedResources),
Name = nameof(Resources.SharedResources.UserCreateDto_Code)
)]
public string Code { get; set; } = null!;
}
使用 IModelValidatorProvider 全域覆寫 RequiredAttribute 多語系資源設定
上面透過建立 CustomRequiredAttribute 這個自訂 Attribute 來達成設定多語系資源是個不錯的解決方式,不過需要建立自訂的類別,不是使用內建的方式,還是不夠好。經過筆者
建立一個名為 CustomValidationProvider 的物件,實作 IModelValidatorProvider 介面,將所有的 RequiredAttribute 都指定 ErrorMessageResourceType 和 ErrorMessageResourceName
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
public class CustomValidationProvider : IModelValidatorProvider
{
public void CreateValidators(ModelValidatorProviderContext context)
{
foreach (var result in context.Results)
{
if (result.ValidatorMetadata is RequiredAttribute requiredAttribute)
{
requiredAttribute.ErrorMessageResourceType = typeof(Resources.SharedResources);
requiredAttribute.ErrorMessageResourceName = nameof(Resources.SharedResources.RequiredAttribute_ValidationError);
}
}
}
}
在 Program.cs 中註冊服務:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(options =>
{
options.ModelValidatorProviders.Add(new CustomValidationProvider());
});
var app = builder.Build();
這樣在 API 的 DTO 就只要使用內建的 RequiredAttribute 就可以自動達成多語系了!
public class UserCreateDto
{
[Required]
[Display(
ResourceType = typeof(Resources.SharedResources),
Name = nameof(Resources.SharedResources.UserCreateDto_Code)
)]
public string Code { get; set; } = null!;
}
參考資料:
Microsoft.Learn - RequiredAttribute Class
Microsoft.Learn - DisplayAttribute Class
Microsoft.Learn - Microsoft.AspNetCore.Mvc.ModelBinding.Validation/IModelValidatorProvider Interface
留言
張貼留言
如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com