在ASP.NET Core Web API中上载文件和JSON

如何使用分段上传将文件(图像)和json数据列表上载到ASP.NET Core Web API控制器?

我可以成功接收一个文件列表,使用multipart/form-data内容类型上传,如下所示:

 public async Task Upload(IList files) 

当然,我可以使用默认的JSON格式化程序成功接收格式化为我的对象的HTTP请求正文:

 public void Post([FromBody]SomeObject value) 

但是如何在一个控制器动作中将这两者结合起来呢? 如何上传图像和JSON数据并将它们绑定到我的对象?

显然,没有内置的方法来做我想要的。 所以我最终编写了自己的ModelBinder来处理这种情况。 我没有找到任何关于自定义模型绑定的官方文档,但我使用这篇文章作为参考。

自定义ModelBinder将搜索使用FromJson属性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 Files { get; set; } [FromJson] public MyModel Model { get; set; } // <-- JSON will be deserialized to this object } // Controller action: public async Task Upload(MyModelWrapper modelWrapper) { } // Add custom binder provider in Startup.cs ConfigureServices services.AddMvc(properties => { properties.ModelBinderProviders.Insert(0, new JsonModelBinderProvider()); }); 

简单,代码少,没有包装模型

有一个更简单的解决方案,深受Andrius的回答启发。 通过使用ModelBinderAttribute您不必指定模型或绑定程序提供程序。 这节省了大量代码。 您的控制器操作如下所示:

 public IActionResult Upload( [ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value, IList files) { // Use serialized json object 'value' // Use uploaded 'files' } 

履行

JsonModelBinder背后的JsonModelBinder (或使用完整的NuGet包 ):

 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知道哪个部分绑定到控制器操作中的哪个参数。

绑定到IFormFilefilename还需要指定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可用于调用操作并测试服务器端代码。 这非常简单,主要是UI驱动。 创建一个新请求并在Body -Tab中选择表单数据 。 现在,您可以为需求的每个部分选择文本文件

在此处输入图像描述

按照@ bruno-zell的优秀答案,如果你只有一个文件(我没有使用IList测试),你也可以将控制器声明为:

 public async Task Create([FromForm] CreateParameters parameters, IFormFile file) { const string filePath = "./Files/"; if (file.Length > 0) { using (var stream = new FileStream($"{filePath}{file.FileName}", FileMode.Create)) { await file.CopyToAsync(stream); } } // Save CreateParameters properties to database var myThing = _mapper.Map(parameters); myThing.FileName = file.FileName; _efContext.Things.Add(myThing); _efContext.SaveChanges(); return Ok(_mapper.Map(myThing)); } 

然后你可以使用Bruno的答案中显示的Postman方法来调用你的控制器。

我不确定你是否可以一步完成这两件事。

我过去如何实现这一点是通过ajax上传文件并将文件url返回到响应中,然后将其与post请求一起传递以保存实际记录。