Skip to content

[API Proposal] Localization support for Microsoft.Extensions.Validation #66392

Description

@oroztocil

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.Localization into 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 ErrorMessage on every attribute instance.

Issues:

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:

  1. $"{MemberName}_{DeclaringTypeName}_{AttributeName}_Error"
  2. $"{DeclaringTypeName}_{AttributeName}_Error"
  3. $"{AttributeName}_Error"
  4. 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

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    area-blazorIncludes: Blazor, Razor Components
    api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviews
    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etc
    feature-validationIssues related to model validation in minimal and controller-based APIs
    on Apr 20, 2026
  2. dotnet-policy-service commented on Apr 20, 2026

    @dotnet-policy-service
    Contributor

    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:

    • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
    • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
    • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.
  3. removed
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Apr 20, 2026
  4. added this to the 11.0-preview5 milestone on Apr 20, 2026
  5. github-actions commented on Apr 20, 2026

    @github-actions
    Contributor

    Triage Summary

    Area: area-blazor + area-minimal (already labeled — proposal targets Microsoft.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 discussion

    Potential Duplicates

    Notes

    Generated by Issue Triage Agent for dotnet/aspnetcore for issue #66392 · ● 382K · ◷

  6. added
    api-needs-workAPI needs work before it is approved, it is NOT ready for implementation
    and removed
    api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviews
    on Jun 18, 2026
  7. 5 remaining items

  8. modified the milestones: 11.0.0, 11.0-rc1 on Jul 28, 2026
  9. danroth27 commented on Jul 29, 2026

    @danroth27
    Member

    It would be great if we could eventually ship a default MessageKeyProvider so that #4848 is addressed out of the box — today MessageKeyProvider defaults to null, so an app that enables localization but doesn't configure a convention still needs an explicit ErrorMessage on every attribute, which is the state #4848 has been open about since 2017.

    The obstacle is that a convention keyed on ValidatorType alone can't match the message set the framework itself produces, because several built-in attributes select a template based on attribute state:

    • StringLengthAttribute_ValidationError vs. StringLengthAttribute_ValidationErrorIncludingMinimum
    • RangeAttribute_ValidationError plus _MinExclusive, _MaxExclusive, and _MinExclusive_MaxExclusive

    MinimumLength and the exclusive-bound flags aren't reachable from ValidationMessageKeyContext, 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 ValidationAttribute instance on the context, as ErrorMessageLocalizationContext does 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 IValidationMessageFormatter lets 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?

  10. added
    api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviews
    and removed
    api-needs-workAPI needs work before it is approved, it is NOT ready for implementation
    on Jul 30, 2026
  11. dotnet-policy-service commented on Jul 30, 2026

    @dotnet-policy-service
    Contributor

    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:

    • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
    • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
    • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.
  12. halter73 commented on Jul 30, 2026

    @halter73
    Member

    API Review Notes:

    • "Customer Name" looked up as a resource key vs. "NameRequired" looked up as the message key. Are these different keys?
      • No. They are the same like MVC. We could try to distinguish, but it would require more API.
    • Does ValidationMessages in (_, factory) => factory.Create(typeof(ValidationMessages) need to have anything special in/on it?
      • No. This is just used to look up the right assembly for the resources.
    • For IValidationMessageFormatter, what if you don't own the ValidationAttribute?
      • Not with the current design
      • Can we resolve something from DI? Maybe IValidationMessageFormatter<TAttribute>? Keyed DI?
      • Let's table it for now. Using Keyed DI to look up an IValidationMessageFormatter is technically not breaking API change.
    • "(for example, typeof(RequiredAttribute))" Is the object always Type?
      • It basically never is. We want to be able to see what parameters are set to by inspecting the Attribute object instance.
      • Why is the Validator type object instead of ValidationAttribute?
        • This is the last tie between Microsoft.Extensions.Validation and System.ComponentModel
    • MessageKeyProvider is only used as a fallback when ValidatationAttributes don't specify an ErrorMessage, e.g. [Required]
    • Can we not add MessageKeyProvider for now?
      • It'd be nice to not require people to specify one, so we might want a convention anyway.
      • We could always add an optional MessageKeyProvider later.
      • Let's remove it for now.
      • This might involve multiple lookups with less specificity. We like something similar to the current "Alternative design"
        1. $"{MemberName}_{DeclaringTypeName}_{AttributeName}_Error"
        2. $"{DeclaringTypeName}_{AttributeName}_Error"
        3. $"{AttributeName}_Error"
        4. Fall back to non-localized message

    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)
    +}
  13. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed on Jul 30, 2026
  14. removed
    api-ready-for-reviewAPI is ready for formal API review - https://github.com/dotnet/apireviews
    on Jul 30, 2026
  15. oroztocil commented on Aug 3, 2026

    @oroztocil
    MemberAuthor

    Conventional error message keys: variants and open questions

    During the API review, we decided to remove the public MessageKeyProvider customization and instead implement a built-in set of conventions for error message key fallbacks. They are used only when an attribute sets neither ErrorMessage nor ErrorMessageResourceType. 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:

    • the member name: property name, parameter name, or, for an attribute applied to a type, each member name reported by the validation result
    • the declaring type name, the parameter type name, or the validated type name for type-level attributes
    • the attribute type name

    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:

    Tier Scope Example, using A2 below
    1 the annotated member CustomerModel_Name_RequiredAttribute_Error
    2 the declaring type CustomerModel_RequiredAttribute_Error
    3 global default for the attribute RequiredAttribute_Error

    A1 suffix chain

    Name_CustomerModel_RequiredAttribute_Error
    CustomerModel_RequiredAttribute_Error
    RequiredAttribute_Error
    
    • Pro: each tier is a literal suffix of the tier above, so the chain is pure prefix stripping.
    • Pro: this is the shape sketched in the API review notes.
    • Con: worst grouping in a sorted resx. Keys about one model scatter across the file.
    • Con: reads backwards against .NET naming. Nobody writes Name.CustomerModel.

    A2 type first

    CustomerModel_Name_RequiredAttribute_Error
    CustomerModel_RequiredAttribute_Error
    RequiredAttribute_Error
    
    • Pro: groups by model, so all messages for one form or screen sit together.
    • Pro: reads like a normal .NET qualified name, so keys are guessable without docs.
    • Pro: matches the Rails i18n cascade tier for tier.
    • Con: tier 1 to tier 2 drops a middle segment rather than a prefix.
    • Con: a per member override does not sit near the global default it overrides.

    A3 attribute first

    RequiredAttribute_CustomerModel_Name_Error
    RequiredAttribute_CustomerModel_Error
    RequiredAttribute_Error
    
    • Pro: every override lands next to the default it overrides, helping translators keep tone consistent.
    • Pro: groups by attribute, suiting "translate all the built ins" as the dominant task.
    • Con: splits everything about one model across the file.
    • Con: leads with the least meaningful segment when hunting for a specific field.

    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:

    CustomerModel_Name_RequiredAttribute_Error
    RequiredAttribute_Error
    
    • Pro: tier 2 means "every [Required] on CustomerModel", which is a rare thing to want in
      practice. 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.
    • Pro: less IStringLocalizer lookups.
    • Pro: adding a tier later is compatible in the sense that matters, since it only causes keys that
      previously missed to start resolving.
    • Con: no way to restyle the tone of one attribute for one model without repeating the key for every
      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?

    • Pro omit: built-in types do not carry business-specific meaning.
    • Con omit: special-casing complicates the implementation and explainability of the conventions.

    C. Attribute segment (proposed: full name)

    Should the attribute segment of the key keep the word "Attribute"? I.e. RequiredAttribute_Error or Required_Error?

    C1 full type name

    CustomerModel_Name_RequiredAttribute_Error
    CustomerModel_Name_StringLengthAttribute_Error
    
    • Pro: zero logic, exactly Type.Name.
    • Pro: a future analyzer could verify keys mechanically.
    • Con: not the form the developer sees in their own source.
    • Con: nine extra characters on every key.

    C2 trimmed

    CustomerModel_Name_Required_Error
    CustomerModel_Name_StringLength_Error
    
    • Pro: matches [Required] and [StringLength(50)] as written in the model.
    • Pro: the rule is identical to C# attribute name resolution, which every .NET developer knows.
    • Con: needs a documented trimming rule.
    • Con: generic names like Length_Error read 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.

    CustomerModel_Name_Required_Error     vs     CustomerModel.Name.Required.Error
    
    • Pro underscore: of 431 distinct resx keys across src/Http, src/Mvc and src/Components, zero contain a dot and 347 contain an underscore.
    • Pro underscore: resx is flat, so a dot conveys no structure, unlike Rails and Spring where the file format is hierarchical.
    • Pro underscore: ResXFileCodeGenerator mangles dots into underscores anyway, and some JSON backed localizers treat dots as nesting.
    • Pro dot: unambiguous segmentation, since a dot cannot appear in a C# identifier.

    E. Generic suffix (proposed: use _Error)

    Should the conventional keys contain a generic suffix? The proposal is to add _Error to every conventional key.

    • Pro suffix: tier 3 keys are otherwise bare words like Required, Range, Length, which are plausible existing UI label keys in a shared resx.
    • Pro suffix: reserves room if MEV ever localizes other types of messages.
    • Con suffix: six extra characters.
  16. oroztocil commented on Aug 3, 2026

    @oroztocil
    MemberAuthor

    @danroth27

    The obstacle is that a convention keyed on ValidatorType alone can't match the message set the framework itself produces, because several built-in attributes select a template based on attribute state:

    * `StringLengthAttribute_ValidationError` vs. `StringLengthAttribute_ValidationErrorIncludingMinimum`
    
    * `RangeAttribute_ValidationError` plus `_MinExclusive`, `_MaxExclusive`, and `_MinExclusive_MaxExclusive`
    

    MinimumLength and the exclusive-bound flags aren't reachable from ValidationMessageKeyContext, 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.

    Important context here is that these two multi message attributes were never really compatible with localization in the first place. The extra templates for StringLength and Range attributes are only used on the default path where no custom message is supplied. Both existing override mechanisms (ErrorMessage, or ErrorMessageResourceType + 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.

  17. halter73 commented on Aug 10, 2026

    @halter73
    Member

    API follow up notes:

    • Of the proposed "segment orders", A1-A4, what convention do we like the most?
      • A2 as proposed
    • What about omitting framework types for minimal API parameters
      • Having the "CustomerModel" being the type of the named parameter which could be a built-in like int or string instead of the parent/declaring type is weird
      • However, dropping type info from the lookup could be weirder. If we omitted any type, there could be conflicts between parameter names and "CustomerModel" types
      • We don't want to omit types from the lookup at all, and we especially don't want to special-case built-in types.
    • Should the "attribute segment" include the full type name? Or remove the "Attribute" suffix if any (e.g. RequiredAttribute_Error or Required_Error)
      • Let's keep the "Attribute" suffix.
    • Should we use _ or . as the separator? Something else?
      • Let's use _. It reads better, and it's on you if you use _ in your type names.
    • We like the _Error suffix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-blazorIncludes: Blazor, Razor Componentsarea-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-validationIssues related to model validation in minimal and controller-based APIs

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions