ASP.NET Core 2.0 中RequiredAttribute 的本地化

Posted

技术标签:

【中文标题】ASP.NET Core 2.0 中RequiredAttribute 的本地化【英文标题】:Localization of RequiredAttribute in ASP.NET Core 2.0 【发布时间】:2018-07-23 23:08:43 【问题描述】:

我正在为我的新 .NET Core 项目的本地化而苦苦挣扎。 我有 2 个项目:

具有模型和 DataAnnotations 的 DataAccess 项目(例如 RequiredAttribute) 带有 MVC 视图等的 Web 项目

我的愿望是将所有验证属性全局本地化到一个地方,以实现类似 MVC 5 的行为。这可能吗?

我不想为模型/视图等设置单独的语言文件。

Microsoft 的文档对使用带有本地化 DataAnnotation 消息的 SharedResources.resx 文件不是很清楚。

在 MVC 5 中我没有处理它。我只需要将语言环境设置为我的语言,一切都很好。

我尝试在 DataAccess 项目中将 ErrorMessageResourceName 和 ErrorMessageResourceType 设置为我的共享资源文件名“Strings.resx”和“Strings.de.resx”:

[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]

我也尝试将设置名称设为 RequiredAttribute_ValidationError - 但它不起作用。

我已经在 Startup.cs 中添加了.AddDataAnnotationsLocalization() - 但它似乎什么也没做。

我已经阅读了几篇文章,但我找不到它不起作用的原因。

编辑: 到目前为止我所拥有的:

1.) LocService 类

 public class LocService
    
        private readonly IStringLocalizer _localizer;

        public LocService(IStringLocalizerFactory factory)
        
            _localizer = factory.Create(typeof(Strings));
        

        public LocalizedString GetLocalizedhtmlString(string key)
        
            return _localizer[key];
        
    

2.) 使用 Strings.cs 添加文件夹“Resources”(带有虚拟构造函数的空类)

3.) 添加了 Strings.de-DE.resx 文件,其中包含一项“RequiredAttribute_ValidationError”

4.) 修改了我的 Startup.cs

public void ConfigureServices(IServiceCollection services)
        
            services.AddTransient<MessageService>();
            services.AddDbContext<DataContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddSingleton<LocService>();
            services.AddLocalization(options => options.ResourcesPath = "Resources");
            services.AddMvc()
                .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver())
                .AddDataAnnotationsLocalization(
                    options =>
                    
                        options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Strings));
                    );

            services.Configure<RequestLocalizationOptions>(
                opts =>
                
                    var supportedCultures = new List<CultureInfo>
                    
                        new CultureInfo("de-DE"),
                    ;

                    opts.DefaultRequestCulture = new RequestCulture("de-DE");
                    // Formatting numbers, dates, etc.
                    opts.SupportedCultures = supportedCultures;
                    // UI strings that we have localized.
                    opts.SupportedUICultures = supportedCultures;
                );
        

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        
            if (env.IsDevelopment())
            
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            
            else
            
                app.UseExceptionHandler("/Home/Error");
            

            var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();

            app.UseRequestLocalization(locOptions.Value);
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        

我已按照此处的说明进行操作,但它不起作用: https://damienbod.com/2017/11/01/shared-localization-in-asp-net-core-mvc/

请记住,我的模型保存在一个单独的项目中。

【问题讨论】:

您可能想在 MS 文档上留言和/或在 GitHub 上打开问题以告知他们文档不清楚。 如果您想让我们知道发生了什么,您需要添加一个完整的 Startup 类。请阅读如何创建minimal reproducible example 请仔细查看文档。 resx 文件必须有一个特殊的名称才能工作或更改其搜索的名称 docs.microsoft.com/en-us/aspnet/core/fundamentals/… (抱歉没有时间详细回答,也许我在家的时候)。它必须由 ViewModel 文件命名,或者您设置共享资源(docs9 中的两个示例 @Tseng:你为我指明了正确的方向。线索是包含共享资源的 resx 文件必须与应用程序位于同一根命名空间中。由于我修改了命名空间,现在一切正常。但我仍然想知道 Localization 是否可以使用简单的 [Required] 注释。现在我必须写[Required(ErrorMessage = "RequiredAttribute_ValidationError")] 【参考方案1】:

正如@Sven 在他对Tseng's answer 的评论中指出的那样,它仍然需要您指定一个明确的ErrorMessage,这会变得非常乏味。

问题来自ValidationAttributeAdapter&lt;TAttribute&gt;.GetErrorMessage() 用于决定是否使用提供的IStringLocalizer 的逻辑。 我使用以下解决方案来解决该问题:

    创建一个使用默认ValidationAttributeAdapterProvider 的自定义IValidationAttributeAdapterProvider 实现,如下所示:

    public class LocalizedValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
    
        private readonly ValidationAttributeAdapterProvider _originalProvider = new ValidationAttributeAdapterProvider();
    
        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);
        
    
    

    Startup.ConfigureServices()注册适配器在调用AddMvc()之前:

    services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider, LocalizedValidationAttributeAdapterProvider>();
    

我更喜欢根据实际属性使用“更严格”的资源名称,所以上面的代码会寻找像“Required”和“DataType_Password”这样的资源名称,但这当然可以通过多种方式进行自定义。

如果您更喜欢基于属性的默认消息的资源名称,您可以编写如下内容:

attribute.ErrorMessage = attribute.FormatErrorMessage("0");

【讨论】:

这似乎不起作用;我的函数 GetAttributeAdapter 根本没有被调用,即使我在 MVC 之前添加了单例服务。有什么想法吗? @youen,这听起来很奇怪。您是否将AddDataAnnotationsLocalization()-call 附加到AddMvc()-call 上?此外,MVC 框架会进行大量缓存,因此不一定会在每个请求上调用该方法,因此断点等可能并不总是在您期望的时候被命中。 @Anders 我已经尝试过您的解决方案,但我遇到了一个奇怪的问题:当我使用 [EmailAddress] 注释时会调用自定义 GetAttributeAdapter,但在使用 [Required] 或 [RequiredAttribute] 时不会调用它]. @Anders 看起来是同一个问题:***.com/questions/50888963/… 我也有同样的问题 - GetAttributeAdapter 仅针对某些属性(例如 EmailAddress)被调用,而不是必需、MaxLength 等。【参考方案2】:

我尝试在 DataAccess 项目中将 ErrorMessageResourceName 和 ErrorMessageResourceType 设置为我的共享资源文件名“Strings.resx”和“Strings.de.resx”:

   [Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]

我也尝试将设置名称设为 RequiredAttribute_ValidationError - 但它不起作用。

您在正确的轨道上,但您不一定需要设置 ErrorMessageResourceName / ErrorMessageResourceType 属性。

我们是否可以在ValidationAttributeAdapter&lt;TAttribute&gt; 的source code 中看到,使用_stringLocalizer 版本的条件是ErrorMessage 不是nullErrorMessageResourceName/ErrorMessageResourceTypenull

换句话说,当您不设置任何属性或仅设置ErrorMessage 时。所以一个普通的[Required] 应该可以工作(参见source 传递给基类构造函数的位置)。

现在,当我们查看 DataAnnotations resource file 时,我们看到名称设置为“RequiredAttribute_ValidationError”,值设置为“0 字段是必需的”。这是默认的英文翻译。

现在,如果您在“Strings.de-DE.resx”中使用带有德语翻译的“RequiredAttribute_ValidationError”(或仅将 Strings.resx 作为后备),它应该使用正确的命名空间厘米。

因此,使用上述配置和 GitHub 存储库中的字符串,您应该能够在没有额外属性的情况下使本地化工作。

【讨论】:

您好曾,感谢您的回答,部分正确。但是如果不设置ErrorMessage 属性,它就不起作用。一个普通的 [RequiredAttribute] 不足以显示翻译后的验证消息。 @Sven:你真的把翻译放在正确的文件里了吗?请记住,在您的资源文件中,您必须使用 RequiredAttribute_ValidationError 及其特定于语言的模型文件(如 Models/MyModel.resxModels/MyModel.de-DE.resx【参考方案3】:

事实证明ValidationAttributeAdapterProvider 方法不起作用,因为它仅用于“客户端验证属性”(这对我来说没有多大意义,因为属性是在服务器模型上指定的) .

但我找到了一个解决方案,可以用自定义消息覆盖所有属性。它还能够注入字段名称翻译,而不会到处乱吐[Display]。这是实际操作中的约定优于配置。

此外,作为奖励,此解决方案会覆盖甚至在验证发生之前使用的默认模型绑定错误文本。一个警告 - 如果您收到 JSON 数据,那么 Json.Net 错误将被合并到 ModelState 错误中,并且不会使用默认绑定错误。我还没有想出如何防止这种情况发生。

因此,您需要以下三个课程:

    public class LocalizableValidationMetadataProvider : IValidationMetadataProvider
    
        private IStringLocalizer _stringLocalizer;
        private Type _injectableType;

        public LocalizableValidationMetadataProvider(IStringLocalizer stringLocalizer, Type injectableType)
        
            _stringLocalizer = stringLocalizer;
            _injectableType = injectableType;
        

        public void CreateValidationMetadata(ValidationMetadataProviderContext context)
        
            // ignore non-properties and types that do not match some model base type
            if (context.Key.ContainerType == null ||
                !_injectableType.IsAssignableFrom(context.Key.ContainerType))
                return;

            // In the code below I assume that expected use of ErrorMessage will be:
            // 1 - not set when it is ok to fill with the default translation from the resource file
            // 2 - set to a specific key in the resources file to override my defaults
            // 3 - never set to a final text value
            var propertyName = context.Key.Name;
            var modelName = context.Key.ContainerType.Name;

            // sanity check 
            if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
                return;

            foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
            
                var tAttr = attribute as ValidationAttribute;
                if (tAttr != null)
                               
                    // at first, assume the text to be generic error
                    var errorName = tAttr.GetType().Name;
                    var fallbackName = errorName + "_ValidationError";      
                    // Will look for generic widely known resource keys like
                    // MaxLengthAttribute_ValidationError
                    // RangeAttribute_ValidationError
                    // EmailAddressAttribute_ValidationError
                    // RequiredAttribute_ValidationError
                    // etc.

                    // Treat errormessage as resource name, if it's set,
                    // otherwise assume default.
                    var name = tAttr.ErrorMessage ?? fallbackName;

                    // At first, attempt to retrieve model specific text
                    var localized = _stringLocalizer[name];

                    // Some attributes come with texts already preset (breaking the rule 3), 
                    // even if we didn't do that explicitly on the attribute.
                    // For example [EmailAddress] has entire message already filled in by MVC.
                    // Therefore we first check if we could find the value by the given key;
                    // if not, then fall back to default name.

                    // Final attempt - default name from property alone
                    if (localized.ResourceNotFound) // missing key or prefilled text
                        localized = _stringLocalizer[fallbackName];

                    // If not found yet, then give up, leave initially determined name as it is
                    var text = localized.ResourceNotFound ? name : localized;

                    tAttr.ErrorMessage = text;
                
            
        
    
    public class LocalizableInjectingDisplayNameProvider : IDisplayMetadataProvider
    
        private IStringLocalizer _stringLocalizer;
        private Type _injectableType;

        public LocalizableInjectingDisplayNameProvider(IStringLocalizer stringLocalizer, Type injectableType)
        
            _stringLocalizer = stringLocalizer;
            _injectableType = injectableType;
        

        public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
        
            // ignore non-properties and types that do not match some model base type
            if (context.Key.ContainerType == null || 
                !_injectableType.IsAssignableFrom(context.Key.ContainerType))
                return;

            // In the code below I assume that expected use of field name will be:
            // 1 - [Display] or Name not set when it is ok to fill with the default translation from the resource file
            // 2 - [Display(Name = x)]set to a specific key in the resources file to override my defaults

            var propertyName = context.Key.Name;
            var modelName = context.Key.ContainerType.Name;

            // sanity check 
            if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
                return;

            var fallbackName = propertyName + "_FieldName";
            // If explicit name is missing, will try to fall back to generic widely known field name,
            // which should exist in resources (such as "Name_FieldName", "Id_FieldName", "Version_FieldName", "DateCreated_FieldName" ...)

            var name = fallbackName;

            // If Display attribute was given, use the last of it
            // to extract the name to use as resource key
            foreach (var attribute in context.PropertyAttributes)
            
                var tAttr = attribute as DisplayAttribute;
                if (tAttr != null)
                
                    // Treat Display.Name as resource name, if it's set,
                    // otherwise assume default. 
                    name = tAttr.Name ?? fallbackName;
                
            

            // At first, attempt to retrieve model specific text
            var localized = _stringLocalizer[name];

            // Final attempt - default name from property alone
            if (localized.ResourceNotFound)
                localized = _stringLocalizer[fallbackName];

            // If not found yet, then give up, leave initially determined name as it is
            var text = localized.ResourceNotFound ? name : localized;

            context.DisplayMetadata.DisplayName = () => text;
        

    
    public static class LocalizedModelBindingMessageExtensions
    
        public static IMvcBuilder AddModelBindingMessagesLocalizer(this IMvcBuilder mvc,
            IServiceCollection services, Type modelBaseType)
        
            var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
            var VL = factory.Create(typeof(ValidationMessagesResource));
            var DL = factory.Create(typeof(FieldNamesResource));

            return mvc.AddMvcOptions(o =>
            
                // for validation error messages
                o.ModelMetadataDetailsProviders.Add(new LocalizableValidationMetadataProvider(VL, modelBaseType));

                // for field names
                o.ModelMetadataDetailsProviders.Add(new LocalizableInjectingDisplayNameProvider(DL, modelBaseType));

                // does not work for JSON models - Json.Net throws its own error messages into ModelState :(
                // ModelBindingMessageProvider is only for FromForm
                // Json works for FromBody and needs a separate format interceptor
                DefaultModelBindingMessageProvider provider = o.ModelBindingMessageProvider;

                provider.SetValueIsInvalidAccessor((v) => VL["FormatHtmlGeneration_ValueIsInvalid", v]);
                provider.SetAttemptedValueIsInvalidAccessor((v, x) => VL["FormatModelState_AttemptedValueIsInvalid", v, x]);
                provider.SetMissingBindRequiredValueAccessor((v) => VL["FormatModelBinding_MissingBindRequiredMember", v]);
                provider.SetMissingKeyOrValueAccessor(() => VL["FormatKeyValuePair_BothKeyAndValueMustBePresent" ]);
                provider.SetMissingRequestBodyRequiredValueAccessor(() => VL["FormatModelBinding_MissingRequestBodyRequiredMember"]);
                provider.SetNonPropertyAttemptedValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyAttemptedValueIsInvalid", v]);
                provider.SetNonPropertyUnknownValueIsInvalidAccessor(() => VL["FormatModelState_UnknownValueIsInvalid"]);
                provider.SetUnknownValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyUnknownValueIsInvalid", v]);
                provider.SetValueMustNotBeNullAccessor((v) => VL["FormatModelBinding_NullValueNotValid", v]);
                provider.SetValueMustBeANumberAccessor((v) => VL["FormatHtmlGeneration_ValueMustBeNumber", v]);
                provider.SetNonPropertyValueMustBeANumberAccessor(() => VL["FormatHtmlGeneration_NonPropertyValueMustBeNumber"]);
            );
        
    

在 Startup.cs 文件的 ConfigureServices 中:

services.AddMvc( ... )
            .AddModelBindingMessagesLocalizer(services, typeof(IDtoModel));

我在这里使用了我的自定义空IDtoModel 接口并将其应用于我所有需要自动本地化错误和字段名称的 API 模型。

创建一个文件夹 Resources 并将空类 ValidationMessagesResource 和 FieldNamesResource 放入其中。 创建 ValidationMessagesResource.ab-CD.resx 和 FieldNamesResource .ab-CD.resx 文件(将 ab-CD 替换为所需的文化)。 填写您需要的键的值,例如FormatModelBinding_MissingBindRequiredMember, MaxLengthAttribute_ValidationError ...

从浏览器启动 API 时,确保将 accept-languages 标头修改为您的文化名称,否则 Core 将使用它而不是默认值。对于只需要单一语言的 API,我更喜欢使用以下代码完全禁用文化提供者:

private readonly CultureInfo[] _supportedCultures = new[] 
                            new CultureInfo("ab-CD")
                        ;

...
var ci = new CultureInfo("ab-CD");

// can customize decimal separator to match your needs - some customers require to go against culture defaults and, for example, use . instead of , as decimal separator or use different date format
/*
  ci.NumberFormat.NumberDecimalSeparator = ".";
  ci.NumberFormat.CurrencyDecimalSeparator = ".";
*/

_defaultRequestCulture = new RequestCulture(ci, ci);


...

services.Configure<RequestLocalizationOptions>(options =>
            
                options.DefaultRequestCulture = _defaultRequestCulture;
                options.SupportedCultures = _supportedCultures;
                options.SupportedUICultures = _supportedCultures;
                options.RequestCultureProviders = new List<IRequestCultureProvider>(); // empty list - use default value always
            );


【讨论】:

它只适用于一个语言环境,但是一旦我改变了语言环境,它将显示上一个语言环境的旧值。似乎这些显示和验证值在应用程序的生命周期内被缓存在某个地方,这个问题有什么解决方案吗? @WajdyEssam 是发生在您的所有自定义文本中还是仅发生在那些 provider.SetValueIsInvalidAccessor 中?此外,对于多语言支持,您应该跳过最后一个带有 RequestLocalizationOptions 的代码,因为这实际上会禁用多语言支持。【参考方案4】:

不幸的是,将数据属性的所有错误消息本地化在一个地方并不是那么简单!因为有不同类型的错误信息,

标准数据属性的错误消息:

[Required]
[Range]
[StringLength]
[Compare]
...etc.

ModelBinding 的错误消息:

ValueIsInvalid
ValueMustNotBeNull
PropertyValueMustBeANumber
...etc.

和身份错误消息:

DuplicateEmail
DuplicateRoleName
InvalidUserName
PasswordRequiresLower
PasswordRequiresUpper
...etc

必须在启动文件中配置每个。还必须考虑额外的客户端验证。

您可以查看这些文章了解更多详细信息,它包含 GitHub 上的实时演示和示例项目:

开发多元文化的网络应用程序: http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application

本地化数据注释: http://www.ziyad.info/en/articles/16-Localizing_DataAnnotations

本地化 ModelBinding 错误消息: http://www.ziyad.info/en/articles/18-Localizing_ModelBinding_Error_Messages

本地化身份错误消息: http://www.ziyad.info/en/articles/20-Localizing_Identity_Error_Messages

和客户端验证: http://ziyad.info/en/articles/19-Configuring_Client_Side_Validation

希望对你有帮助:)

【讨论】:

【参考方案5】:

    public class RequiredExAttribute : RequiredAttribute
    
        public override string FormatErrorMessage(string name)
        
            string Format = GetAFormatStringFromSomewhereAccordingToCurrentCulture();
            return string.Format(Format, name);
        
    

    ...

    public class MyModel
    
       [RequiredEx]
       public string Name  get; set; 
    

【讨论】:

以上是关于ASP.NET Core 2.0 中RequiredAttribute 的本地化的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET CORE 2.0 中的 FromUri

ApplicationUser在Asp.Net Core 2.0中的ActionFilter中?

ASP.NET Core 2.0 中的多个身份

如何在 ASP.NET Core 2.0 中根据路由配置服务身份验证

ASP.NET Core 2.0 中RequiredAttribute 的本地化

跟着老桂学ASP.NET Core 2.0