ASP.NET Core 2.0中RequiredAttribute的本地化

Sve*_*ven 6 c# asp.net-core

我在新的.NET Core项目中苦苦挣扎.我有2个项目:

  • 具有模型和数据注释的DataAccess项目(例如RequiredAttribute)
  • 具有MVC视图等的Web项目

我希望在一个地方全局本地化所有验证属性,以获得类似MVC 5的行为.这可能吗?

我不想为模型/视图等提供单独的语言文件.

使用SharedResources.resx文件和本地化的DataAnnotation消息时,Microsofts文档不是很清楚.

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

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

[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
Run Code Online (Sandbox Code Playgroud)

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

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

我读过几篇文章,但我找不到它为什么不起作用的原因.

编辑:我到目前为止:

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];
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

3.)添加了一个带有"RequiredAttribute_ValidationError"项的Strings.de-DE.resx文件

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();
        }
Run Code Online (Sandbox Code Playgroud)

我按照这里的说明操作但不起作用:https: //damienbod.com/2017/11/01/shared-localization-in-asp-net-core-mvc/

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

And*_*ers 11

正如@Sven在对Tseng的回答中所指出的那样,它仍然要求你指定一个明确的ErrorMessage,这非常繁琐.

问题产生于逻辑ValidationAttributeAdapter<TAttribute>.GetErrorMessage()用于决定是否使用提供的IStringLocalizer.我使用以下解决方案来解决该问题:

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

    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);
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. Startup.ConfigureServices()在调用之前注册适配器AddMvc():

    services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider, LocalizedValidationAttributeAdapterProvider>();
    
    Run Code Online (Sandbox Code Playgroud)

我更喜欢根据实际属性使用"更严格"的资源名称,因此上面的代码将查找"必需"和"DataType_Password"等资源名称,但这当然可以通过多种方式进行自定义.

如果您更喜欢基于属性的默认消息的资源名称,您可以改为:

attribute.ErrorMessage = attribute.FormatErrorMessage("{0}");
Run Code Online (Sandbox Code Playgroud)


Tse*_*eng 5

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

   [Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
Run Code Online (Sandbox Code Playgroud)

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

您的方向正确,但不一定需要设置ErrorMessageResourceName/ ErrorMessageResourceType属性。

我们可以在源代码中看到,ValidationAttributeAdapter<TAttribute>使用_stringLocalizer版本的条件是when ErrorMessage不是nulland ErrorMessageResourceName/ ErrorMessageResourceTypeare null

换句话说,当您未设置任何属性或仅设置时ErrorMessage。因此,普通的格式[Required]应该可以正常工作(请参见将传递到基类构造函数的地方)。

现在,当我们查看DataAnnotations资源文件时,我们看到名称设置为“ RequiredAttribute_ValidationError”,值设置为“ {0}字段为必填”。这是默认的英语翻译。

现在,如果您在“ Strings.de-DE.resx”中使用德语翻译的“ RequiredAttribute_ValidationError”(或仅将Strings.resx作为后备),则它与注释中已更正的名称空间一起使用。

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


Jus*_*tin 5

事实证明,该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;
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)
    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;
        }

    }
Run Code Online (Sandbox Code Playgroud)
    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"]);
            });
        }
    }
Run Code Online (Sandbox Code Playgroud)

在 Startup.cs 文件中的 ConfigureServices 中:

services.AddMvc( ... )
            .AddModelBindingMessagesLocalizer(services, typeof(IDtoModel));
Run Code Online (Sandbox Code Playgroud)

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

创建一个文件夹 Resources 并将空类 ValidationMessagesResource 和 FieldNamesResource 放入其中。创建 ValidationMessagesResource.ab-CD.resx 和 FieldNamesResource .ab-CD.resx 文件(将 ab-CD 替换为您所需的区域性)。填写您需要的键的值,例如FormatModelBinding_MissingBindRequiredMemberMaxLengthAttribute_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
            });


Run Code Online (Sandbox Code Playgroud)