在 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 的文件还需要在请求的第二部分中指定filenameContent-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。现在您可以为请求的每个部分在 textfile 之间进行选择。

【讨论】:

很好的解决方案,谢谢!我现在唯一的问题是如何从 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 =&gt; o.ModelBinderProviders.Insert(0, new JsonModelBinderProvider())); inside startup.cs ConfigureServices【参考方案4】:

按照@bruno-zell 的出色回答,如果您只有一个文件(我没有使用IList&lt;IFormFile&gt; 进行测试),您也可以将您的控制器声明为:

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当中应用)

ASP.NET Core Web Api 自动帮助页面

ASP.NET Core Web API

ASP.NET Core Web API 在 API GET 请求中检索空白记录

Asp.Net Core 1.1 消费web api

使用 ASP.NET Core MVC 创建 Web API