Adds inferred [Required] for non-null ref types (#9978)
* Adds inferred [Required] for non-null ref types Follow up from #9194 This change adds the automatic inference of [Required] for non-nullable properties and parameters. This means that if you opt into nullable context in C#8, we'll start treating those types as-if you put [Required] on them. This provides a nice invariant to rely on, namely that MVC will honor your declared nullability contract OR report a validation error. This reinforces the guidance already published by the C# team for using POCOs/DTOs with nullability. See https://github.com/aspnet/specs/blob/master/notes/3_0/nullable.md for my analysis on the topic. * preemptively fix PR feedback xD * PR feedback and functional test * more * Fix test failures * fix * more * Do a barrel roll
This commit is contained in:
parent
620c673705
commit
91e6839c8d
|
|
@ -884,6 +884,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public System.Text.Json.Serialization.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public int? SslPort { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool SuppressAsyncSuffixInActionNames { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool SuppressImplicitRequiredAttributeForNonNullableReferenceTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool SuppressInputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool SuppressOutputFormatterBuffering { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public bool ValidateComplexTypesIfChildValidationFails { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
|
|
@ -2817,6 +2818,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
public ValidationMetadataProviderContext(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity key, Microsoft.AspNetCore.Mvc.ModelBinding.ModelAttributes attributes) { }
|
||||
public System.Collections.Generic.IReadOnlyList<object> Attributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ModelMetadataIdentity Key { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public System.Collections.Generic.IReadOnlyList<object> ParameterAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public System.Collections.Generic.IReadOnlyList<object> PropertyAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public System.Collections.Generic.IReadOnlyList<object> TypeAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata ValidationMetadata { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
|
||||
Key = key;
|
||||
Attributes = attributes.Attributes;
|
||||
ParameterAttributes = attributes.ParameterAttributes;
|
||||
PropertyAttributes = attributes.PropertyAttributes;
|
||||
TypeAttributes = attributes.TypeAttributes;
|
||||
|
||||
|
|
@ -43,6 +44,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
/// </summary>
|
||||
public ModelMetadataIdentity Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter attributes.
|
||||
/// </summary>
|
||||
public IReadOnlyList<object> ParameterAttributes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the property attributes.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
|
@ -102,6 +103,30 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
public FormatterCollection<IInputFormatter> InputFormatters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that detemines if the inference of <see cref="RequiredAttribute"/> for
|
||||
/// for properties and parameters of non-nullable reference types is suppressed. If <c>false</c>
|
||||
/// (the default), then all non-nullable reference types will behave as-if <c>[Required]</c> has
|
||||
/// been applied. If <c>true</c>, this behavior will be suppressed; nullable reference types and
|
||||
/// non-nullable reference types will behave the same for the purposes of validation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This option controls whether MVC model binding and validation treats nullable and non-nullable
|
||||
/// reference types differently.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// By default, MVC will treat a non-nullable reference type parameters and properties as-if
|
||||
/// <c>[Required]</c> has been applied, resulting in validation errors when no value was bound.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// MVC does not support non-nullable reference type annotations on type arguments and type parameter
|
||||
/// contraints. The framework will not infer any validation attributes for generic-typed properties
|
||||
/// or collection elements.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool SuppressImplicitRequiredAttributeForNonNullableReferenceTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that determines if buffering is disabled for input formatters that
|
||||
/// synchronously read from the HTTP request body.
|
||||
|
|
|
|||
|
|
@ -23,11 +23,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
IDisplayMetadataProvider,
|
||||
IValidationMetadataProvider
|
||||
{
|
||||
// The [Nullable] attribute is synthesized by the compiler. It's best to just compare the type name.
|
||||
private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute";
|
||||
private const string NullableFlagsFieldName = "NullableFlags";
|
||||
|
||||
private readonly IStringLocalizerFactory _stringLocalizerFactory;
|
||||
private readonly MvcOptions _options;
|
||||
private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions;
|
||||
|
||||
public DataAnnotationsMetadataProvider(
|
||||
IOptions<MvcDataAnnotationsLocalizationOptions> options,
|
||||
MvcOptions options,
|
||||
IOptions<MvcDataAnnotationsLocalizationOptions> localizationOptions,
|
||||
IStringLocalizerFactory stringLocalizerFactory)
|
||||
{
|
||||
if (options == null)
|
||||
|
|
@ -35,7 +41,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
_localizationOptions = options.Value;
|
||||
if (localizationOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(localizationOptions));
|
||||
}
|
||||
|
||||
_options = options;
|
||||
_localizationOptions = localizationOptions.Value;
|
||||
_stringLocalizerFactory = stringLocalizerFactory;
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +340,29 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
// RequiredAttribute marks a property as required by validation - this means that it
|
||||
// must have a non-null value on the model during validation.
|
||||
var requiredAttribute = attributes.OfType<RequiredAttribute>().FirstOrDefault();
|
||||
|
||||
// For non-nullable reference types, treat them as-if they had an implicit [Required].
|
||||
// This allows the developer to specify [Required] to customize the error message, so
|
||||
// if they already have [Required] then there's no need for us to do this check.
|
||||
if (!_options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes &&
|
||||
requiredAttribute == null &&
|
||||
!context.Key.ModelType.IsValueType &&
|
||||
|
||||
// Look specifically at attributes on the property/parameter. [Nullable] on
|
||||
// the type has a different meaning.
|
||||
IsNonNullable(context.ParameterAttributes ?? context.PropertyAttributes ?? Array.Empty<object>()))
|
||||
{
|
||||
// Since this behavior specifically relates to non-null-ness, we will use the non-default
|
||||
// option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in
|
||||
// a validation error by default because we convert empty/whitespace strings to null
|
||||
// unless you say otherwise.
|
||||
requiredAttribute = new RequiredAttribute()
|
||||
{
|
||||
AllowEmptyStrings = true,
|
||||
};
|
||||
attributes.Add(requiredAttribute);
|
||||
}
|
||||
|
||||
if (requiredAttribute != null)
|
||||
{
|
||||
context.ValidationMetadata.IsRequired = true;
|
||||
|
|
@ -380,5 +415,35 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal static bool IsNonNullable(IEnumerable<object> attributes)
|
||||
{
|
||||
// [Nullable] is compiler synthesized, comparing by name.
|
||||
var nullableAttribute = attributes
|
||||
.Where(a => string.Equals(a.GetType().FullName, NullableAttributeFullTypeName, StringComparison.Ordinal))
|
||||
.FirstOrDefault();
|
||||
if (nullableAttribute == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We don't handle cases where generics and NNRT are used. This runs into a
|
||||
// fundamental limitation of ModelMetadata - we use a single Type and Property/Parameter
|
||||
// to look up the metadata. However when generics are involved and NNRT is in use
|
||||
// the distance between the [Nullable] and member we're looking at is potentially
|
||||
// unbounded.
|
||||
//
|
||||
// See: https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-reference-types.md#annotations
|
||||
if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field &&
|
||||
field.GetValue(nullableAttribute) is byte[] flags &&
|
||||
flags.Length >= 0 &&
|
||||
flags[0] == 1) // First element is the property/parameter type.
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
}
|
||||
|
||||
options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider(
|
||||
options,
|
||||
_dataAnnotationLocalizationOptions,
|
||||
_stringLocalizerFactory));
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
|
@ -90,9 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
object expected)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForType(typeof(string));
|
||||
var context = new DisplayMetadataProviderContext(key, GetModelAttributes(new object[] { attribute }));
|
||||
|
|
@ -109,9 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_FindsDisplayFormat_FromDataType()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var dataType = new DataTypeAttribute(DataType.Currency);
|
||||
var displayFormat = dataType.DisplayFormat; // Non-null for DataType.Currency.
|
||||
|
|
@ -131,9 +128,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_FindsDisplayFormat_OverridingDataType()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var dataType = new DataTypeAttribute(DataType.Time); // Has a non-null DisplayFormat.
|
||||
var displayFormat = new DisplayFormatAttribute() // But these values override the values from DataType
|
||||
|
|
@ -156,9 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateBindingMetadata_EditableAttributeFalse_SetsReadOnlyTrue()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var editable = new EditableAttribute(allowEdit: false);
|
||||
|
||||
|
|
@ -177,9 +170,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateBindingMetadata_EditableAttributeTrue_SetsReadOnlyFalse()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var editable = new EditableAttribute(allowEdit: true);
|
||||
|
||||
|
|
@ -198,11 +189,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_DisplayAttribute_OverridesDisplayNameAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var localizationOptions = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
localizationOptions,
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var displayName = new DisplayNameAttribute("DisplayNameAttributeValue");
|
||||
var display = new DisplayAttribute()
|
||||
|
|
@ -225,11 +212,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_DisplayAttribute_OverridesDisplayNameAttribute_IfNameEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var localizationOptions = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
localizationOptions,
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var displayName = new DisplayNameAttribute("DisplayNameAttributeValue");
|
||||
var display = new DisplayAttribute()
|
||||
|
|
@ -252,11 +235,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_DisplayAttribute_DoesNotOverrideDisplayNameAttribute_IfNameNull()
|
||||
{
|
||||
// Arrange
|
||||
var localizationOptions = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
localizationOptions,
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var displayName = new DisplayNameAttribute("DisplayNameAttributeValue");
|
||||
var display = new DisplayAttribute()
|
||||
|
|
@ -289,15 +268,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
.Setup(s => s.Create(typeof(EmptyClass)))
|
||||
.Returns(() => sharedLocalizer.Object);
|
||||
|
||||
var localizationOptions = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
localizationOptions.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
var localizationOptions = new MvcDataAnnotationsLocalizationOptions();
|
||||
localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
{
|
||||
return stringLocalizerFactory.Create(typeof(EmptyClass));
|
||||
};
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
localizationOptions,
|
||||
stringLocalizerFactory: stringLocalizerFactoryMock.Object);
|
||||
var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object);
|
||||
|
||||
var displayName = new DisplayNameAttribute("DisplayNameValue");
|
||||
|
||||
|
|
@ -329,15 +306,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
.Setup(s => s.Create(typeof(EmptyClass)))
|
||||
.Returns(() => sharedLocalizer.Object);
|
||||
|
||||
var localizationOptions = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
localizationOptions.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
var localizationOptions = new MvcDataAnnotationsLocalizationOptions();
|
||||
localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
{
|
||||
return stringLocalizerFactory.Create(typeof(EmptyClass));
|
||||
};
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
localizationOptions,
|
||||
stringLocalizerFactory: stringLocalizerFactoryMock.Object);
|
||||
var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object);
|
||||
|
||||
var displayName = new DisplayNameAttribute("DisplayNameValue");
|
||||
|
||||
|
|
@ -363,15 +338,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
.Setup(s => s.Create(typeof(EmptyClass)))
|
||||
.Returns(() => sharedLocalizer.Object);
|
||||
|
||||
var options = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
var localizationOptions = new MvcDataAnnotationsLocalizationOptions();
|
||||
var dataAnnotationLocalizerProviderWasUsed = false;
|
||||
options.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
{
|
||||
dataAnnotationLocalizerProviderWasUsed = true;
|
||||
return stringLocalizerFactory.Create(typeof(EmptyClass));
|
||||
};
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(options, stringLocalizerFactoryMock.Object);
|
||||
var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object);
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -395,9 +370,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_DisplayAttribute_NameFromResources_NullLocalizer()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -432,9 +405,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
stringLocalizerFactory
|
||||
.Setup(s => s.Create(It.IsAny<Type>()))
|
||||
.Returns(() => stringLocalizer.Object);
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory.Object);
|
||||
var provider = CreateProvider(stringLocalizerFactory: stringLocalizerFactory.Object);
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -469,9 +440,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
stringLocalizerFactory
|
||||
.Setup(s => s.Create(It.IsAny<Type>()))
|
||||
.Returns(() => stringLocalizer.Object);
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory.Object);
|
||||
var provider = CreateProvider(stringLocalizerFactory: stringLocalizerFactory.Object);
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -500,9 +469,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_DisplayAttribute_DescriptionFromResources_NullLocalizer()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -537,9 +504,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
stringLocalizerFactory
|
||||
.Setup(s => s.Create(It.IsAny<Type>()))
|
||||
.Returns(() => stringLocalizer.Object);
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory.Object);
|
||||
var provider = CreateProvider(stringLocalizerFactory: stringLocalizerFactory.Object);
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -568,9 +533,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_DisplayAttribute_PromptFromResources_NullLocalizer()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -613,13 +576,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
stringLocalizerFactoryMock
|
||||
.Setup(f => f.Create(It.IsAny<Type>()))
|
||||
.Returns(stringLocalizer.Object);
|
||||
var options = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
options.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
var localizationOptions = new MvcDataAnnotationsLocalizationOptions();
|
||||
localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) =>
|
||||
{
|
||||
return stringLocalizerFactory.Create(type);
|
||||
};
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(options, stringLocalizerFactoryMock.Object);
|
||||
var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object);
|
||||
|
||||
var display = new DisplayAttribute()
|
||||
{
|
||||
|
|
@ -671,9 +634,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_IsEnum_ReflectsModelType(Type type, bool expectedIsEnum)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForType(type);
|
||||
var attributes = new object[0];
|
||||
|
|
@ -707,9 +668,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateDisplayMetadata_IsFlagsEnum_ReflectsModelType(Type type, bool expectedIsFlagsEnum)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForType(type);
|
||||
var attributes = new object[0];
|
||||
|
|
@ -841,9 +800,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
IReadOnlyDictionary<string, string> expectedDictionary)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForType(type);
|
||||
var attributes = new object[0];
|
||||
|
|
@ -887,9 +844,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
.Setup(f => f.Create(It.IsAny<Type>()))
|
||||
.Returns(stringLocalizer.Object);
|
||||
|
||||
var options = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
options.Value.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) => stringLocalizerFactory.Create(modelType);
|
||||
var provider = new DataAnnotationsMetadataProvider(options, stringLocalizerFactoryMock.Object);
|
||||
var localizationOptions = new MvcDataAnnotationsLocalizationOptions();
|
||||
localizationOptions.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) => stringLocalizerFactory.Create(modelType);
|
||||
var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object);
|
||||
|
||||
// Act
|
||||
provider.CreateDisplayMetadata(context);
|
||||
|
|
@ -1021,9 +978,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
IEnumerable<KeyValuePair<EnumGroupAndName, string>> expectedKeyValuePairs)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForType(type);
|
||||
var attributes = new object[0];
|
||||
|
|
@ -1051,9 +1006,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
new KeyValuePair<EnumGroupAndName, string>(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayOrder.Null)), "3"),
|
||||
};
|
||||
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var key = ModelMetadataIdentity.ForType(typeof(EnumWithDisplayOrder));
|
||||
var attributes = new object[0];
|
||||
|
|
@ -1153,9 +1106,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateValidationMetadata_RequiredAttribute_SetsIsRequiredToTrue()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var required = new RequiredAttribute();
|
||||
|
||||
|
|
@ -1177,9 +1128,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateValidationMetadata_NoRequiredAttribute_IsRequiredLeftAlone(bool? initialValue)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var attributes = new Attribute[] { };
|
||||
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
|
||||
|
|
@ -1193,13 +1142,83 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
Assert.Equal(initialValue, context.ValidationMetadata.IsRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationMetadata_InfersRequiredAttribute_NoNonNullableProperty()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
|
||||
var attributes = ModelAttributes.GetAttributesForProperty(
|
||||
typeof(NullableReferenceTypes),
|
||||
typeof(NullableReferenceTypes).GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceType)));
|
||||
var key = ModelMetadataIdentity.ForProperty(
|
||||
typeof(NullableReferenceTypes),
|
||||
nameof(NullableReferenceTypes.NonNullableReferenceType), typeof(string));
|
||||
var context = new ValidationMetadataProviderContext(key, attributes);
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.ValidationMetadata.IsRequired);
|
||||
var attribute = Assert.Single(context.ValidationMetadata.ValidatorMetadata, m => m is RequiredAttribute);
|
||||
Assert.True(((RequiredAttribute)attribute).AllowEmptyStrings); // non-Default for [Required]
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationMetadata_InfersRequiredAttribute_NoNonNullableProperty_PrefersExistingRequiredAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider();
|
||||
|
||||
var attributes = ModelAttributes.GetAttributesForProperty(
|
||||
typeof(NullableReferenceTypes),
|
||||
typeof(NullableReferenceTypes).GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceTypeWithRequired)));
|
||||
var key = ModelMetadataIdentity.ForProperty(
|
||||
typeof(NullableReferenceTypes),
|
||||
nameof(NullableReferenceTypes.NonNullableReferenceTypeWithRequired), typeof(string));
|
||||
var context = new ValidationMetadataProviderContext(key, attributes);
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.ValidationMetadata.IsRequired);
|
||||
var attribute = Assert.Single(context.ValidationMetadata.ValidatorMetadata, m => m is RequiredAttribute a);
|
||||
Assert.Equal("Test", ((RequiredAttribute)attribute).ErrorMessage);
|
||||
Assert.False(((RequiredAttribute)attribute).AllowEmptyStrings); // Default for [Required]
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationMetadata_SuppressRequiredInference_Noops()
|
||||
{
|
||||
// Arrange
|
||||
var provider = CreateProvider(options: new MvcOptions()
|
||||
{
|
||||
SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true,
|
||||
});
|
||||
|
||||
var attributes = ModelAttributes.GetAttributesForProperty(
|
||||
typeof(NullableReferenceTypes),
|
||||
typeof(NullableReferenceTypes).GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceType)));
|
||||
var key = ModelMetadataIdentity.ForProperty(
|
||||
typeof(NullableReferenceTypes),
|
||||
nameof(NullableReferenceTypes.NonNullableReferenceType), typeof(string));
|
||||
var context = new ValidationMetadataProviderContext(key, attributes);
|
||||
|
||||
// Act
|
||||
provider.CreateValidationMetadata(context);
|
||||
|
||||
// Assert
|
||||
Assert.Null(context.ValidationMetadata.IsRequired);
|
||||
Assert.DoesNotContain(context.ValidationMetadata.ValidatorMetadata, m => m is RequiredAttribute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateValidationMetadata_WillAddValidationAttributes_From_ValidationProviderAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
var validationProviderAttribute = new FooCompositeValidationAttribute(
|
||||
attributes: new List<ValidationAttribute>
|
||||
{
|
||||
|
|
@ -1232,9 +1251,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateBindingMetadata_RequiredAttribute_IsBindingRequiredLeftAlone(bool initialValue)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var attributes = new Attribute[] { new RequiredAttribute() };
|
||||
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
|
||||
|
|
@ -1255,9 +1272,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateBindingDetails_NoEditableAttribute_IsReadOnlyLeftAlone(bool? initialValue)
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var attributes = new Attribute[] { };
|
||||
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
|
||||
|
|
@ -1275,9 +1290,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateValidationDetails_ValidatableObject_ReturnsObject()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var attribute = new TestValidationAttribute();
|
||||
var attributes = new Attribute[] { attribute };
|
||||
|
|
@ -1296,9 +1309,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
public void CreateValidationDetails_ValidatableObject_AlreadyInContext_Ignores()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new DataAnnotationsMetadataProvider(
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null);
|
||||
var provider = CreateProvider();
|
||||
|
||||
var attribute = new TestValidationAttribute();
|
||||
var attributes = new Attribute[] { attribute };
|
||||
|
|
@ -1314,6 +1325,64 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
Assert.Same(attribute, validatorMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNonNullable_FindsNonNullableProperty()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(NullableReferenceTypes);
|
||||
var property = type.GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceType));
|
||||
|
||||
// Act
|
||||
var result = DataAnnotationsMetadataProvider.IsNonNullable(property.GetCustomAttributes(inherit: true));
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNonNullable_FindsNullableProperty()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(NullableReferenceTypes);
|
||||
var property = type.GetProperty(nameof(NullableReferenceTypes.NullableReferenceType));
|
||||
|
||||
// Act
|
||||
var result = DataAnnotationsMetadataProvider.IsNonNullable(property.GetCustomAttributes(inherit: true));
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNonNullable_FindsNonNullableParameter()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(NullableReferenceTypes);
|
||||
var method = type.GetMethod(nameof(NullableReferenceTypes.Method));
|
||||
var parameter = method.GetParameters().Where(p => p.Name == "nonNullableParameter").Single();
|
||||
|
||||
// Act
|
||||
var result = DataAnnotationsMetadataProvider.IsNonNullable(parameter.GetCustomAttributes(inherit: true));
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNonNullable_FindsNullableParameter()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(NullableReferenceTypes);
|
||||
var method = type.GetMethod(nameof(NullableReferenceTypes.Method));
|
||||
var parameter = method.GetParameters().Where(p => p.Name == "nullableParameter").Single();
|
||||
|
||||
// Act
|
||||
var result = DataAnnotationsMetadataProvider.IsNonNullable(parameter.GetCustomAttributes(inherit: true));
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<EnumGroupAndName, string>> GetLocalizedEnumGroupedDisplayNamesAndValues(
|
||||
bool useStringLocalizer)
|
||||
{
|
||||
|
|
@ -1328,6 +1397,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
return context.DisplayMetadata.EnumGroupedDisplayNamesAndValues;
|
||||
}
|
||||
|
||||
private DataAnnotationsMetadataProvider CreateProvider(
|
||||
MvcOptions options = null,
|
||||
MvcDataAnnotationsLocalizationOptions localizationOptions = null,
|
||||
IStringLocalizerFactory stringLocalizerFactory = null)
|
||||
{
|
||||
return new DataAnnotationsMetadataProvider(
|
||||
options ?? new MvcOptions(),
|
||||
Options.Create(localizationOptions ?? new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory);
|
||||
}
|
||||
|
||||
private DataAnnotationsMetadataProvider CreateIStringLocalizerProvider(bool useStringLocalizer)
|
||||
{
|
||||
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
|
||||
|
|
@ -1343,12 +1423,10 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
.Setup(factory => factory.Create(typeof(EnumWithLocalizedDisplayNames)))
|
||||
.Returns(stringLocalizer.Object);
|
||||
|
||||
var options = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
options.Value.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType);
|
||||
var localizationOptions = new MvcDataAnnotationsLocalizationOptions();
|
||||
localizationOptions.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType);
|
||||
|
||||
return new DataAnnotationsMetadataProvider(
|
||||
options,
|
||||
useStringLocalizer ? stringLocalizerFactory.Object : null);
|
||||
return CreateProvider(options: null, localizationOptions, useStringLocalizer ? stringLocalizerFactory.Object : null);
|
||||
}
|
||||
|
||||
private ModelAttributes GetModelAttributes(IEnumerable<object> typeAttributes)
|
||||
|
|
@ -1532,5 +1610,21 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
return _attributes;
|
||||
}
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private class NullableReferenceTypes
|
||||
{
|
||||
public string NonNullableReferenceType { get; set; } = default!;
|
||||
|
||||
[Required(ErrorMessage = "Test")]
|
||||
public string NonNullableReferenceTypeWithRequired { get; set; } = default!;
|
||||
|
||||
public string? NullableReferenceType { get; set; } = default!;
|
||||
|
||||
public void Method(string nonNullableParameter, string? nullableParameter)
|
||||
{
|
||||
}
|
||||
}
|
||||
#nullable restore
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<UseSharedCompilation>false</UseSharedCompilation>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1052,6 +1052,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
|||
{
|
||||
new DefaultBindingMetadataProvider(),
|
||||
new DataAnnotationsMetadataProvider(
|
||||
new MvcOptions(),
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
{
|
||||
private static DataAnnotationsMetadataProvider CreateDefaultDataAnnotationsProvider(IStringLocalizerFactory stringLocalizerFactory)
|
||||
{
|
||||
var options = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
options.Value.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType);
|
||||
var localizationOptions = Options.Create(new MvcDataAnnotationsLocalizationOptions());
|
||||
localizationOptions.Value.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType);
|
||||
|
||||
return new DataAnnotationsMetadataProvider(options, stringLocalizerFactory);
|
||||
return new DataAnnotationsMetadataProvider(new MvcOptions(), localizationOptions, stringLocalizerFactory);
|
||||
}
|
||||
|
||||
// Creates a provider with all the defaults - includes data annotations
|
||||
|
|
@ -50,6 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
new DefaultBindingMetadataProvider(),
|
||||
new DefaultValidationMetadataProvider(),
|
||||
new DataAnnotationsMetadataProvider(
|
||||
new MvcOptions(),
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null),
|
||||
new DataMemberRequiredBindingMetadataProvider(),
|
||||
|
|
@ -92,6 +93,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
new DefaultBindingMetadataProvider(),
|
||||
new DefaultValidationMetadataProvider(),
|
||||
new DataAnnotationsMetadataProvider(
|
||||
new MvcOptions(),
|
||||
Options.Create(new MvcDataAnnotationsLocalizationOptions()),
|
||||
stringLocalizerFactory: null),
|
||||
detailsProvider
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom.Html;
|
||||
using AngleSharp.Parser.Html;
|
||||
using BasicWebSite.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
{
|
||||
public class NonNullableReferenceTypesTest : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting>>
|
||||
{
|
||||
public NonNullableReferenceTypesTest(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
|
||||
{
|
||||
Client = fixture.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private HttpClient Client { get; set; }
|
||||
|
||||
[Fact]
|
||||
public async Task CanUseNonNullableReferenceType_WithController_OmitData_ValidationErrors()
|
||||
{
|
||||
// Arrange
|
||||
var parser = new HtmlParser();
|
||||
|
||||
// Act 1
|
||||
var response = await Client.GetAsync("http://localhost/NonNullable");
|
||||
|
||||
// Assert 1
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var document = parser.Parse(content);
|
||||
var errors = document.QuerySelectorAll("#errors > ul > li");
|
||||
var li = Assert.Single(errors);
|
||||
Assert.Empty(li.TextContent);
|
||||
|
||||
var cookieToken = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(response);
|
||||
var formToken = document.RetrieveAntiforgeryToken();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/NonNullable");
|
||||
request.Headers.Add("Cookie", cookieToken.Key + "=" + cookieToken.Value);
|
||||
request.Content = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("__RequestVerificationToken", formToken),
|
||||
});
|
||||
|
||||
// Act 2
|
||||
response = await Client.SendAsync(request);
|
||||
|
||||
// Assert 2
|
||||
//
|
||||
// OK means there were validation errors.
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
document = parser.Parse(content);
|
||||
errors = errors = document.QuerySelectorAll("#errors > ul > li");
|
||||
Assert.Equal(2, errors.Length); // Not validating BCL error messages
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanUseNonNullableReferenceType_WithController_SubmitData_NoError()
|
||||
{
|
||||
// Arrange
|
||||
var parser = new HtmlParser();
|
||||
|
||||
// Act 1
|
||||
var response = await Client.GetAsync("http://localhost/NonNullable");
|
||||
|
||||
// Assert 1
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var document = parser.Parse(content);
|
||||
var errors = document.QuerySelectorAll("#errors > ul > li");
|
||||
var li = Assert.Single(errors);
|
||||
Assert.Empty(li.TextContent);
|
||||
|
||||
var cookieToken = AntiforgeryTestHelper.RetrieveAntiforgeryCookie(response);
|
||||
var formToken = document.RetrieveAntiforgeryToken();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/NonNullable");
|
||||
request.Headers.Add("Cookie", cookieToken.Key + "=" + cookieToken.Value);
|
||||
request.Content = new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("__RequestVerificationToken", formToken),
|
||||
new KeyValuePair<string, string>("Name", "Pranav"),
|
||||
new KeyValuePair<string, string>("description", "Meme")
|
||||
});
|
||||
|
||||
// Act 2
|
||||
response = await Client.SendAsync(request);
|
||||
|
||||
// Assert 2
|
||||
//
|
||||
// Redirect means there were no validation errors.
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.Redirect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1715,7 +1715,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
private class Order8
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public KeyValuePair<string, int> ProductId { get; set; }
|
||||
}
|
||||
|
|
@ -1869,13 +1869,15 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Equal("bill", model.Name);
|
||||
Assert.Equal(default, model.ProductId);
|
||||
|
||||
Assert.Single(modelState);
|
||||
Assert.Equal(0, modelState.ErrorCount);
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Equal(1, modelState.ErrorCount);
|
||||
Assert.False(modelState.IsValid);
|
||||
|
||||
var entry = Assert.Single(modelState, e => e.Key == "parameter.Name").Value;
|
||||
Assert.Equal("bill", entry.AttemptedValue);
|
||||
Assert.Equal("bill", entry.RawValue);
|
||||
|
||||
entry = Assert.Single(modelState, e => e.Key == "parameter.ProductId.Key").Value;
|
||||
Assert.Single(entry.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1916,9 +1918,11 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.Null(model.Name);
|
||||
Assert.Equal(default, model.ProductId);
|
||||
|
||||
Assert.Empty(modelState);
|
||||
Assert.Equal(0, modelState.ErrorCount);
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Equal(1, modelState.ErrorCount);
|
||||
Assert.False(modelState.IsValid);
|
||||
|
||||
var entry = Assert.Single(modelState, e => e.Key == "ProductId.Key").Value;
|
||||
Assert.Single(entry.Errors);
|
||||
}
|
||||
|
||||
private class Car4
|
||||
|
|
|
|||
|
|
@ -337,9 +337,11 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
Assert.Equal(new KeyValuePair<string, int>(), modelBindingResult.Model);
|
||||
|
||||
Assert.Empty(modelState);
|
||||
Assert.Equal(0, modelState.ErrorCount);
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Equal(1, modelState.ErrorCount);
|
||||
Assert.False(modelState.IsValid);
|
||||
|
||||
var entry = Assert.Single(modelState, kvp => kvp.Key == "Key").Value;
|
||||
Assert.Single(entry.Errors);
|
||||
}
|
||||
|
||||
private class Person
|
||||
|
|
@ -500,9 +502,14 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
Assert.Equal(new KeyValuePair<string, Person>(), modelBindingResult.Model);
|
||||
|
||||
Assert.Empty(modelState);
|
||||
Assert.Equal(0, modelState.ErrorCount);
|
||||
Assert.True(modelState.IsValid);
|
||||
Assert.Equal(2, modelState.ErrorCount);
|
||||
Assert.False(modelState.IsValid);
|
||||
|
||||
var entry = Assert.Single(modelState, kvp => kvp.Key == "Key").Value;
|
||||
Assert.Single(entry.Errors);
|
||||
|
||||
entry = Assert.Single(modelState, kvp => kvp.Key == "Value").Value;
|
||||
Assert.Single(entry.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<UseSharedCompilation>false</UseSharedCompilation>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,212 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||
{
|
||||
public class NullableReferenceTypeIntegrationTest
|
||||
{
|
||||
#nullable enable
|
||||
private class Person1
|
||||
{
|
||||
public string FirstName { get; set; } = default!;
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
[Fact]
|
||||
public async Task BindProperty_WithNonNullableReferenceType_NoData_ValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = typeof(Person1)
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
|
||||
// ModelBindingResult
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
|
||||
// Model
|
||||
var boundPerson = Assert.IsType<Person1>(modelBindingResult.Model);
|
||||
Assert.Null(boundPerson.FirstName);
|
||||
|
||||
// ModelState
|
||||
Assert.False(modelState.IsValid);
|
||||
Assert.Collection(
|
||||
modelState.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("FirstName", kvp.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
|
||||
|
||||
// Not validating framework error message.
|
||||
Assert.Single(kvp.Value.Errors);
|
||||
});
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private class Person2
|
||||
{
|
||||
public string? FirstName { get; set; }
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
[Fact]
|
||||
public async Task BindProperty_WithNullableReferenceType_NoData_NoValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = typeof(Person2)
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
|
||||
// ModelBindingResult
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
|
||||
// Model
|
||||
var boundPerson = Assert.IsType<Person2>(modelBindingResult.Model);
|
||||
Assert.Null(boundPerson.FirstName);
|
||||
|
||||
// ModelState
|
||||
Assert.True(modelState.IsValid);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private class Person3
|
||||
{
|
||||
[Required(ErrorMessage = "Test")]
|
||||
public string FirstName { get; set; } = default!;
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
[Fact]
|
||||
public async Task BindProperty_WithNonNullableReferenceType_NoData_ValidationError_CustomMessage()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "Parameter1",
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = typeof(Person3)
|
||||
};
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||
|
||||
// Assert
|
||||
|
||||
// ModelBindingResult
|
||||
Assert.True(modelBindingResult.IsModelSet);
|
||||
|
||||
// Model
|
||||
var boundPerson = Assert.IsType<Person3>(modelBindingResult.Model);
|
||||
Assert.Null(boundPerson.FirstName);
|
||||
|
||||
// ModelState
|
||||
Assert.False(modelState.IsValid);
|
||||
Assert.Collection(
|
||||
modelState.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("FirstName", kvp.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
|
||||
|
||||
var error = Assert.Single(kvp.Value.Errors);
|
||||
Assert.Equal("Test", error.ErrorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private void NonNullableParameter(string param1)
|
||||
{
|
||||
}
|
||||
#nullable restore
|
||||
|
||||
[Fact]
|
||||
public async Task BindParameter_WithNonNullableReferenceType_NoData_ValidationError()
|
||||
{
|
||||
// Arrange
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var parameter = new ParameterDescriptor()
|
||||
{
|
||||
Name = "param1",
|
||||
BindingInfo = new BindingInfo(),
|
||||
ParameterType = typeof(string)
|
||||
};
|
||||
|
||||
var method = GetType().GetMethod(nameof(NonNullableParameter), BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
var parameterInfo = method.GetParameters().Single();
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var modelMetadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo);
|
||||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext();
|
||||
var modelState = testContext.ModelState;
|
||||
|
||||
// Act
|
||||
var modelBindingResult = await parameterBinder.BindModelAsync(
|
||||
parameter,
|
||||
testContext,
|
||||
modelMetadataProvider,
|
||||
modelMetadata);
|
||||
|
||||
// Assert
|
||||
|
||||
// ModelBindingResult
|
||||
Assert.False(modelBindingResult.IsModelSet);
|
||||
|
||||
// Model
|
||||
Assert.Null(modelBindingResult.Model);
|
||||
|
||||
// ModelState
|
||||
Assert.False(modelState.IsValid);
|
||||
Assert.Collection(
|
||||
modelState.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal("param1", kvp.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
|
||||
|
||||
// Not validating framework error message.
|
||||
Assert.Single(kvp.Value.Errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
#nullable enable
|
||||
using BasicWebSite.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BasicWebSite.Controllers
|
||||
{
|
||||
public class NonNullableController : Controller
|
||||
{
|
||||
public ActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Index(NonNullablePerson person, string description)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
return RedirectToAction();
|
||||
}
|
||||
|
||||
return View(person);
|
||||
}
|
||||
|
||||
public class NonNullablePerson
|
||||
{
|
||||
public string Name { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
@model BasicWebSite.Controllers.NonNullableController.NonNullablePerson
|
||||
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
|
||||
|
||||
<div asp-validation-summary="All" id="errors"></div>
|
||||
|
||||
<form asp-action="Index">
|
||||
<p>Name:</p>
|
||||
<input asp-for="Name" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
Loading…
Reference in New Issue