ASP.NET Core Web API DataAnnotations 多語系(i18n)

書接上回,在 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 屬性來標記尋找多語系的字段:
    
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 來達成設定多語系資源是個不錯的解決方式,不過需要建立自訂的類別,不是使用內建的方式,還是不夠好。

經過筆者 一翻 好幾翻的研究,終於找到筆者認為最好的解決方式,就是透過自訂 ModelValidatorProvider 來修改全部 RequiredAttribute 的多語系設定。

建立一個名為 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

留言