.Net 5 模型验证错误信息本地化(包含国际化)
环境:
- .Net 5
前言:.Net5 官方已经写好了基础的模型验证,但是由于默认语言为en-us
,官方文档也并没有讲清楚如何本地化,因此本文在基于官方文档以及StackOverflow
梳理了.Net5中文模型本地化的代码。
-
基础准备(依赖注入):
Globalization and localization in ASP.NET Core | Microsoft Docs
根据官方文档:
官方提供了根据
CultureInfo
进行字典映射的IStringLocalizer
类、IHtmlLocalizer
类,{ public class TestController : Controller { private readonly IStringLocalizer _localizer; private readonly IStringLocalizer _localizer2; public TestController(IStringLocalizerFactory factory) { var type = typeof(SharedResource); var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName); _localizer = factory.Create(type); _localizer2 = factory.Create("SharedResource", assemblyName.Name); } public IActionResult About() { ViewData["Message"] = _localizer["Your application description page."] + " loc 2: " + _localizer2["Your application description page."];
如官方示例所示:字符串本地化工厂提供了两个本地化示例
_localizer
以及_localizer2
,通过工厂函数,这两个本地化示例与对应的资源TestController.resx
和ShareResource.resx
(默认的资源,如果有特定的语言,例如中文,则命名为TestController.zh-Hans.resx
)(位置在Resources文件夹下,根据下述的ResourcesPath)相关联resx文件如下图所示
准备好resx资源文件后,还需要添加本地化服务以及中间件。
// Startup.cs // ConfigureServices services.AddLocalization(options=>options.ResourcesPath="Resources"); services.Configure<RequestLocalizationOptions>(options=>{ // Chinese Culture Info : zh-Hans var supportedCultures = new List<CultureInfo> { new("zh-Hans"), }; options.DefaultRequestCulture = new RequestCulture("zh-Hans"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; })
同时添加中间管道文件
// Startup.cs // Configure var defaultCulture = new CultureInfo("zh-Hans"); var localizationOptions = new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(defaultCulture), SupportedCultures = new List<CultureInfo> { defaultCulture }, SupportedUICultures = new List<CultureInfo> { defaultCulture }, ApplyCurrentCultureToResponseHeaders = true // 这个属性为html response header里添加了Content-Language,方便查看是否添加成功 }; app.UseRequestLocalization(localizationOptions); // Routing app.UseRouting(); // and so on.
此时,就可以在Controllers 里注入本地化示例了
-
模型验证
在.Net 5下官方已经写好了模型验证的类库,为
Controller
标上[ApiController]
就可以自动进行模型验证过程,而不需要在每个Controller
的Action
里验证Model.IsValid
来看模型是否有效,默认行为错误会自动返回官方类ProblemDetails
,以及对应参数的错误信息。{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-e572b834f3d0ed42b4b0f4577fc6c1b8-923a173ae3655a43-00", "errors": { "test": [ "The test field is required." ] } }
例如以上示例。由以上可见,默认行为返回是英文,这对于中文网站是非常糟糕的。此时,便有一个需求:如何客制化该返回信息,使其能够返回所需的客制化类,以及信息提示是否能修改为中文(或者其他语言,根据Culture Info)。
依照官网文档 Globalization and localization in ASP.NET Core | Microsoft Docs 该节的内容:
错误返回只需要设置
ErrorMessage
与对应类的resx里的ResourceKey
相同,则会自动根据CultureInfo
翻译为对应的语言例如上图的Resources在模型验证属性中可以这样写
[HttpGet] public IEnumerable<WeatherForecast> Get([Required(ErrorMessage="RequiredAttribute_ValidationError")]int test) { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); }
这个时候用
PostMan
工具不传输test
参数测试即可显示出如下的错误{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-e572b834f3d0ed42b4b0f4577fc6c1b8-923a173ae3655a43-00", "errors": { "test": [ "test 不能为空" ] } }
此时可以看到错误信息已经本地化了。
但是为每个类都复制resx是非常麻烦的,因此官方提供了一种方案,也就是共享类
SharedResource
,将资源都绑定在这个类上,都从这个类对应的resx获取字典// 空类 public class SharedResource { }
此时需要新增配置
// Startup.cs // ConfigureService services.AddControllers() // 新增下述代码 .AddDataAnnotationsLocalization(options => { options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResource)); });
此时模型验证的本地化工作已经基本完成
但是现在仍存在一个问题:每次写
Required
属性,都需要写对应的ErrorMessage
,非常麻烦,虽然由官方源代码可以知道RequiredAttribute_ValidatorError
这个ResourceKey
是默认的,但是如果不手动指定是不会走本地化分支,这也是为什么一开始如果不设置ErrorMessage
本地化没有生效的原因.这种高度重复的无用代码是不愿意见到的,因此需要有一种方法覆盖官方默认的行为,使得默认错误信息可以走本地化翻译。
参考c# - Localization of RequiredAttribute in ASP.NET Core 2.0 - Stack Overflow高票答案的方案:
namespace Model.Validator { public static class ModelValidatorLocalizationExtensions { public static IServiceCollection AddModelValidatorLocalization<T>(this IServiceCollection services) { services.AddSingleton<IValidationAttributeAdapterProvider, LocalizedValidationAttributeAdapterProvider>(); return services; } } public class LocalizedValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider { private readonly ValidationAttributeAdapterProvider _originalProvider = new(); public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer) { //attribute.ErrorMessage = attribute.GetType().Name.Replace("Attribute", string.Empty); //if (attribute is DataTypeAttribute dataTypeAttribute) // attribute.ErrorMessage += "_" + dataTypeAttribute.DataType; return _originalProvider.GetAttributeAdapter(attribute, stringLocalizer); } } }
并在
ConfigureService
里注入服务(注意这个必须在AddControllers()
前注入),在该提供器里断点测试,可以发现原生的Required
并没有触发断点(是的,这也是最疑惑的地方,可能得深入整个源码才能发现原因),但是自己编写的客制化Attribute是可以触发的,包括直接对Required
进行派生的类都可以触发断点。由于无法解决该问题,因此在c# - How to localize standard error messages of validation attributes in ASP.NET Core - Stack Overflow找到了另外一种方案,也就是利用
IValidationMetadataProvider
,该过程比IValidationAttributeAdapterProvider
更早。在该过程中根据
ErrorMessage
是否为空修改对应默认的错误信息,使得其可以触发本地化翻译过程分支public class ValidationMetadataLocalizationProvider:IValidationMetadataProvider { public void CreateValidationMetadata(ValidationMetadataProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var validators = context.ValidationMetadata.ValidatorMetadata; // add [Required] for value-types (int/DateTime etc) // to set ErrorMessage before asp.net does it var theType = context.Key.ModelType; var underlyingType = Nullable.GetUnderlyingType(theType); if (theType.IsValueType && underlyingType == null && // not nullable type validators.All(m => m.GetType() != typeof(RequiredAttribute))) { validators.Add(new RequiredAttribute()); } foreach (var obj in validators) { if (!(obj is ValidationAttribute attribute)) { continue; } if (attribute.ErrorMessage == null && attribute.ErrorMessageResourceName==null) { attribute.ErrorMessage = $"{attribute.GetType().Name}_ValidationError"; } } } }
在
ConfigureService
里配置services.AddControllers() // 添加下述选项 .AddMvcOptions(options => { options.ModelMetadataDetailsProviders.Add(new ValidationMetadataLocalizationProvider()); }) .AddDataAnnotationsLocalization(options => { options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResource)); });
至此本地化工作就已全部完成,后续只需根据使用需要添加
SharedResource.resx
这个文件,非常方便,而且可以方便兼容旧的代码。修改
ProblemDetails
类默认显示可以参考另外一篇博文[.NetCore] 统一模型验证拦截器 - minskiter - 博客园 (cnblogs.com)