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;
|
||||
}
|
||||
|
||||
// 3. The model name is not prefixed and a value provider can directly provide a value for the model name.
|
||||
// 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.
|
||||
// 3. Any of the model properties can be bound using a value provider.
|
||||
if (await CanValueBindAnyModelProperties(context))
|
||||
{
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -718,7 +718,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ModelBinding_LimitsErrorsToMaxErrorCount()
|
||||
public async Task ModelBinding_LimitsErrorsToMaxErrorCount_DoesNotValidateMembersOfMissingProperties()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||
|
|
@ -730,20 +730,21 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
//Assert
|
||||
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
||||
|
||||
// 8 is the value of MaxModelValidationErrors for the application being tested.
|
||||
Assert.Equal(8, json.Count);
|
||||
Assert.Equal("The Field1 field is required.", json["Field1.Field1"]);
|
||||
Assert.Equal("The Field2 field is required.", json["Field1.Field2"]);
|
||||
Assert.Equal("The Field3 field is required.", json["Field1.Field3"]);
|
||||
Assert.Equal("The Field1 field is required.", json["Field2.Field1"]);
|
||||
Assert.Equal("The Field2 field is required.", json["Field2.Field2"]);
|
||||
Assert.Equal("The Field3 field is required.", json["Field2.Field3"]);
|
||||
Assert.Equal("The Field1 field is required.", json["Field3.Field1"]);
|
||||
Assert.Equal("The Field1 field is required.", json["Field1"]);
|
||||
Assert.Equal("The Field2 field is required.", json["Field2"]);
|
||||
Assert.Equal("The Field3 field is required.", json["Field3"]);
|
||||
Assert.Equal("The Field4 field is required.", json["Field4"]);
|
||||
Assert.Equal("The Field5 field is required.", json["Field5"]);
|
||||
Assert.Equal("The Field6 field is required.", json["Field6"]);
|
||||
Assert.Equal("The Field7 field is required.", json["Field7"]);
|
||||
Assert.Equal("The maximum number of allowed model errors has been reached.", json[""]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ModelBinding_ValidatesAllPropertiesInModel()
|
||||
public async Task ModelBinding_FallsBackAndValidatesAllPropertiesInModel()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||
|
|
@ -752,12 +753,72 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
// Act
|
||||
var response = await client.GetStringAsync("http://localhost/Home/ModelWithFewValidationErrors?model=");
|
||||
|
||||
//Assert
|
||||
// Assert
|
||||
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
||||
Assert.Equal(3, json.Count);
|
||||
Assert.Equal("The Field1 field is required.", json["model.Field1"]);
|
||||
Assert.Equal("The Field2 field is required.", json["model.Field2"]);
|
||||
Assert.Equal("The Field3 field is required.", json["model.Field3"]);
|
||||
Assert.Equal("The Field1 field is required.", json["Field1"]);
|
||||
Assert.Equal("The Field2 field is required.", json["Field2"]);
|
||||
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]
|
||||
|
|
@ -1918,7 +1979,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindModelAsync_WithIncorrectlyFormattedNestedCollectionValue()
|
||||
public async Task BindModelAsync_WithIncorrectlyFormattedNestedCollectionValue_BindsSingleNullEntry()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||
|
|
@ -1935,9 +1996,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
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);
|
||||
Assert.Null(address.AddressLines);
|
||||
Assert.Null(address.ZipCode);
|
||||
Assert.Null(address);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1979,7 +2044,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindModelAsync_WithNestedCollectionContainingRecursiveRelation_WithMalformedValue()
|
||||
public async Task
|
||||
BindModelAsync_WithNestedCollectionContainingRecursiveRelation_WithMalformedValue_BindsSingleNullEntry()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestHelper.CreateServer(_app, SiteName, _configureServices);
|
||||
|
|
@ -1996,9 +2062,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
|
||||
// Assert
|
||||
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);
|
||||
Assert.Null(person.Name);
|
||||
Assert.Null(person.Parent);
|
||||
Assert.Null(person);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -145,6 +145,127 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
|||
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]
|
||||
public async Task ModelBinder_ReturnsTrue_SetsNullValue_SetsModelStateKey()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -193,6 +193,46 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
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]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), false)]
|
||||
[InlineData(typeof(TypeWithNoBinderMetadata), true)]
|
||||
|
|
@ -603,14 +643,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
// Arrange
|
||||
var expectedPropertyNames = new[]
|
||||
{
|
||||
"DateOfBirth",
|
||||
"DateOfDeath",
|
||||
"ValueTypeRequired",
|
||||
"FirstName",
|
||||
"LastName",
|
||||
"PropertyWithDefaultValue",
|
||||
"PropertyWithInitializedValue",
|
||||
"PropertyWithInitializedValueAndDefault",
|
||||
nameof(Person.DateOfBirth),
|
||||
nameof(Person.DateOfDeath),
|
||||
nameof(Person.ValueTypeRequired),
|
||||
nameof(Person.ValueTypeRequiredWithDefaultValue),
|
||||
nameof(Person.FirstName),
|
||||
nameof(Person.LastName),
|
||||
nameof(Person.PropertyWithDefaultValue),
|
||||
nameof(Person.PropertyWithInitializedValue),
|
||||
nameof(Person.PropertyWithInitializedValueAndDefault),
|
||||
};
|
||||
var bindingContext = new ModelBindingContext
|
||||
{
|
||||
|
|
@ -707,6 +748,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
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]
|
||||
[ReplaceCulture]
|
||||
public void ProcessDto_BindRequiredFieldMissing_RaisesModelError()
|
||||
|
|
@ -842,7 +905,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
// Check Age error.
|
||||
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);
|
||||
Assert.Null(modelError.Exception);
|
||||
|
|
@ -851,7 +914,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
Assert.Equal(expected, modelError.ErrorMessage);
|
||||
|
||||
// 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);
|
||||
Assert.Null(modelError.Exception);
|
||||
|
|
@ -895,7 +958,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
|
||||
// Check City error.
|
||||
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);
|
||||
Assert.Null(modelError.Exception);
|
||||
|
|
@ -922,53 +985,156 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
// Assert
|
||||
var modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Single(modelStateDictionary);
|
||||
Assert.Equal(2, modelStateDictionary.Count);
|
||||
|
||||
// Check ValueTypeRequired error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState));
|
||||
var modelStateEntry = Assert.Single(
|
||||
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);
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(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]
|
||||
public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage()
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void ProcessDto_RequiredFieldNull_RaisesModelErrorWithMessage(bool isModelSet)
|
||||
{
|
||||
// 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();
|
||||
|
||||
// ValueTypeRequiredWithDefaultValue value comes from [DefaultValue] when !isModelSet.
|
||||
var expectedValue = isModelSet ? 0 : 42;
|
||||
|
||||
// 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(
|
||||
null,
|
||||
isModelSet: true,
|
||||
key: "theModel.ValueTypeRequired");
|
||||
isModelSet: isModelSet,
|
||||
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
|
||||
testableBinder.ProcessDto(bindingContext, dto);
|
||||
|
||||
// Assert
|
||||
var modelStateDictionary = bindingContext.ModelState;
|
||||
Assert.False(modelStateDictionary.IsValid);
|
||||
Assert.Single(modelStateDictionary);
|
||||
|
||||
// Check ValueTypeRequired error.
|
||||
ModelState modelState;
|
||||
Assert.True(modelStateDictionary.TryGetValue("theModel.ValueTypeRequired", out modelState));
|
||||
var modelStateEntry = Assert.Single(
|
||||
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);
|
||||
Assert.Null(modelError.Exception);
|
||||
Assert.NotNull(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]
|
||||
|
|
@ -1426,6 +1592,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
|||
[Required(ErrorMessage = "Sample message")]
|
||||
public int ValueTypeRequired { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Another sample message")]
|
||||
[DefaultValue(42)]
|
||||
public int ValueTypeRequiredWithDefaultValue { get; set; }
|
||||
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; 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()
|
||||
{
|
||||
var services = new Mock<IServiceProvider>(MockBehavior.Strict);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,18 @@ namespace ModelBindingWebSite.Controllers
|
|||
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)
|
||||
{
|
||||
return CreateValidationDictionary();
|
||||
|
|
|
|||
|
|
@ -18,5 +18,17 @@ namespace ModelBindingWebSite.Models
|
|||
|
||||
[Required]
|
||||
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