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