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
This commit is contained in:
parent
6a824a4394
commit
437eb93bde
|
|
@ -2,7 +2,10 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Hosting;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.AspNet.Mvc;
|
using Microsoft.AspNet.Mvc;
|
||||||
using Microsoft.AspNet.Mvc.Rendering;
|
using Microsoft.AspNet.Mvc.Rendering;
|
||||||
|
|
@ -89,6 +92,26 @@ namespace MvcSample.Web
|
||||||
return View("MyView", user);
|
return View("MyView", user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Activate]
|
||||||
|
public IHostingEnvironment HostingEnvironment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Action that shows multiple file upload.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<ActionResult> PostFile(IList<IFormFile> 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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Action that exercises input formatter
|
/// Action that exercises input formatter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>File Upload Successful</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>File upload successful.</h2>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -51,6 +51,13 @@
|
||||||
<h1>ASP.NET</h1>
|
<h1>ASP.NET</h1>
|
||||||
<p class="lead">ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.</p>
|
<p class="lead">ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.</p>
|
||||||
<p><a href="http://asp.net" class="btn btn-primary btn-large">Learn more »</a></p>
|
<p><a href="http://asp.net" class="btn btn-primary btn-large">Learn more »</a></p>
|
||||||
|
File Upload Demo: <br/>
|
||||||
|
<form action="Home/PostFile" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="files" id="file1" />
|
||||||
|
<input type="file" name="files" id="file2" />
|
||||||
|
<input type="submit" value="submit" />
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h3 title="@(Model?.Name)" class="@nullValue">Hello @Html.DisplayTextFor(User => User)! Happy @(Model?.Age) birthday.</h3>
|
<h3 title="@(Model?.Name)" class="@nullValue">Hello @Html.DisplayTextFor(User => User)! Happy @(Model?.Age) birthday.</h3>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Modelbinder to bind posted files to <see cref="IFormFile"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class FormFileModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> 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<IFormFile>).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<List<IFormFile>> GetFormFilesAsync(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
var request = bindingContext.OperationBindingContext.HttpContext.Request;
|
||||||
|
var postedFiles = new List<IFormFile>();
|
||||||
|
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 <input type="file" ... /> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
|
|
@ -31,7 +32,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
var values = request.Headers.GetCommaSeparatedValues(bindingContext.ModelName);
|
var values = request.Headers.GetCommaSeparatedValues(bindingContext.ModelName);
|
||||||
if (values != null)
|
if (values != null)
|
||||||
{
|
{
|
||||||
bindingContext.Model = ConvertValuesToCollectionType(bindingContext.ModelType, values);
|
bindingContext.Model = ModelBindingHelper.ConvertValuesToCollectionType(bindingContext.ModelType, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
|
|
@ -39,43 +40,5 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
|
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ConvertValuesToCollectionType(Type modelType, IList<string> values)
|
|
||||||
{
|
|
||||||
// There's a limited set of collection types we can support here.
|
|
||||||
//
|
|
||||||
// For the simple cases - choose a string[] or List<string> if the destination type supports
|
|
||||||
// it.
|
|
||||||
//
|
|
||||||
// For more complex cases, if the destination type is a class and implements ICollection<string>
|
|
||||||
// then activate it and add the values.
|
|
||||||
//
|
|
||||||
// Otherwise just give up.
|
|
||||||
if (typeof(List<string>).IsAssignableFrom(modelType))
|
|
||||||
{
|
|
||||||
return new List<string>(values);
|
|
||||||
}
|
|
||||||
else if (typeof(string[]).IsAssignableFrom(modelType))
|
|
||||||
{
|
|
||||||
return values.ToArray();
|
|
||||||
}
|
|
||||||
else if (
|
|
||||||
modelType.GetTypeInfo().IsClass &&
|
|
||||||
!modelType.GetTypeInfo().IsAbstract &&
|
|
||||||
typeof(ICollection<string>).IsAssignableFrom(modelType))
|
|
||||||
{
|
|
||||||
var result = (ICollection<string>)Activator.CreateInstance(modelType);
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
result.Add(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
|
|
@ -117,5 +119,47 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static object ConvertValuesToCollectionType<T>(Type modelType, IList<T> values)
|
||||||
|
{
|
||||||
|
// There's a limited set of collection types we can support here.
|
||||||
|
//
|
||||||
|
// For the simple cases - choose a T[] or List<T> if the destination type supports
|
||||||
|
// it.
|
||||||
|
//
|
||||||
|
// For more complex cases, if the destination type is a class and implements ICollection<T>
|
||||||
|
// then activate it and add the values.
|
||||||
|
//
|
||||||
|
// Otherwise just give up.
|
||||||
|
if (typeof(List<T>).IsAssignableFrom(modelType))
|
||||||
|
{
|
||||||
|
return new List<T>(values);
|
||||||
|
}
|
||||||
|
else if (typeof(T[]).IsAssignableFrom(modelType))
|
||||||
|
{
|
||||||
|
return values.ToArray();
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
modelType.GetTypeInfo().IsClass &&
|
||||||
|
!modelType.GetTypeInfo().IsAbstract &&
|
||||||
|
typeof(ICollection<T>).IsAssignableFrom(modelType))
|
||||||
|
{
|
||||||
|
var result = (ICollection<T>)Activator.CreateInstance(modelType);
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
result.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else if (typeof(IEnumerable<T>).IsAssignableFrom(modelType))
|
||||||
|
{
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,19 @@
|
||||||
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.ModelBinding
|
namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
{
|
{
|
||||||
public class FormValueProviderFactory : IValueProviderFactory
|
public class FormValueProviderFactory : IValueProviderFactory
|
||||||
{
|
{
|
||||||
private static MediaTypeHeaderValue _formEncodedContentType =
|
|
||||||
MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
|
|
||||||
|
|
||||||
public IValueProvider GetValueProvider([NotNull] ValueProviderFactoryContext context)
|
public IValueProvider GetValueProvider([NotNull] ValueProviderFactoryContext context)
|
||||||
{
|
{
|
||||||
var request = context.HttpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
|
|
||||||
if (IsSupportedContentType(request))
|
if (request.HasFormContentType)
|
||||||
{
|
{
|
||||||
var culture = GetCultureInfo(request);
|
var culture = GetCultureInfo(request);
|
||||||
|
|
||||||
return new ReadableStringCollectionValueProvider<IFormDataValueProviderMetadata>(
|
return new ReadableStringCollectionValueProvider<IFormDataValueProviderMetadata>(
|
||||||
async () => await request.ReadFormAsync(),
|
async () => await request.ReadFormAsync(),
|
||||||
culture);
|
culture);
|
||||||
|
|
@ -27,13 +24,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
return null;
|
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)
|
private static CultureInfo GetCultureInfo(HttpRequest request)
|
||||||
{
|
{
|
||||||
return CultureInfo.CurrentCulture;
|
return CultureInfo.CurrentCulture;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
private IReadableStringCollection _values;
|
private IReadableStringCollection _values;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a NameValuePairsProvider wrapping an existing set of key value pairs.
|
/// Creates a provider for <see cref="IReadableStringCollection"/> wrapping an existing set of key value pairs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="values">The key value pairs to wrap.</param>
|
/// <param name="values">The key value pairs to wrap.</param>
|
||||||
/// <param name="culture">The culture to return with ValueProviderResult instances.</param>
|
/// <param name="culture">The culture to return with ValueProviderResult instances.</param>
|
||||||
|
|
@ -31,6 +31,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
_culture = culture;
|
_culture = culture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a provider for <see cref="IReadableStringCollection"/> wrapping an
|
||||||
|
/// existing set of key value pairs provided by the delegate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">The delegate that provides the key value pairs to wrap.</param>
|
||||||
|
/// <param name="culture">The culture to return with ValueProviderResult instances.</param>
|
||||||
public ReadableStringCollectionValueProvider([NotNull] Func<Task<IReadableStringCollection>> valuesFactory,
|
public ReadableStringCollectionValueProvider([NotNull] Func<Task<IReadableStringCollection>> valuesFactory,
|
||||||
CultureInfo culture)
|
CultureInfo culture)
|
||||||
{
|
{
|
||||||
|
|
@ -46,18 +52,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<bool> ContainsPrefixAsync(string prefix)
|
public override async Task<bool> ContainsPrefixAsync(string prefix)
|
||||||
{
|
{
|
||||||
var prefixContainer = await GetPrefixContainerAsync();
|
var prefixContainer = await GetPrefixContainerAsync();
|
||||||
return prefixContainer.ContainsPrefix(prefix);
|
return prefixContainer.ContainsPrefix(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public virtual async Task<IDictionary<string, string>> GetKeysFromPrefixAsync([NotNull] string prefix)
|
public virtual async Task<IDictionary<string, string>> GetKeysFromPrefixAsync([NotNull] string prefix)
|
||||||
{
|
{
|
||||||
var prefixContainer = await GetPrefixContainerAsync();
|
var prefixContainer = await GetPrefixContainerAsync();
|
||||||
return prefixContainer.GetKeysFromPrefix(prefix);
|
return prefixContainer.GetKeysFromPrefix(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<ValueProviderResult> GetValueAsync([NotNull] string key)
|
public override async Task<ValueProviderResult> GetValueAsync([NotNull] string key)
|
||||||
{
|
{
|
||||||
var collection = await GetValueCollectionAsync();
|
var collection = await GetValueCollectionAsync();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNet.Http": "1.0.0-*",
|
"Microsoft.AspNet.Http": "1.0.0-*",
|
||||||
|
"Microsoft.AspNet.Http.Extensions": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
|
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
|
||||||
"Microsoft.Framework.DependencyInjection": "1.0.0-*",
|
"Microsoft.Framework.DependencyInjection": "1.0.0-*",
|
||||||
"Microsoft.Net.Http.Headers": "1.0.0-*",
|
"Microsoft.Net.Http.Headers": "1.0.0-*",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
options.ModelBinders.Add(new TypeMatchModelBinder());
|
options.ModelBinders.Add(new TypeMatchModelBinder());
|
||||||
options.ModelBinders.Add(new CancellationTokenModelBinder());
|
options.ModelBinders.Add(new CancellationTokenModelBinder());
|
||||||
options.ModelBinders.Add(new ByteArrayModelBinder());
|
options.ModelBinders.Add(new ByteArrayModelBinder());
|
||||||
|
options.ModelBinders.Add(new FormFileModelBinder());
|
||||||
options.ModelBinders.Add(typeof(GenericModelBinder));
|
options.ModelBinders.Add(typeof(GenericModelBinder));
|
||||||
options.ModelBinders.Add(new MutableObjectModelBinder());
|
options.ModelBinders.Add(new MutableObjectModelBinder());
|
||||||
options.ModelBinders.Add(new ComplexModelDtoModelBinder());
|
options.ModelBinders.Add(new ComplexModelDtoModelBinder());
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -1327,5 +1326,111 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
Assert.Equal(expectedContent, body);
|
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<FileDetails>(
|
||||||
|
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<FileDetails[]>(
|
||||||
|
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<IDictionary<string, IList<FileDetails>>>(
|
||||||
|
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<KeyValuePair<string, FileDetails>>(
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<IFormFile>), httpContext);
|
||||||
|
var binder = new FormFileModelBinder();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await binder.BindModelAsync(bindingContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
var files = Assert.IsAssignableFrom<IList<IFormFile>>(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<IFormFile>(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<IFormFile>().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>();
|
||||||
|
httpContext.Setup(h => h.Request.ReadFormAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.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<IFormCollection>();
|
||||||
|
formCollection.Setup(f => f.Files).Returns(formFiles);
|
||||||
|
return formCollection.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IFormFile GetMockFormFile(string modelName, string filename)
|
||||||
|
{
|
||||||
|
var formFile = new Mock<IFormFile>();
|
||||||
|
formFile.Setup(f => f.ContentDisposition)
|
||||||
|
.Returns(string.Format("form-data; name={0}; filename={1}", modelName, filename));
|
||||||
|
return formFile.Object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.PipelineCore;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -32,7 +33,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("application/x-www-form-urlencoded")]
|
[InlineData("application/x-www-form-urlencoded")]
|
||||||
[InlineData("application/x-www-form-urlencoded;charset=utf-8")]
|
[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
|
// Arrange
|
||||||
var context = CreateContext(contentType);
|
var context = CreateContext(contentType);
|
||||||
|
|
@ -52,6 +55,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
|
||||||
var request = new Mock<HttpRequest>();
|
var request = new Mock<HttpRequest>();
|
||||||
request.Setup(f => f.ReadFormAsync(CancellationToken.None)).Returns(Task.FromResult(collection));
|
request.Setup(f => f.ReadFormAsync(CancellationToken.None)).Returns(Task.FromResult(collection));
|
||||||
request.SetupGet(r => r.ContentType).Returns(contentType);
|
request.SetupGet(r => r.ContentType).Returns(contentType);
|
||||||
|
request.SetupGet(r => r.HasFormContentType).Returns(new FormFeature(request.Object).HasFormContentType);
|
||||||
|
|
||||||
var context = new Mock<HttpContext>();
|
var context = new Mock<HttpContext>();
|
||||||
context.SetupGet(c => c.Request).Returns(request.Object);
|
context.SetupGet(c => c.Request).Returns(request.Object);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var i = 0;
|
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(BinderTypeBasedModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
||||||
Assert.Equal(typeof(ServicesModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
Assert.Equal(typeof(ServicesModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
||||||
Assert.Equal(typeof(BodyModelBinder), 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(TypeMatchModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
||||||
Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
Assert.Equal(typeof(CancellationTokenModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
||||||
Assert.Equal(typeof(ByteArrayModelBinder), 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(GenericModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
||||||
Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
||||||
Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[i++].OptionType);
|
||||||
|
|
|
||||||
|
|
@ -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<IFormFile> files)
|
||||||
|
{
|
||||||
|
var fileDetailsList = new List<FileDetails>();
|
||||||
|
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<string, IList<FileDetails>> UploadMultipleList(IEnumerable<IFormFile> filelist1,
|
||||||
|
IEnumerable<IFormFile> filelist2)
|
||||||
|
{
|
||||||
|
var fileDetailsDict = new Dictionary<string, IList<FileDetails>>
|
||||||
|
{
|
||||||
|
{ "filelist1", new List<FileDetails>() },
|
||||||
|
{ "filelist2", new List<FileDetails>() }
|
||||||
|
};
|
||||||
|
var fileDetailsList = new List<FileDetails>();
|
||||||
|
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<string, FileDetails> 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<string, FileDetails>(book.Name, fileDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue