• [.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)

  • 相关阅读:
    QuartusII13.0使用教程详解(一个完整的工程建立)
    基于Vivado调用ROM IP core设计DDS
    FPGA学习之路——一路走来
    基于basys2用verilog设计多功能数字钟(重写)
    基于basys2驱动LCDQC12864B的verilog设计图片显示
    PWM(脉宽调制)——LED特效呼吸灯设计
    Isim你不得不知道的技巧(整理)
    ISE、vivado、QuartusII调用notepad++、UE汇总(整理)
    java环境配置为1.7jdk为什么cmd java -version查看版本是1.8
    TCP/IP三次握手和HTTP过程
  • 原文地址:https://www.cnblogs.com/minskiter/p/15306121.html
Copyright © 2020-2023  润新知