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


74

如何使用分段上传将文件(图像)和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
文件应与一起发送multipart/form-data。JSON应该与一起发送application/json。您只能发送一种类型。因此,没有干净的方法可以做到这一点。
弗雷德(Fred)

Answers:


68

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

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

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

实作

背后的代码JsonModelBinder(请参阅GitHub或使用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;
    }
}

范例要求

这是上述控制器操作接受的原始http请求的示例Upload

multipart/form-data请求分为多个部分,每个部分由指定的分隔boundary=12345。每个部分的Content-Disposition-header中都有一个分配的名称。使用这些名称,默认值ASP.Net-Core知道控制器操作中哪个部分绑定到哪个参数。

绑定到的文件IFormFile还需要filename在请求的第二部分中指定a 。Content-Type不需要。

还要注意的另一件事是,json部分需要反序列化为控制器操作中定义的参数类型。因此,在这种情况下,类型SomeObject应该有一个属性key类型string

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驱动。创建一个新的请求,并选择表单数据身体-Tab。现在,您可以为需求的每个部分在文本文件之间进行选择。

在此处输入图片说明


2
伟大的解决方案,谢谢!我现在唯一的问题是如何调用邮递员的上载路由进行集成测试?如何用JSON表示IFormFile?
Patrice Cote

@PatriceCote我已经更新了答案。请看一下:)
Bruno Zell,

非常感谢,这正是我想要的。但是,然后,也许是一个简单的FromForm而不是FromBody就能完成我所想的技巧。
Patrice Cote

@PatriceCote很好。我已经不那么喜欢它了,但是我认为我还没有尝试过FromForm哈哈……
Bruno Zell

2
这正是我在寻找的东西,但是我不知道如何使用HttpClient:/发布请求。请提供任何帮助:)
Mohamed BOUZIDI

20

显然,没有内置的方法可以执行我想要的操作。所以我最终写了我自己的书ModelBinder来应对这种情况。我没有找到有关自定义模型绑定的任何官方文档,但我将这篇文章用作参考。

CustomModelBinder将搜索以属性修饰的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来接收数据作为多部分/表单数据?如果content-type为multipart / form-data,则会出现错误500。
Asatur Galstyan

您确定拯救了我的生活,Andrius。我整天都在想这个。我在我的API中使用了swagger,当模型中的嵌套对象只有通用类型属性时,这很好。Swagger会像这样发送它们:“ NestedObject.Id”,依此类推,但是当涉及到数组时-> JSON绑定器是唯一可行的解​​决方案!
Alex Wyler

感谢您的解决方案。如何使用邮递员向api发送请求?我正在使用表单数据发送..但是它收到错误415
Majid821

13

我正在前端使用Angular 7,因此我利用了FormData该类,该类允许您将字符串或斑点附加到表单。可以使用[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'
}

现在,您在文件和对象上有了一个句柄。请注意,您在控制器操作的params列表中提供的名称必须与在追加到FormData前端对象上时提供的名称匹配。


如何处理多个文件?
Tzof

@Tzof看一下MDN页面上的最后一个示例
andreisrob '19

比这里的其他示例容易得多。效果很好。每个Tzof的?只需对每个其他具有相同名称的文件执行另一个追加。
VirtualLife

9

遵循@ 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($"{filePath}{file.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方法来调用您的控制器。


2
太好了,如果尝试执行HttpClient.PostAsync调用来上传文件,C#客户端代码是什么样的?
马克·雷德曼

我想您要做的就是像调用同步一样调用它,并在调用之前添加“ await”,或在末尾添加“ .Result”
Patrice Cote

1
我用[FromForm]做了一些测试,得到了正确类型的对象,但是未设置参数。
Rasmus Christensen

同意@Mukus。文件始终为空
Rukshan Dangalla

我输入了第一个参数(MyModel paramters,它没有用。我将其更改为(字符串参数,并且我能够将对象作为json :)
Ali Keserwan,

0

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

我过去的实现方式是通过ajax上传文件,然后在响应中返回文件url,然后将其与发布请求一起传递以保存实际记录。


是的,这肯定是可能的,但是我试图避免一项任务与服务器建立两个不同的连接,只是为了使客户端和服务器之间的所有内容保持同步。我想我已经找到了解决我问题的方法。等更多时间后,我会在这里发布。
Andrius

0

我遇到了类似的问题,并通过使用[FromForm]attribute和FileUploadModelView函数来解决此问题,如下所示:

[HttpPost("Save")]
public async Task<IActionResult> Save([FromForm] ProfileEditViewModel model)
{          
  return null;
}

0

我想使用Vue前端和.net核心api进行相同的操作。但是出于某些奇怪的原因,IFormFile总是返回null。因此,我不得不将其更改为IFormCollection并进行整理。这是任何遇到相同问题的人的代码:)

public async Task<IActionResult> Post([FromForm]IFormCollection files)

0

从角度发布到ASP核心api时,我遇到了类似的问题。

Chrome:表格数据

------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();
    }
}

尽管这种方式不受欢迎,但它确实起作用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.