在 ASP.NET Core Web API 中上传文件和 JSON
Posted
技术标签:
【中文标题】在 ASP.NET Core Web API 中上传文件和 JSON【英文标题】:Upload files and JSON in ASP.NET Core Web API 【发布时间】:2017-05-13 01:32:58 【问题描述】:如何使用分段上传将文件(图像)列表和 json 数据上传到 ASP.NET Core Web API 控制器?
我可以成功接收到文件列表,上传的内容类型为multipart/form-data
:
public async Task<IActionResult> Upload(IList<IFormFile> files)
当然,我可以使用默认的 JSON 格式化程序成功接收格式化为我的对象的 HTTP 请求正文:
public void Post([FromBody]SomeObject value)
但是我怎样才能将这两者结合在一个控制器动作中呢?如何上传图像和 JSON 数据并将它们绑定到我的对象?
【问题讨论】:
【参考方案1】:简单、代码少、无包装模型
有一个更简单的解决方案,深受Andrius' answer 的启发。通过使用
ModelBinderAttribute
您不必指定模型或活页夹提供者。这样可以节省很多代码。您的控制器操作如下所示:
public IActionResult Upload(
[ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value,
IList<IFormFile> files)
// Use serialized json object 'value'
// Use uploaded 'files'
实施
JsonModelBinder
后面的代码(参见GitHub 或使用NuGet package):
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class JsonModelBinder : IModelBinder
public Task BindModelAsync(ModelBindingContext bindingContext)
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
var result = Newtonsoft.Json.JsonConvert.DeserializeObject(valueAsString, bindingContext.ModelType);
if (result != null)
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
return Task.CompletedTask;
示例请求
以下是上面的控制器操作Upload
接受的原始 http 请求示例。
multipart/form-data
请求被拆分为多个部分,每个部分由指定的 boundary=12345
分隔。每个部分都在其Content-Disposition
-header 中分配了一个名称。使用这些名称默认ASP.Net-Core
知道哪个部分绑定到控制器操作中的哪个参数。
绑定到IFormFile
的文件还需要在请求的第二部分中指定filename
。 Content-Type
不是必需的。
另外需要注意的是,json 部分需要反序列化为控制器操作中定义的参数类型。所以在这种情况下,SomeObject
类型应该具有string
类型的属性key
。
POST http://localhost:5000/home/upload HTTP/1.1
Host: localhost:5000
Content-Type: multipart/form-data; boundary=12345
Content-Length: 218
--12345
Content-Disposition: form-data; name="value"
"key": "value"
--12345
Content-Disposition: form-data; name="files"; filename="file.txt"
Content-Type: text/plain
This is a simple text file
--12345--
使用 Postman 进行测试
Postman 可用于调用操作并测试您的服务器端代码。这非常简单,主要是 UI 驱动的。创建一个新请求并在 Body-Tab 中选择 form-data。现在您可以为请求的每个部分在 text 和 file 之间进行选择。
【讨论】:
很好的解决方案,谢谢!我现在唯一的问题是如何从 Postman 调用 Upload 路由进行集成测试?如何在 JSON 中表示 IFormFile? @PatriceCote 我已经更新了答案。请看一下:) 非常感谢,这正是我想要的。但是,也许一个简单的 FromForm 而不是 FromBody 会完成我猜想的伎俩。 这正是我正在寻找的,但我不知道如何使用 HttpClient 发布请求:/。任何帮助请:) 这不会生成正确的 swagger 模型。并且 json 不会作为正文发送,而是查询参数 ...【参考方案2】:我在前端使用 Angular 7,所以我使用 FormData
类,它允许您将字符串或 blob 附加到表单。可以使用[FromForm]
属性在控制器操作中将它们从表单中拉出。我将文件添加到FormData
对象,然后将我希望与文件一起发送的数据字符串化,将其附加到FormData
对象,并在我的控制器操作中反序列化字符串。
像这样:
//front-end:
let formData: FormData = new FormData();
formData.append('File', fileToUpload);
formData.append('jsonString', JSON.stringify(myObject));
//request using a var of type HttpClient
http.post(url, formData);
//controller action
public Upload([FromForm] IFormFile File, [FromForm] string jsonString)
SomeType myObj = JsonConvert.DeserializeObject<SomeType>(jsonString);
//do stuff with 'File'
//do stuff with 'myObj'
您现在有了文件和对象的句柄。请注意,您在控制器操作的参数列表中提供的名称必须与您在附加到前端的 FormData
对象时提供的名称匹配。
【讨论】:
如何处理多个文件? @Tzof 看看MDN页面上的last example 比这里的其他例子简单多了。效果很好。每佐夫的?只需对每个具有相同名称的附加文件进行另一个附加。【参考方案3】:显然没有内置的方法可以做我想做的事。所以我最终写了自己的ModelBinder
来处理这种情况。我没有找到任何关于自定义模型绑定的官方文档,但我使用了this post 作为参考。
自定义ModelBinder
将搜索用FromJson
属性修饰的属性并将来自多部分请求的字符串反序列化为JSON。我将我的模型包装在另一个具有模型和IFormFile
属性的类(包装器)中。
IJsonAttribute.cs:
public interface IJsonAttribute
object TryConvert(string modelValue, Type targertType, out bool success);
FromJsonAttribute.cs:
using Newtonsoft.Json;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class FromJsonAttribute : Attribute, IJsonAttribute
public object TryConvert(string modelValue, Type targetType, out bool success)
var value = JsonConvert.DeserializeObject(modelValue, targetType);
success = value != null;
return value;
JsonModelBinderProvider.cs:
public class JsonModelBinderProvider : IModelBinderProvider
public IModelBinder GetBinder(ModelBinderProviderContext context)
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsComplexType)
var propName = context.Metadata.PropertyName;
var propInfo = context.Metadata.ContainerType?.GetProperty(propName);
if(propName == null || propInfo == null)
return null;
// Look for FromJson attributes
var attribute = propInfo.GetCustomAttributes(typeof(FromJsonAttribute), false).FirstOrDefault();
if (attribute != null)
return new JsonModelBinder(context.Metadata.ModelType, attribute as IJsonAttribute);
return null;
JsonModelBinder.cs:
public class JsonModelBinder : IModelBinder
private IJsonAttribute _attribute;
private Type _targetType;
public JsonModelBinder(Type type, IJsonAttribute attribute)
if (type == null) throw new ArgumentNullException(nameof(type));
_attribute = attribute as IJsonAttribute;
_targetType = type;
public Task BindModelAsync(ModelBindingContext bindingContext)
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
bool success;
var result = _attribute.TryConvert(valueAsString, _targetType, out success);
if (success)
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
return Task.CompletedTask;
用法:
public class MyModelWrapper
public IList<IFormFile> Files get; set;
[FromJson]
public MyModel Model get; set; // <-- JSON will be deserialized to this object
// Controller action:
public async Task<IActionResult> Upload(MyModelWrapper modelWrapper)
// Add custom binder provider in Startup.cs ConfigureServices
services.AddMvc(properties =>
properties.ModelBinderProviders.Insert(0, new JsonModelBinderProvider());
);
【讨论】:
我应该使用什么 InputFormatter 来接收作为 multipart/form-data 的数据?如果内容类型是多部分/表单数据,则会出现错误 500。 你救了我的命,Andrius,当然。我花了一整天的时间思考这个问题。我在我的 API 中使用 swagger,当模型中的嵌套对象只有公共类型属性时,这很好。 Swagger 会像这样发送它们:“NestedObject.Id”等等,但是当涉及到数组时 -> 你的 JSON 绑定器是唯一可行的解决方案! 感谢您的解决方案。如何使用邮递员向 api 发送请求?我正在使用表单数据发送..但它得到错误 415 谁在使用web api 使用services.AddControllers(o => o.ModelBinderProviders.Insert(0, new JsonModelBinderProvider()));
inside startup.cs
ConfigureServices
【参考方案4】:
按照@bruno-zell 的出色回答,如果您只有一个文件(我没有使用IList<IFormFile>
进行测试),您也可以将您的控制器声明为:
public async Task<IActionResult> Create([FromForm] CreateParameters parameters, IFormFile file)
const string filePath = "./Files/";
if (file.Length > 0)
using (var stream = new FileStream($"filePathfile.FileName", FileMode.Create))
await file.CopyToAsync(stream);
// Save CreateParameters properties to database
var myThing = _mapper.Map<Models.Thing>(parameters);
myThing.FileName = file.FileName;
_efContext.Things.Add(myThing);
_efContext.SaveChanges();
return Ok(_mapper.Map<SomeObjectReturnDto>(myThing));
然后您可以使用 Bruno 的答案中显示的 Postman 方法来调用您的控制器。
【讨论】:
这很好,如果尝试执行 HttpClient.PostAsync 调用来上传文件,C# 客户端代码是什么样的? 我想你所要做的就是像同步一样调用它,然后在调用之前添加“await”或在末尾添加“.Result” 我用 [FromForm] 做了一些测试,我得到了一个正确类型的对象,但参数没有设置。 同意@Mukus。文件始终为空 我输入了第一个参数(MyModel 参数,它不起作用。我将其更改为(字符串参数,并且我能够将对象作为 json 获取:)【参考方案5】:.net 5
的更新版本基于 @bruno-zell 的回答,增加了对多个文件的支持
using System;
using System.Collections;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
public class JsonModelBinder : IModelBinder
private readonly JsonOptions _jsonOptions;
public JsonModelBinder(IOptions<JsonOptions> jsonOptions)
_jsonOptions = jsonOptions.Value;
public Task BindModelAsync(ModelBindingContext bindingContext)
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
string toSerialize;
// Attempt to convert the input value
if (typeof(IEnumerable).IsAssignableFrom(bindingContext.ModelType))
toSerialize = "[" + string.Join<string>(',', valueProviderResult.Values) + "]";
else
toSerialize = valueProviderResult.FirstValue;
var result = JsonSerializer.Deserialize(toSerialize, bindingContext.ModelType, _jsonOptions.JsonSerializerOptions);
if (result != null)
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
return Task.CompletedTask;
【讨论】:
【参考方案6】:我不确定您是否可以一步完成这两件事。
我过去如何实现这一点是通过 ajax 上传文件并在响应中返回文件 url,然后将其与 post 请求一起传递以保存实际记录。
【讨论】:
是的,这当然是可能的,但我试图避免为一项任务与服务器建立两个不同的连接,只是为了让客户端和服务器之间的所有内容保持同步。我想我已经找到了解决我的问题的方法。有时间我会在这里发布。【参考方案7】:我遇到了类似的问题,我通过在函数中使用[FromForm]
属性和FileUploadModelView
解决了这个问题,如下所示:
[HttpPost("Save")]
public async Task<IActionResult> Save([FromForm] ProfileEditViewModel model)
return null;
【讨论】:
【参考方案8】:我想使用 Vue 前端和 .net core api 来做同样的事情。但出于某种奇怪的原因,IFormFile 总是返回 null。因此,我不得不将其更改为 IFormCollection 并对其进行整理。这是任何面临相同问题的人的代码:)
public async Task<IActionResult> Post([FromForm]IFormCollection files)
【讨论】:
【参考方案9】:我在从 Angular 发布到 asp core api 时遇到了类似的问题。
铬: 表单数据
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="file1"
undefined
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="file2"
undefined
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="reportData"; filename="blob"
Content-Type: application/json
"id":2,"report":3,"code":"XX0013","business":"01","name":"Test","description":"Description"
------WebKitFormBoundarydowgB6BX0wiwKeOk--
这是我的做法:
我使用reportData作为上传的文件数据,然后读取文件的内容。
[HttpPost]
public async Task<IActionResult> Set([FromForm] IFormFile file1, [FromForm] IFormFile file2, [FromForm] IFormFile reportData)
try
ReportFormModel.Result result = default;
if (reportData != null)
string reportJson = await reportData.ReadFormFileAsync();
ReportFormModel.Params reportParams = reportJson.JsonToObject<ReportFormModel.Params>();
if (reportParams != null)
//OK
return Ok(result);
catch (Exception ex)
return BadRequest();
public static class Utilities
public static async Task<string> ReadFormFileAsync(this IFormFile file)
if (file == null || file.Length == 0)
return await Task.FromResult((string)null);
using var reader = new StreamReader(file.OpenReadStream());
return await reader.ReadToEndAsync();
这种方式虽然不受欢迎,但确实有效。
【讨论】:
以上是关于在 ASP.NET Core Web API 中上传文件和 JSON的主要内容,如果未能解决你的问题,请参考以下文章
ASP.NET Core Web 应用程序系列- 使用ASP.NET Core内置的IoC容器DI进行批量依赖注入(MVC当中应用)