From 68c0abfd2bca828f59dacb38b398e776229eb2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Tue, 4 Aug 2026 18:43:23 +0200 Subject: [PATCH 1/2] Replace ValidationOptions.MessageKeyProvider with built-in key conventions --- .../src/Forms/DataAnnotationsLocalizer.cs | 72 +++++++--- .../ClientValidationProviderTests.cs | 28 ++-- .../gen/Templates/LocalizationHelpers.cs | 44 +++++++ .../gen/Templates/ValidatableInfo.cs | 20 +-- src/Validation/src/PublicAPI.Unshipped.txt | 10 -- .../src/ValidationMessageKeyContext.cs | 34 ----- src/Validation/src/ValidationOptions.cs | 12 -- ...Only#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Type#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...bute#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...bute#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...eral#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...bute#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Only#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Type#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...erty#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Name#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...eter#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Only#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Type#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...bute#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Only#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...Type#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...imes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...bute#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...nore#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ject#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ions#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...aces#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ters#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...dler#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...bute#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ties#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...sses#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ties#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ters#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ties#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ance#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ties#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ypes#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- ...ions#ValidatableInfoResolver.g.verified.cs | 64 ++++++--- .../ValidationLocalizationIntegrationTests.cs | 123 +++++++++++++++--- 51 files changed, 2236 insertions(+), 859 deletions(-) delete mode 100644 src/Validation/src/ValidationMessageKeyContext.cs diff --git a/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs b/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs index 19affdcd681d..db0aef976ade 100644 --- a/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs +++ b/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs @@ -54,38 +54,76 @@ public string ResolveDisplayName(in ClientValidationFieldMetadata metadata, bool return attribute.FormatErrorMessage(displayName); } - var lookupKey = GetErrorMessageKey(attribute, memberName, type); - - if (string.IsNullOrEmpty(lookupKey)) - { - return attribute.FormatErrorMessage(displayName); - } - var localizer = GetStringLocalizer(type, localizerFactory); - var localizedTemplate = localizer[lookupKey]; + var localizedTemplate = FindLocalizedTemplate(localizer, attribute, memberName, type); - if (localizedTemplate.ResourceNotFound) + if (localizedTemplate is null) { return attribute.FormatErrorMessage(displayName); } // Format the localized template with attribute-specific arguments - return FormatMessage(attribute, CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatMessage(attribute, CultureInfo.CurrentCulture, localizedTemplate, displayName); } - private string? GetErrorMessageKey(ValidationAttribute attribute, string memberName, Type type) + // Resolves the localized message template for a validation attribute. + // + // An explicit ErrorMessage is used verbatim as the lookup key. Otherwise the built-in key + // convention is applied, walking from the most specific key to the least specific one: + // + // {DeclaringType}_{MemberName}_{AttributeType}_Error + // {DeclaringType}_{AttributeType}_Error + // {AttributeType}_Error + // + // Returns null when no key resolves, in which case the caller falls back to the + // non-localized message produced by the attribute itself. + // + // Keep in sync with the generated LocalizationHelpers.FindLocalizedTemplate in + // src/Validation/gen/Templates/LocalizationHelpers.cs. + private static string? FindLocalizedTemplate( + IStringLocalizer localizer, + ValidationAttribute attribute, + string memberName, + Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) { - return attribute.ErrorMessage; + var explicitMatch = localizer[attribute.ErrorMessage]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; } - return options.MessageKeyProvider?.Invoke(new ValidationMessageKeyContext + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + // The member-specific tier is skipped for type-level attributes that report no member + // name, where the caller passes the declaring type name as the member name. + if (!string.Equals(memberName, declaringType.Name, StringComparison.Ordinal)) { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = type, - }); + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name[..arityIndex]; } private IStringLocalizer GetStringLocalizer(Type type, IStringLocalizerFactory localizerFactory) diff --git a/src/Components/Endpoints/test/FormValidation/ClientValidationProviderTests.cs b/src/Components/Endpoints/test/FormValidation/ClientValidationProviderTests.cs index 4e271b311a2f..324f5bb91314 100644 --- a/src/Components/Endpoints/test/FormValidation/ClientValidationProviderTests.cs +++ b/src/Components/Endpoints/test/FormValidation/ClientValidationProviderTests.cs @@ -173,12 +173,9 @@ public void Localizer_LocalizesDisplayNameAndErrorMessage_OnMevPath() var translations = new Dictionary { ["Custom Label"] = "Étiquette", - ["req-key"] = "{0} est requis.", + ["LocalizedFieldModel_Field_RequiredAttribute_Error"] = "{0} est requis.", }; var options = CreateMevOptions(typeof(LocalizedFieldModel)); -#pragma warning disable ASP0029 // Microsoft.Extensions.Validation evaluation APIs. - options.MessageKeyProvider = _ => "req-key"; -#pragma warning restore ASP0029 var factory = new TestStringLocalizerFactory(translations); var rule = SingleRule( @@ -198,10 +195,10 @@ public void Localizer_DoesNotLocalize_OnStaticValidatorPath() var translations = new Dictionary { ["Custom Label"] = "Étiquette", - ["req-key"] = "{0} est requis.", + ["LocalizedFieldModel_Field_RequiredAttribute_Error"] = "{0} est requis.", }; #pragma warning disable ASP0029 // Microsoft.Extensions.Validation evaluation APIs. - var options = new ValidationOptions { MessageKeyProvider = _ => "req-key" }; + var options = new ValidationOptions(); #pragma warning restore ASP0029 var factory = new TestStringLocalizerFactory(translations); @@ -218,14 +215,15 @@ public void Localizer_ResolvesFromDeclaringType_ForInheritedProperty() // The validated property is declared on the base type but the form model is the derived type. // Server-side validation resolves the localizer, message key, and display name from the // *declaring* type, so the client payload must do the same rather than use the derived - // (runtime container) type. The factory only knows translations for the base type, so a - // localized result proves the declaring type flowed through. + // (runtime container) type. The factory only knows translations for the base type, and the + // conventional key itself is built from the base type name, so a localized result proves the + // declaring type flowed through both the localizer lookup and the key convention. var byType = new Dictionary> { [typeof(InheritedFieldBaseModel)] = new Dictionary { ["Base Label"] = "Étiquette", - ["req-key"] = "{0} est requis.", + ["InheritedFieldBaseModel_Field_RequiredAttribute_Error"] = "{0} est requis.", }, // The derived type has no translations; if it were (incorrectly) used, both the display // name and the message template would fall back to their non-localized values. @@ -233,23 +231,13 @@ public void Localizer_ResolvesFromDeclaringType_ForInheritedProperty() }; var factory = new TypeAwareStringLocalizerFactory(byType); - Type? messageKeyDeclaringType = null; var options = CreateMevOptions(typeof(DerivedFieldModel)); -#pragma warning disable ASP0029 // Microsoft.Extensions.Validation evaluation APIs. - options.MessageKeyProvider = context => - { - messageKeyDeclaringType = context.DeclaringType; - return "req-key"; - }; -#pragma warning restore ASP0029 var rule = SingleRule( GetMevData(options, factory, (nameof(DerivedFieldModel.Field), "Model." + nameof(DerivedFieldModel.Field)))!, "Model." + nameof(DerivedFieldModel.Field)); - // The declaring (base) type is used for the message key context... - Assert.Equal(typeof(InheritedFieldBaseModel), messageKeyDeclaringType); - // ...and for both the display-name and error-message localizer lookups. + // The declaring (base) type is used for both the display-name and error-message lookups. Assert.Equal("Étiquette est requis.", rule.Message); } diff --git a/src/Validation/gen/Templates/LocalizationHelpers.cs b/src/Validation/gen/Templates/LocalizationHelpers.cs index 47ebab904924..0b8397d2c48a 100644 --- a/src/Validation/gen/Templates/LocalizationHelpers.cs +++ b/src/Validation/gen/Templates/LocalizationHelpers.cs @@ -7,4 +7,48 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } diff --git a/src/Validation/gen/Templates/ValidatableInfo.cs b/src/Validation/gen/Templates/ValidatableInfo.cs index ab86d41100db..cd30a0ddb7d4 100644 --- a/src/Validation/gen/Templates/ValidatableInfo.cs +++ b/src/Validation/gen/Templates/ValidatableInfo.cs @@ -85,29 +85,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/src/PublicAPI.Unshipped.txt b/src/Validation/src/PublicAPI.Unshipped.txt index 7e293127be09..ab952a550de0 100644 --- a/src/Validation/src/PublicAPI.Unshipped.txt +++ b/src/Validation/src/PublicAPI.Unshipped.txt @@ -34,18 +34,8 @@ Microsoft.Extensions.Validation.ValidateContext.ServiceProvider.get -> System.IS Microsoft.Extensions.Validation.ValidateContext.ServiceProvider.init -> void Microsoft.Extensions.Validation.ValidateContext.ValidationErrors.get -> System.Collections.Generic.IReadOnlyDictionary!>? Microsoft.Extensions.Validation.ValidateContext.ValidationOptions.init -> void -Microsoft.Extensions.Validation.ValidationMessageKeyContext -Microsoft.Extensions.Validation.ValidationMessageKeyContext.DeclaringType.get -> System.Type! -Microsoft.Extensions.Validation.ValidationMessageKeyContext.DeclaringType.init -> void -Microsoft.Extensions.Validation.ValidationMessageKeyContext.MemberName.get -> string! -Microsoft.Extensions.Validation.ValidationMessageKeyContext.MemberName.init -> void -Microsoft.Extensions.Validation.ValidationMessageKeyContext.ValidationMessageKeyContext() -> void -Microsoft.Extensions.Validation.ValidationMessageKeyContext.ValidatorType.get -> System.Type! -Microsoft.Extensions.Validation.ValidationMessageKeyContext.ValidatorType.init -> void Microsoft.Extensions.Validation.ValidationOptions.LocalizerProvider.get -> System.Func! Microsoft.Extensions.Validation.ValidationOptions.LocalizerProvider.set -> void -Microsoft.Extensions.Validation.ValidationOptions.MessageKeyProvider.get -> System.Func? -Microsoft.Extensions.Validation.ValidationOptions.MessageKeyProvider.set -> void Microsoft.Extensions.Validation.ValidationOptions.TryGetValidatableParameterInfo(System.Reflection.ParameterInfo! parameterInfo, out Microsoft.Extensions.Validation.IValidatableParameterInfo? validatableInfo) -> bool Microsoft.Extensions.Validation.ValidationOptions.TryGetValidatableTypeInfo(System.Type! type, out Microsoft.Extensions.Validation.IValidatableTypeInfo? validatableTypeInfo) -> bool *REMOVED*abstract Microsoft.Extensions.Validation.ValidatableParameterInfo.GetValidationAttributes() -> System.ComponentModel.DataAnnotations.ValidationAttribute![]! diff --git a/src/Validation/src/ValidationMessageKeyContext.cs b/src/Validation/src/ValidationMessageKeyContext.cs deleted file mode 100644 index 2fdf142929e3..000000000000 --- a/src/Validation/src/ValidationMessageKeyContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Validation; - -/// -/// Provides contextual information used to compute the resource key for looking up a localized -/// validation error message. -/// -/// -/// An instance is passed to when a custom key -/// convention is configured. The returned key is then resolved against the configured -/// . -/// -public sealed class ValidationMessageKeyContext -{ - /// - /// Gets the type of the validator that produced the error being localized. For DataAnnotations, - /// this is the validation attribute type (for example, typeof(RequiredAttribute)). - /// - public required Type ValidatorType { get; init; } - - /// - /// 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. - /// - public required string MemberName { get; init; } - - /// - /// 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. - /// - public required Type DeclaringType { get; init; } -} diff --git a/src/Validation/src/ValidationOptions.cs b/src/Validation/src/ValidationOptions.cs index d35350df0db8..07d9d3c93b74 100644 --- a/src/Validation/src/ValidationOptions.cs +++ b/src/Validation/src/ValidationOptions.cs @@ -45,18 +45,6 @@ public class ValidationOptions public Func LocalizerProvider { get; set; } = (type, factory) => factory.Create(type); - /// - /// Gets or sets a delegate that computes the resource key used to look up a localized validation - /// message. - /// - /// - /// The provider supplies the lookup key by convention (for example, keyed by - /// ) for validators that do not specify an - /// explicit message. When a validator specifies an explicit message, that message is used as the - /// lookup key and the provider is not consulted. - /// - public Func? MessageKeyProvider { get; set; } - /// /// Attempts to get validation information for the specified type. /// diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index 9317a5c66053..24425f207633 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -309,6 +309,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -399,29 +443,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index 9317a5c66053..24425f207633 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -309,6 +309,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -399,29 +443,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index 7a0b20e9dade..7fdadd335b7f 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index fad6d39374ea..638e337c5da9 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs index c627d7084fc0..6b6e276a5c86 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index 853edfc29dee..9170f6568dc7 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index d68b2a59e4e9..407f4397f496 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index 8d631ddb4bb4..ce1b8c68c926 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs index c13082a727eb..52f3fe9f57c1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs @@ -334,6 +334,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -424,29 +468,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs index c56af96e56ea..e3090a20a835 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs index dcdc58eaedf7..f90fe96382ee 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index fdbbfd176d6d..b254ba551c5c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index d4ed50318028..2cd38580c7da 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index 72a0cedc12f7..0f6d8070e37e 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -318,6 +318,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -408,29 +452,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index c5835716f8c4..cf1177c62bbc 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -318,6 +318,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -408,29 +452,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index 695eed15c3ed..ddda09c3fb6b 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -318,6 +318,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -408,29 +452,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs index bb6a3eaae4cd..aa7a98f49687 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs @@ -326,6 +326,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -416,29 +460,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index 0372351bd36e..79f8dfd9fc37 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -405,6 +405,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -495,29 +539,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs index 2e99c9f892f6..40b3e1a46aa6 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -411,6 +411,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -501,29 +545,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index 62208b72990d..ceb1471f6210 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -341,6 +341,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -431,29 +475,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs index 6b845d81bd92..1bc667458920 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -362,6 +362,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -452,29 +496,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs index 8686a4076ff9..a38df0e2d211 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs @@ -368,6 +368,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -458,29 +502,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs index dc343d5bba59..2a1d6f3a20a8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -341,6 +341,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -431,29 +475,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index 122ceeea89e5..4a1e00d0fb79 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs index 9efb51d42b05..d6f4ae4c1a33 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs @@ -331,6 +331,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -421,29 +465,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs index 4f6e75b15e47..13cc2e14dc70 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -394,6 +394,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -484,29 +528,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs index d857eefe276c..c7cd237c0a0a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs @@ -359,6 +359,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -449,29 +493,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index 5c9122188aa9..e9b20f8c6310 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -439,6 +439,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -529,29 +573,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index 0372351bd36e..79f8dfd9fc37 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -405,6 +405,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -495,29 +539,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs index 1c0e08db958e..92b015f22cb8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -331,6 +331,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -421,29 +465,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs index 1ac700621fc2..9ad988c33eb0 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -373,6 +373,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -463,29 +507,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs index f14260dcec03..806907e22419 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -346,6 +346,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -436,29 +480,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs index 8b20db0a3c88..b7af2812521c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs index 59c0b47a940c..d00a340879c7 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs @@ -385,6 +385,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -475,29 +519,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs index 8b20db0a3c88..b7af2812521c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs index e986954efd51..1b529465b422 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs @@ -341,6 +341,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -431,29 +475,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs index 8dd51d5495ac..0269fd687800 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs @@ -318,6 +318,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -408,29 +452,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs index bf9026d54b91..0da46502c61e 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs index 9317a5c66053..24425f207633 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs @@ -309,6 +309,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -399,29 +443,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs index 6ff2ca3b265e..885a51c9a3c8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs @@ -325,6 +325,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -415,29 +459,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs index 70f1c43eccac..1efeb8922aad 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs @@ -347,6 +347,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -437,29 +481,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs index 8b54e31865cd..923a1247241e 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs @@ -347,6 +347,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -437,29 +481,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs index 4de9caaa00a1..3c65a263abb8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs @@ -337,6 +337,50 @@ file static class LocalizationHelpers => context.ValidationOptions.LocalizerProvider(type, factory) ?? throw new global::System.InvalidOperationException( $"The ValidationOptions.LocalizerProvider delegate returned null for type '{type.FullName}'. The delegate must return a non-null IStringLocalizer instance."); + + public static string? FindLocalizedTemplate( + global::Microsoft.Extensions.Localization.IStringLocalizer localizer, + global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, + string memberName, + global::System.Type declaringType) + { + if (!string.IsNullOrEmpty(attribute.ErrorMessage)) + { + var explicitMatch = localizer[attribute.ErrorMessage!]; + + return explicitMatch.ResourceNotFound ? null : explicitMatch.Value; + } + + var attributeName = attribute.GetType().Name; + var typeName = GetKeySegment(declaringType); + + if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + { + var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + if (!memberMatch.ResourceNotFound) + { + return memberMatch.Value; + } + } + + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } + + var globalMatch = localizer[$"{attributeName}_Error"]; + + return globalMatch.ResourceNotFound ? null : globalMatch.Value; + } + + private static string GetKeySegment(global::System.Type type) + { + var name = type.Name; + var arityIndex = name.IndexOf('`'); + + return arityIndex < 0 ? name : name.Substring(0, arityIndex); + } } @@ -427,29 +471,15 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo return result.ErrorMessage; } - var lookupKey = !string.IsNullOrEmpty(attribute.ErrorMessage) - ? attribute.ErrorMessage - : context.ValidationOptions.MessageKeyProvider?.Invoke(new global::Microsoft.Extensions.Validation.ValidationMessageKeyContext - { - ValidatorType = attribute.GetType(), - MemberName = memberName, - DeclaringType = declaringType, - }); - - if (string.IsNullOrEmpty(lookupKey)) - { - return result.ErrorMessage; - } - var localizer = LocalizationHelpers.CreateStringLocalizer(context, declaringType, localizerFactory); - var localizedTemplate = localizer[lookupKey!]; - if (localizedTemplate.ResourceNotFound) + var localizedTemplate = LocalizationHelpers.FindLocalizedTemplate(localizer, attribute, memberName, declaringType); + if (localizedTemplate is null) { return result.ErrorMessage; } - return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate.Value, displayName); + return FormatErrorMessage(attribute, global::System.Globalization.CultureInfo.CurrentCulture, localizedTemplate, displayName); } // Keep in sync with DataAnnotationsLocalizer.FormatMessage in diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs index daa764ad274d..f0f71cad8b76 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs @@ -10,8 +10,8 @@ namespace Microsoft.Extensions.Validation.Tests; // End-to-end coverage for the validation localization pipeline that is now emitted into the -// generated code and driven purely by ValidationOptions.LocalizerProvider / MessageKeyProvider and a -// registered IStringLocalizerFactory. +// generated code and driven purely by ValidationOptions.LocalizerProvider, the built-in message +// key convention, and a registered IStringLocalizerFactory. public class ValidationLocalizationIntegrationTests : ValidationTestBase { [Theory] @@ -117,15 +117,53 @@ public async Task Property_SelfFormattingAttribute_UsesFormatMessageHook(bool us [Theory] [InlineData(true)] [InlineData(false)] - public async Task Property_MessageKeyProvider_ComputesLookupKey(bool useAsync) + public async Task Property_ConventionKey_MemberTier_TakesPrecedence(bool useAsync) { + // All three conventional keys resolve, so the most specific one wins. var translations = new Dictionary { - ["RequiredAttribute"] = "{0} is mandatory.", + ["LocalizedDefaultModel_Name_RequiredAttribute_Error"] = "{0} is required for this member.", + ["LocalizedDefaultModel_RequiredAttribute_Error"] = "{0} is required for this type.", + ["RequiredAttribute_Error"] = "{0} is mandatory.", }; - var (provider, options) = CreateServices( - translations, - o => o.MessageKeyProvider = ctx => ctx.ValidatorType.Name); + var (provider, options) = CreateServices(translations); + var typeInfo = GeneratedValidationTestHelpers.GetTypeInfo(options); + var context = GeneratedValidationTestHelpers.CreateContext(provider, options); + + await ValidateAsync(typeInfo, new LocalizedDefaultModel(), context, useAsync, default); + + Assert.Equal("Name is required for this member.", Single(context, "Name")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Property_ConventionKey_TypeTier_UsedWhenMemberKeyMissing(bool useAsync) + { + var translations = new Dictionary + { + ["LocalizedDefaultModel_RequiredAttribute_Error"] = "{0} is required for this type.", + ["RequiredAttribute_Error"] = "{0} is mandatory.", + }; + var (provider, options) = CreateServices(translations); + var typeInfo = GeneratedValidationTestHelpers.GetTypeInfo(options); + var context = GeneratedValidationTestHelpers.CreateContext(provider, options); + + await ValidateAsync(typeInfo, new LocalizedDefaultModel(), context, useAsync, default); + + Assert.Equal("Name is required for this type.", Single(context, "Name")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Property_ConventionKey_GlobalTier_UsedWhenSpecificKeysMissing(bool useAsync) + { + var translations = new Dictionary + { + ["RequiredAttribute_Error"] = "{0} is mandatory.", + }; + var (provider, options) = CreateServices(translations); var typeInfo = GeneratedValidationTestHelpers.GetTypeInfo(options); var context = GeneratedValidationTestHelpers.CreateContext(provider, options); @@ -137,28 +175,64 @@ public async Task Property_MessageKeyProvider_ComputesLookupKey(bool useAsync) [Theory] [InlineData(true)] [InlineData(false)] - public async Task Property_ExplicitErrorMessage_WinsOverProvider(bool useAsync) + public async Task Property_ConventionKey_NoMatch_FallsBackToAttributeMessage(bool useAsync) { - // An explicit ErrorMessage on the validator is used as the key directly; the convention - // provider is not consulted. + // A factory is registered but no conventional key resolves, so the attribute's own + // non-localized message is used. var translations = new Dictionary { - ["RequiredKey"] = "Explicit {0}.", - ["ConventionKey"] = "Convention {0}.", + ["UnrelatedKey"] = "unused", }; - var providerCalled = false; - var (provider, options) = CreateServices(translations, o => o.MessageKeyProvider = _ => + var (provider, options) = CreateServices(translations); + var typeInfo = GeneratedValidationTestHelpers.GetTypeInfo(options); + var context = GeneratedValidationTestHelpers.CreateContext(provider, options); + + await ValidateAsync(typeInfo, new LocalizedDefaultModel(), context, useAsync, default); + + Assert.Equal("The Name field is required.", Single(context, "Name")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TypeLevelAttribute_WithoutMemberNames_SkipsMemberTier(bool useAsync) + { + // A type-level attribute that reports no member names has no member to key on, so the + // member tier is skipped rather than repeating the type name in the key. + var translations = new Dictionary { - providerCalled = true; - return "ConventionKey"; - }); + ["LocalizedTypeLevelModel_LocalizedTypeLevelModel_AlwaysFailsAttribute_Error"] = "doubled key", + ["LocalizedTypeLevelModel_AlwaysFailsAttribute_Error"] = "{0} failed type-level validation.", + }; + var (provider, options) = CreateServices(translations); + var typeInfo = GeneratedValidationTestHelpers.GetTypeInfo(options); + var context = GeneratedValidationTestHelpers.CreateContext(provider, options); + + await ValidateAsync(typeInfo, new LocalizedTypeLevelModel(), context, useAsync, default); + + Assert.Equal("LocalizedTypeLevelModel failed type-level validation.", Single(context, string.Empty)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Property_ExplicitErrorMessage_WinsOverConvention(bool useAsync) + { + // An explicit ErrorMessage on the validator is used as the key directly; the built-in + // convention is not consulted. + var translations = new Dictionary + { + ["RequiredKey"] = "Explicit {0}.", + ["LocalizedKeyedModel_Name_RequiredAttribute_Error"] = "Convention {0}.", + ["RequiredAttribute_Error"] = "Convention {0}.", + }; + var (provider, options) = CreateServices(translations); var typeInfo = GeneratedValidationTestHelpers.GetTypeInfo(options); var context = GeneratedValidationTestHelpers.CreateContext(provider, options); await ValidateAsync(typeInfo, new LocalizedKeyedModel(), context, useAsync, default); Assert.Equal("Explicit Customer Name.", Single(context, "Name")); - Assert.False(providerCalled); } [Theory] @@ -348,6 +422,19 @@ public class LocalizedSelfFormattingModel public string? Value { get; set; } } +[ValidatableType] +[AlwaysFails] +public class LocalizedTypeLevelModel +{ + public string? Value { get; set; } +} + +[AttributeUsage(AttributeTargets.Class)] +public sealed class AlwaysFailsAttribute : ValidationAttribute +{ + public override bool IsValid(object? value) => false; +} + [ValidatableType] public class LocalizedResourceErrorModel { From c1ec062799153c48ece256eeb94c723af9fbf8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Rozto=C4=8Dil?= Date: Mon, 10 Aug 2026 12:11:50 +0200 Subject: [PATCH 2/2] Use explicit null for "no member" and omit framework type key segments Replace the sentinel-based member tier skip with a nullable memberName. The type-level branch that reports no member names passed the type's own name as the member name, and the convention re-derived "there is no member" by comparing it back against declaringType.Name. That coupling was invisible in both signatures, mixed raw and arity-stripped type names, and silently dropped the member tier for a parameter named like its own type. Also omit the declaring type segment when that type lives in the System namespace. A parameter's declaring type is the parameter's own type, so the convention produced keys such as String_value_RequiredAttribute_Error and String_RequiredAttribute_Error, which carry no app-specific meaning. The member tier collapses to {MemberName}_{AttributeType}_Error and the type tier is skipped, since without a type segment it would duplicate the global tier. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eb69d5a2-eda2-4178-b364-20e310274542 --- .../src/Forms/DataAnnotationsLocalizer.cs | 41 ++++++++++++---- .../gen/Templates/LocalizationHelpers.cs | 32 ++++++++++--- .../gen/Templates/ValidatableInfo.cs | 2 +- .../gen/Templates/ValidatableTypeInfo.cs | 2 +- ...Only#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Type#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...bute#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...bute#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...eral#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...bute#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Only#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Type#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...erty#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Name#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...eter#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Only#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Type#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...bute#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Only#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...Type#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...imes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...bute#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...nore#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ject#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ions#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...aces#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ters#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...dler#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...bute#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ties#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...sses#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ties#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ters#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ties#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ance#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ties#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ypes#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- ...ions#ValidatableInfoResolver.g.verified.cs | 36 ++++++++++---- .../ValidationLocalizationIntegrationTests.cs | 48 +++++++++++++++++++ 48 files changed, 1267 insertions(+), 406 deletions(-) diff --git a/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs b/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs index db0aef976ade..4a6727a65182 100644 --- a/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs +++ b/src/Components/Endpoints/src/Forms/DataAnnotationsLocalizer.cs @@ -43,7 +43,7 @@ public string ResolveDisplayName(in ClientValidationFieldMetadata metadata, bool // Keep in sync with the generated ResolveAttributeErrorMessage/FormatErrorMessage in // src/Validation/gen/Templates/ValidatableInfo.cs, which this mirrors for the SSR client payload. public string? ResolveAttributeErrorMessage( - string memberName, + string? memberName, string displayName, Type type, ValidationAttribute attribute, @@ -75,6 +75,9 @@ public string ResolveDisplayName(in ClientValidationFieldMetadata metadata, bool // {DeclaringType}_{AttributeType}_Error // {AttributeType}_Error // + // The {DeclaringType} segment is omitted when the declaring type is a framework type, which + // collapses the first key to {MemberName}_{AttributeType}_Error and drops the second. + // // Returns null when no key resolves, in which case the caller falls back to the // non-localized message produced by the attribute itself. // @@ -83,7 +86,7 @@ public string ResolveDisplayName(in ClientValidationFieldMetadata metadata, bool private static string? FindLocalizedTemplate( IStringLocalizer localizer, ValidationAttribute attribute, - string memberName, + string? memberName, Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -96,21 +99,29 @@ public string ResolveDisplayName(in ClientValidationFieldMetadata metadata, bool var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - // The member-specific tier is skipped for type-level attributes that report no member - // name, where the caller passes the declaring type name as the member name. - if (!string.Equals(memberName, declaringType.Name, StringComparison.Ordinal)) + // The member-specific tier is skipped when there is no member to key on, which is the case + // for a type-level attribute that reports no member names. + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -118,8 +129,18 @@ public string ResolveDisplayName(in ClientValidationFieldMetadata metadata, bool return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", StringComparison.Ordinal) || + ns.StartsWith("System.", StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); diff --git a/src/Validation/gen/Templates/LocalizationHelpers.cs b/src/Validation/gen/Templates/LocalizationHelpers.cs index 0b8397d2c48a..873b80d81cda 100644 --- a/src/Validation/gen/Templates/LocalizationHelpers.cs +++ b/src/Validation/gen/Templates/LocalizationHelpers.cs @@ -11,7 +11,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -24,19 +24,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -44,8 +52,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); diff --git a/src/Validation/gen/Templates/ValidatableInfo.cs b/src/Validation/gen/Templates/ValidatableInfo.cs index cd30a0ddb7d4..c8fc298882ce 100644 --- a/src/Validation/gen/Templates/ValidatableInfo.cs +++ b/src/Validation/gen/Templates/ValidatableInfo.cs @@ -69,7 +69,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, diff --git a/src/Validation/gen/Templates/ValidatableTypeInfo.cs b/src/Validation/gen/Templates/ValidatableTypeInfo.cs index ba11c1c10fa1..45f47af19840 100644 --- a/src/Validation/gen/Templates/ValidatableTypeInfo.cs +++ b/src/Validation/gen/Templates/ValidatableTypeInfo.cs @@ -365,7 +365,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index 24425f207633..f6463a64187a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -313,7 +313,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -326,19 +326,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -346,8 +354,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -427,7 +445,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1064,7 +1082,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index 24425f207633..f6463a64187a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.ParameterDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -313,7 +313,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -326,19 +326,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -346,8 +354,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -427,7 +445,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1064,7 +1082,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index 7fdadd335b7f..adb23a55dce4 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeResourceTypeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index 638e337c5da9..b0cabc5b05bf 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_DisplayAttributeTakesPrecedenceOverDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs index 6b6e276a5c86..151a9d8621b1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithControlCharacters_EmitsValidLiteral#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index 9170f6568dc7..9bc0921c177b 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index 407f4397f496..1ac451b4f970 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index ce1b8c68c926..5aac48f14c40 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs index 52f3fe9f57c1..70eb9ee7d043 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithResourceType_OnHiddenGenericProperty#ValidatableInfoResolver.g.verified.cs @@ -338,7 +338,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -351,19 +351,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -371,8 +379,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -452,7 +470,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1089,7 +1107,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs index e3090a20a835..58eda703a2d7 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.PropertyDisplayName_WithoutDisplayAttribute_UsesPropertyName#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs index f90fe96382ee..ddda2a48cf43 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_LiteralOnConstructorParameter#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index b254ba551c5c..fef762630523 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index 2cd38580c7da..4825c59721c4 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.RecordPropertyDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs index 0f6d8070e37e..a7e92d10ea64 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithDisplayNameAttribute#ValidatableInfoResolver.g.verified.cs @@ -322,7 +322,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -335,19 +335,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -355,8 +363,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -436,7 +454,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1073,7 +1091,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs index cf1177c62bbc..0add10d01940 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithNameOnly#ValidatableInfoResolver.g.verified.cs @@ -322,7 +322,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -335,19 +335,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -355,8 +363,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -436,7 +454,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1073,7 +1091,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs index ddda09c3fb6b..c9b124368368 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorDisplayNameTests.TypeDisplayName_WithResourceType#ValidatableInfoResolver.g.verified.cs @@ -322,7 +322,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -335,19 +335,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -355,8 +363,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -436,7 +454,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1073,7 +1091,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs index aa7a98f49687..43bf165f22a1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanGenerateWhenAddValidationCalledMultipleTimes#ValidatableInfoResolver.g.verified.cs @@ -330,7 +330,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -343,19 +343,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -363,8 +371,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -444,7 +462,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1081,7 +1099,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index 79f8dfd9fc37..eb6ececfa842 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateClassTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -409,7 +409,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -422,19 +422,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -442,8 +450,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -523,7 +541,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1160,7 +1178,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs index 40b3e1a46aa6..007e264cac37 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypes#ValidatableInfoResolver.g.verified.cs @@ -415,7 +415,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -428,19 +428,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -448,8 +456,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -529,7 +547,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1166,7 +1184,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs index ceb1471f6210..8796026bdd30 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateComplexTypesWithJsonIgnore#ValidatableInfoResolver.g.verified.cs @@ -345,7 +345,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -358,19 +358,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -378,8 +386,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -459,7 +477,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1096,7 +1114,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs index 1bc667458920..bf3d265a0141 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject#ValidatableInfoResolver.g.verified.cs @@ -366,7 +366,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -379,19 +379,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -399,8 +407,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -480,7 +498,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1117,7 +1135,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs index a38df0e2d211..443b40f6a905 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateIValidatableObject_WithoutPropertyValidations#ValidatableInfoResolver.g.verified.cs @@ -372,7 +372,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -385,19 +385,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -405,8 +413,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -486,7 +504,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1123,7 +1141,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs index 2a1d6f3a20a8..57e9c72dcb82 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateMultipleNamespaces#ValidatableInfoResolver.g.verified.cs @@ -345,7 +345,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -358,19 +358,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -378,8 +386,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -459,7 +477,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1096,7 +1114,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs index 4a1e00d0fb79..f28937c04efe 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParameters#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs index d6f4ae4c1a33..51bd197ce3a1 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateParametersFromDelegateVariableHandler#ValidatableInfoResolver.g.verified.cs @@ -335,7 +335,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -348,19 +348,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -368,8 +376,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -449,7 +467,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1086,7 +1104,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs index 13cc2e14dc70..9b429339213c 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidatePolymorphicTypes#ValidatableInfoResolver.g.verified.cs @@ -398,7 +398,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -411,19 +411,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -431,8 +439,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -512,7 +530,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1149,7 +1167,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs index c7cd237c0a0a..fb6c178c45b8 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordStructTypes#ValidatableInfoResolver.g.verified.cs @@ -363,7 +363,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -376,19 +376,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -396,8 +404,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -477,7 +495,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1114,7 +1132,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs index e9b20f8c6310..aa111f9ca8ac 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypes#ValidatableInfoResolver.g.verified.cs @@ -443,7 +443,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -456,19 +456,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -476,8 +484,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -557,7 +575,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1194,7 +1212,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs index 79f8dfd9fc37..eb6ececfa842 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecordTypesWithAttribute#ValidatableInfoResolver.g.verified.cs @@ -409,7 +409,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -422,19 +422,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -442,8 +450,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -523,7 +541,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1160,7 +1178,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs index 92b015f22cb8..90205c77c817 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateRecursiveTypes#ValidatableInfoResolver.g.verified.cs @@ -335,7 +335,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -348,19 +348,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -368,8 +376,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -449,7 +467,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1086,7 +1104,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs index 9ad988c33eb0..77fc4d21cae6 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs @@ -377,7 +377,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -390,19 +390,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -410,8 +418,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -491,7 +509,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1128,7 +1146,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs index 806907e22419..82d388890dfc 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.CanValidateValidationAttributesOnClasses#ValidatableInfoResolver.g.verified.cs @@ -350,7 +350,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -363,19 +363,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -383,8 +391,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -464,7 +482,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1101,7 +1119,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs index b7af2812521c..793d3c6640b7 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmitForExemptTypes#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs index d00a340879c7..9922fffb3f44 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnClassProperties#ValidatableInfoResolver.g.verified.cs @@ -389,7 +389,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -402,19 +402,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -422,8 +430,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -503,7 +521,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1140,7 +1158,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs index b7af2812521c..793d3c6640b7 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnEndpointParameters#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs index 1b529465b422..f3ffe7a68a8b 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.DoesNotEmit_ForSkipValidationAttribute_OnRecordProperties#ValidatableInfoResolver.g.verified.cs @@ -345,7 +345,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -358,19 +358,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -378,8 +386,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -459,7 +477,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1096,7 +1114,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs index 0269fd687800..2b0d85364c86 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.IValidatableObject_ReceivesValidatedInstanceAsObjectInstance#ValidatableInfoResolver.g.verified.cs @@ -322,7 +322,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -335,19 +335,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -355,8 +363,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -436,7 +454,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1073,7 +1091,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs index 0da46502c61e..77cd049f61db 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsClassesWithNonAccessibleTypes#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs index 24425f207633..f6463a64187a 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsFileLocalTypes#ValidatableInfoResolver.g.verified.cs @@ -313,7 +313,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -326,19 +326,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -346,8 +354,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -427,7 +445,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1064,7 +1082,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs index 885a51c9a3c8..9bb9933dc3ea 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsIndexerPropertiesOnTypes#ValidatableInfoResolver.g.verified.cs @@ -329,7 +329,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -342,19 +342,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -362,8 +370,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -443,7 +461,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1080,7 +1098,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs index 1efeb8922aad..adb51d764e42 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.SkipsNonReadableAndStaticProperties#ValidatableInfoResolver.g.verified.cs @@ -351,7 +351,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -364,19 +364,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -384,8 +392,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -465,7 +483,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1102,7 +1120,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs index 923a1247241e..3755089b32ed 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesInternalTypes#ValidatableInfoResolver.g.verified.cs @@ -351,7 +351,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -364,19 +364,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -384,8 +392,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -465,7 +483,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1102,7 +1120,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs index 3c65a263abb8..512480c7672e 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/snapshots/ValidationsGeneratorTests.ValidatesPropertiesWithJsonIgnoreWhenWritingConditions#ValidatableInfoResolver.g.verified.cs @@ -341,7 +341,7 @@ file static class LocalizationHelpers public static string? FindLocalizedTemplate( global::Microsoft.Extensions.Localization.IStringLocalizer localizer, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, - string memberName, + string? memberName, global::System.Type declaringType) { if (!string.IsNullOrEmpty(attribute.ErrorMessage)) @@ -354,19 +354,27 @@ file static class LocalizationHelpers var attributeName = attribute.GetType().Name; var typeName = GetKeySegment(declaringType); - if (!string.Equals(memberName, declaringType.Name, global::System.StringComparison.Ordinal)) + if (memberName is not null) { - var memberMatch = localizer[$"{typeName}_{memberName}_{attributeName}_Error"]; + var memberKey = typeName is null + ? $"{memberName}_{attributeName}_Error" + : $"{typeName}_{memberName}_{attributeName}_Error"; + + var memberMatch = localizer[memberKey]; if (!memberMatch.ResourceNotFound) { return memberMatch.Value; } } - var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; - if (!typeMatch.ResourceNotFound) + // Without a type segment the type tier would duplicate the global tier. + if (typeName is not null) { - return typeMatch.Value; + var typeMatch = localizer[$"{typeName}_{attributeName}_Error"]; + if (!typeMatch.ResourceNotFound) + { + return typeMatch.Value; + } } var globalMatch = localizer[$"{attributeName}_Error"]; @@ -374,8 +382,18 @@ file static class LocalizationHelpers return globalMatch.ResourceNotFound ? null : globalMatch.Value; } - private static string GetKeySegment(global::System.Type type) + // Framework types carry no app-specific meaning as a key segment, so they are omitted. This + // mainly affects parameters, whose declaring type is the parameter's own type. + private static string? GetKeySegment(global::System.Type type) { + var ns = type.Namespace; + if (ns is not null && + (string.Equals(ns, "System", global::System.StringComparison.Ordinal) || + ns.StartsWith("System.", global::System.StringComparison.Ordinal))) + { + return null; + } + var name = type.Name; var arityIndex = name.IndexOf('`'); @@ -455,7 +473,7 @@ private protected static bool TryGetRequiredAttribute(global::System.ComponentMo private protected static string? ResolveAttributeErrorMessage( global::Microsoft.Extensions.Validation.ValidateContext context, - string memberName, + string? memberName, string displayName, global::System.Type declaringType, global::System.ComponentModel.DataAnnotations.ValidationAttribute attribute, @@ -1092,7 +1110,7 @@ private protected override void ReportError(global::Microsoft.Extensions.Validat // If no member names are specified, then treat this as a top-level error var errorMessage = ResolveAttributeErrorMessage( context, - memberName: Type.Name, + memberName: null, displayName, declaringType: Type, attribute, diff --git a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs index f0f71cad8b76..42f0f3699253 100644 --- a/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs +++ b/src/Validation/test/Microsoft.Extensions.Validation.Tests/ValidationLocalizationIntegrationTests.cs @@ -347,6 +347,54 @@ public async Task Parameter_LocalizerProvider_InvokedWithParameterType_AndUsed(b Assert.Equal("The Nom du paramètre field is required.", Single(context, "value")); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Parameter_ConventionKey_OmitsFrameworkTypeSegment(bool useAsync) + { + // The parameter's declaring type is its own type (string), which carries no app-specific + // meaning, so the type segment is dropped rather than producing "String_value_...". + var translations = new Dictionary + { + ["String_value_RequiredAttribute_Error"] = "framework-typed key", + ["String_RequiredAttribute_Error"] = "framework-typed key", + ["value_RequiredAttribute_Error"] = "{0} is required for this parameter.", + ["RequiredAttribute_Error"] = "{0} is mandatory.", + }; + var (provider, options) = CreateServices(translations); + var parameterInfo = typeof(LocalizedParameterActions) + .GetMethod(nameof(LocalizedParameterActions.Action))! + .GetParameters()[0]; + Assert.True(options.TryGetValidatableParameterInfo(parameterInfo, out var paramInfo)); + var context = GeneratedValidationTestHelpers.CreateContext(provider, options); + + await ValidateAsync(paramInfo, null, context, useAsync, default); + + Assert.Equal("Parameter Name is required for this parameter.", Single(context, "value")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Parameter_ConventionKey_FallsBackToGlobalTier(bool useAsync) + { + var translations = new Dictionary + { + ["String_RequiredAttribute_Error"] = "framework-typed key", + ["RequiredAttribute_Error"] = "{0} is mandatory.", + }; + var (provider, options) = CreateServices(translations); + var parameterInfo = typeof(LocalizedParameterActions) + .GetMethod(nameof(LocalizedParameterActions.Action))! + .GetParameters()[0]; + Assert.True(options.TryGetValidatableParameterInfo(parameterInfo, out var paramInfo)); + var context = GeneratedValidationTestHelpers.CreateContext(provider, options); + + await ValidateAsync(paramInfo, null, context, useAsync, default); + + Assert.Equal("Parameter Name is mandatory.", Single(context, "value")); + } + private static string Single(ValidateContext context, string key) => Assert.Single(context.ValidationErrors![key].Select(e => e.ErrorMessage));