@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.Validation.GeneratorTests;
11
11
public partial class ValidationsGeneratorTests : ValidationsGeneratorTestBase
12
12
{
13
13
[ Fact ]
14
- public async Task CanValidateTypesWithAttribute ( )
14
+ public async Task CanValidateClassTypesWithAttribute ( )
15
15
{
16
16
var source = """
17
17
#pragma warning disable ASP0029
@@ -378,4 +378,373 @@ async Task ValidInputProducesNoWarnings(IValidatableInfo validatableInfo)
378
378
}
379
379
} ) ;
380
380
}
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
+ }
381
750
}
0 commit comments