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:
Harsh Gupta 2015-04-03 15:18:50 -07:00
Родитель 924d50bd8f
Коммит 53ef8258bb
12 изменённых файлов: 492 добавлений и 29 удалений

Просмотреть файл

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.22710.0 VisualStudioVersion = 14.0.22808.1
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject EndProject
@ -64,6 +64,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch.
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch", "src\Microsoft.AspNet.JsonPatch\Microsoft.AspNet.JsonPatch.xproj", "{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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.ActiveCfg = Release|Any CPU
{4D55F4D8-633B-462F-A5B1-FEB84BD2D534}.Release|Mixed Platforms.Build.0 = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -375,5 +389,6 @@ Global
{F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {F504357E-C2E1-4818-BA5C-9A2EAC25FEE5} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{81C20848-E063-4E12-AC40-0B55A532C16C} = {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} {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{864FA09D-1E48-403A-A6C8-4F079D2A30F0} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

17
Mvc.sln
Просмотреть файл

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.22806.0 VisualStudioVersion = 14.0.22808.1
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject EndProject
@ -162,6 +162,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.JsonPatch"
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JsonPatchWebSite", "test\WebSites\JsonPatchWebSite\JsonPatchWebSite.xproj", "{DAB1252D-577C-4912-98BE-1A812BF83F86}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JsonPatchWebSite", "test\WebSites\JsonPatchWebSite\JsonPatchWebSite.xproj", "{DAB1252D-577C-4912-98BE-1A812BF83F86}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|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.ActiveCfg = Release|Any CPU
{DAB1252D-577C-4912-98BE-1A812BF83F86}.Release|x86.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -1043,5 +1057,6 @@ Global
{81C20848-E063-4E12-AC40-0B55A532C16C} = {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} {4D55F4D8-633B-462F-A5B1-FEB84BD2D534} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{DAB1252D-577C-4912-98BE-1A812BF83F86} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {DAB1252D-577C-4912-98BE-1A812BF83F86} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{864FA09D-1E48-403A-A6C8-4F079D2A30F0} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

Просмотреть файл

@ -63,6 +63,41 @@ namespace Microsoft.AspNet.Mvc
return actionArguments; 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) private void ActivateProperties(object controller, Type containerType, Dictionary<string, object> properties)
{ {
var propertyHelpers = PropertyHelper.GetProperties(controller); var propertyHelpers = PropertyHelper.GetProperties(controller);
@ -88,31 +123,10 @@ namespace Microsoft.AspNet.Mvc
{ {
foreach (var parameter in parameterMetadata) foreach (var parameter in parameterMetadata)
{ {
var metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterType); var modelBindingResult = await BindModelAsync(parameter, modelState, operationContext);
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) if (modelBindingResult != null && modelBindingResult.IsModelSet)
{ {
var modelExplorer = new ModelExplorer(
_modelMetadataProvider,
metadata,
modelBindingResult.Model);
arguments[parameter.Name] = 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); var model = await formatter.ReadAsync(formatterContext);
// key is empty to ensure that the model name is not used as a prefix for validation. var isTopLevelObject = bindingContext.ModelMetadata.ContainerType == null;
return new ModelBindingResult(model, key: string.Empty, isModelSet: true);
// 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) catch (Exception ex)
{ {

Просмотреть файл

@ -187,6 +187,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName; var propertyBindingName = propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName;
var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyBindingName); var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyBindingName);
if (!ValidateNonVisitedNodeAndChildren( if (!ValidateNonVisitedNodeAndChildren(
childKey, childKey,
propertyValidationContext, 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; _attributes = attributes;
} }
protected override DefaultMetadataDetails CreateTypeDetails([NotNull]ModelMetadataIdentity key) protected override DefaultMetadataDetails CreateTypeDetails(ModelMetadataIdentity key)
{ {
var entry = base.CreateTypeDetails(key); var entry = base.CreateTypeDetails(key);
return new DefaultMetadataDetails( return new DefaultMetadataDetails(

Просмотреть файл

@ -59,7 +59,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForProperty( var metadata = metadataProvider.GetMetadataForProperty(
typeof(ModelValidatorAttributeOnProperty), typeof(ModelValidatorAttributeOnProperty),
nameof(ModelValidatorAttributeOnProperty.Property)); nameof(ModelValidatorAttributeOnProperty.Property));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata);
@ -117,7 +117,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Assert.IsType<RangeAttributeAdapter>(Assert.Single(validators)); Assert.IsType<RangeAttributeAdapter>(Assert.Single(validators));
} }
[Fact] [Fact]
public void GetValidators_DataAnnotationsAttribute_DefaultAdapter() public void GetValidators_DataAnnotationsAttribute_DefaultAdapter()
{ {