From 53ef8258bb0f7457ce17ea652f92201f1c60bed2 Mon Sep 17 00:00:00 2001 From: Harsh Gupta Date: Fri, 3 Apr 2015 15:18:50 -0700 Subject: [PATCH] The model state keys for body bound models which are bound at property will use the entire model name with this change for example Consider public class Person { [FromBody] public Address Address { get; set; } } public class Address { [Required] public string Street { get; set; } public int Zip { get; set; } } Request body { "Zip" : 12345 } In this case the error key would be "prefix.Address.Street" (assuming there is a prefix because of additional metadata/positioning for/of the Person model). public class Person { [Required] public string Name { get; set; } } public void Action([FromBody]Person p) { } Request body { } In this case the prefix gets ignored and the error key is Name. Please note this is so that we are compatible with MVC 5.0 public class Person { [Required] public string Name { get; set; } } public void Action([FromBody][ModelBinder(Name = "prefix")] Person p) { } public void Action2([FromBody][Bind(Name = "prefix")] Person p) { } Request body { } In both these cases (Action and Action2) the prefix gets ignored and the error key is Name. This is a slight improvement from mvc, as in MVC the action parameter would be null. The followup for this would be to fix #2416 - This PR ignores the validation assuming that #2416 will address the issues and update the test. NOTE: previous versions of mvc did not have property binding and hence there is no precedence in this case. For MVC and Web API it was possible to body bind an action parameter which used an empty prefix instead of a parameter name for adding errors to model state (In case of MVC if a custom prefix was provided, it failed binding from body i.e the parameter was null). --- Mvc.NoFun.sln | 17 +- Mvc.sln | 17 +- .../DefaultControllerActionArgumentBinder.cs | 58 ++-- .../ModelBinders/BodyModelBinder.cs | 9 +- .../Validation/DefaultObjectValidator.cs | 1 + .../BodyValidationIntegrationTests.cs | 268 ++++++++++++++++++ ...icrosoft.AspNet.Mvc.IntegrationTests.xproj | 20 ++ .../ModelBindingTestHelper.cs | 81 ++++++ .../TestMvcOptions.cs | 24 ++ .../project.json | 20 ++ .../Metadata/ModelMetadataProviderTest.cs | 2 +- .../DefaultModelValidatorProviderTest.cs | 4 +- 12 files changed, 492 insertions(+), 29 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.IntegrationTests/Microsoft.AspNet.Mvc.IntegrationTests.xproj create mode 100644 test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs create mode 100644 test/Microsoft.AspNet.Mvc.IntegrationTests/project.json diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index af813aeb3e..f6ea5814ff 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22710.0 +VisualStudioVersion = 14.0.22808.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -64,6 +64,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch. EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.IntegrationTests", "test\Microsoft.AspNet.Mvc.IntegrationTests\Microsoft.AspNet.Mvc.IntegrationTests.xproj", "{864FA09D-1E48-403A-A6C8-4F079D2A30F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -345,6 +347,18 @@ Global {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|x86.ActiveCfg = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|x86.Build.0 = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Any CPU.Build.0 = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|x86.ActiveCfg = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -375,5 +389,6 @@ Global {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {81C20848-E063-4E12-AC40-0B55A532C16C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {864FA09D-1E48-403A-A6C8-4F079D2A30F0} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/Mvc.sln b/Mvc.sln index 487d3c64e4..917f504240 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22806.0 +VisualStudioVersion = 14.0.22808.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -162,6 +162,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JsonPatchWebSite", "test\WebSites\JsonPatchWebSite\JsonPatchWebSite.xproj", "{DAB1252D-577C-4912-98BE-1A812BF83F86}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.IntegrationTests", "test\Microsoft.AspNet.Mvc.IntegrationTests\Microsoft.AspNet.Mvc.IntegrationTests.xproj", "{864FA09D-1E48-403A-A6C8-4F079D2A30F0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -964,6 +966,18 @@ Global {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|Mixed Platforms.Build.0 = Release|Any CPU {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|x86.ActiveCfg = Release|Any CPU {DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|x86.Build.0 = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Debug|x86.Build.0 = Debug|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Any CPU.Build.0 = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|x86.ActiveCfg = Release|Any CPU + {864FA09D-1E48-403A-A6C8-4F079D2A30F0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1043,5 +1057,6 @@ Global {81C20848-E063-4E12-AC40-0B55A532C16C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {DAB1252D-577C-4912-98BE-1A812BF83F86} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {864FA09D-1E48-403A-A6C8-4F079D2A30F0} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs index d272796a91..2957f1aa20 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerActionArgumentBinder.cs @@ -63,6 +63,41 @@ namespace Microsoft.AspNet.Mvc return actionArguments; } + public async Task BindModelAsync( + ParameterDescriptor parameter, + ModelStateDictionary modelState, + OperationBindingContext operationContext) + { + var metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterType); + var parameterType = parameter.ParameterType; + var modelBindingContext = GetModelBindingContext( + parameter.Name, + metadata, + parameter.BindingInfo, + modelState, + operationContext); + + var modelBindingResult = await operationContext.ModelBinder.BindModelAsync(modelBindingContext); + if (modelBindingResult != null && modelBindingResult.IsModelSet) + { + var key = modelBindingResult.Key; + var modelExplorer = new ModelExplorer( + _modelMetadataProvider, + metadata, + modelBindingResult.Model); + + var validationContext = new ModelValidationContext( + key, + modelBindingContext.BindingSource, + operationContext.ValidatorProvider, + modelState, + modelExplorer); + _validator.Validate(validationContext); + } + + return modelBindingResult; + } + private void ActivateProperties(object controller, Type containerType, Dictionary properties) { var propertyHelpers = PropertyHelper.GetProperties(controller); @@ -88,31 +123,10 @@ namespace Microsoft.AspNet.Mvc { foreach (var parameter in parameterMetadata) { - var metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterType); - var parameterType = parameter.ParameterType; - var modelBindingContext = GetModelBindingContext( - parameter.Name, - metadata, - parameter.BindingInfo, - modelState, - operationContext); - - var modelBindingResult = await operationContext.ModelBinder.BindModelAsync(modelBindingContext); + var modelBindingResult = await BindModelAsync(parameter, modelState, operationContext); if (modelBindingResult != null && modelBindingResult.IsModelSet) { - var modelExplorer = new ModelExplorer( - _modelMetadataProvider, - metadata, - modelBindingResult.Model); - arguments[parameter.Name] = modelBindingResult.Model; - var validationContext = new ModelValidationContext( - modelBindingResult.Key, - modelBindingContext.BindingSource, - operationContext.ValidatorProvider, - modelState, - modelExplorer); - _validator.Validate(validationContext); } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs index b233f20aee..bc09c834f1 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinders/BodyModelBinder.cs @@ -51,8 +51,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { var model = await formatter.ReadAsync(formatterContext); - // key is empty to ensure that the model name is not used as a prefix for validation. - return new ModelBindingResult(model, key: string.Empty, isModelSet: true); + var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null; + + // For compatibility with MVC 5.0 for top level object we want to consider an empty key instead of + // the parameter name/a custom name. In all other cases (like when binding body to a property) we + // consider the entire ModelName as a prefix. + var modelBindingKey = isTopLevelObject ? string.Empty : bindingContext.ModelName; + return new ModelBindingResult(model, key: modelBindingKey, isModelSet: true); } catch (Exception ex) { diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs index f54ecadba8..bbe20104eb 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs @@ -187,6 +187,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName; var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyBindingName); + if (!ValidateNonVisitedNodeAndChildren( childKey, propertyValidationContext, diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs new file mode 100644 index 0000000000..63816190cf --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs @@ -0,0 +1,268 @@ +// 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.DataAnnotations; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.Framework.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public class BodyValidationIntegrationTests + { + private class Person + { + [FromBody] + [Required] + public Address Address { get; set; } + } + + private class Address + { + public string Street { get; set; } + } + + [Fact] + public async Task FromBodyAndRequiredOnProperty_EmptyBody_AddsModelStateError() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + ParameterType = typeof(Person) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var httpContext = operationContext.HttpContext; + + ConfigureHttpRequest(httpContext.Request, string.Empty); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + var key = Assert.Single(modelState.Keys); + Assert.Equal("CustomParameter.Address", key); + Assert.False(modelState.IsValid); + var error = Assert.Single(modelState[key].Errors); + Assert.Equal("The Address field is required.", error.ErrorMessage); + } + + [Fact] + public async Task FromBodyOnActionParameter_EmptyBody_AddsModelStateError() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + BindingSource = BindingSource.Body + }, + ParameterType = typeof(Person) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var httpContext = operationContext.HttpContext; + + ConfigureHttpRequest(httpContext.Request, "{ \"Id\":1234 }"); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + var key = Assert.Single(modelState.Keys); + Assert.Equal("Address", key); + Assert.False(modelState.IsValid); + var error = Assert.Single(modelState[key].Errors); + Assert.Equal("The Address field is required.",error.ErrorMessage); + } + + private class Person4 + { + [FromBody] + [Required] + public int Address { get; set; } + } + + [Fact] + public async Task FromBodyAndRequiredOnValueTypeProperty_EmptyBody_AddsModelStateError() + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + ParameterType = typeof(Person4) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var httpContext = operationContext.HttpContext; + ConfigureHttpRequest(httpContext.Request, string.Empty); + var actionContext = httpContext.RequestServices.GetRequiredService>().Value; + var modelState = actionContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.False(modelState.IsValid); + + // The error with an empty key is a bug(#2416) in our implementation which does not append the prefix and + // use that along with the path. The expected key here would be CustomParameter.Address. + var key = Assert.Single(modelState.Keys, k => k == ""); + var error = Assert.Single(modelState[""].Errors); + Assert.StartsWith( + "No JSON content found and type 'System.Int32' is not nullable.", + error.Exception.Message); + } + + private class Person2 + { + [FromBody] + public Address2 Address { get; set; } + } + + private class Address2 + { + [Required] + public string Street { get; set; } + + public int Zip { get; set; } + } + + [Theory] + [InlineData("{ \"Zip\" : 123 }")] + [InlineData("{}")] + public async Task FromBodyOnTopLevelProperty_RequiredOnSubProperty_AddsModelStateError(string inputText) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + ParameterType = typeof(Person2) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var httpContext = operationContext.HttpContext; + ConfigureHttpRequest(httpContext.Request, inputText); + var modelState = new ModelStateDictionary(); + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.False(modelState.IsValid); + Assert.Equal(2, modelState.Keys.Count); + var zip = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Zip"); + Assert.Equal(ModelValidationState.Valid, modelState[zip].ValidationState); + + var street = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Street"); + Assert.Equal(ModelValidationState.Invalid, modelState[street].ValidationState); + var error = Assert.Single(modelState[street].Errors); + Assert.Equal("The Street field is required.", error.ErrorMessage); + } + + private class Person3 + { + [FromBody] + public Address3 Address { get; set; } + } + + private class Address3 + { + public string Street { get; set; } + + [Required] + public int Zip { get; set; } + } + + [Theory] + [InlineData("{ \"Street\" : \"someStreet\" }")] + [InlineData("{}")] + public async Task FromBodyOnProperty_RequiredOnValueTypeSubProperty_AddsModelStateError(string inputText) + { + // Arrange + var argumentBinder = ModelBindingTestHelper.GetArgumentBinder(); + var parameter = new ParameterDescriptor() + { + BindingInfo = new BindingInfo() + { + BinderModelName = "CustomParameter", + }, + ParameterType = typeof(Person3) + }; + + var operationContext = ModelBindingTestHelper.GetOperationBindingContext(); + var httpContext = operationContext.HttpContext; + ConfigureHttpRequest(httpContext.Request, inputText); + var actionContext = httpContext.RequestServices.GetRequiredService>().Value; + var modelState = actionContext.ModelState; + + // Act + var modelBindingResult = await argumentBinder.BindModelAsync(parameter, modelState, operationContext); + + // Assert + Assert.NotNull(modelBindingResult); + Assert.True(modelBindingResult.IsModelSet); + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson); + Assert.False(modelState.IsValid); + var street = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Street"); + Assert.Equal(ModelValidationState.Valid, modelState[street].ValidationState); + + // The error with an empty key is a bug(#2416) in our implementation which does not append the prefix and + // use that along with the path. The expected key here would be Address. + var zip = Assert.Single(modelState.Keys, k => k == "CustomParameter.Address.Zip"); + Assert.Equal(ModelValidationState.Valid, modelState[zip].ValidationState); + var error = Assert.Single(modelState[""].Errors); + Assert.StartsWith( + "Required property 'Zip' not found in JSON. Path ''", + error.Exception.Message); + } + + private static void ConfigureHttpRequest(HttpRequest request, string jsonContent) + { + request.Body = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + request.ContentType = "application/json"; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/Microsoft.AspNet.Mvc.IntegrationTests.xproj b/test/Microsoft.AspNet.Mvc.IntegrationTests/Microsoft.AspNet.Mvc.IntegrationTests.xproj new file mode 100644 index 0000000000..a93c34954b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/Microsoft.AspNet.Mvc.IntegrationTests.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 864fa09d-1e48-403a-a6c8-4f079d2a30f0 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs new file mode 100644 index 0000000000..11da5127e3 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -0,0 +1,81 @@ +// 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 Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.ModelBinding.Validation; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public static class ModelBindingTestHelper + { + public static OperationBindingContext GetOperationBindingContext() + { + var httpContext = ModelBindingTestHelper.GetHttpContext(); + var actionBindingContextAccessor = + httpContext.RequestServices.GetRequiredService>().Value; + return new OperationBindingContext() + { + BodyBindingState = BodyBindingState.NotBodyBased, + HttpContext = httpContext, + MetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(), + ValidatorProvider = actionBindingContextAccessor.ValidatorProvider, + ValueProvider = actionBindingContextAccessor.ValueProvider, + ModelBinder = actionBindingContextAccessor.ModelBinder + }; + } + + public static DefaultControllerActionArgumentBinder GetArgumentBinder() + { + var options = new TestMvcOptions(); + options.Options.MaxModelValidationErrors = 5; + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + return new DefaultControllerActionArgumentBinder( + metadataProvider, + new DefaultObjectValidator( + options.Options.ValidationExcludeFilters, + metadataProvider)); + } + + public static HttpContext GetHttpContext() + { + var options = (new TestMvcOptions()).Options; + var httpContext = new DefaultHttpContext(); + var serviceCollection = MvcServices.GetDefaultServices(); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var actionContext = new ActionContext(httpContext, new RouteData(), new ControllerActionDescriptor()); + + var actionContextAccessor = + httpContext.RequestServices.GetRequiredService>(); + actionContextAccessor.Value = actionContext; + + var actionBindingContextAccessor = + httpContext.RequestServices.GetRequiredService>(); + actionBindingContextAccessor.Value = GetActionBindingContext(options, actionContext); + return httpContext; + } + + private static ActionBindingContext GetActionBindingContext(MvcOptions options, ActionContext actionContext) + { + var valueProviderFactoryContext = new ValueProviderFactoryContext( + actionContext.HttpContext, + actionContext.RouteData.Values); + + var valueProvider = CompositeValueProvider.Create( + options.ValueProviderFactories, + valueProviderFactoryContext); + + return new ActionBindingContext() + { + InputFormatters = options.InputFormatters, + OutputFormatters = options.OutputFormatters, // Not required for model binding. + ValidatorProvider = new TestModelValidatorProvider(options.ModelValidatorProviders), + ModelBinder = new CompositeModelBinder(options.ModelBinders), + ValueProvider = valueProvider + }; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs new file mode 100644 index 0000000000..d10db14d1d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs @@ -0,0 +1,24 @@ +// 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; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Mvc.IntegrationTests +{ + public class TestMvcOptions : IOptions + { + public TestMvcOptions() + { + Options = new MvcOptions(); + MvcOptionsSetup.ConfigureMvc(Options); + } + + public MvcOptions Options { get; } + + public MvcOptions GetNamedOptions(string name) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/project.json b/test/Microsoft.AspNet.Mvc.IntegrationTests/project.json new file mode 100644 index 0000000000..ee66b3d47f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/project.json @@ -0,0 +1,20 @@ +{ + "compilationOptions": { + "warningsAsErrors": "true" + }, + "dependencies": { + "Microsoft.AspNet.Http.Core": "1.0.0-*", + "Microsoft.AspNet.Mvc":"6.0.0-*", + "Microsoft.AspNet.Mvc.TestCommon": { "version": "6.0.0-*", "type": "build" }, + "Microsoft.AspNet.Testing": "1.0.0-*", + "Moq": "4.2.1312.1622", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataProviderTest.cs index 6ff37ac636..1da8cccf8b 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataProviderTest.cs @@ -977,7 +977,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata _attributes = attributes; } - protected override DefaultMetadataDetails CreateTypeDetails([NotNull]ModelMetadataIdentity key) + protected override DefaultMetadataDetails CreateTypeDetails(ModelMetadataIdentity key) { var entry = base.CreateTypeDetails(key); return new DefaultMetadataDetails( diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultModelValidatorProviderTest.cs index 202d40b7e0..1b5cfdf065 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DefaultModelValidatorProviderTest.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var metadata = metadataProvider.GetMetadataForProperty( - typeof(ModelValidatorAttributeOnProperty), + typeof(ModelValidatorAttributeOnProperty), nameof(ModelValidatorAttributeOnProperty.Property)); var context = new ModelValidatorProviderContext(metadata); @@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation Assert.IsType(Assert.Single(validators)); } - + [Fact] public void GetValidators_DataAnnotationsAttribute_DefaultAdapter() {