From 088bb18eed498d1b2df96bee52ffbaf744254f61 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Mon, 9 Feb 2015 15:16:05 -0800 Subject: [PATCH] Added support for binding FormCollection - Added FormCollectionModelBinder - Added relevant unit and functional tests --- .../MvcSample.Web/Views/Shared/MyView.cshtml | 1 - .../Binders/FormCollectionModelBinder.cs | 50 ++++++ .../project.json | 2 +- src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 1 + .../ModelBindingTest.cs | 91 +++++++++++ .../Binders/FormCollectionModelBinderTest.cs | 148 ++++++++++++++++++ .../MvcOptionsSetupTest.cs | 3 +- .../Controllers/FormCollectionController.cs | 40 +++++ 8 files changed, 333 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormCollectionModelBinder.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormCollectionModelBinderTest.cs create mode 100644 test/WebSites/ModelBindingWebSite/Controllers/FormCollectionController.cs diff --git a/samples/MvcSample.Web/Views/Shared/MyView.cshtml b/samples/MvcSample.Web/Views/Shared/MyView.cshtml index 16e4c3f574..0e2bc5ea94 100644 --- a/samples/MvcSample.Web/Views/Shared/MyView.cshtml +++ b/samples/MvcSample.Web/Views/Shared/MyView.cshtml @@ -56,7 +56,6 @@ -
diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormCollectionModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormCollectionModelBinder.cs new file mode 100644 index 0000000000..50a2407e28 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormCollectionModelBinder.cs @@ -0,0 +1,50 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core.Collections; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Modelbinder to bind form values to . + /// + public class FormCollectionModelBinder : IModelBinder + { + /// + public async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) + { + if (bindingContext.ModelType != typeof(IFormCollection) && + bindingContext.ModelType != typeof(FormCollection)) + { + return false; + } + + var request = bindingContext.OperationBindingContext.HttpContext.Request; + if (request.HasFormContentType) + { + var form = await request.ReadFormAsync(); + if (bindingContext.ModelType.IsAssignableFrom(form.GetType())) + { + bindingContext.Model = form; + } + else + { + var formValuesLookup = form.ToDictionary(p => p.Key, + p => p.Value); + bindingContext.Model = new FormCollection(formValuesLookup, form.Files); + } + } + else + { + bindingContext.Model = new FormCollection(new Dictionary()); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index afa5fd79b1..db27165255 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -5,7 +5,7 @@ "warningsAsErrors": true }, "dependencies": { - "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.AspNet.Http.Core": "1.0.0-*", "Microsoft.AspNet.Http.Extensions": "1.0.0-*", "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, "Microsoft.Framework.DependencyInjection": "1.0.0-*", diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index fdba761395..b1dd8eb837 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -37,6 +37,7 @@ namespace Microsoft.AspNet.Mvc options.ModelBinders.Add(new CancellationTokenModelBinder()); options.ModelBinders.Add(new ByteArrayModelBinder()); options.ModelBinders.Add(new FormFileModelBinder()); + options.ModelBinders.Add(new FormCollectionModelBinder()); options.ModelBinders.Add(typeof(GenericModelBinder)); options.ModelBinders.Add(new MutableObjectModelBinder()); options.ModelBinders.Add(new ComplexModelDtoModelBinder()); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs index 1da95943f7..abe6e66c3d 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTest.cs @@ -11,6 +11,8 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core.Collections; using Microsoft.AspNet.TestHost; using ModelBindingWebSite; using ModelBindingWebSite.ViewModels; @@ -1530,6 +1532,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(expectedContent, body); } + [Fact] public async Task ModelBinder_FormatsDontMatch_ThrowsUserFriendlyException() { // Arrange @@ -1601,5 +1604,93 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var dictionary = JsonConvert.DeserializeObject>(responseContent); Assert.Equal(expectedDictionary, dictionary); } + + [Fact] + public async Task FormCollectionModelBinder_CanBind_FormValues() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FormCollection/ReturnValuesAsList"; + var nameValueCollection = new List> + { + new KeyValuePair("field1", "value1"), + new KeyValuePair("field2", "value2"), + }; + var formData = new FormUrlEncodedContent(nameValueCollection); + + // Act + var response = await client.PostAsync(url, formData); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var valuesList = JsonConvert.DeserializeObject>( + await response.Content.ReadAsStringAsync()); + Assert.Equal(new List { "value1", "value2" }, valuesList); + } + + [Fact] + public async Task FormCollectionModelBinder_CanBind_FormValuesWithDuplicateKeys() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FormCollection/ReturnValuesAsList"; + var nameValueCollection = new List> + { + new KeyValuePair("field1", "value1"), + new KeyValuePair("field2", "value2"), + new KeyValuePair("field1", "value3"), + }; + var formData = new FormUrlEncodedContent(nameValueCollection); + + // Act + var response = await client.PostAsync(url, formData); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var valuesList = JsonConvert.DeserializeObject>( + await response.Content.ReadAsStringAsync()); + Assert.Equal(new List { "value1,value3", "value2" }, valuesList); + } + + [Fact] + public async Task FormCollectionModelBinder_CannotBind_NonFormValues() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FormCollection/ReturnCollectionCount"; + var data = new StringContent("Non form content"); + + // Act + var response = await client.PostAsync(url, data); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var collectionCount = JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync()); + Assert.Equal(0, collectionCount); + } + + [Fact] + public async Task FormCollectionModelBinder_CanBind_FormWithFile() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FormCollection/ReturnFileContent"; + var expectedContent = "Test Content"; + var formData = new MultipartFormDataContent("Upload----"); + formData.Add(new StringContent(expectedContent), "File", "test.txt"); + + // Act + var response = await client.PostAsync(url, formData); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var fileContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, fileContent); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormCollectionModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormCollectionModelBinderTest.cs new file mode 100644 index 0000000000..e00e99d263 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormCollectionModelBinderTest.cs @@ -0,0 +1,148 @@ +// 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. + +#if ASPNET50 + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core.Collections; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class FormCollectionModelBinderTest + { + [Fact] + public async Task FormCollectionModelBinder_ValidType_BindSuccessful() + { + // Arrange + var formCollection = new FormCollection(new Dictionary + { + { "field1", new string[] { "value1" } }, + { "field2", new string[] { "value2" } } + }); + var httpContext = GetMockHttpContext(formCollection); + var bindingContext = GetBindingContext(typeof(FormCollection), httpContext); + var binder = new FormCollectionModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + var form = Assert.IsAssignableFrom(bindingContext.Model); + Assert.Equal(2, form.Count); + Assert.Equal("value1", form["field1"]); + Assert.Equal("value2", form["field2"]); + } + + [Fact] + public async Task FormCollectionModelBinder_InvalidType_BindFails() + { + // Arrange + var formCollection = new FormCollection(new Dictionary + { + { "field1", new string[] { "value1" } }, + { "field2", new string[] { "value2" } } + }); + var httpContext = GetMockHttpContext(formCollection); + var bindingContext = GetBindingContext(typeof(string), httpContext); + var binder = new FormCollectionModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.False(result); + } + + [Fact] + public async Task FormCollectionModelBinder_NoForm_BindSuccessful_ReturnsEmptyFormCollection() + { + // Arrange + var httpContext = GetMockHttpContext(null, hasForm: false); + var bindingContext = GetBindingContext(typeof(IFormCollection), httpContext); + var binder = new FormCollectionModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + Assert.IsType(typeof(FormCollection), bindingContext.Model); + Assert.Empty((FormCollection)bindingContext.Model); + } + + [Fact] + public async Task FormCollectionModelBinder_CustomFormCollection_BindSuccessful() + { + // Arrange + var formCollection = new MyFormCollection(new Dictionary + { + { "field1", new string[] { "value1" } }, + { "field2", new string[] { "value2" } } + }); + var httpContext = GetMockHttpContext(formCollection); + var bindingContext = GetBindingContext(typeof(FormCollection), httpContext); + var binder = new FormCollectionModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + var form = Assert.IsAssignableFrom(bindingContext.Model); + Assert.Equal(2, form.Count); + Assert.Equal("value1", form["field1"]); + Assert.Equal("value2", form["field2"]); + } + + private static HttpContext GetMockHttpContext(IFormCollection formCollection, bool hasForm = true) + { + var httpContext = new Mock(); + httpContext.Setup(h => h.Request.ReadFormAsync(It.IsAny())) + .Returns(Task.FromResult(formCollection)); + httpContext.Setup(h => h.Request.HasFormContentType).Returns(hasForm); + return httpContext.Object; + } + + private static ModelBindingContext GetBindingContext(Type modelType, HttpContext httpContext) + { + var metadataProvider = new EmptyModelMetadataProvider(); + var bindingContext = new ModelBindingContext + { + ModelMetadata = metadataProvider.GetMetadataForType(null, modelType), + ModelName = "file", + OperationBindingContext = new OperationBindingContext + { + ModelBinder = new FormCollectionModelBinder(), + MetadataProvider = metadataProvider, + HttpContext = httpContext, + } + }; + + return bindingContext; + } + + private class MyFormCollection : ReadableStringCollection, IFormCollection + { + public MyFormCollection(IDictionary store) : this(store, new FormFileCollection()) + { + } + + public MyFormCollection(IDictionary store, IFormFileCollection files) : base(store) + { + Files = files; + } + + public IFormFileCollection Files { get; private set; } + } + } +} + +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs index 07f420c24d..0c89c2243f 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionsSetupTest.cs @@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc // Assert var i = 0; - Assert.Equal(12, mvcOptions.ModelBinders.Count); + Assert.Equal(13, mvcOptions.ModelBinders.Count); Assert.Equal(typeof(BinderTypeBasedModelBinder), mvcOptions.ModelBinders[i++].OptionType); Assert.Equal(typeof(ServicesModelBinder), mvcOptions.ModelBinders[i++].OptionType); Assert.Equal(typeof(BodyModelBinder), mvcOptions.ModelBinders[i++].OptionType); @@ -49,6 +49,7 @@ namespace Microsoft.AspNet.Mvc Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[i++].OptionType); Assert.Equal(typeof(ByteArrayModelBinder), mvcOptions.ModelBinders[i++].OptionType); Assert.Equal(typeof(FormFileModelBinder), mvcOptions.ModelBinders[i++].OptionType); + Assert.Equal(typeof(FormCollectionModelBinder), mvcOptions.ModelBinders[i++].OptionType); Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[i++].OptionType); Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[i++].OptionType); Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[i++].OptionType); diff --git a/test/WebSites/ModelBindingWebSite/Controllers/FormCollectionController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FormCollectionController.cs new file mode 100644 index 0000000000..f616d4174a --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Controllers/FormCollectionController.cs @@ -0,0 +1,40 @@ +// 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.IO; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core.Collections; +using Microsoft.AspNet.Mvc; + +namespace ModelBindingWebSite.Controllers +{ + public class FormCollectionController : Controller + { + public IList ReturnValuesAsList(IFormCollection form) + { + var valuesList = new List(); + + valuesList.Add(form["field1"]); + valuesList.Add(form["field2"]); + + return valuesList; + } + + public int ReturnCollectionCount(IFormCollection form) + { + return form.Count; + } + + public ActionResult ReturnFileContent(FormCollection form) + { + var file = form.Files.GetFile("File"); + using (var reader = new StreamReader(file.OpenReadStream())) + { + var fileContent = reader.ReadToEnd(); + + return Content(fileContent); + } + } + } +}