环境:

  • .Net 5

前言:.Net5 官方已经写好了基础的模型验证,但是由于默认语言为 en-us ,官方文档也并没有讲清楚如何本地化,因此本文在基于官方文档以及 StackOverflow 梳理了.Net5 中文模型本地化的代码。

  1. 基础准备(依赖注入):

    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.resxShareResource.resx (默认的资源,如果有特定的语言,例如中文,则命名为 TestController.zh-Hans.resx )(位置在 Resources 文件夹下,根据下述的 ResourcesPath)相关联

    resx 文件如下图所示

    image-20210917180930078

    准备好 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 里注入本地化示例了

  2. 模型验证

    在.Net 5 下官方已经写好了模型验证的类库,为 Controller 标上 [ApiController] 就可以自动进行模型验证过程,而不需要在每个 ControllerAction 里验证 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

更新于 阅读次数