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 ;
13
+ using System . Runtime . InteropServices . ComTypes ;
10
14
using Microsoft . AspNetCore . Mvc . ModelBinding ;
11
15
using Microsoft . AspNetCore . Mvc . ModelBinding . Metadata ;
12
16
using Microsoft . Extensions . Localization ;
@@ -27,6 +31,9 @@ internal class DataAnnotationsMetadataProvider :
27
31
private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute" ;
28
32
private const string NullableFlagsFieldName = "NullableFlags" ;
29
33
34
+ private const string NullableContextAttributeFullName = "System.Runtime.CompilerServices.NullableContextAttribute" ;
35
+ private const string NullableContextFlagsFieldName = "Flag" ;
36
+
30
37
private readonly IStringLocalizerFactory _stringLocalizerFactory ;
31
38
private readonly MvcOptions _options ;
32
39
private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions ;
@@ -350,20 +357,44 @@ public void CreateValidationMetadata(ValidationMetadataProviderContext context)
350
357
if ( ! _options . SuppressImplicitRequiredAttributeForNonNullableReferenceTypes &&
351
358
requiredAttribute == null &&
352
359
! 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 > ( ) ) )
360
+ context . Key . MetadataKind != ModelMetadataKind . Type )
357
361
{
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 ( )
362
+ var addInferredRequiredAttribute = false ;
363
+ if ( context . Key . MetadataKind == ModelMetadataKind . Type )
364
+ {
365
+ // Do nothing.
366
+ }
367
+ else if ( context . Key . MetadataKind == ModelMetadataKind . Property )
368
+ {
369
+ addInferredRequiredAttribute = IsNullableReferenceType (
370
+ context . Key . ContainerType ,
371
+ member : null ,
372
+ context . PropertyAttributes ) ;
373
+ }
374
+ else if ( context . Key . MetadataKind == ModelMetadataKind . Parameter )
363
375
{
364
- AllowEmptyStrings = true ,
365
- } ;
366
- attributes . Add ( requiredAttribute ) ;
376
+ addInferredRequiredAttribute = IsNullableReferenceType (
377
+ context . Key . ParameterInfo ? . Member . ReflectedType ,
378
+ context . Key . ParameterInfo . Member ,
379
+ context . ParameterAttributes ) ;
380
+ }
381
+ else
382
+ {
383
+ throw new InvalidOperationException ( "Unsupported ModelMetadataKind: " + context . Key . MetadataKind ) ;
384
+ }
385
+
386
+ if ( addInferredRequiredAttribute )
387
+ {
388
+ // Since this behavior specifically relates to non-null-ness, we will use the non-default
389
+ // option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in
390
+ // a validation error by default because we convert empty/whitespace strings to null
391
+ // unless you say otherwise.
392
+ requiredAttribute = new RequiredAttribute ( )
393
+ {
394
+ AllowEmptyStrings = true ,
395
+ } ;
396
+ attributes . Add ( requiredAttribute ) ;
397
+ }
367
398
}
368
399
369
400
if ( requiredAttribute != null )
@@ -419,16 +450,26 @@ private static string GetDisplayGroup(FieldInfo field)
419
450
return string . Empty ;
420
451
}
421
452
453
+ internal static bool IsNullableReferenceType ( Type containingType , MemberInfo member , IEnumerable < object > attributes )
454
+ {
455
+ if ( HasNullableAttribute ( attributes , out var result ) )
456
+ {
457
+ return result ;
458
+ }
459
+
460
+ return IsNullableBasedOnContext ( containingType , member ) ;
461
+ }
462
+
422
463
// Internal for testing
423
- internal static bool IsNonNullable ( IEnumerable < object > attributes )
464
+ internal static bool HasNullableAttribute ( IEnumerable < object > attributes , out bool isNullable )
424
465
{
425
466
// [Nullable] is compiler synthesized, comparing by name.
426
467
var nullableAttribute = attributes
427
- . Where ( a => string . Equals ( a . GetType ( ) . FullName , NullableAttributeFullTypeName , StringComparison . Ordinal ) )
428
- . FirstOrDefault ( ) ;
468
+ . FirstOrDefault ( a => string . Equals ( a . GetType ( ) . FullName , NullableAttributeFullTypeName , StringComparison . Ordinal ) ) ;
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,61 @@ 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
+ // The [Nullable] and [NullableContext] attributes are not inherited.
498
+ //
499
+ // The [NullableContext] attribute can appear on a method or on the module.
500
+ var attributes = member ? . GetCustomAttributes ( inherit : false ) ?? Array . Empty < object > ( ) ;
501
+ var isNullable = AttributesHasNullableContext ( attributes ) ;
502
+ if ( isNullable != null )
503
+ {
504
+ return isNullable . Value ;
505
+ }
506
+
507
+ // Check on the containing type
508
+ var type = containingType ;
509
+ do
510
+ {
511
+ attributes = type . GetCustomAttributes ( inherit : false ) ;
512
+ isNullable = AttributesHasNullableContext ( attributes ) ;
513
+ if ( isNullable != null )
514
+ {
515
+ return isNullable . Value ;
516
+ }
517
+
518
+ type = type . DeclaringType ;
519
+ }
520
+ while ( type != null ) ;
521
+
522
+ // If we don't find the attribute on the declaring type then repeat at the module level
523
+ attributes = containingType . Module . GetCustomAttributes ( inherit : false ) ;
524
+ isNullable = AttributesHasNullableContext ( attributes ) ;
525
+ return isNullable ?? false ;
526
+
527
+ bool ? AttributesHasNullableContext ( object [ ] attributes )
528
+ {
529
+ var nullableContextAttribute = attributes
530
+ . FirstOrDefault ( a => string . Equals ( a . GetType ( ) . FullName , NullableContextAttributeFullName , StringComparison . Ordinal ) ) ;
531
+ if ( nullableContextAttribute != null )
532
+ {
533
+ if ( nullableContextAttribute . GetType ( ) . GetField ( NullableContextFlagsFieldName ) is FieldInfo field &&
534
+ field . GetValue ( nullableContextAttribute ) is byte @byte )
535
+ {
536
+ return @byte == 1 ; // [NullableContext] found
537
+ }
538
+ }
539
+
540
+ return null ;
541
+ }
450
542
}
451
543
}
452
544
}
0 commit comments