Skip to content

Commit 332623a

Browse files
Merging main into darc-main-b3d99129-0da1-44dd-9167-5a0ff9ee9993
2 parents 983281d + d12915f commit 332623a

File tree

8 files changed

+117
-17
lines changed

8 files changed

+117
-17
lines changed

.github/policies/resourceManagement.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ configuration:
663663
- isAction:
664664
action: Opened
665665
- isActivitySender:
666-
user: app/dependabot
666+
user: dependabot[bot]
667667
issueAuthor: False
668668
- targetsBranch:
669669
branch: main

src/Http/Http.Abstractions/src/Validation/ValidatableTypeInfo.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,20 @@ public virtual async Task ValidateAsync(object? value, ValidateContext context,
104104
{
105105
if (validationResult != ValidationResult.Success && validationResult.ErrorMessage is not null)
106106
{
107-
var memberName = validationResult.MemberNames.First();
108-
var key = string.IsNullOrEmpty(originalPrefix) ?
109-
memberName :
110-
$"{originalPrefix}.{memberName}";
111-
112-
context.AddOrExtendValidationError(key, validationResult.ErrorMessage);
107+
// Create a validation error for each member name that is provided
108+
foreach (var memberName in validationResult.MemberNames)
109+
{
110+
var key = string.IsNullOrEmpty(originalPrefix) ?
111+
memberName :
112+
$"{originalPrefix}.{memberName}";
113+
context.AddOrExtendValidationError(key, validationResult.ErrorMessage);
114+
}
115+
116+
if (!validationResult.MemberNames.Any())
117+
{
118+
// If no member names are specified, then treat this as a top-level error
119+
context.AddOrExtendValidationError(string.Empty, validationResult.ErrorMessage);
120+
}
113121
}
114122
}
115123

src/Http/Http.Abstractions/test/Validation/ValidatableTypeInfoTests.cs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,95 @@ public async Task Validate_RequiredOnPropertyShortCircuitsOtherValidations()
495495
Assert.Equal("The Password field is required.", error.Value.Single());
496496
}
497497

498+
[Fact]
499+
public async Task Validate_IValidatableObject_WithZeroAndMultipleMemberNames_BehavesAsExpected()
500+
{
501+
var globalType = new TestValidatableTypeInfo(
502+
typeof(GlobalErrorObject),
503+
[]); // no properties – nothing sets MemberName
504+
505+
var context = new ValidateContext
506+
{
507+
ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
508+
{
509+
{ typeof(GlobalErrorObject), globalType }
510+
})
511+
};
512+
513+
var globalErrorInstance = new GlobalErrorObject { Data = -1 };
514+
context.ValidationContext = new ValidationContext(globalErrorInstance);
515+
516+
await globalType.ValidateAsync(globalErrorInstance, context, default);
517+
518+
Assert.NotNull(context.ValidationErrors);
519+
var globalError = Assert.Single(context.ValidationErrors);
520+
Assert.Equal(string.Empty, globalError.Key);
521+
Assert.Equal("Data must be positive.", globalError.Value.Single());
522+
523+
var multiType = new TestValidatableTypeInfo(
524+
typeof(MultiMemberErrorObject),
525+
[
526+
CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "FirstName", "FirstName", []),
527+
CreatePropertyInfo(typeof(MultiMemberErrorObject), typeof(string), "LastName", "LastName", [])
528+
]);
529+
530+
context.ValidationErrors = [];
531+
context.ValidationOptions = new TestValidationOptions(new Dictionary<Type, ValidatableTypeInfo>
532+
{
533+
{ typeof(MultiMemberErrorObject), multiType }
534+
});
535+
536+
var multiErrorInstance = new MultiMemberErrorObject { FirstName = "", LastName = "" };
537+
context.ValidationContext = new ValidationContext(multiErrorInstance);
538+
539+
await multiType.ValidateAsync(multiErrorInstance, context, default);
540+
541+
Assert.NotNull(context.ValidationErrors);
542+
Assert.Collection(context.ValidationErrors,
543+
kvp =>
544+
{
545+
Assert.Equal("FirstName", kvp.Key);
546+
Assert.Equal("FirstName and LastName are required.", kvp.Value.First());
547+
},
548+
kvp =>
549+
{
550+
Assert.Equal("LastName", kvp.Key);
551+
Assert.Equal("FirstName and LastName are required.", kvp.Value.First());
552+
});
553+
}
554+
555+
// Returns no member names to validate https://github.com/dotnet/aspnetcore/issues/61739
556+
private class GlobalErrorObject : IValidatableObject
557+
{
558+
public int Data { get; set; }
559+
560+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
561+
{
562+
if (Data <= 0)
563+
{
564+
yield return new ValidationResult("Data must be positive.");
565+
}
566+
}
567+
}
568+
569+
// Returns multiple member names to validate https://github.com/dotnet/aspnetcore/issues/61739
570+
private class MultiMemberErrorObject : IValidatableObject
571+
{
572+
public string? FirstName { get; set; }
573+
public string? LastName { get; set; }
574+
575+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
576+
{
577+
if (string.IsNullOrEmpty(FirstName) || string.IsNullOrEmpty(LastName))
578+
{
579+
// MULTIPLE member names
580+
yield return new ValidationResult(
581+
"FirstName and LastName are required.",
582+
[nameof(FirstName), nameof(LastName)]);
583+
}
584+
}
585+
}
586+
498587
private ValidatablePropertyInfo CreatePropertyInfo(
499588
Type containingType,
500589
Type propertyType,
@@ -534,7 +623,7 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
534623
{
535624
if (Salary < 0)
536625
{
537-
yield return new ValidationResult("Salary must be a positive value.", new[] { nameof(Salary) });
626+
yield return new ValidationResult("Salary must be a positive value.", ["Salary"]);
538627
}
539628
}
540629
}

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Extensions/ITypeSymbolExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ public static ITypeSymbol UnwrapType(this ITypeSymbol type, INamedTypeSymbol enu
4646

4747
if (type.NullableAnnotation == NullableAnnotation.Annotated)
4848
{
49-
// Extract the underlying type from a reference type
50-
type = type.OriginalDefinition;
49+
// Remove the nullable annotation but keep any generic arguments, e.g. List<int>? → List<int>
50+
// so we can retain them in future steps.
51+
type = type.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
5152
}
5253

5354
if (type is INamedTypeSymbol namedType && namedType.IsEnumerable(enumerable) && namedType.TypeArguments.Length == 1)

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Parsers/ValidationsGenerator.TypesParser.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ internal ImmutableArray<ValidatableType> ExtractValidatableTypes(IInvocationOper
2929
List<ITypeSymbol> visitedTypes = [];
3030
foreach (var parameter in parameters)
3131
{
32-
_ = TryExtractValidatableType(parameter.Type.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)), wellKnownTypes, ref validatableTypes, ref visitedTypes);
32+
_ = TryExtractValidatableType(parameter.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
3333
}
3434
return [.. validatableTypes];
3535
}
3636

37-
internal bool TryExtractValidatableType(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, ref HashSet<ValidatableType> validatableTypes, ref List<ITypeSymbol> visitedTypes)
37+
internal bool TryExtractValidatableType(ITypeSymbol incomingTypeSymbol, WellKnownTypes wellKnownTypes, ref HashSet<ValidatableType> validatableTypes, ref List<ITypeSymbol> visitedTypes)
3838
{
39+
var typeSymbol = incomingTypeSymbol.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable));
3940
if (typeSymbol.SpecialType != SpecialType.None)
4041
{
4142
return false;
@@ -126,7 +127,7 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
126127
// Check if the property's type is validatable, this resolves
127128
// validatable types in the inheritance hierarchy
128129
var hasValidatableType = TryExtractValidatableType(
129-
correspondingProperty.Type.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)),
130+
correspondingProperty.Type,
130131
wellKnownTypes,
131132
ref validatableTypes,
132133
ref visitedTypes);
@@ -153,7 +154,7 @@ internal ImmutableArray<ValidatableProperty> ExtractValidatableMembers(ITypeSymb
153154
continue;
154155
}
155156

156-
var hasValidatableType = TryExtractValidatableType(member.Type.UnwrapType(wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Collections_IEnumerable)), wellKnownTypes, ref validatableTypes, ref visitedTypes);
157+
var hasValidatableType = TryExtractValidatableType(member.Type, wellKnownTypes, ref validatableTypes, ref visitedTypes);
157158
var attributes = ExtractValidationAttributes(member, wellKnownTypes, out var isRequired);
158159

159160
// If the member has no validation attributes or validatable types and is not required, skip it.

src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.ComplexType.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public class ComplexType
4545
4646
public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance("some-value", default);
4747
48-
public List<SubType> ListOfSubTypes { get; set; } = [];
48+
// Nullable to validate https://github.com/dotnet/aspnetcore/issues/61737
49+
public List<SubType>? ListOfSubTypes { get; set; } = [];
4950
5051
[DerivedValidation(ErrorMessage = "Value must be an even number")]
5152
public int IntegerWithDerivedValidationAttribute { get; set; }

src/Http/Http.Extensions/test/ValidationsGenerator/ValidationsGenerator.Parsable.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class ComplexTypeWithParsableProperties
3939
public TimeOnly? TimeOnlyWithRequiredValue { get; set; } = TimeOnly.FromDateTime(DateTime.UtcNow);
4040
4141
[Url(ErrorMessage = "The field Url must be a valid URL.")]
42-
public Uri? Url { get; set; } = new Uri("https://example.com");
42+
public string? Url { get; set; } = "https://example.com";
4343
4444
[Required]
4545
[Range(typeof(DateOnly), "2023-01-01", "2025-12-31", ErrorMessage = "Date must be between 2023-01-01 and 2025-12-31")]

src/Http/Http.Extensions/test/ValidationsGenerator/snapshots/ValidationsGeneratorTests.CanValidateTypeWithParsableProperties#ValidatableInfoResolver.g.verified.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private ValidatableTypeInfo CreateComplexTypeWithParsableProperties()
9595
),
9696
new GeneratedValidatablePropertyInfo(
9797
containingType: typeof(global::ComplexTypeWithParsableProperties),
98-
propertyType: typeof(global::System.Uri),
98+
propertyType: typeof(string),
9999
name: "Url",
100100
displayName: "Url"
101101
),

0 commit comments

Comments
 (0)