@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Validation.GeneratorTests;
1111public 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 , "[email protected] " ) ; 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