зеркало из https://github.com/aspnet/Mvc.git
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).
This commit is contained in:
Родитель
924d50bd8f
Коммит
53ef8258bb
|
@ -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
|
||||
|
|
17
Mvc.sln
17
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
|
||||
|
|
|
@ -63,6 +63,41 @@ namespace Microsoft.AspNet.Mvc
|
|||
return actionArguments;
|
||||
}
|
||||
|
||||
public async Task<ModelBindingResult> 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<string, object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Person>(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<Person>(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<IScopedInstance<ActionContext>>().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<Person4>(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<Person2>(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<IScopedInstance<ActionContext>>().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<Person3>(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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>864fa09d-1e48-403a-a6c8-4f079d2a30f0</ProjectGuid>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
|
@ -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<IScopedInstance<ActionBindingContext>>().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<IScopedInstance<ActionContext>>();
|
||||
actionContextAccessor.Value = actionContext;
|
||||
|
||||
var actionBindingContextAccessor =
|
||||
httpContext.RequestServices.GetRequiredService<IScopedInstance<ActionBindingContext>>();
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<MvcOptions>
|
||||
{
|
||||
public TestMvcOptions()
|
||||
{
|
||||
Options = new MvcOptions();
|
||||
MvcOptionsSetup.ConfigureMvc(Options);
|
||||
}
|
||||
|
||||
public MvcOptions Options { get; }
|
||||
|
||||
public MvcOptions GetNamedOptions(string name)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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": { }
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
Загрузка…
Ссылка в новой задаче