From 437eb93bdec0d9238d672711ebd7bd3097b6537d Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Mon, 8 Dec 2014 12:56:46 -0800 Subject: [PATCH] File upload model binder - Support for binding posted file to type IFormFile - Support for multipart/form-data in FormValueProviderFactory - Updated Mvc Sample - Added relevant unit and functional tests --- samples/MvcSample.Web/HomeController.cs | 23 +++ .../MvcSample.Web/Views/Home/PostFile.cshtml | 11 ++ .../MvcSample.Web/Views/Shared/MyView.cshtml | 7 + .../Binders/FormFileModelBinder.cs | 81 +++++++++ .../Binders/HeaderModelBinder.cs | 41 +---- .../Internal/ModelBindingHelper.cs | 44 +++++ .../FormValueProviderFactory.cs | 14 +- .../ReadableStringCollectionValueProvider.cs | 11 +- .../project.json | 1 + src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs | 1 + .../ModelBindingTests.cs | 107 ++++++++++- .../Binders/FormFileModelBinderTest.cs | 171 ++++++++++++++++++ .../FormValueProviderFactoryTests.cs | 6 +- .../MvcOptionSetupTest.cs | 3 +- .../Controllers/FileUploadController.cs | 95 ++++++++++ .../ModelBindingWebSite/Models/Book.cs | 14 ++ .../ModelBindingWebSite/Models/FileDetails.cs | 12 ++ 17 files changed, 587 insertions(+), 55 deletions(-) create mode 100644 samples/MvcSample.Web/Views/Home/PostFile.cshtml create mode 100644 src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormFileModelBinder.cs create mode 100644 test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormFileModelBinderTest.cs create mode 100644 test/WebSites/ModelBindingWebSite/Controllers/FileUploadController.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/Book.cs create mode 100644 test/WebSites/ModelBindingWebSite/Models/FileDetails.cs diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs index d95dc7ad49..9389183c64 100644 --- a/samples/MvcSample.Web/HomeController.cs +++ b/samples/MvcSample.Web/HomeController.cs @@ -2,7 +2,10 @@ // 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 System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.Rendering; @@ -89,6 +92,26 @@ namespace MvcSample.Web return View("MyView", user); } + [Activate] + public IHostingEnvironment HostingEnvironment { get; set; } + + /// + /// Action that shows multiple file upload. + /// + public async Task PostFile(IList files) + { + if (!ModelState.IsValid) + { + return View("MyView"); + } + + foreach (var f in files) + { + await f.SaveAsAsync(Path.Combine(HostingEnvironment.WebRoot, "test-file" + files.IndexOf(f))); + } + return View(); + } + /// /// Action that exercises input formatter /// diff --git a/samples/MvcSample.Web/Views/Home/PostFile.cshtml b/samples/MvcSample.Web/Views/Home/PostFile.cshtml new file mode 100644 index 0000000000..f2398f19d4 --- /dev/null +++ b/samples/MvcSample.Web/Views/Home/PostFile.cshtml @@ -0,0 +1,11 @@ + + + + + + File Upload Successful + + +

File upload successful.

+ + \ No newline at end of file diff --git a/samples/MvcSample.Web/Views/Shared/MyView.cshtml b/samples/MvcSample.Web/Views/Shared/MyView.cshtml index 2a6574693d..16e4c3f574 100644 --- a/samples/MvcSample.Web/Views/Shared/MyView.cshtml +++ b/samples/MvcSample.Web/Views/Shared/MyView.cshtml @@ -51,6 +51,13 @@

ASP.NET

ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.

Learn more »

+ File Upload Demo:
+
+ + + + +

Hello @Html.DisplayTextFor(User => User)! Happy @(Model?.Age) birthday.

diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormFileModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormFileModelBinder.cs new file mode 100644 index 0000000000..79abd05f59 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/FormFileModelBinder.cs @@ -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 System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.ModelBinding.Internal; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + /// + /// Modelbinder to bind posted files to . + /// + public class FormFileModelBinder : IModelBinder + { + /// + public async Task BindModelAsync([NotNull] ModelBindingContext bindingContext) + { + if (bindingContext.ModelType == typeof(IFormFile)) + { + var postedFiles = await GetFormFilesAsync(bindingContext); + var value = postedFiles.FirstOrDefault(); + if (value != null) + { + bindingContext.Model = value; + } + + return true; + } + else if (typeof(IEnumerable).GetTypeInfo().IsAssignableFrom( + bindingContext.ModelType.GetTypeInfo())) + { + var postedFiles = await GetFormFilesAsync(bindingContext); + var value = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, postedFiles); + if (value != null) + { + bindingContext.Model = value; + } + + return true; + } + + return false; + } + + private async Task> GetFormFilesAsync(ModelBindingContext bindingContext) + { + var request = bindingContext.OperationBindingContext.HttpContext.Request; + var postedFiles = new List(); + if (request.HasFormContentType) + { + var form = await request.ReadFormAsync(); + + foreach (var file in form.Files) + { + ContentDispositionHeaderValue parsedContentDisposition; + ContentDispositionHeaderValue.TryParse(file.ContentDisposition, out parsedContentDisposition); + + // If there is an in the form and is left blank. + if (parsedContentDisposition == null || + (file.Length == 0 && string.IsNullOrEmpty(parsedContentDisposition.FileName))) + { + continue; + } + + var modelName = parsedContentDisposition.Name; + if (modelName.Equals(bindingContext.ModelName, StringComparison.OrdinalIgnoreCase)) + { + postedFiles.Add(file); + } + } + } + + return postedFiles; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs index 7c1ec4c745..c63d224f92 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Binders/HeaderModelBinder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding.Internal; namespace Microsoft.AspNet.Mvc.ModelBinding { @@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var values = request.Headers.GetCommaSeparatedValues(bindingContext.ModelName); if (values != null) { - bindingContext.Model = ConvertValuesToCollectionType(bindingContext.ModelType, values); + bindingContext.Model = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, values); } return Task.FromResult(true); @@ -39,43 +40,5 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return Task.FromResult(false); } - - private object ConvertValuesToCollectionType(Type modelType, IList values) - { - // There's a limited set of collection types we can support here. - // - // For the simple cases - choose a string[] or List if the destination type supports - // it. - // - // For more complex cases, if the destination type is a class and implements ICollection - // then activate it and add the values. - // - // Otherwise just give up. - if (typeof(List).IsAssignableFrom(modelType)) - { - return new List(values); - } - else if (typeof(string[]).IsAssignableFrom(modelType)) - { - return values.ToArray(); - } - else if ( - modelType.GetTypeInfo().IsClass && - !modelType.GetTypeInfo().IsAbstract && - typeof(ICollection).IsAssignableFrom(modelType)) - { - var result = (ICollection)Activator.CreateInstance(modelType); - foreach (var value in values) - { - result.Add(value); - } - - return result; - } - else - { - return null; - } - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs index 4f62884548..8ef8510508 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Internal/ModelBindingHelper.cs @@ -2,7 +2,9 @@ // 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.Globalization; +using System.Linq; using System.Reflection; namespace Microsoft.AspNet.Mvc.ModelBinding.Internal @@ -117,5 +119,47 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal } return false; } + + public static object ConvertValuesToCollectionType(Type modelType, IList values) + { + // There's a limited set of collection types we can support here. + // + // For the simple cases - choose a T[] or List if the destination type supports + // it. + // + // For more complex cases, if the destination type is a class and implements ICollection + // then activate it and add the values. + // + // Otherwise just give up. + if (typeof(List).IsAssignableFrom(modelType)) + { + return new List(values); + } + else if (typeof(T[]).IsAssignableFrom(modelType)) + { + return values.ToArray(); + } + else if ( + modelType.GetTypeInfo().IsClass && + !modelType.GetTypeInfo().IsAbstract && + typeof(ICollection).IsAssignableFrom(modelType)) + { + var result = (ICollection)Activator.CreateInstance(modelType); + foreach (var value in values) + { + result.Add(value); + } + + return result; + } + else if (typeof(IEnumerable).IsAssignableFrom(modelType)) + { + return values; + } + else + { + return null; + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs index de6ed9030f..84212ec7ad 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/FormValueProviderFactory.cs @@ -3,22 +3,19 @@ using System.Globalization; using Microsoft.AspNet.Http; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNet.Mvc.ModelBinding { public class FormValueProviderFactory : IValueProviderFactory { - private static MediaTypeHeaderValue _formEncodedContentType = - MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded"); - public IValueProvider GetValueProvider([NotNull] ValueProviderFactoryContext context) { var request = context.HttpContext.Request; - if (IsSupportedContentType(request)) + if (request.HasFormContentType) { var culture = GetCultureInfo(request); + return new ReadableStringCollectionValueProvider( async () => await request.ReadFormAsync(), culture); @@ -27,13 +24,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return null; } - private bool IsSupportedContentType(HttpRequest request) - { - MediaTypeHeaderValue requestContentType = null; - return MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType) && - _formEncodedContentType.IsSubsetOf(requestContentType); - } - private static CultureInfo GetCultureInfo(HttpRequest request) { return CultureInfo.CurrentCulture; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs index a6fbd97016..c6e6136120 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ValueProviders/ReadableStringCollectionValueProvider.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private IReadableStringCollection _values; /// - /// Creates a NameValuePairsProvider wrapping an existing set of key value pairs. + /// Creates a provider for wrapping an existing set of key value pairs. /// /// The key value pairs to wrap. /// The culture to return with ValueProviderResult instances. @@ -31,6 +31,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding _culture = culture; } + /// + /// Creates a provider for wrapping an + /// existing set of key value pairs provided by the delegate. + /// + /// The delegate that provides the key value pairs to wrap. + /// The culture to return with ValueProviderResult instances. public ReadableStringCollectionValueProvider([NotNull] Func> valuesFactory, CultureInfo culture) { @@ -46,18 +52,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + /// public override async Task ContainsPrefixAsync(string prefix) { var prefixContainer = await GetPrefixContainerAsync(); return prefixContainer.ContainsPrefix(prefix); } + /// public virtual async Task> GetKeysFromPrefixAsync([NotNull] string prefix) { var prefixContainer = await GetPrefixContainerAsync(); return prefixContainer.GetKeysFromPrefix(prefix); } + /// public override async Task GetValueAsync([NotNull] string key) { var collection = await GetValueCollectionAsync(); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json index c5a97faff7..0e5d48452a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/project.json +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/project.json @@ -6,6 +6,7 @@ }, "dependencies": { "Microsoft.AspNet.Http": "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-*", "Microsoft.Net.Http.Headers": "1.0.0-*", diff --git a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs index e9e1b1bf6e..886870402e 100644 --- a/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc/MvcOptionsSetup.cs @@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Mvc options.ModelBinders.Add(new TypeMatchModelBinder()); options.ModelBinders.Add(new CancellationTokenModelBinder()); options.ModelBinders.Add(new ByteArrayModelBinder()); + options.ModelBinders.Add(new FormFileModelBinder()); options.ModelBinders.Add(typeof(GenericModelBinder)); options.ModelBinders.Add(new MutableObjectModelBinder()); options.ModelBinders.Add(new ComplexModelDtoModelBinder()); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs index 5842d66890..c66f456d94 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ModelBindingTests.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Linq.Expressions; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -1327,5 +1326,111 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await response.Content.ReadAsStringAsync(); Assert.Equal(expectedContent, body); } + + [Fact] + public async Task FormFileModelBinder_CanBind_SingleFile() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FileUpload/UploadSingle"; + var formData = new MultipartFormDataContent("Upload----"); + formData.Add(new StringContent("Test Content"), "file", "test.txt"); + + // Act + var response = await client.PostAsync(url, formData); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var fileDetails = JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync()); + Assert.Equal("test.txt", fileDetails.Filename); + Assert.Equal("Test Content", fileDetails.Content); + } + + [Fact] + public async Task FormFileModelBinder_CanBind_MultipleFiles() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FileUpload/UploadMultiple"; + var formData = new MultipartFormDataContent("Upload----"); + formData.Add(new StringContent("Test Content 1"), "files", "test1.txt"); + formData.Add(new StringContent("Test Content 2"), "files", "test2.txt"); + + // Act + var response = await client.PostAsync(url, formData); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var fileDetailsArray = JsonConvert.DeserializeObject( + await response.Content.ReadAsStringAsync()); + Assert.Equal(2, fileDetailsArray.Length); + Assert.Equal("test1.txt", fileDetailsArray[0].Filename); + Assert.Equal("Test Content 1", fileDetailsArray[0].Content); + Assert.Equal("test2.txt", fileDetailsArray[1].Filename); + Assert.Equal("Test Content 2", fileDetailsArray[1].Content); + } + + [Fact] + public async Task FormFileModelBinder_CanBind_MultipleListOfFiles() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FileUpload/UploadMultipleList"; + var formData = new MultipartFormDataContent("Upload----"); + formData.Add(new StringContent("Test Content 1"), "filelist1", "test1.txt"); + formData.Add(new StringContent("Test Content 2"), "filelist1", "test2.txt"); + formData.Add(new StringContent("Test Content 3"), "filelist2", "test3.txt"); + formData.Add(new StringContent("Test Content 4"), "filelist2", "test4.txt"); + + // Act + var response = await client.PostAsync(url, formData); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var fileDetailsLookup = JsonConvert.DeserializeObject>>( + await response.Content.ReadAsStringAsync()); + Assert.Equal(2, fileDetailsLookup.Count); + var fileDetailsList1 = fileDetailsLookup["filelist1"]; + var fileDetailsList2 = fileDetailsLookup["filelist2"]; + Assert.Equal(2, fileDetailsList1.Count); + Assert.Equal(2, fileDetailsList2.Count); + Assert.Equal("test1.txt", fileDetailsList1[0].Filename); + Assert.Equal("Test Content 1", fileDetailsList1[0].Content); + Assert.Equal("test2.txt", fileDetailsList1[1].Filename); + Assert.Equal("Test Content 2", fileDetailsList1[1].Content); + Assert.Equal("test3.txt", fileDetailsList2[0].Filename); + Assert.Equal("Test Content 3", fileDetailsList2[0].Content); + Assert.Equal("test4.txt", fileDetailsList2[1].Filename); + Assert.Equal("Test Content 4", fileDetailsList2[1].Content); + } + + [Fact] + public async Task FormFileModelBinder_CanBind_FileInsideModel() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var url = "http://localhost/FileUpload/UploadModelWithFile"; + var formData = new MultipartFormDataContent("Upload----"); + formData.Add(new StringContent("Test Book"), "Name"); + formData.Add(new StringContent("Test Content"), "File", "test.txt"); + + // Act + var response = await client.PostAsync(url, formData); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var book = JsonConvert.DeserializeObject>( + await response.Content.ReadAsStringAsync()); + var bookName = book.Key; + var fileDetails = book.Value; + Assert.Equal("Test Book", bookName); + Assert.Equal("test.txt", fileDetails.Filename); + Assert.Equal("Test Content", fileDetails.Content); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormFileModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormFileModelBinderTest.cs new file mode 100644 index 0000000000..ecc2c23531 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Binders/FormFileModelBinderTest.cs @@ -0,0 +1,171 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.PipelineCore.Collections; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class FormFileModelBinderTest + { + [Fact] + public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful() + { + // Arrange + var formFiles = new FormFileCollection(); + formFiles.Add(GetMockFormFile("file", "file1.txt")); + formFiles.Add(GetMockFormFile("file", "file2.txt")); + var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); + var bindingContext = GetBindingContext(typeof(IEnumerable), httpContext); + var binder = new FormFileModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + var files = Assert.IsAssignableFrom>(bindingContext.Model); + Assert.Equal(2, files.Count); + } + + [Fact] + public async Task FormFileModelBinder_ExpectSingleFile_BindFirstFile() + { + // Arrange + var formFiles = new FormFileCollection(); + formFiles.Add(GetMockFormFile("file", "file1.txt")); + formFiles.Add(GetMockFormFile("file", "file2.txt")); + var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); + var bindingContext = GetBindingContext(typeof(IFormFile), httpContext); + var binder = new FormFileModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + var file = Assert.IsAssignableFrom(bindingContext.Model); + Assert.Equal("form-data; name=file; filename=file1.txt", + file.ContentDisposition); + } + + [Fact] + public async Task FormFileModelBinder_ReturnsNull_WhenNoFilePosted() + { + // Arrange + var formFiles = new FormFileCollection(); + var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); + var bindingContext = GetBindingContext(typeof(IFormFile), httpContext); + var binder = new FormFileModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + Assert.Null(bindingContext.Model); + } + + [Fact] + public async Task FormFileModelBinder_ReturnsNull_WhenNamesDontMatch() + { + // Arrange + var formFiles = new FormFileCollection(); + formFiles.Add(GetMockFormFile("different name", "file1.txt")); + var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); + var bindingContext = GetBindingContext(typeof(IFormFile), httpContext); + var binder = new FormFileModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + Assert.Null(bindingContext.Model); + } + + [Fact] + public async Task FormFileModelBinder_ReturnsNull_WithEmptyContentDisposition() + { + // Arrange + var formFiles = new FormFileCollection(); + formFiles.Add(new Mock().Object); + var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); + var bindingContext = GetBindingContext(typeof(IFormFile), httpContext); + var binder = new FormFileModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + Assert.Null(bindingContext.Model); + } + + [Fact] + public async Task FormFileModelBinder_ReturnsNull_WithNoFileNameAndZeroLength() + { + // Arrange + var formFiles = new FormFileCollection(); + formFiles.Add(GetMockFormFile("file", "")); + var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles)); + var bindingContext = GetBindingContext(typeof(IFormFile), httpContext); + var binder = new FormFileModelBinder(); + + // Act + var result = await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(result); + Assert.Null(bindingContext.Model); + } + + 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 FormFileModelBinder(), + MetadataProvider = metadataProvider, + HttpContext = httpContext, + } + }; + + return bindingContext; + } + + private static HttpContext GetMockHttpContext(IFormCollection formCollection) + { + var httpContext = new Mock(); + httpContext.Setup(h => h.Request.ReadFormAsync(It.IsAny())) + .Returns(Task.FromResult(formCollection)); + httpContext.Setup(h => h.Request.HasFormContentType).Returns(true); + return httpContext.Object; + } + + private static IFormCollection GetMockFormCollection(FormFileCollection formFiles) + { + var formCollection = new Mock(); + formCollection.Setup(f => f.Files).Returns(formFiles); + return formCollection.Object; + } + + private static IFormFile GetMockFormFile(string modelName, string filename) + { + var formFile = new Mock(); + formFile.Setup(f => f.ContentDisposition) + .Returns(string.Format("form-data; name={0}; filename={1}", modelName, filename)); + return formFile.Object; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs index a28d096ced..7e9ad99627 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/ValueProviders/FormValueProviderFactoryTests.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.PipelineCore; using Moq; using Xunit; @@ -32,7 +33,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test [Theory] [InlineData("application/x-www-form-urlencoded")] [InlineData("application/x-www-form-urlencoded;charset=utf-8")] - public void GetValueProvider_ReturnsValueProviderInstaceWithInvariantCulture(string contentType) + [InlineData("multipart/form-data")] + [InlineData("multipart/form-data;charset=utf-8")] + public void GetValueProvider_ReturnsValueProviderInstanceWithInvariantCulture(string contentType) { // Arrange var context = CreateContext(contentType); @@ -52,6 +55,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test var request = new Mock(); request.Setup(f => f.ReadFormAsync(CancellationToken.None)).Returns(Task.FromResult(collection)); request.SetupGet(r => r.ContentType).Returns(contentType); + request.SetupGet(r => r.HasFormContentType).Returns(new FormFeature(request.Object).HasFormContentType); var context = new Mock(); context.SetupGet(c => c.Request).Returns(request.Object); diff --git a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs index 29a7d58c29..f57b928e44 100644 --- a/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs +++ b/test/Microsoft.AspNet.Mvc.Test/MvcOptionSetupTest.cs @@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc // Assert var i = 0; - Assert.Equal(11, mvcOptions.ModelBinders.Count); + Assert.Equal(12, 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); @@ -48,6 +48,7 @@ namespace Microsoft.AspNet.Mvc Assert.Equal(typeof(TypeMatchModelBinder), mvcOptions.ModelBinders[i++].OptionType); 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(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/FileUploadController.cs b/test/WebSites/ModelBindingWebSite/Controllers/FileUploadController.cs new file mode 100644 index 0000000000..8a6b5183fd --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Controllers/FileUploadController.cs @@ -0,0 +1,95 @@ +// 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 System.Linq; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Http; +using Microsoft.Net.Http.Headers; + +namespace ModelBindingWebSite.Controllers +{ + public class FileUploadController : Controller + { + public FileDetails UploadSingle(IFormFile file) + { + FileDetails fileDetails; + using (var reader = new StreamReader(file.OpenReadStream())) + { + var fileContent = reader.ReadToEnd(); + var parsedContentDisposition = ContentDispositionHeaderValue.Parse(file.ContentDisposition); + fileDetails = new FileDetails + { + Filename = parsedContentDisposition.FileName, + Content = fileContent + }; + } + + return fileDetails; + } + + public FileDetails[] UploadMultiple(IEnumerable files) + { + var fileDetailsList = new List(); + foreach (var file in files) + { + var parsedContentDisposition = ContentDispositionHeaderValue.Parse(file.ContentDisposition); + using (var reader = new StreamReader(file.OpenReadStream())) + { + var fileContent = reader.ReadToEnd(); + var fileDetails = new FileDetails + { + Filename = parsedContentDisposition.FileName, + Content = fileContent + }; + fileDetailsList.Add(fileDetails); + } + } + + return fileDetailsList.ToArray(); + } + + public IDictionary> UploadMultipleList(IEnumerable filelist1, + IEnumerable filelist2) + { + var fileDetailsDict = new Dictionary> + { + { "filelist1", new List() }, + { "filelist2", new List() } + }; + var fileDetailsList = new List(); + foreach (var file in filelist1.Concat(filelist2)) + { + var parsedContentDisposition = ContentDispositionHeaderValue.Parse(file.ContentDisposition); + using (var reader = new StreamReader(file.OpenReadStream())) + { + var fileContent = reader.ReadToEnd(); + var fileDetails = new FileDetails + { + Filename = parsedContentDisposition.FileName, + Content = fileContent + }; + fileDetailsDict[parsedContentDisposition.Name].Add(fileDetails); + } + } + + return fileDetailsDict; + } + + public KeyValuePair UploadModelWithFile(Book book) + { + var file = book.File; + var reader = new StreamReader(file.OpenReadStream()); + var fileContent = reader.ReadToEnd(); + var parsedContentDisposition = ContentDispositionHeaderValue.Parse(file.ContentDisposition); + var fileDetails = new FileDetails + { + Filename = parsedContentDisposition.FileName, + Content = fileContent + }; + + return new KeyValuePair(book.Name, fileDetails); + } + } +} diff --git a/test/WebSites/ModelBindingWebSite/Models/Book.cs b/test/WebSites/ModelBindingWebSite/Models/Book.cs new file mode 100644 index 0000000000..9877796e7c --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/Book.cs @@ -0,0 +1,14 @@ +// 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; + +namespace ModelBindingWebSite +{ + public class Book + { + public string Name { get; set; } + + public IFormFile File { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/ModelBindingWebSite/Models/FileDetails.cs b/test/WebSites/ModelBindingWebSite/Models/FileDetails.cs new file mode 100644 index 0000000000..8ac20fe800 --- /dev/null +++ b/test/WebSites/ModelBindingWebSite/Models/FileDetails.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 FileDetails + { + public string Filename { get; set; } + + public string Content { get; set; } + } +} \ No newline at end of file