Do not infer Required attributes based on context for non-nullable generic types (#13551)
Fixes https://github.com/aspnet/AspNetCore/issues/13512
This commit is contained in:
parent
437f149880
commit
54710e4671
|
|
@ -494,6 +494,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
||||||
|
|
||||||
internal static bool IsNullableBasedOnContext(Type containingType, MemberInfo member)
|
internal static bool IsNullableBasedOnContext(Type containingType, MemberInfo member)
|
||||||
{
|
{
|
||||||
|
// For generic types, inspecting the nullability requirement additionally requires
|
||||||
|
// inspecting the nullability constraint on generic type parameters. This is fairly non-triviial
|
||||||
|
// so we'll just avoid calculating it. Users should still be able to apply an explicit [Required]
|
||||||
|
// attribute on these members.
|
||||||
|
if (containingType.IsGenericType)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The [Nullable] and [NullableContext] attributes are not inherited.
|
// The [Nullable] and [NullableContext] attributes are not inherited.
|
||||||
//
|
//
|
||||||
// The [NullableContext] attribute can appear on a method or on the module.
|
// The [NullableContext] attribute can appear on a method or on the module.
|
||||||
|
|
|
||||||
|
|
@ -1339,6 +1339,38 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsNullableReferenceType_ReturnsFalse_ForKeyValuePairWithoutNullableConstraints()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(KeyValuePair<string, object>);
|
||||||
|
var property = type.GetProperty(nameof(KeyValuePair<string, object>.Key));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = DataAnnotationsMetadataProvider.IsNullableReferenceType(type, member: null, property.GetCustomAttributes(inherit: true));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
[Fact]
|
||||||
|
public void IsNullableReferenceType_ReturnsTrue_ForKeyValuePairWithNullableConstraints()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(KeyValuePair<string, object>);
|
||||||
|
var property = type.GetProperty(nameof(KeyValuePair<string, object>.Key))!;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = DataAnnotationsMetadataProvider.IsNullableReferenceType(type, member: null, property.GetCustomAttributes(inherit: true));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// While we'd like for result to be 'true', we don't have a very good way of actually calculating it correctly.
|
||||||
|
// This test is primarily here to document the behavior.
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
#nullable restore
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsNonNullable_FindsNullableProperty()
|
public void IsNonNullable_FindsNullableProperty()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1126,6 +1126,105 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal(expectedMessage, exception.Message);
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CollectionModelBinder_CollectionOfSimpleTypes_DoesNotResultInValidationError()
|
||||||
|
{
|
||||||
|
// Regression test for https://github.com/aspnet/AspNetCore/issues/13512
|
||||||
|
// Arrange
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = "parameter",
|
||||||
|
ParameterType = typeof(Collection<string>),
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||||
|
request =>
|
||||||
|
{
|
||||||
|
request.QueryString = new QueryString("?[0]=hello&[1]=");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
|
||||||
|
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
Assert.Equal(0, modelState.ErrorCount);
|
||||||
|
|
||||||
|
Assert.True(result.IsModelSet);
|
||||||
|
var model = Assert.IsType<Collection<string>>(result.Model);
|
||||||
|
Assert.Collection(
|
||||||
|
model,
|
||||||
|
item => Assert.Equal("hello", item),
|
||||||
|
item => Assert.Null(item));
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
modelState,
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("[0]", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("[1]", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CollectionModelBinder_CollectionOfNonNullableTypes_AppliesImplicitRequired()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = "parameter",
|
||||||
|
ParameterType = typeof(Collection<string>),
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||||
|
request =>
|
||||||
|
{
|
||||||
|
request.QueryString = new QueryString("?[0]=hello&[1]=");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
|
||||||
|
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
Assert.Equal(0, modelState.ErrorCount);
|
||||||
|
|
||||||
|
Assert.True(result.IsModelSet);
|
||||||
|
var model = Assert.IsType<Collection<string>>(result.Model);
|
||||||
|
Assert.Collection(
|
||||||
|
model,
|
||||||
|
item => Assert.Equal("hello", item),
|
||||||
|
item => Assert.Null(item));
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
modelState,
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("[0]", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("[1]", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private class ClosedGenericCollection : Collection<string>
|
private class ClosedGenericCollection : Collection<string>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
@ -1162,6 +1163,211 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
||||||
Assert.Equal(expectedMessage, exception.Message);
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DictionaryModelBinder_DictionaryOfSimpleType_NullValue_DoesNotResultInRequiredValidation()
|
||||||
|
{
|
||||||
|
// Regression test for https://github.com/aspnet/AspNetCore/issues/13512
|
||||||
|
// Arrange
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = "parameter",
|
||||||
|
ParameterType = typeof(Dictionary<string, string>)
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
request.QueryString = new QueryString("?parameter[key0]=");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
var model = Assert.IsType<Dictionary<string, string>>(modelBindingResult.Model);
|
||||||
|
Assert.Collection(
|
||||||
|
model.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("key0", kvp.Key);
|
||||||
|
Assert.Null(kvp.Value);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
modelState.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key0]", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
});
|
||||||
|
Assert.Equal(0, modelState.ErrorCount);
|
||||||
|
Assert.True(modelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
public class NonNullPerson
|
||||||
|
{
|
||||||
|
public int Age { get; set; }
|
||||||
|
|
||||||
|
// This should be implicitly required
|
||||||
|
public string Name { get; set; } = default!;
|
||||||
|
}
|
||||||
|
#nullable restore
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DictionaryModelBinder_ValuesIsNonNullableType_AppliesImplicitRequired()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = "parameter",
|
||||||
|
ParameterType = typeof(Dictionary<string, NonNullPerson>)
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
request.QueryString = new QueryString("?parameter[key0].Age=¶meter[key0].Name=name0¶meter[key1].Age=27¶meter[key1].Name=");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
var model = Assert.IsType<Dictionary<string, NonNullPerson>>(modelBindingResult.Model);
|
||||||
|
Assert.Collection(
|
||||||
|
model.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("key0", kvp.Key);
|
||||||
|
var person = kvp.Value;
|
||||||
|
Assert.Equal(0, person.Age);
|
||||||
|
Assert.Equal("name0", person.Name);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("key1", kvp.Key);
|
||||||
|
var person = kvp.Value;
|
||||||
|
Assert.Equal(27, person.Age);
|
||||||
|
Assert.Null(person.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
modelState.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key0].Age", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
|
||||||
|
Assert.Equal("The value '' is invalid.", Assert.Single(kvp.Value.Errors).ErrorMessage);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key0].Name", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key1].Age", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key1].Name", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
|
||||||
|
Assert.Equal("The Name field is required.", Assert.Single(kvp.Value.Errors).ErrorMessage);
|
||||||
|
});
|
||||||
|
Assert.Equal(2, modelState.ErrorCount);
|
||||||
|
Assert.False(modelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
public class NonNullPersonWithRequiredProperties
|
||||||
|
{
|
||||||
|
public int Age { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
}
|
||||||
|
#nullable restore
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DictionaryModelBinder_ValuesNullableTypeWithRequiredAttributes_AppliesValidation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||||
|
var parameter = new ParameterDescriptor()
|
||||||
|
{
|
||||||
|
Name = "parameter",
|
||||||
|
ParameterType = typeof(Dictionary<string, NonNullPersonWithRequiredProperties>)
|
||||||
|
};
|
||||||
|
|
||||||
|
var testContext = ModelBindingTestHelper.GetTestContext(request =>
|
||||||
|
{
|
||||||
|
request.QueryString = new QueryString("?parameter[key0].Age=¶meter[key0].Name=name0¶meter[key1].Age=27¶meter[key1].Name=");
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelState = testContext.ModelState;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelBindingResult.IsModelSet);
|
||||||
|
|
||||||
|
var model = Assert.IsType<Dictionary<string, NonNullPersonWithRequiredProperties>>(modelBindingResult.Model);
|
||||||
|
Assert.Collection(
|
||||||
|
model.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("key0", kvp.Key);
|
||||||
|
var person = kvp.Value;
|
||||||
|
Assert.Equal(0, person.Age);
|
||||||
|
Assert.Equal("name0", person.Name);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("key1", kvp.Key);
|
||||||
|
var person = kvp.Value;
|
||||||
|
Assert.Equal(27, person.Age);
|
||||||
|
Assert.Null(person.Name);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Collection(
|
||||||
|
modelState.OrderBy(kvp => kvp.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key0].Age", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
|
||||||
|
Assert.Equal("The value '' is invalid.", Assert.Single(kvp.Value.Errors).ErrorMessage);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key0].Name", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key1].Age", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Valid, kvp.Value.ValidationState);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("parameter[key1].Name", kvp.Key);
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
|
||||||
|
Assert.Equal("The Name field is required.", Assert.Single(kvp.Value.Errors).ErrorMessage);
|
||||||
|
});
|
||||||
|
Assert.Equal(2, modelState.ErrorCount);
|
||||||
|
Assert.False(modelState.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
private class ClosedGenericDictionary : Dictionary<string, string>
|
private class ClosedGenericDictionary : Dictionary<string, string>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue