Repository navigation
Description
Background and Motivation
Microsoft.Extensions.Validation (MEV) currently provides no localization or message customization features beyond those built into System.ComponentModel.DataAnnotations. The only DataAnnotations localization mechanism is static property localization via ErrorMessageResourceType/ErrorMessageResourceName, which requires annotating each attribute instance individually - a verbose approach that also requires recompilation and is impractical for loading translations from databases, JSON files, or other external sources.
ASP.NET Core MVC worked around this with its own IStringLocalizer-based adapter system, but it is tightly coupled to MVC's model metadata pipeline and cannot be reused in Minimal APIs, Blazor, or non-web hosts.
This proposal adds localization support to MEV that:
- Provides integration of
Microsoft.Extensions.Localizationinto MEV that any consumer (Minimal APIs, Blazor, future hosts) picks up automatically. - Localizes validation attribute error messages and display names with the same rules as MVC.
- Provides a mechanism for formatting localized templates with attribute-specific arguments (a framework-independent replacement for MVC's attribute adapter system).
- Supports convention-based resource-key selection without requiring explicit
ErrorMessageon every attribute instance.
Issues:
- dotnet/aspnetcore#12158 (Blazor data annotations localization)
- dotnet/aspnetcore#64626 (Minimal API validation localization)
- dotnet/aspnetcore#4848, #33073, dotnet/runtime#24084 (localize built-in attribute messages without per-attribute
ErrorMessage)
Implementation: #67987
Proposed API
namespace Microsoft.Extensions.Validation;
public class ValidationOptions
{
+ public Func<Type, IStringLocalizerFactory, IStringLocalizer> LocalizerProvider { get; set; } = (type, factory) => factory.Create(type);
+ public Func<ValidationMessageKeyContext, string?>? MessageKeyProvider { get; set; }
}
+public sealed class ValidationMessageKeyContext
+{
+ /// <summary>
+ /// Gets the type of the validator that produced the error being localized. For DataAnnotations,
+ /// this is the validation attribute type (for example, <c>typeof(RequiredAttribute)</c>).
+ /// </summary>
+ public required object Validator { get; init; }
+
+ /// <summary>
+ /// Gets the name of the member being validated: the property name for property validation, the
+ /// parameter name for parameter validation, or the type name for type-level validation.
+ /// </summary>
+ public required string MemberName { get; init; }
+
+ /// <summary>
+ /// Gets the type associated with the member being validated: the containing type for a property,
+ /// the validated type itself for type-level validation, or the parameter's own type for a parameter.
+ /// </summary>
+ public required Type DeclaringType { get; init; }
+}
+public interface IValidationMessageFormatter
+{
+ string FormatMessage(CultureInfo culture, string messageTemplate, string displayName)
+}Usage Examples
Localization activates automatically once an IStringLocalizerFactory is available in DI (e.g. via AddLocalization).
1. Default localization
builder.Services.AddLocalization();
builder.Services.AddValidation();
[ValidatableType]
public class CustomerModel
{
[Display(Name = "Customer Name")] // "Customer Name" looked up as a resource key
[Required(ErrorMessage = "NameRequired")] // "NameRequired" looked up as the message key
public string? Name { get; set; }
}Keys are resolved against CustomerModel's resources. A miss falls back to the attribute's default message.
2. Shared resource file
builder.Services.AddLocalization();
builder.Services.AddValidation(options =>
{
options.LocalizerProvider = (_, factory) => factory.Create(typeof(ValidationMessages));
});3. Non-resource based localization
builder.Services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();
builder.Services.AddValidation();4. Custom attribute formatting
public sealed class DivisibleByAttribute : ValidationAttribute, IValidationMessageFormatter
{
public int Divisor { get; init; }
public string FormatMessage(CultureInfo culture, string template, string displayName)
=> string.Format(culture, template, displayName, Divisor); // fills {0}=name, {1}=divisor
}5. Convention-based keys
builder.Services.AddValidation(options =>
{
// e.g. [Range] -> "RangeAttribute_Error"
options.MessageKeyProvider = ctx => $"{ctx.ValidatorType.Name}_Error";
});The provider is used to compute the key when ErrorMessage is not specified. An explicit [Required(ErrorMessage = "…")] still wins and is used as the key to allow local override. Resource-based attributes (ErrorMessageResourceType, [Display(ResourceType = …)]) bypass this pipeline altogether since they're already localized.
MessageKeyProvider context exposes ValidatorType, MemberName, and DeclaringType.
Alternative Designs
Validator instance passed as object into MessageKeyProvider instead of Type. This covers type-matching (via GetType()) but also attributes with multiple possible messages depending on the instance's properties (see RangeAttribute).
Built-in list of conventions instead of MessageKeyProvider. Instead of exposing optional callback used for fallback key computations, we can simply specify of list of conventions we the default implementation uses and document it for users. E.g., we try the order:
$"{MemberName}_{DeclaringTypeName}_{AttributeName}_Error"$"{DeclaringTypeName}_{AttributeName}_Error"$"{AttributeName}_Error"- Fall back to non-localized message
Localizing IValidatableObject.Validate results. Messages produced by IValidatableObject are not passed through IStringLocalizer - they are expected to be in the user's language already (typically constructed inline using DI-resolved services from ValidationContext). An opt-in pass-through can be added later if demand emerges.
Activity
dotnet-policy-service commentedon Apr 20, 2026
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
github-actions commentedon Apr 20, 2026
Triage Summary
Area:
area-blazor+area-minimal(already labeled — proposal targetsMicrosoft.Extensions.Validation, which is the shared validation layer consumed by both Minimal APIs and Blazor)Type:
api-proposal— formal API addition proposal with detailed proposed shapes, usage examples, and alternative design discussionPotential Duplicates
Notes
api-ready-for-reviewand is assigned to milestone 11.0-preview5, indicating it has progressed through internal review.5 remaining items
danroth27 commentedon Jul 29, 2026
It would be great if we could eventually ship a default
MessageKeyProviderso that #4848 is addressed out of the box — todayMessageKeyProviderdefaults tonull, so an app that enables localization but doesn't configure a convention still needs an explicitErrorMessageon every attribute, which is the state #4848 has been open about since 2017.The obstacle is that a convention keyed on
ValidatorTypealone can't match the message set the framework itself produces, because several built-in attributes select a template based on attribute state:StringLengthAttribute_ValidationErrorvs.StringLengthAttribute_ValidationErrorIncludingMinimumRangeAttribute_ValidationErrorplus_MinExclusive,_MaxExclusive, and_MinExclusive_MaxExclusiveMinimumLengthand the exclusive-bound flags aren't reachable fromValidationMessageKeyContext, so a convention can only produce one key per attribute type. An app localizing[StringLength(20)]and[StringLength(20, MinimumLength = 4)]gets a single template for both.The obvious fix — putting the
ValidationAttributeinstance on the context, asErrorMessageLocalizationContextdoes in the proposal above — runs against one of #67987's stated goals: "No DataAnnotations references in the core MEV API." That decoupling seems right, so I'm not suggesting we undo it.So the question is how a key convention should reach validator state without recoupling MEV to DataAnnotations. Letting the validator supply its own key — similar to the way
IValidationMessageFormatterlets an attribute supply its own formatting — might fit the existing grain better than widening the context. Worth settling before the API locks, or is one key per validator type good enough for a default convention?dotnet-policy-service commentedon Jul 30, 2026
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
halter73 commentedon Jul 30, 2026
API Review Notes:
"Customer Name" looked up as a resource keyvs."NameRequired" looked up as the message key. Are these different keys?ValidationMessagesin(_, factory) => factory.Create(typeof(ValidationMessages)need to have anything special in/on it?IValidationMessageFormatter, what if you don't own theValidationAttribute?IValidationMessageFormatter<TAttribute>? Keyed DI?objectalwaysType?Validatortypeobjectinstead ofValidationAttribute?MessageKeyProvideris only used as a fallback whenValidatationAttributesdon't specify anErrorMessage, e.g.[Required]MessageKeyProviderfor now?MessageKeyProviderlater.$"{MemberName}_{DeclaringTypeName}_{AttributeName}_Error"$"{DeclaringTypeName}_{AttributeName}_Error"$"{AttributeName}_Error"API Approved!
namespace Microsoft.Extensions.Validation; public class ValidationOptions { + public Func<Type, IStringLocalizerFactory, IStringLocalizer> LocalizerProvider { get; set; } = (type, factory) => factory.Create(type); } +public interface IValidationMessageFormatter +{ + string FormatMessage(CultureInfo culture, string messageTemplate, string displayName) +}oroztocil commentedon Aug 3, 2026
Conventional error message keys: variants and open questions
During the API review, we decided to remove the public
MessageKeyProvidercustomization and instead implement a built-in set of conventions for error message key fallbacks. They are used only when an attribute sets neitherErrorMessagenorErrorMessageResourceType. The lookup walks a chain from most specific to least specific, falling back to the framework's English message if every tier misses.The following is a list of questions to decide before we settle on the final set of conventions that will become a part of the behavioral contract.
A. Segment order (proposed: A2 type first)
We dicsussed including the following data in the conventional keys:
These segments can be ordered in multiple ways in the convention (see below). Regardless of the choses order, the idea is that the convention produces three keys, tried in order. The first one that resolves wins:
CustomerModel_Name_RequiredAttribute_ErrorCustomerModel_RequiredAttribute_ErrorRequiredAttribute_ErrorA1 suffix chain
Name.CustomerModel.A2 type first
A3 attribute first
A4 drop tier 2
Orthogonal to A1–A3: regardless of the segment order we pick, we should question whether the
middle tier earns its place. The chain becomes two tiers instead of three:
[Required]onCustomerModel", which is a rare thing to want inpractice. Teams reach for either "this specific field" (tier 1) or "all
[Required]app wide"(tier 3). The per-type tier mostly exists to give the cascade a middle rung.
IStringLocalizerlookups.previously missed to start resolving.
member of that model.
B. Omit framework types (proposed: do not omit)
In case of method parameter validation, the parameter name is used for the type name segment (following MVC convention for localization resource resolution). Should we omit the type name (and the tiers that use it) in case of framework types? Or should we produce conventional keys such as
string_Name_RequiredAttribute_Error?C. Attribute segment (proposed: full name)
Should the attribute segment of the key keep the word "Attribute"? I.e.
RequiredAttribute_ErrororRequired_Error?C1 full type name
Type.Name.C2 trimmed
[Required]and[StringLength(50)]as written in the model.Length_Errorread less clearly in a shared file.Note: neither option matches the runtime's own resource names, which are irregular (
RegexAttribute_ValidationError,CompareAttribute_MustMatch,EmailAddressAttribute_Invalid). Internal framework parity is neither achievable nor beneficial either way.D. Separator (proposed: underscore)
What separator should be put between the segments? The proposal is to use an underscore.
src/Http,src/Mvcandsrc/Components, zero contain a dot and 347 contain an underscore.ResXFileCodeGeneratormangles dots into underscores anyway, and some JSON backed localizers treat dots as nesting.E. Generic suffix (proposed: use
_Error)Should the conventional keys contain a generic suffix? The proposal is to add
_Errorto every conventional key.Required,Range,Length, which are plausible existing UI label keys in a shared resx.oroztocil commentedon Aug 3, 2026
@danroth27
Important context here is that these two multi message attributes were never really compatible with localization in the first place. The extra templates for
StringLengthandRangeattributes are only used on the default path where no custom message is supplied. Both existing override mechanisms (ErrorMessage, orErrorMessageResourceType+ErrorMessageResourceName), replace that whole capability with a single template that is then used for every parameter combination. So an app localizing these messages through the framework's own resource based mechanism today already gets one message per attribute regardless of the parameter values. The convention based fallback proposed here is the same kind of user supplied template, so keying it per attribute type matches the behavior.halter73 commentedon Aug 10, 2026
API follow up notes:
intorstringinstead of the parent/declaring type is weirdRequiredAttribute_ErrororRequired_Error)_or.as the separator? Something else?_. It reads better, and it's on you if you use_in your type names._Errorsuffix.[release/11.0] Add message template formatting to DataAnnotations val…