diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs index fbdc621602..096cebbb27 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs @@ -88,7 +88,8 @@ namespace Microsoft.AspNet.Mvc { var parameterType = parameter.ModelType; var modelBindingContext = GetModelBindingContext(parameter, actionContext, operationBindingContext); - if (await bindingContext.ModelBinder.BindModelAsync(modelBindingContext)) + if (await bindingContext.ModelBinder.BindModelAsync(modelBindingContext) && + modelBindingContext.IsModelSet) { arguments[parameter.PropertyName] = modelBindingContext.Model; } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs index 3a133acbcb..c92fe2540b 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs @@ -27,11 +27,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding propertyMetadata); // bind and propagate the values - // If we can't bind, then leave the result missing (don't add a null). + // If we can't bind then leave the result missing (don't add a null). if (await bindingContext.OperationBindingContext.ModelBinder.BindModelAsync(propertyBindingContext)) { - var result = new ComplexModelDtoResult(propertyBindingContext.Model, - propertyBindingContext.ValidationNode); + var result = ComplexModelDtoResult.FromBindingContext(propertyBindingContext); dto.Results[propertyMetadata] = result; } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs index ac5a2d826a..cd298dd477 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoResult.cs @@ -5,15 +5,25 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public sealed class ComplexModelDtoResult { - public ComplexModelDtoResult(object model, - [NotNull] ModelValidationNode validationNode) + public static ComplexModelDtoResult FromBindingContext([NotNull] ModelBindingContext context) + { + return new ComplexModelDtoResult(context.Model, context.IsModelSet, context.ValidationNode); + } + + public ComplexModelDtoResult( + object model, + bool isModelBound, + [NotNull] ModelValidationNode validationNode) { Model = model; + IsModelBound = isModelBound; ValidationNode = validationNode; } - public object Model { get; private set; } + public bool IsModelBound { get; } - public ModelValidationNode ValidationNode { get; private set; } + public object Model { get; set; } + + public ModelValidationNode ValidationNode { get; set; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs index 9447deb58d..da70d2b8eb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs @@ -291,10 +291,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; } - private static object GetPropertyDefaultValue(PropertyInfo propertyInfo) + private static bool TryGetPropertyDefaultValue(PropertyInfo propertyInfo, out object value) { - var attr = propertyInfo.GetCustomAttribute(); - return (attr != null) ? attr.Value : null; + var attribute = propertyInfo.GetCustomAttribute(); + if (attribute == null) + { + value = null; + return false; + } + else + { + value = attribute.Value; + return true; + } } internal static PropertyValidationInfo GetPropertyValidationInfo(ModelBindingContext bindingContext) @@ -345,7 +354,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var validationInfo = GetPropertyValidationInfo(bindingContext); // Eliminate provided properties from requiredProperties; leaving just *missing* required properties. - validationInfo.RequiredProperties.ExceptWith(dto.Results.Select(r => r.Key.PropertyName)); + var boundProperties = dto.Results.Where(p => p.Value.IsModelBound).Select(p => p.Key.PropertyName); + validationInfo.RequiredProperties.ExceptWith(boundProperties); foreach (var missingRequiredProperty in validationInfo.RequiredProperties) { @@ -407,7 +417,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return; } - var value = dtoResult.Model ?? GetPropertyDefaultValue(property); + object value; + var hasDefaultValue = false; + if (dtoResult.IsModelBound) + { + value = dtoResult.Model; + } + else + { + hasDefaultValue = TryGetPropertyDefaultValue(property, out value); + } + propertyMetadata.Model = value; // 'Required' validators need to run first so that we can provide useful error messages if @@ -429,6 +449,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + if (!dtoResult.IsModelBound && !hasDefaultValue) + { + // If we don't have a value, don't set it on the model and trounce a pre-initialized + // value. + return; + } + if (value != null || property.PropertyType.AllowsNullValue()) { try diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs index 38fa995e78..7116349977 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ParameterBinding/ControllerActionArgumentBinderTests.cs @@ -183,6 +183,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) + .Callback(c => + { + // This value won't go into the arguments, because we return false. + c.Model = "Hello"; + }) .Returns(Task.FromResult(result: false)); var actionContext = new ActionContext( @@ -211,6 +216,59 @@ namespace Microsoft.AspNet.Mvc.Core.Test Assert.Empty(result); } + [Fact] + public async Task GetActionArgumentsAsync_DoesNotAddActionArguments_IfBinderDoesNotSetModel() + { + // Arrange + Func method = foo => 1; + var actionDescriptor = new ControllerActionDescriptor + { + MethodInfo = method.Method, + Parameters = new List + { + new ParameterDescriptor + { + Name = "foo", + ParameterType = typeof(object), + } + } + }; + + var binder = new Mock(); + binder + .Setup(b => b.BindModelAsync(It.IsAny())) + .Callback(c => + { + Assert.False(c.IsModelSet); + }) + .Returns(Task.FromResult(result: true)); + + var actionContext = new ActionContext( + new RouteContext(Mock.Of()), + actionDescriptor) + { + Controller = Mock.Of(), + }; + + var actionBindingContext = new ActionBindingContext() + { + ModelBinder = binder.Object, + }; + + var inputFormattersProvider = new Mock(); + inputFormattersProvider + .SetupGet(o => o.InputFormatters) + .Returns(new List()); + + var invoker = new DefaultControllerActionArgumentBinder(new DataAnnotationsModelMetadataProvider()); + + // Act + var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext); + + // Assert + Assert.Empty(result); + } + [Fact] public async Task GetActionArgumentsAsync_AddsActionArguments_IfBinderReturnsTrue() { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/UpdateDealerVehicle_UpdateSuccessful.txt b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/UpdateDealerVehicle_UpdateSuccessful.txt index c39a47f1f6..d322993b8f 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/UpdateDealerVehicle_UpdateSuccessful.txt +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/UpdateDealerVehicle_UpdateSuccessful.txt @@ -20,5 +20,5 @@
-Tracked by +Tracked by default-tracking-id
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs index 741bb9481d..6c2102562c 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs @@ -99,6 +99,36 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Empty(result.ModelStateErrors); } + // There should be no model state error for a top-level object + [Theory] + [InlineData("transactionId1234", "1e331f25-0869-4c87-8a94-64e6e40cb5a0")] + public async Task FromHeader_BindHeader_ToString_OnParameter_NoValues_DefaultValue( + string headerName, + string headerValue) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/Blog/BindToStringParameterDefaultValue"); + // Intentionally not setting a header value + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("default-value", result.HeaderValue); + Assert.Null(result.HeaderValues); + Assert.Empty(result.ModelStateErrors); + } + // The action that this test hits will echo back the model-bound values [Theory] [InlineData("transactionIds", "1e331f25-0869-4c87-8a94-64e6e40cb5a0")] @@ -187,6 +217,70 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("Title", error); } + // This model sets a value for 'Title', and the model binder won't trounce it. + // + // There's no validation error because we validate the initialized value. + [Fact] + public async Task FromHeader_BindHeader_ToModel_NoValues_InitializedValue_ValidationError() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/Blog/BindToModelWithInitializedValue?author=Marvin"); + + // Intentionally not setting a title or tags + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("How to Make Soup", result.HeaderValue); + Assert.Equal(new[] { "Cooking" }, result.HeaderValues); + + var error = Assert.Single(result.ModelStateErrors); + Assert.Equal("Title", error); + } + + // This model uses default value for 'Title'. + // + // There's no validation error because we validate the default value. + [Fact] + public async Task FromHeader_BindHeader_ToModel_NoValues_DefaultValue_NoValidationError() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/Blog/BindToModelWithDefaultValue?author=Marvin"); + + // Intentionally not setting a title or tags + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("How to Make Soup", result.HeaderValue); + Assert.Equal(new[] { "Cooking" }, result.HeaderValues); + + var error = Assert.Single(result.ModelStateErrors); + Assert.Equal("Title", error); + } + private class Result { public string HeaderValue { get; set; } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs index 4380dc5ca9..0330680e16 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/ComplexModelDtoResultTest.cs @@ -14,10 +14,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var validationNode = GetValidationNode(); // Act - var result = new ComplexModelDtoResult("some string", validationNode); + var result = new ComplexModelDtoResult( + "some string", + isModelBound: true, + validationNode: validationNode); // Assert Assert.Equal("some string", result.Model); + Assert.True(result.IsModelBound); Assert.Equal(validationNode, result.ValidationNode); } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs index b2a583427d..67c2d6cf97 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/MutableObjectModelBinderTest.cs @@ -583,7 +583,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding "ValueTypeRequired", "FirstName", "LastName", - "PropertyWithDefaultValue" + "PropertyWithDefaultValue", + "PropertyWithInitializedValue", + "PropertyWithInitializedValueAndDefault", }; var bindingContext = new ModelBindingContext { @@ -751,7 +753,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var nameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "Name"); - dto.Results[nameProperty] = new ComplexModelDtoResult("John Doe", new ModelValidationNode(nameProperty, "")); + dto.Results[nameProperty] = new ComplexModelDtoResult( + "John Doe", + isModelBound: true, + validationNode: new ModelValidationNode(nameProperty, "")); var testableBinder = new TestableMutableObjectModelBinder(); @@ -805,14 +810,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var testableBinder = new TestableMutableObjectModelBinder(); var propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Name"); - dto.Results[propertyMetadata] = - new ComplexModelDtoResult("John Doe", new ModelValidationNode(propertyMetadata, "theModel.Name")); + dto.Results[propertyMetadata] = new ComplexModelDtoResult( + "John Doe", + isModelBound: true, + validationNode: new ModelValidationNode(propertyMetadata, "theModel.Name")); // Attempt to set non-Nullable property to null. BindRequiredAttribute should not be relevant in this // case because the binding exists. propertyMetadata = dto.PropertyMetadata.Single(o => o.PropertyName == "Age"); - dto.Results[propertyMetadata] = - new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.Age")); + dto.Results[propertyMetadata] = new ComplexModelDtoResult( + null, + isModelBound: true, + validationNode: new ModelValidationNode(propertyMetadata, "theModel.Age")); // Act; must also Validate because null-check error handler is late-bound testableBinder.ProcessDto(bindingContext, dto); @@ -894,11 +903,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Make Age valid and City invalid. var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "Age"); - dto.Results[propertyMetadata] = - new ComplexModelDtoResult(23, new ModelValidationNode(propertyMetadata, "theModel.Age")); + dto.Results[propertyMetadata] = new ComplexModelDtoResult( + 23, + isModelBound: true, + validationNode: new ModelValidationNode(propertyMetadata, "theModel.Age")); + propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "City"); - dto.Results[propertyMetadata] = - new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.City")); + dto.Results[propertyMetadata] = new ComplexModelDtoResult( + null, + isModelBound: true, + validationNode: new ModelValidationNode(propertyMetadata, "theModel.City")); // Act testableBinder.ProcessDto(bindingContext, dto); @@ -963,8 +977,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Make ValueTypeRequired invalid. var propertyMetadata = dto.PropertyMetadata.Single(p => p.PropertyName == "ValueTypeRequired"); - dto.Results[propertyMetadata] = - new ComplexModelDtoResult(null, new ModelValidationNode(propertyMetadata, "theModel.ValueTypeRequired")); + dto.Results[propertyMetadata] = new ComplexModelDtoResult( + null, + isModelBound: true, + validationNode: new ModelValidationNode(propertyMetadata, "theModel.ValueTypeRequired")); // Act testableBinder.ProcessDto(bindingContext, dto); @@ -999,9 +1015,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var dto = new ComplexModelDto(containerMetadata, containerMetadata.Properties); var firstNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "FirstName"); - dto.Results[firstNameProperty] = new ComplexModelDtoResult("John", new ModelValidationNode(firstNameProperty, "")); + dto.Results[firstNameProperty] = new ComplexModelDtoResult( + "John", + isModelBound: true, + validationNode: new ModelValidationNode(firstNameProperty, "")); + var lastNameProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "LastName"); - dto.Results[lastNameProperty] = new ComplexModelDtoResult("Doe", new ModelValidationNode(lastNameProperty, "")); + dto.Results[lastNameProperty] = new ComplexModelDtoResult( + "Doe", + isModelBound: true, + validationNode: new ModelValidationNode(lastNameProperty, "")); + var dobProperty = dto.PropertyMetadata.Single(o => o.PropertyName == "DateOfBirth"); dto.Results[dobProperty] = null; @@ -1025,7 +1049,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var propertyMetadata = bindingContext.ModelMetadata.Properties.First(o => o.PropertyName == "PropertyWithDefaultValue"); var validationNode = new ModelValidationNode(propertyMetadata, "foo"); - var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + + var dtoResult = new ComplexModelDtoResult( + model: null, + isModelBound: false, + validationNode: validationNode); + var requiredValidator = bindingContext.OperationBindingContext .ValidatorProvider .GetValidators(propertyMetadata) @@ -1034,7 +1063,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); // Assert var person = Assert.IsType(bindingContext.Model); @@ -1042,6 +1071,60 @@ namespace Microsoft.AspNet.Mvc.ModelBinding Assert.True(bindingContext.ModelState.IsValid); } + [Fact] + public void SetProperty_PropertyIsPreinitialized_NoValue_DoesNothing() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new Person())); + + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single( + o => o.PropertyName == "PropertyWithInitializedValue"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo"); + + // This value won't be used because IsModelBound = false. + var dtoResult = new ComplexModelDtoResult( + model: "bad-value", + isModelBound: false, + validationNode: validationNode); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); + + // Assert + var person = Assert.IsType(bindingContext.Model); + Assert.Equal("preinitialized", person.PropertyWithInitializedValue); + Assert.True(bindingContext.ModelState.IsValid); + } + + [Fact] + public void SetProperty_PropertyIsPreinitialized_WithDefaultValue_NoValue_CallsSetter() + { + // Arrange + var bindingContext = CreateContext(GetMetadataForObject(new Person())); + + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single( + o => o.PropertyName == "PropertyWithInitializedValueAndDefault"); + var validationNode = new ModelValidationNode(propertyMetadata, "foo"); + + // This value won't be used because IsModelBound = false. + var dtoResult = new ComplexModelDtoResult( + model: "bad-value", + isModelBound: false, + validationNode: validationNode); + + var testableBinder = new TestableMutableObjectModelBinder(); + + // Act + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); + + // Assert + var person = Assert.IsType(bindingContext.Model); + Assert.Equal("default", person.PropertyWithInitializedValueAndDefault); + Assert.True(bindingContext.ModelState.IsValid); + } + [Fact] public void SetProperty_PropertyIsReadOnly_DoesNothing() { @@ -1049,12 +1132,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var bindingContext = CreateContext(GetMetadataForObject(new Person())); var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NonUpdateableProperty"); var validationNode = new ModelValidationNode(propertyMetadata, "foo"); - var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + + var dtoResult = new ComplexModelDtoResult( + model: null, + isModelBound: false, + validationNode: validationNode); var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); // Assert // If didn't throw, success! @@ -1069,7 +1156,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth"); var validationNode = new ModelValidationNode(propertyMetadata, "foo"); - var dtoResult = new ComplexModelDtoResult(new DateTime(2001, 1, 1), validationNode); + + var dtoResult = new ComplexModelDtoResult( + new DateTime(2001, 1, 1), + isModelBound: true, + validationNode: validationNode); + var requiredValidator = bindingContext.OperationBindingContext .ValidatorProvider .GetValidators(propertyMetadata) @@ -1079,7 +1171,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); // Assert validationNode.Validate(validationContext); @@ -1100,12 +1192,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfDeath"); var validationNode = new ModelValidationNode(propertyMetadata, "foo"); - var dtoResult = new ComplexModelDtoResult(new DateTime(1800, 1, 1), validationNode); + var dtoResult = new ComplexModelDtoResult( + new DateTime(1800, 1, 1), + isModelBound: true, + validationNode: validationNode); var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator: null); // Assert Assert.Equal("Date of death can't be before date of birth." + Environment.NewLine @@ -1118,16 +1213,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { // Arrange var bindingContext = CreateContext(GetMetadataForObject(new Person())); + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "DateOfBirth"); var validationNode = new ModelValidationNode(propertyMetadata, "foo"); - var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var dtoResult = new ComplexModelDtoResult( + model: null, + isModelBound: true, + validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); var validationContext = new ModelValidationContext(bindingContext, propertyMetadata); var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); // Assert Assert.True(bindingContext.ModelState.IsValid); @@ -1141,15 +1241,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Arrange var bindingContext = CreateContext(GetMetadataForObject(new Person())); bindingContext.ModelName = " foo"; + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "ValueTypeRequired"); var validationNode = new ModelValidationNode(propertyMetadata, "foo.ValueTypeRequired"); - var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var dtoResult = new ComplexModelDtoResult( + model: null, + isModelBound: true, + validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); // Assert Assert.False(bindingContext.ModelState.IsValid); @@ -1163,15 +1268,20 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Arrange var bindingContext = CreateContext(GetMetadataForObject(new ModelWhosePropertySetterThrows())); bindingContext.ModelName = "foo"; + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "NameNoAttribute"); var validationNode = new ModelValidationNode(propertyMetadata, "foo.NameNoAttribute"); - var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var dtoResult = new ComplexModelDtoResult( + model: null, + isModelBound: true, + validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); // Assert Assert.False(bindingContext.ModelState.IsValid); @@ -1187,15 +1297,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding // Arrange var bindingContext = CreateContext(GetMetadataForObject(new ModelWhosePropertySetterThrows())); bindingContext.ModelName = "foo"; + var propertyMetadata = bindingContext.ModelMetadata.Properties.Single(o => o.PropertyName == "Name"); var validationNode = new ModelValidationNode(propertyMetadata, "foo.Name"); - var dtoResult = new ComplexModelDtoResult(model: null, validationNode: validationNode); + var dtoResult = new ComplexModelDtoResult(model: null, + isModelBound: true, + validationNode: validationNode); + var requiredValidator = GetRequiredValidator(bindingContext, propertyMetadata); var testableBinder = new TestableMutableObjectModelBinder(); // Act - testableBinder.SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + testableBinder.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); // Assert Assert.False(bindingContext.ModelState.IsValid); @@ -1290,6 +1404,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding [DefaultValue(typeof(decimal), "123.456")] public decimal PropertyWithDefaultValue { get; set; } + + public string PropertyWithInitializedValue { get; set; } = "preinitialized"; + + [DefaultValue("default")] + public string PropertyWithInitializedValueAndDefault { get; set; } = "preinitialized"; } private class PersonWithBindExclusion @@ -1497,20 +1616,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return base.GetMetadataForProperties(bindingContext); } - public virtual void SetPropertyPublic(ModelBindingContext bindingContext, - ModelMetadata propertyMetadata, - ComplexModelDtoResult dtoResult, - IModelValidator requiredValidator) - { - base.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); - } - - protected override void SetProperty(ModelBindingContext bindingContext, + public new void SetProperty(ModelBindingContext bindingContext, ModelMetadata propertyMetadata, ComplexModelDtoResult dtoResult, IModelValidator requiredValidator) { - SetPropertyPublic(bindingContext, propertyMetadata, dtoResult, requiredValidator); + base.SetProperty(bindingContext, propertyMetadata, dtoResult, requiredValidator); } } } diff --git a/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs b/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs index 59b90b73b9..0b5bcde811 100644 --- a/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs +++ b/test/WebSites/FiltersWebSite/Controllers/ActionFilterController.cs @@ -30,12 +30,19 @@ namespace FiltersWebSite public override void OnActionExecuting(ActionExecutingContext context) { - if (context.ActionArguments["fromGlobalActionFilter"] == null) + object obj; + List filters; + + if (context.ActionArguments.TryGetValue("fromGlobalActionFilter", out obj)) { - context.ActionArguments["fromGlobalActionFilter"] = new List(); + filters = (List)obj; } - (context.ActionArguments["fromGlobalActionFilter"] as List) - .Add(Helpers.GetContentResult(context.Result, "Controller override - OnActionExecuting")); + { + filters = new List(); + context.ActionArguments.Add("fromGlobalActionFilter", filters); + } + + filters.Add(Helpers.GetContentResult(context.Result, "Controller override - OnActionExecuting")); } public override void OnActionExecuted(ActionExecutedContext context) diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs index eafaf0bc4d..3a5bdae409 100644 --- a/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs +++ b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.AspNet.Mvc; @@ -21,6 +22,17 @@ namespace ModelBindingWebSite.Controllers }; } + // Echo back the header value + [HttpGet("BindToStringParameterDefaultValue")] + public object BindToStringParameterDefaultValue([FromHeader] string transactionId = "default-value") + { + return new Result() + { + HeaderValue = transactionId, + ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(), + }; + } + // Echo back the header values [HttpGet("BindToStringArrayParameter")] public object BindToStringArrayParameter([FromHeader] string[] transactionIds) @@ -53,6 +65,29 @@ namespace ModelBindingWebSite.Controllers }; } + [HttpGet("BindToModelWithInitializedValue")] + public object BindToModelWithInitializedValue(BlogPostWithInitializedValue blogPost) + { + return new Result() + { + HeaderValue = blogPost.Title, + HeaderValues = blogPost.Tags, + ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(), + }; + } + + [HttpGet("BindToModelWithDefaultValue")] + public object BindToModelWithDefaultValue(BlogPostWithDefaultValue blogPost) + { + return new Result() + { + HeaderValue = blogPost.Title, + HeaderValues = blogPost.Tags, + ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(), + }; + } + + private class Result { public string HeaderValue { get; set; } @@ -73,5 +108,31 @@ namespace ModelBindingWebSite.Controllers public string Author { get; set; } } + + public class BlogPostWithInitializedValue + { + [Required] + [FromHeader] + public string Title { get; set; } = "How to Make Soup"; + + [FromHeader] + public string[] Tags { get; set; } = new string[] { "Cooking" }; + + public string Author { get; set; } + } + + public class BlogPostWithDefaultValue + { + [Required] + [FromHeader] + [DefaultValue("How to Make Soup")] + public string Title { get; set; } + + [FromHeader] + [DefaultValue(new string[] { "Cooking" })] + public string[] Tags { get; set; } + + public string Author { get; set; } + } } } \ No newline at end of file