Skip to content

Add localization support to Microsoft.Extensions.Validation#66646

Merged
oroztocil merged 15 commits into
mainfrom
oroztocil/validation-localization4
May 18, 2026
Merged

Add localization support to Microsoft.Extensions.Validation#66646
oroztocil merged 15 commits into
mainfrom
oroztocil/validation-localization4

Conversation

@oroztocil

@oroztocil oroztocil commented May 11, 2026

Copy link
Copy Markdown
Member

Description

This PR adds localization support for validation error messages and display names to Microsoft.Extensions.Validation and transitively to the consumers of the package - in particular, Minimal APIs (endpoint filter) and Blazor (DataAnnotationsValidator). It introduces a single extensibility point on ValidationOptions and ships a default IStringLocalizer-based implementation in a separate optional package.

Closes #12158
Design doc: #65539

Motivation

System.ComponentModel.DataAnnotations only supports localization through ErrorMessageResourceType/ErrorMessageResourceName and DisplayAttribute.ResourceType - verbose, recompile-bound, and tied to static resource properties. ASP.NET Core MVC works around this with attribute adapters, but the adapter system is MVC-specific and does not extend to Minimal APIs, Blazor SSR, or non-web hosts.

Microsoft.Extensions.Validation (introduced in .NET 10) is the new validation infrastructure for Minimal APIs and Blazor. Until now it had no localization story beyond what the BCL provides. This PR closes that gap with a single extensibility point that any framework integration picks up automatically.

How it works

flowchart LR
    subgraph Val["Microsoft.Extensions.Validation"]
        VO["<b>ValidationOptions</b><br/>Localizer property"]
        VPI["<b>Validatable*Info</b><br/>(Type / Property / Parameter)"]
        DNI["<b>DisplayNameInfo</b><br/>display-name strategy"]
        IVL["<b>IValidationLocalizer</b><br/>extensibility point"]
        VO -. provides .-> IVL
        VPI --> DNI
        DNI --> IVL
    end

    subgraph ValLoc["Microsoft.Extensions.Validation.Localization (optional)"]
        AVL["<b>AddValidationLocalization()</b>"]
        DVL["<b>DefaultValidationLocalizer</b>"]
        Reg["<b>ValidationAttributeFormatterRegistry</b><br/>+ built-in formatters"]
        AVL --> DVL
        DVL --> Reg
    end

    subgraph Loc["Microsoft.Extensions.Localization"]
       ISL["<b>IStringLocalizer</b><br/>via IStringLocalizerFactory"]
    end

    DVL --> ISL
    AVL -.->|"options.Localizer ??= ..."| VO
    DVL -. implements .-> IVL
Loading
  1. Core package (Microsoft.Extensions.Validation) gets one new property - ValidationOptions.Localizer of type IValidationLocalizer?. When set, the validation pipeline calls into it to resolve display names and error messages. When null (the default), behavior is unchanged.
  2. Localization package (Microsoft.Extensions.Validation.Localization) provides AddValidationLocalization(). It registers an IConfigureOptions<ValidationOptions> bridge that uses ??= to set ValidationOptions.Localizer to a default IStringLocalizer-backed implementation. Explicit user assignment always wins.
  3. Source generator emits a DisplayNameInfo subclass per validated member that knows how to produce its display name. For literal [Display(Name = "...")] and [DisplayName("...")] the emitted strategy carries the literal value and runs it through IValidationLocalizer.ResolveDisplayName when a localizer is configured. For [Display(Name = ..., ResourceType = ...)] the emitted strategy calls DisplayAttribute.GetName() and bypasses the localizer (the resource lookup is the canonical localized name; double-localizing through IValidationLocalizer would be incorrect). See [API Proposal] Improve static display name resolution in Microsoft.Extensions.Validation #66378 for the DisplayNameInfo API.
  4. Custom validation attributes that use additional template arguments (beyond {0} for the display name) participate via IValidationAttributeFormatter - either by implementing the interface on the attribute itself, or by registering a factory in ValidationLocalizationOptions.AttributeFormatters.
sequenceDiagram
    participant Pipeline as Validation pipeline
    participant DNI as DisplayNameInfo
    participant Localizer as IValidationLocalizer
    participant Formatter as IValidationAttributeFormatter

    Note over Pipeline: Per-attribute validation<br/>produces a ValidationResult
    Pipeline->>DNI: GetDisplayName(context, memberName, declaringType)
    Note over DNI: Literal strategy may delegate to localizer.<br/>Resource strategy bypasses it.
    DNI-->>Pipeline: display name<br/>(or null → fall back to member name)

    Pipeline->>Localizer: ResolveErrorMessage(context)
    Note over Localizer: bypassed if attribute has<br/>ErrorMessageResourceType
    Localizer->>Formatter: FormatErrorMessage(culture, template, displayName)
    Formatter-->>Localizer: fully formatted message
    Localizer-->>Pipeline: localized error message<br/>(or null → use attribute default)
Loading

New public API surface

Core: Microsoft.Extensions.Validation

namespace Microsoft.Extensions.Validation;

// Single integration point for any localizer implementation
public interface IValidationLocalizer
{
    string? ResolveDisplayName(in DisplayNameLocalizationContext context);
    string? ResolveErrorMessage(in ErrorMessageLocalizationContext context);
}

public readonly struct DisplayNameLocalizationContext
{
    public Type? DeclaringType { get; init; }
    public required string? DisplayName { get; init; }   // [Display(Name="...")] literal
    public required string MemberName { get; init; }     // CLR member name (fallback)
}

public readonly struct ErrorMessageLocalizationContext
{
    public required string MemberName { get; init; }
    public required string DisplayName { get; init; }    // already-resolved display name
    public Type? DeclaringType { get; init; }
    public required ValidationAttribute Attribute { get; init; }
}

public class ValidationOptions
{
    public IValidationLocalizer? Localizer { get; set; }
    // (existing members elided)
}

New package: Microsoft.Extensions.Validation.Localization

namespace Microsoft.Extensions.DependencyInjection;

public static class ValidationLocalizationServiceCollectionExtensions
{
    public static IServiceCollection AddValidationLocalization(
        this IServiceCollection services,
        Action<ValidationLocalizationOptions>? configureOptions = null);

    public static IServiceCollection AddValidationLocalization<TResource>(
        this IServiceCollection services,
        Action<ValidationLocalizationOptions>? configureOptions = null);
}

namespace Microsoft.Extensions.Validation.Localization;

public class ValidationLocalizationOptions
{
    public Func<Type?, IStringLocalizerFactory, IStringLocalizer>? LocalizerProvider { get; set; }
    public Func<ErrorMessageLocalizationContext, string?>? ErrorMessageKeyProvider { get; set; }
    public ValidationAttributeFormatterRegistry AttributeFormatters { get; } = new();
}

public interface IValidationAttributeFormatter
{
    string FormatErrorMessage(CultureInfo culture, string messageTemplate, string displayName);
}

public sealed class ValidationAttributeFormatterRegistry
{
    public ValidationAttributeFormatterRegistry();
    public void AddFormatter<TAttribute>(Func<TAttribute, IValidationAttributeFormatter> factory)
        where TAttribute : ValidationAttribute;
    public IValidationAttributeFormatter? GetFormatter(ValidationAttribute attribute);
}

Constructor signature changes on Validatable*Info

ValidatablePropertyInfo, ValidatableParameterInfo, and ValidatableTypeInfo (all [Experimental("ASP0029")]) get an optional DisplayNameInfo? parameter that carries the per-member display-name resolution strategy. The previous string displayName parameter is removed; the validation pipeline falls back to the CLR member name when no strategy is supplied. Full API shape and rationale: #66378.

Before After
ValidatablePropertyInfo(Type declaringType, Type propertyType, string name, string displayName) ValidatablePropertyInfo(Type declaringType, Type propertyType, string name, DisplayNameInfo? displayNameInfo = null)
ValidatableParameterInfo(Type parameterType, string name, string displayName) ValidatableParameterInfo(Type parameterType, string name, DisplayNameInfo? displayNameInfo = null)
ValidatableTypeInfo(Type type, IReadOnlyList<...> members) ValidatableTypeInfo(Type type, IReadOnlyList<...> members, DisplayNameInfo? displayNameInfo = null)

Code that consumes the source generator is unaffected (the generator emits the new constructor calls and the corresponding DisplayNameInfo strategies). Code that manually subclasses these abstractions (rare) needs to pass null for displayNameInfo, or supply a custom strategy.

Built-in attribute formatters

Registered automatically by AddValidationLocalization(). Cover all standard DataAnnotations attributes whose default templates use placeholders beyond {0}:

Attribute Format arguments
RangeAttribute displayName, Minimum, Maximum
MinLengthAttribute displayName, Length
MaxLengthAttribute displayName, Length
LengthAttribute displayName, MinimumLength, MaximumLength
StringLengthAttribute displayName, MaximumLength, MinimumLength
RegularExpressionAttribute displayName, Pattern
FileExtensionsAttribute displayName, Extensions
CompareAttribute displayName, OtherPropertyDisplayName ?? OtherProperty

Attributes whose template only uses {0} (e.g. RequiredAttribute, EmailAddressAttribute) need no formatter - the localization pipeline substitutes the display name directly.

Usage

Default - per-type resource files

builder.Services.AddValidation();
builder.Services.AddValidationLocalization();

IStringLocalizerFactory.Create(declaringType) is called for every validated member. Resource files follow the standard convention (Resources/Models.Customer.fr.resx for type Models.Customer).

Shared resource file (recommended for Minimal APIs)

builder.Services.AddValidation();
builder.Services.AddValidationLocalization<SharedValidationMessages>();

The generic overload pre-configures LocalizerProvider to always resolve against typeof(SharedValidationMessages), with a closure-cached singleton IStringLocalizer so the factory is invoked only once per app lifetime.

Convention-based key selection

builder.Services.AddValidationLocalization(options =>
{
    options.ErrorMessageKeyProvider = ctx => $"{ctx.Attribute.GetType().Name}_Error";
});

When ErrorMessage is not set on the attribute, the delegate produces a fallback lookup key (e.g. "RequiredAttribute_Error"). This makes it possible to translate the BCL's built-in messages without setting ErrorMessage on every attribute instance.

Custom validation attributes

Two ways to participate. Option 1 - self-formatting:

class MyRangeAttribute : ValidationAttribute, IValidationAttributeFormatter
{
    public string FormatErrorMessage(CultureInfo culture, string messageTemplate, string displayName)
        => string.Format(culture, messageTemplate, displayName, /* attribute-specific args */);
}

Option 2 - register a formatter (when you don't own the attribute):

builder.Services.AddValidationLocalization(options =>
{
    options.AttributeFormatters.AddFormatter<ThirdPartyAttribute>(
        attr => new ThirdPartyAttributeFormatter(attr));
});

Custom localization backend (no IStringLocalizer)

Implement IValidationLocalizer directly:

builder.Services.AddValidation(options =>
{
    options.Localizer = new MyValidationLocalizer(/* deps */);
});

For DI-resolved dependencies, use PostConfigure<ValidationOptions>.

Decorator pattern (logging, fallback, composition)

Wrap an existing localizer to add cross-cutting behavior:

builder.Services.PostConfigure<ValidationOptions>(options =>
{
    var inner = options.Localizer
        ?? throw new InvalidOperationException("AddValidationLocalization must be called first.");
    options.Localizer = new LoggingValidationLocalizer(inner, logger);
});

Key design decisions

Two packages, not one. Microsoft.Extensions.Validation does not depend on Microsoft.Extensions.Localization. Apps that don't enable localization pay nothing extra; the IConfigureOptions<ValidationOptions> bridge in the localization package is the only point of contact. This keeps the core package small and self-contained, and lets the localization package evolve independently.

Single IValidationLocalizer interface as the extensibility point. The validation pipeline sees one abstraction with two methods (ResolveDisplayName, ResolveErrorMessage). Custom implementations are plain DI services that receive their dependencies via constructor injection. The interface form is discoverable, cleanly composable via the decorator pattern, and lets framework integrations (Minimal APIs, Blazor, future hosts) treat localization as a single unit.

Microsoft.Extensions.Validation.Localization is one of possible implementations. The default IStringLocalizer-based implementation lives behind AddValidationLocalization() in the localization package, but the pipeline does not require it. Users with an existing translation backend can implement IValidationLocalizer directly and assign it to ValidationOptions.Localizer; the extensibility shape does not assume IStringLocalizer.

Resource-attribute paths bypass the localizer to avoid double-localization. When a member is decorated with [Display(Name = ..., ResourceType = ...)], the source generator emits a DisplayNameInfo strategy that produces the already-localized display name via DisplayAttribute.GetName(); IValidationLocalizer.ResolveDisplayName is not invoked on that path. The same applies to error messages - when the attribute carries ErrorMessageResourceType, ResolveErrorMessage is not invoked. Running an already-localized string through IStringLocalizer would treat it as a lookup key and silently break.

IValidatableObject.Validate results are not localized. Messages from IValidatableObject pass through unmodified - 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 a real demand emerges.

Framework integrations

Minimal APIs. The validation endpoint filter calls into the pipeline directly, so ValidationOptions.Localizer is picked up automatically once AddValidationLocalization() (or a custom IValidationLocalizer) is configured. Because top-level Minimal API parameters have no declaring type, the validation pipeline passes typeof(object) to IStringLocalizerFactory.Create under the default per-type lookup - this resolves to the object-source resource file, which almost no application has. The recommended pattern for Minimal APIs is AddValidationLocalization<TSharedResource>() (or set LocalizerProvider explicitly). This caveat is called out in the XML docs of both AddValidationLocalization() and ValidationLocalizationOptions.LocalizerProvider.

Blazor DataAnnotationsValidator. When the application calls AddValidation() and the validated model has an IValidatableInfo registered (typically via the source generator), DataAnnotationsValidator routes validation through the Microsoft.Extensions.Validation pipeline and so picks up ValidationOptions.Localizer automatically - no additional Blazor-side wiring is needed. Forms whose models are validated via the legacy Validator.TryValidateObject path (i.e. without AddValidation()) do not benefit from IValidationLocalizer; in that mode the existing BCL-only localization mechanisms apply.

Testing

162 tests across three projects:

  • 58 tests in Microsoft.Extensions.Validation.Tests (existing tests, unchanged behavior + 11 new pipeline-integration tests with a recording localizer test double, including DisplayNameInfo-throws propagation).
  • 62 tests in the new Microsoft.Extensions.Validation.Localization.Tests project covering:
    • DefaultValidationLocalizer contract (literal lookup hit/miss, key provider fallback, range attribute formatting, custom attribute formatter, LocalizerProvider invocation, per-type isolation, shared-resource pattern, LocalizerProvider-returns-null defensive throw)
    • ValidationAttributeFormatterRegistry (built-ins registered, custom registration, override semantics, self-formatting precedence, null guards)
    • All 8 built-in formatters (parameterized over BCL templates)
    • DI shape: registration idempotency, order independence, <TResource> overload single-instance optimization, user-supplied Localizer always wins
    • Pipeline integration: real IStringLocalizer factory through the full ValidatableTypeInfo pipeline; Required, Range, parameter-level shared resource, type-level attribute, per-invocation override, StringLength two-template behavior, lookup miss fallback, ErrorMessageResourceType bypass, key-provider convention.
  • 42 tests in Microsoft.Extensions.Validation.GeneratorTests (existing 28 + 14 new DisplayName snapshot tests covering property/parameter/record/type-level x NameOnly/WithResourceType/WithDisplayNameAttribute/WithoutDisplayAttribute, plus precedence between Display and DisplayName).

All snapshots are committed and match the current emitter output.

Breaking change in experimental API

ValidatableTypeInfo, ValidatablePropertyInfo, and ValidatableParameterInfo are decorated with [Experimental("ASP0029")]. This PR changes their constructor signatures by removing the string displayName parameter and adding an optional DisplayNameInfo? displayNameInfo = null parameter in its place.

Diff

 protected ValidatablePropertyInfo(
     [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
     Type declaringType,
     Type propertyType,
     string name,
-    string displayName);
+    DisplayNameInfo? displayNameInfo = null);

 protected ValidatableParameterInfo(
     Type parameterType,
     string name,
-    string displayName);
+    DisplayNameInfo? displayNameInfo = null);

 protected ValidatableTypeInfo(
     [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
     Type type,
-    IReadOnlyList<ValidatablePropertyInfo> members);
+    IReadOnlyList<ValidatablePropertyInfo> members,
+    DisplayNameInfo? displayNameInfo = null);

The PublicAPI.Unshipped.txt baseline tracks this with *REMOVED* entries paired with the new constructor signatures, plus the new public abstract DisplayNameInfo class.

Reasoning

Hardcoding a literal displayName at metadata-construction time cannot represent the resource-based scenario ([Display(Name = X, ResourceType = T)]), which needs a runtime call to DisplayAttribute.GetName() or to a generated accessor. Modelling display-name resolution as a separate strategy object (DisplayNameInfo) lets each metadata instance carry the appropriate lookup policy - literal, resource-based, or custom - without exposing that branching to the metadata classes themselves, and lets the literal-vs-resource localization decision live in a single place (the strategy implementation). The parameter is optional and defaults to null, so callers that don't need a custom display name can ignore it - the validation pipeline falls back to the CLR member or type name.

Who is affected

Consumer Impact
Source-generator users (the typical Minimal API / Blazor scenario) None. The generator emits the new constructor calls and the DisplayNameInfo strategies.
Direct subclassers of the three abstractions Source-incompatible. Subclasses must replace the string displayName argument with a DisplayNameInfo? (or pass null). Mostly affects test doubles. Open-source code search shows we are the only implementors of the abstract types.

@oroztocil oroztocil requested a review from Copilot May 11, 2026 15:54
@github-actions github-actions Bot added the area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework label May 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a localization extensibility point to Microsoft.Extensions.Validation (used by Minimal APIs validation and Blazor DataAnnotations validation) and adds an optional companion package (Microsoft.Extensions.Validation.Localization) that provides an IStringLocalizer-backed default implementation.

Changes:

  • Added ValidationOptions.Localizer + new localization context types and helpers to enable localized display names and error messages in the core validation pipeline.
  • Introduced a new Microsoft.Extensions.Validation.Localization package with DI registration (AddValidationLocalization*), a default IStringLocalizer implementation, and attribute-specific formatter support.
  • Updated the validations source generator and snapshots to support resource-based [Display(..., ResourceType=...)] display names via runtime accessors.

Reviewed changes

Copilot reviewed 86 out of 86 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
AspNetCore.slnx Adds the new localization projects to the repo solution structure.
eng/ProjectReferences.props Registers Microsoft.Extensions.Validation.Localization as a project reference provider.
eng/SharedFramework.Local.props Adds Microsoft.Extensions.Validation.Localization to shared framework reference/package list.
eng/ShippingAssemblies.props Marks Microsoft.Extensions.Validation.Localization as a shipping assembly.
eng/TrimmableProjects.props Adds the new localization assembly to the trimmable projects list.
src/Components/Components.slnf Includes the new localization projects in the Components solution filter.
src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs Adds a TODO note for future Blazor per-field validation localization integration.
src/Http/Routing/src/ValidationEndpointFilterFactory.cs Adjusts minimal API validation context initialization to rely on parameter validation resolving (localized) display names.
src/Shared/RoslynUtils/WellKnownTypeData.cs Adds System.ComponentModel.DisplayNameAttribute as a well-known type for generator analysis.
src/Validation/Localization/src/BuiltInFormatters.cs Adds built-in formatters for attributes whose templates use placeholders beyond {0}.
src/Validation/Localization/src/DefaultValidationLocalizer.cs Implements the default IStringLocalizer-based IValidationLocalizer.
src/Validation/Localization/src/IValidationAttributeFormatter.cs Introduces the formatter interface used to format localized templates with attribute-specific arguments.
src/Validation/Localization/src/Microsoft.Extensions.Validation.Localization.csproj Defines the new localization package project and references.
src/Validation/Localization/src/PublicAPI.Shipped.txt Initializes shipped API baseline for the new package.
src/Validation/Localization/src/PublicAPI.Unshipped.txt Declares the new public API surface for the localization package.
src/Validation/Localization/src/ValidationAttributeFormatterRegistry.cs Adds a registry for mapping attributes to formatter factories (with built-in registrations).
src/Validation/Localization/src/ValidationLocalizationOptions.cs Adds configurable options for localizer selection, key selection, and formatter registration.
src/Validation/Localization/src/ValidationLocalizationServiceCollectionExtensions.cs Adds AddValidationLocalization() / AddValidationLocalization<TResource>() DI registration entrypoints.
src/Validation/Localization/src/ValidationLocalizationSetup.cs Bridges localization options into ValidationOptions.Localizer via IConfigureOptions<ValidationOptions>.
src/Validation/Localization/test/BuiltInFormattersTests.cs Tests built-in formatter behavior for standard DataAnnotations attributes.
src/Validation/Localization/test/Microsoft.Extensions.Validation.Localization.Tests.csproj Adds the new test project for the localization package.
src/Validation/Localization/test/TestStringLocalizerFactory.cs Provides test localizer factory/localizer implementations for localization tests.
src/Validation/Localization/test/ValidationAttributeFormatterRegistryTests.cs Tests formatter registry precedence, overrides, and null-guard behavior.
src/Validation/Localization/test/ValidationLocalizationServiceCollectionExtensionsTests.cs Tests DI wiring, ordering, idempotency, and shared-resource behavior.
src/Validation/Validation.slnf Includes the new localization projects in the Validation solution filter.
src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs Updates generated metadata emission for nullable display names and resource-based display-name accessors.
src/Validation/gen/Extensions/ISymbolExtensions.cs Adds symbol inspection for [Display] / [DisplayName], including resource-based display detection.
src/Validation/gen/Models/ValidatableProperty.cs Extends generator model to carry nullable display name and resource-display flag.
src/Validation/gen/Models/ValidatableType.cs Extends generator model to carry type display name and resource-display flag.
src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.cs Extracts type/member display info (including record primary-ctor parameter precedence) for generation.
src/Validation/src/DisplayNameLocalizationContext.cs Adds the public context type for display-name localization.
src/Validation/src/ErrorMessageLocalizationContext.cs Adds the public context type for error-message localization.
src/Validation/src/IValidationLocalizer.cs Adds the public IValidationLocalizer extensibility interface.
src/Validation/src/LocalizationHelper.cs Adds internal helpers to resolve display names and attribute error messages via resource accessors/localizer.
src/Validation/src/PublicAPI.Unshipped.txt Updates public API baseline for new types + constructor signature changes.
src/Validation/src/RuntimeValidatableParameterInfoResolver.cs Adds runtime parameter display info extraction supporting [Display] resource-path and [DisplayName].
src/Validation/src/ValidatableParameterInfo.cs Extends metadata to carry nullable display name + optional resource accessor; updates pipeline integration.
src/Validation/src/ValidatablePropertyInfo.cs Extends metadata to carry nullable display name + optional resource accessor; updates pipeline integration.
src/Validation/src/ValidatableTypeInfo.cs Extends metadata to carry display info; integrates localization into type-level validation and IValidatableObject path.
src/Validation/src/ValidationOptions.cs Adds ValidationOptions.Localizer configuration hook and documentation.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs New snapshot coverage for display-name emission (name-only).
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs New snapshot coverage for resource-based display-name emission (ResourceType).
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanDiscoverGeneratedValidatableTypeAttribute#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanUseBothFrameworkAndGeneratedValidatableTypeAttributes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs Updates snapshot for new constructor signatures and display metadata emission.
src/Validation/test/Microsoft.Extensions.Validation.Tests/Microsoft.Extensions.Validation.Tests.csproj Adds localization references used by new/updated validation tests.
src/Validation/test/Microsoft.Extensions.Validation.Tests/RuntimeValidatableParameterInfoResolverTests.cs Updates tests to reflect nullable display-name metadata for parameters.
src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidatableInfoResolverTests.cs Updates tests for the experimental constructor signature change.

Comment thread src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs Outdated
@oroztocil oroztocil force-pushed the oroztocil/validation-localization4 branch 2 times, most recently from 414bc2a to 6b2aeaf Compare May 11, 2026 16:18
@oroztocil oroztocil requested a review from Copilot May 11, 2026 16:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 87 out of 87 changed files in this pull request and generated 2 comments.

Comment thread src/Validation/src/ValidatableTypeInfo.cs
Comment thread src/Validation/src/ErrorMessageLocalizationContext.cs
@oroztocil oroztocil force-pushed the oroztocil/validation-localization4 branch from 6b2aeaf to 6c19448 Compare May 11, 2026 19:27
@oroztocil oroztocil marked this pull request as ready for review May 11, 2026 19:27
@oroztocil oroztocil requested review from a team, halter73, tdykstra and wtgodbe as code owners May 11, 2026 19:27
@oroztocil oroztocil force-pushed the oroztocil/validation-localization4 branch from 6c19448 to aa640cb Compare May 11, 2026 20:40
var validationContext = new ValidationContext(argument, entry.DisplayName, context.HttpContext.RequestServices, items: null);
// ValidationContext.DisplayName is overwritten by ValidatableParameterInfo.ValidateAsync
// once the localized display name is resolved; the parameter name acts as a placeholder.
var validationContext = new ValidationContext(argument, entry.Name, context.HttpContext.RequestServices, items: null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ValidationContext is passed to IValidatableInfo.ValidateAsync (experimental public API).

But it seems like we are changing the meaning of "displayName" (initially) so that it's just the actual parameter name (to be overwritten later). Are we risking any user implementations to be accessing ValidateContext.ValidationContext.DisplayName at a point when it's not (yet) the correct display name?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The re-setting of ValidateContext.ValidationContext.DisplayName was always happening (in ValidableParemeterInfo and the other entrypoints), even before this PR. The code I am removing here is practically no-op in the shipped code.

The workflow is such that the user's validation code (in validation attributes etc.) does not run before ValidableParemeterInfo.ValidateAsync sets the proper (possibly localized) display name on the ValidationContext instance.

The only way someone could see a different value is when someone would return custom ValidableParemeterInfo implementation with overriden ValidateAsync - and even there they would have a meaningful value (= the member name). The contract here is "if you are doing something so low level as to implement your own validation pipeline, you are responsible for localization yourself". This is, I think, cleaner then doing a partial pre-localization for such advanced user.

Finally, the practical reason why we pass any display name value here at all, is that this is the only constructor of ValidationContext that is not annotated with trimming warnings - the other ones cause warnings because the built-in validation logic might need to go through reflection path to get a display name. To avoid that we just need to pass some value here, and the parameter name is the one that makes the most sense.

Comment thread src/Validation/gen/Emitters/ValidationsGenerator.Emitter.cs
{
var parameter = global::System.Linq.Enumerable.FirstOrDefault(
constructor.GetParameters(),
p => string.Equals(p.Name, k.PropertyName, global::System.StringComparison.OrdinalIgnoreCase));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's not a realistic scenario, but for correctness, wouldn't this fail in cases like:

record R(string s, [Display(...)] string S);

If we request to validate property S on the record, the first parameter will match because of the case insensitive comparison and we won't consider the DisplayAttribute right?

That makes me leaning more towards not trying to "infer" attributes via primary constructors and instead ask users to use [property: Display(...)] instead.

@oroztocil oroztocil May 14, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the name comparison should be case sensitive. I made it case sensitive in the new display attribute lookup.

However, it should be noted that the already existing lookup for the validation attributes (in the generated ValidationAttributeCache) was shipped as case-insensitive. That is also a bug which I noticed in the past. I did not want to make that (albeit small) behavioral change in this unrelated PR. But we should fix it at some point.

Comment on lines +259 to +261
return _typeCache.GetOrAdd(type, static t =>
global::System.Reflection.CustomAttributeExtensions
.GetCustomAttribute<global::System.ComponentModel.DataAnnotations.DisplayAttribute>(t, inherit: true));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought reflection internally maintains some sort of cache already?
Maybe worth to have some micro benchmark so we know better the perf impact here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree benchmarking would be good. However, this is replicating the existing pattern for validation attributes. More importantly, it is all file private generated code so we can safely remove it later, if we determine that the caching is not worth it at this level.

Comment thread src/Validation/gen/Extensions/ISymbolExtensions.cs Outdated
Comment on lines +18 to +19
/// for a given declaring type. The declaring type is the type that contains the property
/// being validated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation isn't necessarily for a property right?

In case validation of a type, this is the type itself, I think?

For parameters, if I'm following the code correctly, the DefaultValidationLocalizer will receive null and transform it to typeof(object) which also doesn't sound reasonable to me?

In Mvc, DataAnnotationLocalizerProvider is almost always passed context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType which (if Mvc validates parameters), I assume will produce the type that contains the method that contains the parameter being validate.

In addition, in Mvc, DataAnnotationsMetadataProvider.CreateDisplayMetadata unwraps value types via Nullable.GetUnderlyingType(context.Key.ModelType) ?? context.Key.ModelType. I'm not sure if we need similar behavior here as well.

In addition, I would like us to have more clarity on the exact definition of the Type passed to this delegate in case of inheritance.

// some validation attributes on the model itself.
public class MyBaseModel
{
    // some validatable properties.
}

// some validation attributes on the model itself.
public class Derived : MyBaseModel
{
    // more validatable properties.
}

When executing validation for an object of type Derived but we are validating a property from the base class, which Type do we expect to have in this case. I also would like us to know the Mvc behavior and document differences when possible. For sure we will just keep Mvc behavior the same and we can decide to deviate (if needed) when we think it's reasonable to deviate.

/// <c>options.LocalizerProvider = (_, factory) =&gt; factory.Create(typeof(SharedValidationMessages));</c>
/// </para>
/// </remarks>
public Func<Type, IStringLocalizerFactory, IStringLocalizer>? LocalizerProvider { get; set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Mvc naming for the equivalent property is DataAnnotationLocalizerProvider. I'm not sure if we will want to have similar naming, or if the LocalizerProvider here is actually intended to be more generic and not specific to data annotations.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd defer this discussion to the API review. I don't mind changing the name, although I would be against putting DataAnnotation into the name. We do not refer to DataAnnotation in the naming used in the core M.E.Validation package even though that is arguably much more tied to the DataAnnotation types than the localization integration.

/// that cost across calls.
/// </para>
/// <para>
/// <b>Minimal API parameter validation note:</b> for top-level method parameters there is no

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing to read, this part partially answers a previous question I had.

I think my next question is why do we want to pass a dummy typeof(object) instead of passing null and annotating this as nullable (i.e, Func<Type?, IStringLocalizerFactory, IStringLocalizer>?).

Also, still question for whether Mvc has any equivalent scenario and how does it behave today.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on your feedback I changed the signature to Func<Type?, IStringLocalizerFactory, IStringLocalizer>? to allow users to select what to do with "missing" type more cleanly (e.g. have a fallback type). The code path that when LocalizerProvider is not set and IStringLocalizerFactory.Create is used directly still falls back to using typeof(object) because that API requires non-null type. Throwing instead does not seem correct to me.

MVC used the non-nullable signature for its DataAnnotationLocalizerProvider. However, due to the typical structure of an MVC app, you would always have a concrete declaring/container type for the localization resources to map from (a view, a controller). In fact, the IStringLocalizerFactory API is designed with such structure in mind - which can make using it in different context a bit awkward.

@oroztocil oroztocil requested a review from BrennanConroy as a code owner May 15, 2026 08:26

@Youssef1313 Youssef1313 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - but prefer to merge upon a second approval.

@javiercn javiercn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. That said, I want to point out the excessive amount of tests that this PR adds (mostly AI generated). It makes it incredibly difficult and time consuming for anyone to review the behavior and the tests because so much time is wasted on reviewing the same code being tested across multiple layers of the stack.

In the future take care of following the principle of the testing pyramid and minimizing the overlap across tests. If something is already well covered at a higher level, it doesn't have to be covered at lower levels. Things like service container registration and similar things are not normally aspects that we test unless we are looking for something specific as they get already coverage through E2E tests and other mechanisms.

return;
}

if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider some level of localization support without Microsoft.Extensions.Validation here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already get static resource localization if you use ValidationAttribute.ErrorMessageResourceType/DisplayAttribute.ResourceType.

For us to support more, we would need to write our own validation logic using the ValidationAttribute API directly, rather than rely on the static Validator.TryValidate* methods. And if we would do that, we would be arguably in a better spot with completely replacing both Microsoft.Extensions.Validation and the static Validator.

Comment thread src/Components/Forms/test/DefaultClientValidationServiceTest.cs Outdated
services.AddValidation();
services.AddValidation(options =>
{
options.Localizer = new TestValidationLocalizer();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird that in an E2E test we aren't testing with the real thing

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced it with standard AddValidationLocalization call - i.e. the full pipeline is exercised in the tests. I just used in-memory IStringLocalizer implementation in order to avoid issues with resx files.

Comment on lines +184 to +185
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. We should wire up the real localization pipeline

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced this with standard UseRequestLocalization call

/// <param name="messageTemplate">The error message template containing format placeholders.</param>
/// <param name="displayName">The resolved display name of the member being validated.</param>
/// <returns>The fully formatted error message.</returns>
string FormatErrorMessage(CultureInfo culture, string messageTemplate, string displayName);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead use FormattableString here? FormattableStringFactory already gives you the ability to create one string with the arguments inside it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the built-in formatters then just disappear.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand the use of FormattableStringFactory that you refer to.

The localized template comes from IStringLocalizer as a string, not a compile-time interpolated literal, so FormattableStringFactory.Create would still need a per-attribute argument projection ({ MinimumLength, MaximumLength } for LengthAttribute etc.). This is what the built-in formatters encapsulate. The API of the interface is modelled after the existing ValidationAttribute.FormatErrorMessage - just with the added template as parameter.

Are you proposing instead of having "give me formatted string for this attribute" API to have a "give me object[] with args for this attribute" API? (We discussed these alternatives during one of our offline sessions.) I am not against taking that approach - I just wouldn't say that the formatters just disappear then, a very similiar piece of code is still needed.

/// <param name="configureOptions">An optional callback to configure
/// <see cref="ValidationLocalizationOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> for chaining.</returns>
public static IServiceCollection AddValidationLocalization(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I raised this, but we shouldn't have this on serviceCollection. If we need to update AddValidation() to return a builder, we should

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type,
IReadOnlyList<ValidatablePropertyInfo> members)
IReadOnlyList<ValidatablePropertyInfo> members,
DisplayNameInfo? displayNameInfo = null)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the values of having DisplayNameInfo is that we can always provide the value for it, can we not? Even for the implementation without DisplayName we can provide an implementation that returns the property name.

Comment on lines +815 to +825
private interface IAuditable
{
DateTime CreatedAt { get; }
}

private class AuditableThing : IAuditable
{
public DateTime CreatedAt { get; set; }
public string Name { get; set; } = string.Empty;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the interface matter for. Don't we simply look at the type hierarchy and ignore interfaces? Do we special case interfaces in any way? (If so, we need to capture the behavior for implicit vs explicit implementations as well as usages of new if apply here)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M.E.V. actually looks up both implemented interfaces and the base type chain for each type it creates IValidatableTypeInfo for.

See

public static List<Type> GetAllImplementedTypes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type)

@oroztocil oroztocil enabled auto-merge (squash) May 17, 2026 19:51
@oroztocil oroztocil merged commit c472800 into main May 18, 2026
25 checks passed
@oroztocil oroztocil deleted the oroztocil/validation-localization4 branch May 18, 2026 01:08
@dotnet-policy-service dotnet-policy-service Bot added this to the 11.0-preview5 milestone May 18, 2026
ANcpLua added a commit to ANcpLua/aspnetcore that referenced this pull request May 18, 2026
…dling

Resolves the conflict introduced by upstream commit c472800
("Add localization support to Microsoft.Extensions.Validation"
(dotnet#66646)), which lands the day before this merge and rewrites
the same call sites in `ValidationsGenerator.TypesParser.cs`
that this PR modifies, in addition to changing the generator
emitter format and the `ValidatableType`/`ValidatableProperty`
model constructors.

Conflict resolution in `ValidationsGenerator.TypesParser.cs`:

* `ITypeParameterSymbol` branch (line 83-95) auto-merges cleanly
  with the new `displayAttributeSymbol`/`displayNameAttributeSymbol`
  lookups added at line 105-106, because the two insertions sit
  on opposite sides of the `DeclaredAccessibility` guard with
  enough unchanged context between them.
* Record primary-constructor property path: `ContainsTypeParameter`
  early-`continue` guard is placed before the new `GetDisplayInfo`
  calls. The order matters: a property whose type is an unresolved
  type parameter (e.g. `TSelf` from `RequestBase<TSelf>`) is
  skipped immediately rather than computing literal/resource
  display info that would be discarded.
* Regular property path: same ordering — guard before
  `GetDisplayInfo`.
* `ContainsTypeParameter` helper at the end of the class is
  untouched by the merge.

Snapshot regeneration:

The emitter template in commit c472800 replaces the
`displayName: "..."` constructor argument with
`displayNameInfo: <DisplayNameInfo>` and emits four new
file-scoped helper classes (`LiteralDisplayName`,
`PropertyResourceDisplayName`, `TypeResourceDisplayName`,
`DisplayAttributeCache`) into every generated resolver. The
two snapshots owned by this PR
(`CanValidateOpenTypeParameterReachableThroughConstraint` and
`CanValidateTypesWithGenericBaseClass`) are therefore regenerated
from the new emitter rather than hand-merged. The bug witness
in `CanValidateOpenTypeParameterReachableThroughConstraint` is
preserved: the resolver still contains
`typeof(global::UserRequest)` and emits the `Name`/`Age` member
entries that the constraint walk discovers — without the
`ITypeParameterSymbol` branch the resolver body is empty for
that type.

Verification: `dotnet test
src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests`
reports 45/45 passed locally on Debug net11.0 against the
merged tree (the 15 new `DisplayName*` tests added by dotnet#66646
plus the 28 pre-existing generator tests plus the two
type-parameter tests added by this PR).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@danroth27 danroth27 added area-blazor Includes: Blazor, Razor Components feature-validation Issues related to model validation in minimal and controller-based APIs and removed area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework labels May 28, 2026
@oroztocil

oroztocil commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Fixes #66955

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components feature-validation Issues related to model validation in minimal and controller-based APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Blazor] Data annotations localization support

5 participants