2
2
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3
3
4
4
using System ;
5
+ using System . Collections ;
5
6
using System . Collections . Generic ;
6
7
using System . ComponentModel ;
7
8
using System . ComponentModel . DataAnnotations ;
9
+ using System . Diagnostics . Contracts ;
8
10
using System . Linq ;
9
11
using System . Reflection ;
12
+ using System . Runtime . InteropServices . ComTypes ;
10
13
using Microsoft . AspNetCore . Mvc . ModelBinding ;
11
14
using Microsoft . AspNetCore . Mvc . ModelBinding . Metadata ;
12
15
using Microsoft . Extensions . Localization ;
@@ -27,6 +30,9 @@ internal class DataAnnotationsMetadataProvider :
27
30
private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute" ;
28
31
private const string NullableFlagsFieldName = "NullableFlags" ;
29
32
33
+ private const string NullableContextAttributeFullName = "System.Runtime.CompilerServices.NullableContextAttribute" ;
34
+ private const string NullableContextFlagsFieldName = "Flag" ;
35
+
30
36
private readonly IStringLocalizerFactory _stringLocalizerFactory ;
31
37
private readonly MvcOptions _options ;
32
38
private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions ;
@@ -350,20 +356,44 @@ public void CreateValidationMetadata(ValidationMetadataProviderContext context)
350
356
if ( ! _options . SuppressImplicitRequiredAttributeForNonNullableReferenceTypes &&
351
357
requiredAttribute == null &&
352
358
! context . Key . ModelType . IsValueType &&
353
-
354
- // Look specifically at attributes on the property/parameter. [Nullable] on
355
- // the type has a different meaning.
356
- IsNonNullable ( context . ParameterAttributes ?? context . PropertyAttributes ?? Array . Empty < object > ( ) ) )
359
+ context . Key . MetadataKind != ModelMetadataKind . Type )
357
360
{
358
- // Since this behavior specifically relates to non-null-ness, we will use the non-default
359
- // option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in
360
- // a validation error by default because we convert empty/whitespace strings to null
361
- // unless you say otherwise.
362
- requiredAttribute = new RequiredAttribute ( )
361
+ var addInferredRequiredAttribute = false ;
362
+ if ( context . Key . MetadataKind == ModelMetadataKind . Type )
363
+ {
364
+ // Do nothing.
365
+ }
366
+ else if ( context . Key . MetadataKind == ModelMetadataKind . Property )
367
+ {
368
+ addInferredRequiredAttribute = IsNullableReferenceType (
369
+ context . Key . ContainerType ,
370
+ member : null ,
371
+ context . PropertyAttributes ) ;
372
+ }
373
+ else if ( context . Key . MetadataKind == ModelMetadataKind . Parameter )
374
+ {
375
+ addInferredRequiredAttribute = IsNullableReferenceType (
376
+ context . Key . ParameterInfo ? . Member . ReflectedType ,
377
+ context . Key . ParameterInfo . Member ,
378
+ context . ParameterAttributes ) ;
379
+ }
380
+ else
363
381
{
364
- AllowEmptyStrings = true ,
365
- } ;
366
- attributes . Add ( requiredAttribute ) ;
382
+ throw new InvalidOperationException ( "Unsupported ModelMetadataKind: " + context . Key . MetadataKind ) ;
383
+ }
384
+
385
+ if ( addInferredRequiredAttribute )
386
+ {
387
+ // Since this behavior specifically relates to non-null-ness, we will use the non-default
388
+ // option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in
389
+ // a validation error by default because we convert empty/whitespace strings to null
390
+ // unless you say otherwise.
391
+ requiredAttribute = new RequiredAttribute ( )
392
+ {
393
+ AllowEmptyStrings = true ,
394
+ } ;
395
+ attributes . Add ( requiredAttribute ) ;
396
+ }
367
397
}
368
398
369
399
if ( requiredAttribute != null )
@@ -419,16 +449,27 @@ private static string GetDisplayGroup(FieldInfo field)
419
449
return string . Empty ;
420
450
}
421
451
452
+ internal static bool IsNullableReferenceType ( Type containingType , MemberInfo member , IEnumerable < object > attributes )
453
+ {
454
+ if ( HasNullableAttribute ( attributes , out var result ) )
455
+ {
456
+ return result ;
457
+ }
458
+
459
+ return IsNullableBasedOnContext ( containingType , member ) ;
460
+ }
461
+
422
462
// Internal for testing
423
- internal static bool IsNonNullable ( IEnumerable < object > attributes )
463
+ internal static bool HasNullableAttribute ( IEnumerable < object > attributes , out bool isNullable )
424
464
{
425
465
// [Nullable] is compiler synthesized, comparing by name.
426
466
var nullableAttribute = attributes
427
467
. Where ( a => string . Equals ( a . GetType ( ) . FullName , NullableAttributeFullTypeName , StringComparison . Ordinal ) )
428
468
. FirstOrDefault ( ) ;
429
469
if ( nullableAttribute == null )
430
470
{
431
- return false ;
471
+ isNullable = false ;
472
+ return false ; // [Nullable] not found
432
473
}
433
474
434
475
// We don't handle cases where generics and NNRT are used. This runs into a
@@ -443,10 +484,51 @@ internal static bool IsNonNullable(IEnumerable<object> attributes)
443
484
flags . Length >= 0 &&
444
485
flags [ 0 ] == 1 ) // First element is the property/parameter type.
445
486
{
446
- return true ;
487
+ isNullable = true ;
488
+ return true ; // [Nullable] found and type is an NNRT
447
489
}
448
490
449
- return false ;
491
+ isNullable = false ;
492
+ return true ; // [Nullable] found but type is not an NNRT
493
+ }
494
+
495
+ internal static bool IsNullableBasedOnContext ( Type containingType , MemberInfo member )
496
+ {
497
+ var attributes = member ? . GetCustomAttributes ( inherit : true ) ?? Array . Empty < object > ( ) ;
498
+ var isNullable = AttributesHasNullableContext ( attributes ) ;
499
+ if ( isNullable != null )
500
+ {
501
+ return isNullable . Value ;
502
+ }
503
+
504
+ attributes = containingType . GetCustomAttributes ( inherit : false ) ;
505
+ isNullable = AttributesHasNullableContext ( attributes ) ;
506
+ if ( isNullable != null )
507
+ {
508
+ return isNullable . Value ;
509
+ }
510
+
511
+ // If we don't find the attribute on the declaring type then repeat at the module level
512
+ attributes = containingType . Module . GetCustomAttributes ( inherit : false ) ;
513
+ isNullable = AttributesHasNullableContext ( attributes ) ;
514
+ return isNullable ?? false ;
515
+
516
+ bool ? AttributesHasNullableContext ( object [ ] attributes )
517
+ {
518
+ var nullableContextAttribute = attributes
519
+ . Where ( a => string . Equals ( a . GetType ( ) . FullName , NullableContextAttributeFullName , StringComparison . Ordinal ) )
520
+ . FirstOrDefault ( ) ;
521
+ if ( nullableContextAttribute != null )
522
+ {
523
+ if ( nullableContextAttribute . GetType ( ) . GetField ( NullableContextFlagsFieldName ) is FieldInfo field &&
524
+ field . GetValue ( nullableContextAttribute ) is byte @byte )
525
+ {
526
+ return @byte == 1 ; // [NullableContext] found
527
+ }
528
+ }
529
+
530
+ return null ;
531
+ }
450
532
}
451
533
}
452
534
}
0 commit comments