Skip to content

Commit d5d2a31

Browse files
authored
Enhance validation for classes and records (#62633)
1 parent 238af05 commit d5d2a31

4 files changed

+609
-2
lines changed

src/Validation/gen/Parsers/ValidationsGenerator.AttributeParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public sealed partial class ValidationsGenerator : IIncrementalGenerator
1414
{
1515
internal static bool ShouldTransformSymbolWithAttribute(SyntaxNode syntaxNode, CancellationToken cancellationToken)
1616
{
17-
return syntaxNode is ClassDeclarationSyntax;
17+
return syntaxNode is ClassDeclarationSyntax or RecordDeclarationSyntax;
1818
}
1919

2020
internal ImmutableArray<ValidatableType> TransformValidatableTypeWithAttribute(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)

src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests/ValidationsGenerator.ValidatableType.cs

Lines changed: 370 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Validation.GeneratorTests;
1111
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
1212
{
1313
[Fact]
14-
public async Task CanValidateTypesWithAttribute()
14+
public async Task CanValidateClassTypesWithAttribute()
1515
{
1616
var source = """
1717
#pragma warning disable ASP0029
@@ -378,4 +378,373 @@ async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo)
378378
}
379379
});
380380
}
381+
382+
[Fact]
383+
public async Task CanValidateRecordTypesWithAttribute()
384+
{
385+
var source = """
386+
#pragma warning disable ASP0029
387+
388+
using System;
389+
using System.ComponentModel.DataAnnotations;
390+
using System.Collections.Generic;
391+
using System.Linq;
392+
using Microsoft.AspNetCore.Builder;
393+
using Microsoft.AspNetCore.Http;
394+
using Microsoft.Extensions.Validation;
395+
using Microsoft.AspNetCore.Routing;
396+
using Microsoft.Extensions.DependencyInjection;
397+
398+
var builder = WebApplication.CreateBuilder();
399+
400+
builder.Services.AddValidation();
401+
402+
var app = builder.Build();
403+
404+
app.Run();
405+
406+
[ValidatableType]
407+
public record ComplexType
408+
{
409+
[Range(10, 100)]
410+
public int IntegerWithRange { get; set; } = 10;
411+
412+
[Range(10, 100), Display(Name = "Valid identifier")]
413+
public int IntegerWithRangeAndDisplayName { get; set; } = 50;
414+
415+
[Required]
416+
public SubType PropertyWithMemberAttributes { get; set; } = new SubType();
417+
418+
public SubType PropertyWithoutMemberAttributes { get; set; } = new SubType();
419+
420+
public SubTypeWithInheritance PropertyWithInheritance { get; set; } = new SubTypeWithInheritance();
421+
422+
public List<SubType> ListOfSubTypes { get; set; } = [];
423+
424+
[CustomValidation(ErrorMessage = "Value must be an even number")]
425+
public int IntegerWithCustomValidationAttribute { get; set; }
426+
427+
[CustomValidation, Range(10, 100)]
428+
public int PropertyWithMultipleAttributes { get; set; } = 10;
429+
}
430+
431+
public class CustomValidationAttribute : ValidationAttribute
432+
{
433+
public override bool IsValid(object? value) => value is int number && number % 2 == 0;
434+
}
435+
436+
public record SubType
437+
{
438+
[Required]
439+
public string RequiredProperty { get; set; } = "some-value";
440+
441+
[StringLength(10)]
442+
public string? StringWithLength { get; set; }
443+
}
444+
445+
public record SubTypeWithInheritance : SubType
446+
{
447+
[EmailAddress]
448+
public string? EmailString { get; set; }
449+
}
450+
""";
451+
await Verify(source, out var compilation);
452+
VerifyValidatableType(compilation, "ComplexType", async (validationOptions, type) =>
453+
{
454+
Assert.True(validationOptions.TryGetValidatableTypeInfo(type, out var validatableTypeInfo));
455+
456+
await InvalidIntegerWithRangeProducesError(validatableTypeInfo);
457+
await InvalidIntegerWithRangeAndDisplayNameProducesError(validatableTypeInfo);
458+
await MissingRequiredSubtypePropertyProducesError(validatableTypeInfo);
459+
await InvalidRequiredSubtypePropertyProducesError(validatableTypeInfo);
460+
await InvalidSubTypeWithInheritancePropertyProducesError(validatableTypeInfo);
461+
await InvalidListOfSubTypesProducesError(validatableTypeInfo);
462+
await InvalidPropertyWithDerivedValidationAttributeProducesError(validatableTypeInfo);
463+
await InvalidPropertyWithMultipleAttributesProducesError(validatableTypeInfo);
464+
await InvalidPropertyWithCustomValidationProducesError(validatableTypeInfo);
465+
await ValidInputProducesNoWarnings(validatableTypeInfo);
466+
467+
async Task InvalidIntegerWithRangeProducesError(IValidatableInfo validatableInfo)
468+
{
469+
var instance = Activator.CreateInstance(type);
470+
type.GetProperty("IntegerWithRange")?.SetValue(instance, 5);
471+
var context = new ValidateContext
472+
{
473+
ValidationOptions = validationOptions,
474+
ValidationContext = new ValidationContext(instance)
475+
};
476+
477+
await validatableTypeInfo.ValidateAsync(instance, context, CancellationToken.None);
478+
479+
Assert.Collection(context.ValidationErrors, kvp =>
480+
{
481+
Assert.Equal("IntegerWithRange", kvp.Key);
482+
Assert.Equal("The field IntegerWithRange must be between 10 and 100.", kvp.Value.Single());
483+
});
484+
}
485+
486+
async Task InvalidIntegerWithRangeAndDisplayNameProducesError(IValidatableInfo validatableInfo)
487+
{
488+
var instance = Activator.CreateInstance(type);
489+
type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 5);
490+
var context = new ValidateContext
491+
{
492+
ValidationOptions = validationOptions,
493+
ValidationContext = new ValidationContext(instance)
494+
};
495+
496+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
497+
498+
Assert.Collection(context.ValidationErrors, kvp =>
499+
{
500+
Assert.Equal("IntegerWithRangeAndDisplayName", kvp.Key);
501+
Assert.Equal("The field Valid identifier must be between 10 and 100.", kvp.Value.Single());
502+
});
503+
}
504+
505+
async Task MissingRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo)
506+
{
507+
var instance = Activator.CreateInstance(type);
508+
type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, null);
509+
var context = new ValidateContext
510+
{
511+
ValidationOptions = validationOptions,
512+
ValidationContext = new ValidationContext(instance)
513+
};
514+
515+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
516+
517+
Assert.Collection(context.ValidationErrors, kvp =>
518+
{
519+
Assert.Equal("PropertyWithMemberAttributes", kvp.Key);
520+
Assert.Equal("The PropertyWithMemberAttributes field is required.", kvp.Value.Single());
521+
});
522+
}
523+
524+
async Task InvalidRequiredSubtypePropertyProducesError(IValidatableInfo validatableInfo)
525+
{
526+
var instance = Activator.CreateInstance(type);
527+
var subType = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
528+
subType.GetType().GetProperty("RequiredProperty")?.SetValue(subType, "");
529+
subType.GetType().GetProperty("StringWithLength")?.SetValue(subType, "way-too-long");
530+
type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType);
531+
var context = new ValidateContext
532+
{
533+
ValidationOptions = validationOptions,
534+
ValidationContext = new ValidationContext(instance)
535+
};
536+
537+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
538+
539+
Assert.Collection(context.ValidationErrors,
540+
kvp =>
541+
{
542+
Assert.Equal("PropertyWithMemberAttributes.RequiredProperty", kvp.Key);
543+
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
544+
},
545+
kvp =>
546+
{
547+
Assert.Equal("PropertyWithMemberAttributes.StringWithLength", kvp.Key);
548+
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
549+
});
550+
}
551+
552+
async Task InvalidSubTypeWithInheritancePropertyProducesError(IValidatableInfo validatableInfo)
553+
{
554+
var instance = Activator.CreateInstance(type);
555+
var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!);
556+
inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, "");
557+
inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "way-too-long");
558+
inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "not-an-email");
559+
type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType);
560+
var context = new ValidateContext
561+
{
562+
ValidationOptions = validationOptions,
563+
ValidationContext = new ValidationContext(instance)
564+
};
565+
566+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
567+
568+
Assert.Collection(context.ValidationErrors,
569+
kvp =>
570+
{
571+
Assert.Equal("PropertyWithInheritance.EmailString", kvp.Key);
572+
Assert.Equal("The EmailString field is not a valid e-mail address.", kvp.Value.Single());
573+
},
574+
kvp =>
575+
{
576+
Assert.Equal("PropertyWithInheritance.RequiredProperty", kvp.Key);
577+
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
578+
},
579+
kvp =>
580+
{
581+
Assert.Equal("PropertyWithInheritance.StringWithLength", kvp.Key);
582+
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
583+
});
584+
}
585+
586+
async Task InvalidListOfSubTypesProducesError(IValidatableInfo validatableInfo)
587+
{
588+
var instance = Activator.CreateInstance(type);
589+
var subTypeList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!));
590+
591+
// Create first invalid item
592+
var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
593+
subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, "");
594+
subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "way-too-long");
595+
596+
// Create second invalid item
597+
var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
598+
subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid");
599+
subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "way-too-long");
600+
601+
// Create valid item
602+
var subType3 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
603+
subType3.GetType().GetProperty("RequiredProperty")?.SetValue(subType3, "valid");
604+
subType3.GetType().GetProperty("StringWithLength")?.SetValue(subType3, "valid");
605+
606+
// Add to list
607+
subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType1]);
608+
subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType2]);
609+
subTypeList.GetType().GetMethod("Add")?.Invoke(subTypeList, [subType3]);
610+
611+
type.GetProperty("ListOfSubTypes")?.SetValue(instance, subTypeList);
612+
var context = new ValidateContext
613+
{
614+
ValidationOptions = validationOptions,
615+
ValidationContext = new ValidationContext(instance)
616+
};
617+
618+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
619+
620+
Assert.Collection(context.ValidationErrors,
621+
kvp =>
622+
{
623+
Assert.Equal("ListOfSubTypes[0].RequiredProperty", kvp.Key);
624+
Assert.Equal("The RequiredProperty field is required.", kvp.Value.Single());
625+
},
626+
kvp =>
627+
{
628+
Assert.Equal("ListOfSubTypes[0].StringWithLength", kvp.Key);
629+
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
630+
},
631+
kvp =>
632+
{
633+
Assert.Equal("ListOfSubTypes[1].StringWithLength", kvp.Key);
634+
Assert.Equal("The field StringWithLength must be a string with a maximum length of 10.", kvp.Value.Single());
635+
});
636+
}
637+
638+
async Task InvalidPropertyWithDerivedValidationAttributeProducesError(IValidatableInfo validatableInfo)
639+
{
640+
var instance = Activator.CreateInstance(type);
641+
type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 5); // Odd number, should fail
642+
var context = new ValidateContext
643+
{
644+
ValidationOptions = validationOptions,
645+
ValidationContext = new ValidationContext(instance)
646+
};
647+
648+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
649+
650+
Assert.Collection(context.ValidationErrors, kvp =>
651+
{
652+
Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key);
653+
Assert.Equal("Value must be an even number", kvp.Value.Single());
654+
});
655+
}
656+
657+
async Task InvalidPropertyWithMultipleAttributesProducesError(IValidatableInfo validatableInfo)
658+
{
659+
var instance = Activator.CreateInstance(type);
660+
type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 5);
661+
var context = new ValidateContext
662+
{
663+
ValidationOptions = validationOptions,
664+
ValidationContext = new ValidationContext(instance)
665+
};
666+
667+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
668+
669+
Assert.Collection(context.ValidationErrors, kvp =>
670+
{
671+
Assert.Equal("PropertyWithMultipleAttributes", kvp.Key);
672+
Assert.Collection(kvp.Value,
673+
error =>
674+
{
675+
Assert.Equal("The field PropertyWithMultipleAttributes is invalid.", error);
676+
},
677+
error =>
678+
{
679+
Assert.Equal("The field PropertyWithMultipleAttributes must be between 10 and 100.", error);
680+
});
681+
});
682+
}
683+
684+
async Task InvalidPropertyWithCustomValidationProducesError(IValidatableInfo validatableInfo)
685+
{
686+
var instance = Activator.CreateInstance(type);
687+
type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 3); // Odd number should fail
688+
var context = new ValidateContext
689+
{
690+
ValidationOptions = validationOptions,
691+
ValidationContext = new ValidationContext(instance)
692+
};
693+
694+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
695+
696+
Assert.Collection(context.ValidationErrors, kvp =>
697+
{
698+
Assert.Equal("IntegerWithCustomValidationAttribute", kvp.Key);
699+
Assert.Equal("Value must be an even number", kvp.Value.Single());
700+
});
701+
}
702+
703+
async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo)
704+
{
705+
var instance = Activator.CreateInstance(type);
706+
707+
// Set all properties with valid values
708+
type.GetProperty("IntegerWithRange")?.SetValue(instance, 50);
709+
type.GetProperty("IntegerWithRangeAndDisplayName")?.SetValue(instance, 50);
710+
711+
// Create and set PropertyWithMemberAttributes
712+
var subType1 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
713+
subType1.GetType().GetProperty("RequiredProperty")?.SetValue(subType1, "valid");
714+
subType1.GetType().GetProperty("StringWithLength")?.SetValue(subType1, "valid");
715+
type.GetProperty("PropertyWithMemberAttributes")?.SetValue(instance, subType1);
716+
717+
// Create and set PropertyWithoutMemberAttributes
718+
var subType2 = Activator.CreateInstance(type.Assembly.GetType("SubType")!);
719+
subType2.GetType().GetProperty("RequiredProperty")?.SetValue(subType2, "valid");
720+
subType2.GetType().GetProperty("StringWithLength")?.SetValue(subType2, "valid");
721+
type.GetProperty("PropertyWithoutMemberAttributes")?.SetValue(instance, subType2);
722+
723+
// Create and set PropertyWithInheritance
724+
var inheritanceType = Activator.CreateInstance(type.Assembly.GetType("SubTypeWithInheritance")!);
725+
inheritanceType.GetType().GetProperty("RequiredProperty")?.SetValue(inheritanceType, "valid");
726+
inheritanceType.GetType().GetProperty("StringWithLength")?.SetValue(inheritanceType, "valid");
727+
inheritanceType.GetType().GetProperty("EmailString")?.SetValue(inheritanceType, "test@example.com");
728+
type.GetProperty("PropertyWithInheritance")?.SetValue(instance, inheritanceType);
729+
730+
// Create empty list for ListOfSubTypes
731+
var emptyList = Activator.CreateInstance(typeof(List<>).MakeGenericType(type.Assembly.GetType("SubType")!));
732+
type.GetProperty("ListOfSubTypes")?.SetValue(instance, emptyList);
733+
734+
// Set custom validation attributes
735+
type.GetProperty("IntegerWithCustomValidationAttribute")?.SetValue(instance, 2); // Even number should pass
736+
type.GetProperty("PropertyWithMultipleAttributes")?.SetValue(instance, 12);
737+
738+
var context = new ValidateContext
739+
{
740+
ValidationOptions = validationOptions,
741+
ValidationContext = new ValidationContext(instance)
742+
};
743+
744+
await validatableInfo.ValidateAsync(instance, context, CancellationToken.None);
745+
746+
Assert.Null(context.ValidationErrors);
747+
}
748+
});
749+
}
381750
}

0 commit comments

Comments
 (0)