diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs
index 891a1af3de..a3b0a3a7d5 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromFormAttribute.cs
@@ -10,9 +10,12 @@ namespace Microsoft.AspNet.Mvc
/// Specifies that a parameter or property should be bound using form-data in the request body.
///
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
- public class FromFormAttribute : Attribute, IBindingSourceMetadata
+ public class FromFormAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
///
public BindingSource BindingSource { get { return BindingSource.Form; } }
+
+ ///
+ public string Name { get; set; }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs
index 3ae9be21ed..3ef1a2e1b7 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromQueryAttribute.cs
@@ -10,9 +10,12 @@ namespace Microsoft.AspNet.Mvc
/// Specifies that a parameter or property should be bound using the request query string.
///
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
- public class FromQueryAttribute : Attribute, IBindingSourceMetadata
+ public class FromQueryAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
///
public BindingSource BindingSource { get { return BindingSource.Query; } }
+
+ ///
+ public string Name { get; set; }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs
index ecbcf923dd..9836edfd4f 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/BinderMetadata/FromRouteAttribute.cs
@@ -10,9 +10,12 @@ namespace Microsoft.AspNet.Mvc
/// Specifies that a parameter or property should be bound using route-data from the current request.
///
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
- public class FromRouteAttribute : Attribute, IBindingSourceMetadata
+ public class FromRouteAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
///
public BindingSource BindingSource { get { return BindingSource.Path; } }
+
+ ///
+ public string Name { get; set; }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs
index 7e65069dcc..42e50d6247 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/ComplexModelDtoModelBinder.cs
@@ -22,8 +22,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var dto = (ComplexModelDto)bindingContext.ModelMetadata.Model;
foreach (var propertyMetadata in dto.PropertyMetadata)
{
- var propertyModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName,
- propertyMetadata.PropertyName);
+ var propertyModelName = ModelBindingHelper.CreatePropertyModelName(
+ bindingContext.ModelName,
+ propertyMetadata.BinderModelName ?? propertyMetadata.PropertyName);
var propertyBindingContext = new ModelBindingContext(bindingContext,
propertyModelName,
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs
index e34104e597..a2f141cdcc 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/CompositeModelBinder.cs
@@ -125,7 +125,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// to just those that match. We can skip filtering when IsGreedy == true, because that can't use
// value providers.
//
- // We also want to base this filtering on the - top-level value profider in case the data source
+ // We also want to base this filtering on the - top-level value provider in case the data source
// on this property doesn't intersect with the ambient data source.
//
// Ex:
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs
index c8fa10028b..736ab1046d 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs
@@ -9,7 +9,7 @@ using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
///
- /// An which binds models from the request headers when a model
+ /// An which binds models from the request headers when a model
/// has the binding source /
///
public class HeaderModelBinder : BindingSourceModelBinder
@@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var request = bindingContext.OperationBindingContext.HttpContext.Request;
var modelMetadata = bindingContext.ModelMetadata;
- // Property name can be null if the model metadata represents a type (rahter than a property or parameter).
+ // Property name can be null if the model metadata represents a type (rather than a property or parameter).
var headerName = modelMetadata.BinderModelName ?? modelMetadata.PropertyName ?? bindingContext.ModelName;
object model = null;
if (bindingContext.ModelType == typeof(string))
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs
index 8b98d5b1ca..9ae1c4942f 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/MutableObjectModelBinder.cs
@@ -176,8 +176,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
- var propertyModelName = ModelBindingHelper.CreatePropertyModelName(bindingContext.ModelName,
- metadata.PropertyName);
+ var propertyModelName = ModelBindingHelper.CreatePropertyModelName(
+ bindingContext.ModelName,
+ metadata.BinderModelName ?? metadata.PropertyName);
if (await valueProvider.ContainsPrefixAsync(propertyModelName))
{
@@ -357,14 +358,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
foreach (var missingRequiredProperty in validationInfo.RequiredProperties)
{
var addedError = false;
- var modelStateKey = ModelBindingHelper.CreatePropertyModelName(
- bindingContext.ModelName, missingRequiredProperty);
// Update Model as SetProperty() would: Place null value where validator will check for non-null. This
// ensures a failure result from a required validator (if any) even for a non-nullable property.
// (Otherwise, propertyMetadata.Model is likely already null.)
var propertyMetadata = bindingContext.ModelMetadata.Properties[missingRequiredProperty];
propertyMetadata.Model = null;
+ var propertyName = propertyMetadata.BinderModelName ?? missingRequiredProperty;
+ var modelStateKey = ModelBindingHelper.CreatePropertyModelName(
+ bindingContext.ModelName,
+ propertyName);
// Execute validator (if any) to get custom error message.
IModelValidator validator;
@@ -379,7 +382,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
bindingContext.ModelState.TryAddModelError(
modelStateKey,
- Resources.FormatMissingRequiredMember(missingRequiredProperty));
+ Resources.FormatMissingRequiredMember(propertyName));
}
}
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs
index d7349d95f2..721d27fa39 100644
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs
+++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DefaultObjectValidator.cs
@@ -156,7 +156,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
foreach (var childMetadata in metadata.Properties)
{
- var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, childMetadata.PropertyName);
+ var propertyName = childMetadata.BinderModelName ?? childMetadata.PropertyName;
+ var childKey = ModelBindingHelper.CreatePropertyModelName(currentModelKey, propertyName);
if (!ValidateNonVisitedNodeAndChildren(childKey, childMetadata, validationContext, validators: null))
{
isValid = false;
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationNode.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationNode.cs
deleted file mode 100644
index 9c7500c229..0000000000
--- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ModelValidationNode.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-// 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 System.Collections.Generic;
-using System.Linq;
-using Microsoft.AspNet.Mvc.ModelBinding.Internal;
-
-namespace Microsoft.AspNet.Mvc.ModelBinding
-{
-}
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromFormTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromFormTest.cs
new file mode 100644
index 0000000000..4afbab5275
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromFormTest.cs
@@ -0,0 +1,154 @@
+// 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 System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.TestHost;
+using ModelBindingWebSite;
+using ModelBindingWebSite.Controllers;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.FunctionalTests
+{
+ public class ModelBindingFromFormTest
+ {
+ private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(ModelBindingWebSite));
+ private readonly Action _app = new ModelBindingWebSite.Startup().Configure;
+
+ [Fact]
+ public async Task FromForm_CustomModelPrefix_ForParameter()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url = "http://localhost/FromFormAttribute_Company/CreateCompany";
+ var request = new HttpRequestMessage(HttpMethod.Post, url);
+ var nameValueCollection = new List>
+ {
+ new KeyValuePair("customPrefix.Employees[0].Name", "somename"),
+ };
+
+ request.Content = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+
+ Assert.Equal("somename", employee.Name);
+ }
+
+ [Fact]
+ public async Task FromForm_CustomModelPrefix_ForCollectionParameter()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url = "http://localhost/FromFormAttribute_Company/CreateCompanyFromEmployees";
+ var request = new HttpRequestMessage(HttpMethod.Post, url);
+ var nameValueCollection = new List>
+ {
+ new KeyValuePair("customPrefix[0].Department", "Contoso"),
+ };
+ request.Content = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+ Assert.Equal("Contoso", employee.Department);
+ }
+
+ [Fact]
+ public async Task FromForm_CustomModelPrefix_ForProperty()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url = "http://localhost/FromFormAttribute_Company/CreateCompany";
+ var request = new HttpRequestMessage(HttpMethod.Post, url);
+ var nameValueCollection = new List>
+ {
+ new KeyValuePair("customPrefix.Employees[0].EmployeeSSN", "123132131"),
+ };
+ request.Content = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+ Assert.Equal("123132131", employee.SSN);
+ }
+
+ [Fact]
+ public async Task FromForm_CustomModelPrefix_ForCollectionProperty()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url = "http://localhost/FromFormAttribute_Company/CreateDepartment";
+ var request = new HttpRequestMessage(HttpMethod.Post, url);
+ var nameValueCollection = new List>
+ {
+ new KeyValuePair("department.TestEmployees[0].EmployeeSSN", "123132131"),
+ };
+ request.Content = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var department = JsonConvert.DeserializeObject<
+ FromFormAttribute_CompanyController.FromForm_Department>(body);
+
+ var employee = Assert.Single(department.Employees);
+ Assert.Equal("123132131", employee.SSN);
+ }
+
+ [Fact]
+ public async Task FromForm_NonExistingValueAddsValidationErrors_OnProperty_UsingCustomModelPrefix()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url = "http://localhost/FromFormAttribute_Company/ValidateDepartment";
+ var request = new HttpRequestMessage(HttpMethod.Post, url);
+
+ // No values.
+ var nameValueCollection = new List>();
+ request.Content = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+ Assert.Null(result.Value);
+ var error = Assert.Single(result.ModelStateErrors);
+ Assert.Equal("TestEmployees", error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs
index d68e1ddfcc..19db618f5c 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromHeaderTest.cs
@@ -93,8 +93,32 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var result = JsonConvert.DeserializeObject(body);
Assert.Equal(tags, result.HeaderValues);
var error = Assert.Single(result.ModelStateErrors);
- Assert.Equal("Title", error);
+ Assert.Equal("BlogTitle", error);
}
+
+ [Fact]
+ public async Task FromHeader_NonExistingHeaderAddsValidationErrors_OnCollectionProperty_CustomName()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Blog/BindToProperty/CustomName");
+ request.Headers.TryAddWithoutValidation("BlogTitle", "Cooking Receipes.");
+
+ // 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("Cooking Receipes.", result.HeaderValue);
+ var error = Assert.Single(result.ModelStateErrors);
+ Assert.Equal("BlogTags", error);
+ }
+
// The action that this test hits will echo back the model-bound value
[Fact]
public async Task FromHeader_BindHeader_ToString_OnParameter_CustomName()
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromQueryTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromQueryTest.cs
new file mode 100644
index 0000000000..8fb2d7c716
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromQueryTest.cs
@@ -0,0 +1,130 @@
+// 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 System.Threading.Tasks;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.TestHost;
+using ModelBindingWebSite;
+using Newtonsoft.Json;
+using Xunit;
+using ModelBindingWebSite.Controllers;
+
+namespace Microsoft.AspNet.Mvc.FunctionalTests
+{
+ public class ModelBindingFromQueryTest
+ {
+ private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(ModelBindingWebSite));
+ private readonly Action _app = new ModelBindingWebSite.Startup().Configure;
+
+ [Fact]
+ public async Task FromQuery_CustomModelPrefix_ForParameter()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // [FromQuery(Name = "customPrefix")] is used to apply a prefix
+ var url =
+ "http://localhost/FromQueryAttribute_Company/CreateCompany?customPrefix.Employees[0].Name=somename";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+ Assert.Equal("somename", employee.Name);
+ }
+
+ [Fact]
+ public async Task FromQuery_CustomModelPrefix_ForCollectionParameter()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url =
+ "http://localhost/FromQueryAttribute_Company/CreateCompanyFromEmployees?customPrefix[0].Name=somename";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+ Assert.Equal("somename", employee.Name);
+ }
+
+ [Fact]
+ public async Task FromQuery_CustomModelPrefix_ForProperty()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // [FromQuery(Name = "EmployeeId")] is used to apply a prefix
+ var url =
+ "http://localhost/FromQueryAttribute_Company/CreateCompany?customPrefix.Employees[0].EmployeeId=1234";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+
+ Assert.Equal(1234, employee.Id);
+ }
+
+ [Fact]
+ public async Task FromQuery_CustomModelPrefix_ForCollectionProperty()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url =
+ "http://localhost/FromQueryAttribute_Company/CreateDepartment?TestEmployees[0].EmployeeId=1234";
+
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var department = JsonConvert.DeserializeObject<
+ FromQueryAttribute_CompanyController.FromQuery_Department>(body);
+
+ var employee = Assert.Single(department.Employees);
+ Assert.Equal(1234, employee.Id);
+ }
+
+ [Fact]
+ public async Task FromQuery_NonExistingValueAddsValidationErrors_OnProperty_UsingCustomModelPrefix()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url =
+ "http://localhost/FromQueryAttribute_Company/ValidateDepartment?TestEmployees[0].Department=contoso";
+
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+ var error = Assert.Single(result.ModelStateErrors);
+ Assert.Equal("TestEmployees[0].EmployeeId", error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromRouteTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromRouteTest.cs
new file mode 100644
index 0000000000..c2c40c2024
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingFromRouteTest.cs
@@ -0,0 +1,93 @@
+// 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 System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.TestHost;
+using ModelBindingWebSite;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.FunctionalTests
+{
+ public class ModelBindingFromRouteTest
+ {
+ private readonly IServiceProvider _services = TestHelper.CreateServices(nameof(ModelBindingWebSite));
+ private readonly Action _app = new ModelBindingWebSite.Startup().Configure;
+
+ [Fact]
+ public async Task FromRoute_CustomModelPrefix_ForParameter()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // [FromRoute(Name = "customPrefix")] is used to apply a prefix
+ var url =
+ "http://localhost/FromRouteAttribute_Company/CreateEmployee/somename";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var employee = JsonConvert.DeserializeObject(body);
+ Assert.Equal("somename", employee.Name);
+ }
+
+ [Fact]
+ public async Task FromRoute_CustomModelPrefix_ForProperty()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // [FromRoute(Name = "EmployeeId")] is used to apply a prefix
+ var url =
+ "http://localhost/FromRouteAttribute_Company/CreateEmployee/somename/1234";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var employee = JsonConvert.DeserializeObject(body);
+ Assert.Equal(1234, employee.TaxId);
+ }
+
+
+ [Fact]
+ public async Task FromRoute_NonExistingValueAddsValidationErrors_OnProperty_UsingCustomModelPrefix()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ // [FromRoute(Name = "TestEmployees")] is used to apply a prefix
+ var url =
+ "http://localhost/FromRouteAttribute_Company/ValidateDepartment/contoso";
+ var request = new HttpRequestMessage(HttpMethod.Post, url);
+
+ // No values.
+ var nameValueCollection = new List>();
+ request.Content = new FormUrlEncodedContent(nameValueCollection);
+
+ // Act
+ var response = await client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject(body);
+ Assert.Null(result.Value);
+ var error = Assert.Single(result.ModelStateErrors);
+ Assert.Equal("TestEmployees", error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingModelBinderAttributeTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingModelBinderAttributeTest.cs
index 9bb00261a7..822567b3b6 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingModelBinderAttributeTest.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingModelBinderAttributeTest.cs
@@ -44,6 +44,27 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("somename", employee.Name);
}
+ [Fact]
+ public async Task ModelBinderAttribute_CustomModelPrefix_OnProperty()
+ {
+ // Arrange
+ var server = TestServer.Create(_services, _app);
+ var client = server.CreateClient();
+
+ var url =
+ "http://localhost/ModelBinderAttribute_Company/CreateCompany?employees[0].Alias=somealias";
+
+ // Act
+ var response = await client.GetAsync(url);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ var company = JsonConvert.DeserializeObject(body);
+
+ var employee = Assert.Single(company.Employees);
+ Assert.Equal("somealias", employee.EmailAlias);
+ }
+
[Theory]
[InlineData("GetBinderType_UseModelBinderOnType")]
[InlineData("GetBinderType_UseModelBinderProviderOnType")]
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromFormAttribute_CompanyController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromFormAttribute_CompanyController.cs
new file mode 100644
index 0000000000..86e6120617
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Controllers/FromFormAttribute_CompanyController.cs
@@ -0,0 +1,45 @@
+// 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.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Microsoft.AspNet.Mvc;
+
+namespace ModelBindingWebSite.Controllers
+{
+ [Route("FromFormAttribute_Company/[action]")]
+ public class FromFormAttribute_CompanyController : Controller
+ {
+ public Company CreateCompany([FromForm(Name = "customPrefix")] Company company)
+ {
+ return company;
+ }
+
+ public FromForm_Department CreateDepartment(FromForm_Department department)
+ {
+ return department;
+ }
+
+ public Company CreateCompanyFromEmployees([FromForm(Name = "customPrefix")] IList employees)
+ {
+ return new Company { Employees = employees };
+ }
+
+ public object ValidateDepartment(FromForm_Department department)
+ {
+ return new Result()
+ {
+ Value = department.Employees,
+ ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(),
+ };
+ }
+
+ public class FromForm_Department
+ {
+ [FromForm(Name = "TestEmployees")]
+ [Required]
+ public IEnumerable Employees { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs
index ac89820650..d09720d7ad 100644
--- a/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs
+++ b/test/WebSites/ModelBindingWebSite/Controllers/FromHeader_BlogController.cs
@@ -128,6 +128,7 @@ namespace ModelBindingWebSite.Controllers
public string Title { get; set; }
[FromHeader(Name = "BlogTags")]
+ [Required]
public string[] Tags { get; set; }
public string Author { get; set; }
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromQueryAttribute_CompanyController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromQueryAttribute_CompanyController.cs
new file mode 100644
index 0000000000..1e7b3e2004
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Controllers/FromQueryAttribute_CompanyController.cs
@@ -0,0 +1,45 @@
+// 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.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Microsoft.AspNet.Mvc;
+
+namespace ModelBindingWebSite.Controllers
+{
+ [Route("FromQueryAttribute_Company/[action]")]
+ public class FromQueryAttribute_CompanyController : Controller
+ {
+ public Company CreateCompany([FromQuery(Name = "customPrefix")] Company company)
+ {
+ return company;
+ }
+
+ public Company CreateCompanyFromEmployees([FromQuery(Name = "customPrefix")] IList employees)
+ {
+ return new Company { Employees = employees };
+ }
+
+ public FromQuery_Department CreateDepartment(FromQuery_Department department)
+ {
+ return department;
+ }
+
+ public object ValidateDepartment(FromQuery_Department department)
+ {
+ return new Result()
+ {
+ Value = department.Employees,
+ ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(),
+ };
+ }
+
+ public class FromQuery_Department
+ {
+ [FromQuery(Name = "TestEmployees")]
+ [Required]
+ public IEnumerable Employees { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FromRouteAttribute_CompanyController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FromRouteAttribute_CompanyController.cs
new file mode 100644
index 0000000000..a49d08bb9a
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Controllers/FromRouteAttribute_CompanyController.cs
@@ -0,0 +1,41 @@
+// 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.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Microsoft.AspNet.Mvc;
+
+namespace ModelBindingWebSite.Controllers
+{
+ [Route("FromRouteAttribute_Company/[action]/{customPrefix.Name}")]
+ public class FromRouteAttribute_CompanyController : Controller
+ {
+ [HttpGet("{customPrefix.EmployeeTaxId?}")]
+ public Employee CreateEmployee([FromRoute(Name = "customPrefix")] Employee employee)
+ {
+ return employee;
+ }
+
+ public Company CreateCompanyFromEmployees([FromRoute(Name = "customPrefix")] IList employees)
+ {
+ return new Company { Employees = employees };
+ }
+
+ public object ValidateDepartment(FromRoute_Department department)
+ {
+ return new Result()
+ {
+ Value = department.Employees,
+ ModelStateErrors = ModelState.Where(kvp => kvp.Value.Errors.Count > 0).Select(kvp => kvp.Key).ToArray(),
+ };
+ }
+
+ public class FromRoute_Department
+ {
+ [FromRoute(Name = "TestEmployees")]
+ [Required]
+ public IEnumerable Employees { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_CompanyController.cs b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_CompanyController.cs
index 0c022002cc..e19679275a 100644
--- a/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_CompanyController.cs
+++ b/test/WebSites/ModelBindingWebSite/Controllers/ModelBinderAttribute_CompanyController.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.Collections.Generic;
using Microsoft.AspNet.Mvc;
namespace ModelBindingWebSite.Controllers
@@ -13,5 +14,10 @@ namespace ModelBindingWebSite.Controllers
{
return company;
}
+
+ public Company CreateCompany(IList employees)
+ {
+ return new Company { Employees = employees };
+ }
}
}
\ No newline at end of file
diff --git a/test/WebSites/ModelBindingWebSite/Models/Employee.cs b/test/WebSites/ModelBindingWebSite/Models/Employee.cs
index 8da623a504..f211ab0410 100644
--- a/test/WebSites/ModelBindingWebSite/Models/Employee.cs
+++ b/test/WebSites/ModelBindingWebSite/Models/Employee.cs
@@ -2,6 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNet.Mvc;
+
namespace ModelBindingWebSite
{
public class Employee : Person
@@ -9,5 +12,18 @@ namespace ModelBindingWebSite
public string Department { get; set; }
public string Location { get; set; }
+
+ [FromQuery(Name = "EmployeeId")]
+ [Range(1, 10000)]
+ public int Id { get; set; }
+
+ [FromRoute(Name = "EmployeeTaxId")]
+ public int TaxId { get; set; }
+
+ [FromForm(Name = "EmployeeSSN")]
+ public string SSN { get; set; }
+
+ [ModelBinder(Name = "Alias")]
+ public string EmailAlias { get; set; }
}
}
\ No newline at end of file
diff --git a/test/WebSites/ModelBindingWebSite/Result.cs b/test/WebSites/ModelBindingWebSite/Result.cs
new file mode 100644
index 0000000000..a5d28a03e0
--- /dev/null
+++ b/test/WebSites/ModelBindingWebSite/Result.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace ModelBindingWebSite
+{
+ public class Result
+ {
+ public object Value { get; set; }
+
+ public string[] ModelStateErrors { get; set; }
+ }
+}
\ No newline at end of file