Bind POCO model correctly; fallback to empty prefix despite exact name match
- #1865 - change `MutableObjectModelBinder` to ignore exact match in value providers - had an incorrect assumption: don't want exact model name to match since this binder supports only complex objects - also ignored `BinderModelName`, value provider filtering, et cetera - reduces over-binding e.g. `[Required]` validation within missing properties also add more tests of #2129 scenarios
This commit is contained in:
parent
94e326f953
commit
533474d07c
|
|
@ -92,16 +92,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. The model name is not prefixed and a value provider can directly provide a value for the model name.
|
// 3. Any of the model properties can be bound using a value provider.
|
||||||
// The fact that it is not prefixed means that the containsPrefixAsync call checks for the exact
|
|
||||||
// model name instead of doing a prefix match.
|
|
||||||
if (!bindingContext.ModelName.Contains(".") &&
|
|
||||||
await bindingContext.ValueProvider.ContainsPrefixAsync(bindingContext.ModelName))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Any of the model properties can be bound using a value provider.
|
|
||||||
if (await CanValueBindAnyModelProperties(context))
|
if (await CanValueBindAnyModelProperties(context))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -718,7 +718,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ModelBinding_LimitsErrorsToMaxErrorCount()
|
public async Task ModelBinding_LimitsErrorsToMaxErrorCount_DoesNotValidateMembersOfMissingProperties()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||||
|
|
@ -730,20 +730,21 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
||||||
|
|
||||||
// 8 is the value of MaxModelValidationErrors for the application being tested.
|
// 8 is the value of MaxModelValidationErrors for the application being tested.
|
||||||
Assert.Equal(8, json.Count);
|
Assert.Equal(8, json.Count);
|
||||||
Assert.Equal("The Field1 field is required.", json["Field1.Field1"]);
|
Assert.Equal("The Field1 field is required.", json["Field1"]);
|
||||||
Assert.Equal("The Field2 field is required.", json["Field1.Field2"]);
|
Assert.Equal("The Field2 field is required.", json["Field2"]);
|
||||||
Assert.Equal("The Field3 field is required.", json["Field1.Field3"]);
|
Assert.Equal("The Field3 field is required.", json["Field3"]);
|
||||||
Assert.Equal("The Field1 field is required.", json["Field2.Field1"]);
|
Assert.Equal("The Field4 field is required.", json["Field4"]);
|
||||||
Assert.Equal("The Field2 field is required.", json["Field2.Field2"]);
|
Assert.Equal("The Field5 field is required.", json["Field5"]);
|
||||||
Assert.Equal("The Field3 field is required.", json["Field2.Field3"]);
|
Assert.Equal("The Field6 field is required.", json["Field6"]);
|
||||||
Assert.Equal("The Field1 field is required.", json["Field3.Field1"]);
|
Assert.Equal("The Field7 field is required.", json["Field7"]);
|
||||||
Assert.Equal("The maximum number of allowed model errors has been reached.", json[""]);
|
Assert.Equal("The maximum number of allowed model errors has been reached.", json[""]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ModelBinding_ValidatesAllPropertiesInModel()
|
public async Task ModelBinding_FallsBackAndValidatesAllPropertiesInModel()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||||
|
|
@ -752,12 +753,72 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
// Act
|
// Act
|
||||||
var response = await client.GetStringAsync("http://localhost/Home/ModelWithFewValidationErrors?model=");
|
var response = await client.GetStringAsync("http://localhost/Home/ModelWithFewValidationErrors?model=");
|
||||||
|
|
||||||
//Assert
|
// Assert
|
||||||
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
||||||
Assert.Equal(3, json.Count);
|
Assert.Equal(3, json.Count);
|
||||||
Assert.Equal("The Field1 field is required.", json["model.Field1"]);
|
Assert.Equal("The Field1 field is required.", json["Field1"]);
|
||||||
Assert.Equal("The Field2 field is required.", json["model.Field2"]);
|
Assert.Equal("The Field2 field is required.", json["Field2"]);
|
||||||
Assert.Equal("The Field3 field is required.", json["model.Field3"]);
|
Assert.Equal("The Field3 field is required.", json["Field3"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ModelBinding_FallsBackAndSuccessfullyBindsStructCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var contentDictionary = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "[0]", "23" },
|
||||||
|
{ "[1]", "97" },
|
||||||
|
{ "[2]", "103" },
|
||||||
|
};
|
||||||
|
var requestContent = new FormUrlEncodedContent(contentDictionary);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("http://localhost/integers", requestContent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var array = JsonConvert.DeserializeObject<int[]>(responseContent);
|
||||||
|
|
||||||
|
Assert.Equal(3, array.Length);
|
||||||
|
Assert.Equal(23, array[0]);
|
||||||
|
Assert.Equal(97, array[1]);
|
||||||
|
Assert.Equal(103, array[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ModelBinding_FallsBackAndSuccessfullyBindsPOCOCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var contentDictionary = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "[0].CityCode", "YYZ" },
|
||||||
|
{ "[0].CityName", "Toronto" },
|
||||||
|
{ "[1].CityCode", "SEA" },
|
||||||
|
{ "[1].CityName", "Seattle" },
|
||||||
|
};
|
||||||
|
var requestContent = new FormUrlEncodedContent(contentDictionary);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.PostAsync("http://localhost/cities", requestContent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var list = JsonConvert.DeserializeObject<List<City>>(responseContent);
|
||||||
|
|
||||||
|
Assert.Equal(2, list.Count);
|
||||||
|
Assert.Equal(contentDictionary["[0].CityCode"], list[0].CityCode);
|
||||||
|
Assert.Equal(contentDictionary["[0].CityName"], list[0].CityName);
|
||||||
|
Assert.Equal(contentDictionary["[1].CityCode"], list[1].CityCode);
|
||||||
|
Assert.Equal(contentDictionary["[1].CityName"], list[1].CityName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -1918,7 +1979,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BindModelAsync_WithIncorrectlyFormattedNestedCollectionValue()
|
public async Task BindModelAsync_WithIncorrectlyFormattedNestedCollectionValue_BindsSingleNullEntry()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||||
|
|
@ -1935,9 +1996,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var result = await ReadValue<UserWithAddress>(response);
|
var result = await ReadValue<UserWithAddress>(response);
|
||||||
|
|
||||||
|
// Though posted content did not contain any valid Addresses, it is bound as a single-element List
|
||||||
|
// containing null. Slightly odd behavior is specific to this unusual error case: CollectionModelBinder
|
||||||
|
// attempted to bind a comma-separated string as a collection and Address lacks a from-string conversion.
|
||||||
|
// MutableObjectModelBinder does not create model when value providers have no data (unless at top level).
|
||||||
var address = Assert.Single(result.Addresses);
|
var address = Assert.Single(result.Addresses);
|
||||||
Assert.Null(address.AddressLines);
|
Assert.Null(address);
|
||||||
Assert.Null(address.ZipCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -1979,7 +2044,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task BindModelAsync_WithNestedCollectionContainingRecursiveRelation_WithMalformedValue()
|
public async Task
|
||||||
|
BindModelAsync_WithNestedCollectionContainingRecursiveRelation_WithMalformedValue_BindsSingleNullEntry()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||||
|
|
@ -1996,9 +2062,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var result = await ReadValue<PeopleModel>(response);
|
var result = await ReadValue<PeopleModel>(response);
|
||||||
|
|
||||||
|
// Though posted content did not contain any valid People, it is bound as a single-element List
|
||||||
|
// containing null. Slightly odd behavior is specific to this unusual error case: CollectionModelBinder
|
||||||
|
// attempted to bind a comma-separated string as a collection and Address lacks a from-string conversion.
|
||||||
|
// MutableObjectModelBinder does not create model when value providers have no data (unless at top level).
|
||||||
var person = Assert.Single(result.People);
|
var person = Assert.Single(result.People);
|
||||||
Assert.Null(person.Name);
|
Assert.Null(person);
|
||||||
Assert.Null(person.Parent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,127 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ModelBinder_FallsBackToEmpty_IfBinderMatchesButDoesNotSetModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
FallbackToEmptyPrefix = true,
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
|
||||||
|
ModelName = "someName",
|
||||||
|
ModelState = new ModelStateDictionary(),
|
||||||
|
ValueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someOtherName", "dummyValue" }
|
||||||
|
},
|
||||||
|
OperationBindingContext = new OperationBindingContext
|
||||||
|
{
|
||||||
|
ValidatorProvider = GetValidatorProvider()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
var modelBinder = new Mock<IModelBinder>();
|
||||||
|
modelBinder
|
||||||
|
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Callback<ModelBindingContext>(context =>
|
||||||
|
{
|
||||||
|
// Expect two calls; the second with empty ModelName.
|
||||||
|
Assert.InRange(count, 0, 1);
|
||||||
|
count++;
|
||||||
|
if (count == 1)
|
||||||
|
{
|
||||||
|
Assert.Equal("someName", context.ModelName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Empty(context.ModelName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false)));
|
||||||
|
|
||||||
|
var composite = CreateCompositeBinder(modelBinder.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var result = await composite.BindModelAsync(bindingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ModelBinder_DoesNotFallBackToEmpty_IfFallbackToEmptyPrefixFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
FallbackToEmptyPrefix = false,
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
|
||||||
|
ModelName = "someName",
|
||||||
|
ModelState = new ModelStateDictionary(),
|
||||||
|
ValueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someOtherName", "dummyValue" }
|
||||||
|
},
|
||||||
|
OperationBindingContext = new OperationBindingContext
|
||||||
|
{
|
||||||
|
ValidatorProvider = GetValidatorProvider()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var modelBinder = new Mock<IModelBinder>();
|
||||||
|
modelBinder
|
||||||
|
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Callback<ModelBindingContext>(context =>
|
||||||
|
{
|
||||||
|
Assert.Equal("someName", context.ModelName);
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false)))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var composite = CreateCompositeBinder(modelBinder.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var result = await composite.BindModelAsync(bindingContext);
|
||||||
|
modelBinder.Verify(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ModelBinder_DoesNotFallBackToEmpty_IfErrorsAreAdded()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
FallbackToEmptyPrefix = false,
|
||||||
|
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List<int>)),
|
||||||
|
ModelName = "someName",
|
||||||
|
ModelState = new ModelStateDictionary(),
|
||||||
|
ValueProvider = new SimpleHttpValueProvider
|
||||||
|
{
|
||||||
|
{ "someOtherName", "dummyValue" }
|
||||||
|
},
|
||||||
|
OperationBindingContext = new OperationBindingContext
|
||||||
|
{
|
||||||
|
ValidatorProvider = GetValidatorProvider()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var modelBinder = new Mock<IModelBinder>();
|
||||||
|
modelBinder
|
||||||
|
.Setup(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()))
|
||||||
|
.Callback<ModelBindingContext>(context =>
|
||||||
|
{
|
||||||
|
Assert.Equal("someName", context.ModelName);
|
||||||
|
context.ModelState.AddModelError(context.ModelName, "this is an error message");
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false)))
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var composite = CreateCompositeBinder(modelBinder.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var result = await composite.BindModelAsync(bindingContext);
|
||||||
|
modelBinder.Verify(mb => mb.BindModelAsync(It.IsAny<ModelBindingContext>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ModelBinder_ReturnsTrue_SetsNullValue_SetsModelStateKey()
|
public async Task ModelBinder_ReturnsTrue_SetsNullValue_SetsModelStateKey()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,46 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
Assert.True(retModel);
|
Assert.True(retModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public async Task CanCreateModel_ReturnsTrue_ForNonTopLevelModel_BasedOnValueAvailability(bool valueAvailable)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockValueProvider = new Mock<IValueProvider>(MockBehavior.Strict);
|
||||||
|
mockValueProvider
|
||||||
|
.Setup(provider => provider.ContainsPrefixAsync("SimpleContainer.Simple.Name"))
|
||||||
|
.Returns(Task.FromResult(valueAvailable));
|
||||||
|
|
||||||
|
var typeMetadata = GetMetadataForType(typeof(SimpleContainer));
|
||||||
|
var modelMetadata = typeMetadata.Properties[nameof(SimpleContainer.Simple)];
|
||||||
|
var bindingContext = new MutableObjectBinderContext
|
||||||
|
{
|
||||||
|
ModelBindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
ModelMetadata = modelMetadata,
|
||||||
|
ModelName = "SimpleContainer.Simple",
|
||||||
|
OperationBindingContext = new OperationBindingContext
|
||||||
|
{
|
||||||
|
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
|
||||||
|
ValueProvider = mockValueProvider.Object,
|
||||||
|
MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(),
|
||||||
|
},
|
||||||
|
ValueProvider = mockValueProvider.Object,
|
||||||
|
},
|
||||||
|
PropertyMetadata = modelMetadata.Properties,
|
||||||
|
};
|
||||||
|
|
||||||
|
var mutableBinder = new MutableObjectModelBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await mutableBinder.CanCreateModel(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Result matches whether first Simple property can bind.
|
||||||
|
Assert.Equal(valueAvailable, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
|
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
|
||||||
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
|
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
|
||||||
|
|
@ -603,14 +643,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
// Arrange
|
// Arrange
|
||||||
var expectedPropertyNames = new[]
|
var expectedPropertyNames = new[]
|
||||||
{
|
{
|
||||||
"DateOfBirth",
|
nameof(Person.DateOfBirth),
|
||||||
"DateOfDeath",
|
nameof(Person.DateOfDeath),
|
||||||
"ValueTypeRequired",
|
nameof(Person.ValueTypeRequired),
|
||||||
"FirstName",
|
nameof(Person.ValueTypeRequiredWithDefaultValue),
|
||||||
"LastName",
|
nameof(Person.FirstName),
|
||||||
"PropertyWithDefaultValue",
|
nameof(Person.LastName),
|
||||||
"PropertyWithInitializedValue",
|
nameof(Person.PropertyWithDefaultValue),
|
||||||
"PropertyWithInitializedValueAndDefault",
|
nameof(Person.PropertyWithInitializedValue),
|
||||||
|
nameof(Person.PropertyWithInitializedValueAndDefault),
|
||||||
};
|
};
|
||||||
var bindingContext = new ModelBindingContext
|
var bindingContext = new ModelBindingContext
|
||||||
{
|
{
|
||||||
|
|
@ -707,6 +748,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
Assert.Equal(new[] { "Never" }, validationInfo.SkipProperties);
|
Assert.Equal(new[] { "Never" }, validationInfo.SkipProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPropertyValidationInfo_WithIndexerProperties_Succeeds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bindingContext = new ModelBindingContext
|
||||||
|
{
|
||||||
|
// Any type, even an otherwise-simple POCO with an indexer property, would do here.
|
||||||
|
ModelMetadata = GetMetadataForType(typeof(List<Person>)),
|
||||||
|
OperationBindingContext = new OperationBindingContext
|
||||||
|
{
|
||||||
|
ValidatorProvider = Mock.Of<IModelValidatorProvider>(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var validationInfo = MutableObjectModelBinder.GetPropertyValidationInfo(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Enumerable.Empty<string>(), validationInfo.RequiredProperties);
|
||||||
|
Assert.Equal(Enumerable.Empty<string>(), validationInfo.SkipProperties);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
[ReplaceCulture]
|
[ReplaceCulture]
|
||||||
public void ProcessDto_BindRequiredFieldMissing_RaisesModelError()
|
public void ProcessDto_BindRequiredFieldMissing_RaisesModelError()
|
||||||
|
|
@ -842,7 +905,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
|
||||||
// Check Age error.
|
// Check Age error.
|
||||||
ModelState modelState;
|
ModelState modelState;
|
||||||
Assert.True(modelStateDictionary.TryGetValue("theModel.Age", out modelState));
|
Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.Age), out modelState));
|
||||||
|
|
||||||
var modelError = Assert.Single(modelState.Errors);
|
var modelError = Assert.Single(modelState.Errors);
|
||||||
Assert.Null(modelError.Exception);
|
Assert.Null(modelError.Exception);
|
||||||
|
|
@ -851,7 +914,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
Assert.Equal(expected, modelError.ErrorMessage);
|
Assert.Equal(expected, modelError.ErrorMessage);
|
||||||
|
|
||||||
// Check City error.
|
// Check City error.
|
||||||
Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState));
|
Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.City), out modelState));
|
||||||
|
|
||||||
modelError = Assert.Single(modelState.Errors);
|
modelError = Assert.Single(modelState.Errors);
|
||||||
Assert.Null(modelError.Exception);
|
Assert.Null(modelError.Exception);
|
||||||
|
|
@ -895,7 +958,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
|
||||||
// Check City error.
|
// Check City error.
|
||||||
ModelState modelState;
|
ModelState modelState;
|
||||||
Assert.True(modelStateDictionary.TryGetValue("theModel.City", out modelState));
|
Assert.True(modelStateDictionary.TryGetValue("theModel." + nameof(ModelWithRequired.City), out modelState));
|
||||||
|
|
||||||
var modelError = Assert.Single(modelState.Errors);
|
var modelError = Assert.Single(modelState.Errors);
|
||||||
Assert.Null(modelError.Exception);
|
Assert.Null(modelError.Exception);
|
||||||
|
|
@ -922,53 +985,156 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
// Assert
|
// Assert
|
||||||
var modelStateDictionary = bindingContext.ModelState;
|
var modelStateDictionary = bindingContext.ModelState;
|
||||||
Assert.False(modelStateDictionary.IsValid);
|
Assert.False(modelStateDictionary.IsValid);
|
||||||
Assert.Single(modelStateDictionary);
|
Assert.Equal(2, modelStateDictionary.Count);
|
||||||
|
|
||||||
// Check ValueTypeRequired error.
|
// Check ValueTypeRequired error.
|
||||||
ModelState modelState;
|
var modelStateEntry = Assert.Single(
|
||||||
Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState));
|
modelStateDictionary,
|
||||||
|
entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequired));
|
||||||
|
Assert.Equal("theModel." + nameof(Person.ValueTypeRequired), modelStateEntry.Key);
|
||||||
|
|
||||||
|
var modelState = modelStateEntry.Value;
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
|
||||||
|
|
||||||
var modelError = Assert.Single(modelState.Errors);
|
var modelError = Assert.Single(modelState.Errors);
|
||||||
Assert.Null(modelError.Exception);
|
Assert.Null(modelError.Exception);
|
||||||
Assert.NotNull(modelError.ErrorMessage);
|
Assert.NotNull(modelError.ErrorMessage);
|
||||||
Assert.Equal("Sample message", modelError.ErrorMessage);
|
Assert.Equal("Sample message", modelError.ErrorMessage);
|
||||||
|
|
||||||
|
// Check ValueTypeRequiredWithDefaultValue error.
|
||||||
|
modelStateEntry = Assert.Single(
|
||||||
|
modelStateDictionary,
|
||||||
|
entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
|
||||||
|
Assert.Equal("theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue), modelStateEntry.Key);
|
||||||
|
|
||||||
|
modelState = modelStateEntry.Value;
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
|
||||||
|
|
||||||
|
modelError = Assert.Single(modelState.Errors);
|
||||||
|
Assert.Null(modelError.Exception);
|
||||||
|
Assert.NotNull(modelError.ErrorMessage);
|
||||||
|
Assert.Equal("Another sample message", modelError.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage()
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage(bool isModelSet)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new Person();
|
var model = new Person();
|
||||||
var containerMetadata = GetMetadataForType(model.GetType());
|
var containerMetadata = GetMetadataForType(model.GetType());
|
||||||
|
|
||||||
var bindingContext = CreateContext(containerMetadata, model);
|
var bindingContext = CreateContext(containerMetadata, model);
|
||||||
|
var modelStateDictionary = bindingContext.ModelState;
|
||||||
|
|
||||||
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||||
var testableBinder = new TestableMutableObjectModelBinder();
|
var testableBinder = new TestableMutableObjectModelBinder();
|
||||||
|
|
||||||
|
// ValueTypeRequiredWithDefaultValue value comes from [DefaultValue] when !isModelSet.
|
||||||
|
var expectedValue = isModelSet ? 0 : 42;
|
||||||
|
|
||||||
// Make ValueTypeRequired invalid.
|
// Make ValueTypeRequired invalid.
|
||||||
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "ValueTypeRequired");
|
var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired));
|
||||||
dto.Results[propertyMetadata] = new ModelBindingResult(
|
dto.Results[propertyMetadata] = new ModelBindingResult(
|
||||||
null,
|
null,
|
||||||
isModelSet: true,
|
isModelSet: isModelSet,
|
||||||
key: "theModel.ValueTypeRequired");
|
key: "theModel." + nameof(Person.ValueTypeRequired));
|
||||||
|
|
||||||
|
// Make ValueTypeRequiredWithDefaultValue invalid
|
||||||
|
propertyMetadata = dto.PropertyMetadata
|
||||||
|
.Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue));
|
||||||
|
dto.Results[propertyMetadata] = new ModelBindingResult(
|
||||||
|
model: null,
|
||||||
|
isModelSet: isModelSet,
|
||||||
|
key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
testableBinder.ProcessDto(bindingContext, dto);
|
testableBinder.ProcessDto(bindingContext, dto);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var modelStateDictionary = bindingContext.ModelState;
|
|
||||||
Assert.False(modelStateDictionary.IsValid);
|
Assert.False(modelStateDictionary.IsValid);
|
||||||
Assert.Single(modelStateDictionary);
|
|
||||||
|
|
||||||
// Check ValueTypeRequired error.
|
// Check ValueTypeRequired error.
|
||||||
ModelState modelState;
|
var modelStateEntry = Assert.Single(
|
||||||
Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState));
|
modelStateDictionary,
|
||||||
|
entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequired));
|
||||||
|
Assert.Equal("theModel." + nameof(Person.ValueTypeRequired), modelStateEntry.Key);
|
||||||
|
|
||||||
|
var modelState = modelStateEntry.Value;
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
|
||||||
|
|
||||||
var modelError = Assert.Single(modelState.Errors);
|
var modelError = Assert.Single(modelState.Errors);
|
||||||
Assert.Null(modelError.Exception);
|
Assert.Null(modelError.Exception);
|
||||||
Assert.NotNull(modelError.ErrorMessage);
|
Assert.NotNull(modelError.ErrorMessage);
|
||||||
Assert.Equal("Sample message", modelError.ErrorMessage);
|
Assert.Equal("Sample message", modelError.ErrorMessage);
|
||||||
|
|
||||||
|
// Check ValueTypeRequiredWithDefaultValue error.
|
||||||
|
modelStateEntry = Assert.Single(
|
||||||
|
modelStateDictionary,
|
||||||
|
entry => entry.Key == "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
|
||||||
|
Assert.Equal("theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue), modelStateEntry.Key);
|
||||||
|
|
||||||
|
modelState = modelStateEntry.Value;
|
||||||
|
Assert.Equal(ModelValidationState.Invalid, modelState.ValidationState);
|
||||||
|
|
||||||
|
modelError = Assert.Single(modelState.Errors);
|
||||||
|
Assert.Null(modelError.Exception);
|
||||||
|
Assert.NotNull(modelError.ErrorMessage);
|
||||||
|
Assert.Equal("Another sample message", modelError.ErrorMessage);
|
||||||
|
|
||||||
|
Assert.Equal(0, model.ValueTypeRequired);
|
||||||
|
Assert.Equal(expectedValue, model.ValueTypeRequiredWithDefaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProcessDto_ProvideRequiredFields_Success()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new Person();
|
||||||
|
var containerMetadata = GetMetadataForType(model.GetType());
|
||||||
|
|
||||||
|
var bindingContext = CreateContext(containerMetadata, model);
|
||||||
|
var modelStateDictionary = bindingContext.ModelState;
|
||||||
|
|
||||||
|
var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties);
|
||||||
|
var testableBinder = new TestableMutableObjectModelBinder();
|
||||||
|
|
||||||
|
// Make ValueTypeRequired valid.
|
||||||
|
var propertyMetadata = dto.PropertyMetadata
|
||||||
|
.Single(p => p.PropertyName == nameof(Person.ValueTypeRequired));
|
||||||
|
dto.Results[propertyMetadata] = new ModelBindingResult(
|
||||||
|
41,
|
||||||
|
isModelSet: true,
|
||||||
|
key: "theModel." + nameof(Person.ValueTypeRequired));
|
||||||
|
|
||||||
|
// Make ValueTypeRequiredWithDefaultValue valid.
|
||||||
|
propertyMetadata = dto.PropertyMetadata
|
||||||
|
.Single(p => p.PropertyName == nameof(Person.ValueTypeRequiredWithDefaultValue));
|
||||||
|
dto.Results[propertyMetadata] = new ModelBindingResult(
|
||||||
|
model: 57,
|
||||||
|
isModelSet: true,
|
||||||
|
key: "theModel." + nameof(Person.ValueTypeRequiredWithDefaultValue));
|
||||||
|
|
||||||
|
// Also remind ProcessDto about PropertyWithDefaultValue -- as ComplexModelDtoModelBinder would.
|
||||||
|
propertyMetadata = dto.PropertyMetadata
|
||||||
|
.Single(p => p.PropertyName == nameof(Person.PropertyWithDefaultValue));
|
||||||
|
dto.Results[propertyMetadata] = new ModelBindingResult(
|
||||||
|
model: null,
|
||||||
|
isModelSet: false,
|
||||||
|
key: "theModel." + nameof(Person.PropertyWithDefaultValue));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
testableBinder.ProcessDto(bindingContext, dto);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(modelStateDictionary.IsValid);
|
||||||
|
Assert.Empty(modelStateDictionary);
|
||||||
|
|
||||||
|
// Model gets provided values.
|
||||||
|
Assert.Equal(41, model.ValueTypeRequired);
|
||||||
|
Assert.Equal(57, model.ValueTypeRequiredWithDefaultValue);
|
||||||
|
Assert.Equal(123.456m, model.PropertyWithDefaultValue); // from [DefaultValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -1426,6 +1592,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
[Required(ErrorMessage = "Sample message")]
|
[Required(ErrorMessage = "Sample message")]
|
||||||
public int ValueTypeRequired { get; set; }
|
public int ValueTypeRequired { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Another sample message")]
|
||||||
|
[DefaultValue(42)]
|
||||||
|
public int ValueTypeRequiredWithDefaultValue { get; set; }
|
||||||
|
|
||||||
public string FirstName { get; set; }
|
public string FirstName { get; set; }
|
||||||
public string LastName { get; set; }
|
public string LastName { get; set; }
|
||||||
public string NonUpdateableProperty { get; private set; }
|
public string NonUpdateableProperty { get; private set; }
|
||||||
|
|
@ -1593,6 +1763,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SimpleContainer
|
||||||
|
{
|
||||||
|
public Simple Simple { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Simple
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
private IServiceProvider CreateServices()
|
private IServiceProvider CreateServices()
|
||||||
{
|
{
|
||||||
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
|
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,18 @@ namespace ModelBindingWebSite.Controllers
|
||||||
return Content(System.Text.Encoding.UTF8.GetString(byteValues));
|
return Content(System.Text.Encoding.UTF8.GetString(byteValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("/integers")]
|
||||||
|
public IActionResult CollectionToJson(int[] model)
|
||||||
|
{
|
||||||
|
return Json(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("/cities")]
|
||||||
|
public IActionResult PocoCollectionToJson(List<City> model)
|
||||||
|
{
|
||||||
|
return Json(model);
|
||||||
|
}
|
||||||
|
|
||||||
public object ModelWithTooManyValidationErrors(LargeModelWithValidation model)
|
public object ModelWithTooManyValidationErrors(LargeModelWithValidation model)
|
||||||
{
|
{
|
||||||
return CreateValidationDictionary();
|
return CreateValidationDictionary();
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,17 @@ namespace ModelBindingWebSite.Models
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public ModelWithValidation Field4 { get; set; }
|
public ModelWithValidation Field4 { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public ModelWithValidation Field5 { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public ModelWithValidation Field6 { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public ModelWithValidation Field7 { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public ModelWithValidation Field8 { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue