在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
知道哪个部分绑定到控制器操作中的哪个参数。
绑定到IFormFile
的filename
还需要指定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请求一起传递以保存实际记录。
- 在.net core 1.0 WEB API Projects中设置应用程序图标
- 在没有Microsoft.Office.Interop的情况下,将.NET doc和docx格式转换为.NET Core中的PDF
- 通过.NET Core上的MEF将参数传递给插件构造函数?
- 如何将相同的列添加到EF Core中的所有实体?
- Linux上的.net核心应用程序目标.net框架4.5.2
- Swashbuckle.AspNetCore v1.0.0 with OAuth2,flow:application – > IdentityServer4
- 使用IFormFile拒绝ASP.Net对路径的核心访问
- AWS Lambda环境变量和dependency injection
- ASP.NET Core appsettings.json在代码中更新