Loading

[.Net] Web API 本地化与全球化

.Net 5 模型验证错误信息本地化(包含国际化)

环境:

  1. .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_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)

posted @ 2021-09-17 20:05  minskiter  阅读(319)  评论(0编辑  收藏  举报