环境:
- .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_ValidatorError")]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
参数测试即可显示出如下的错误n {
"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