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

And*_*ius 47 c# file-upload .net-core asp.net-core

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

我可以成功收到一个文件列表,上传的multipart/form-data内容类型如下:

public async Task<IActionResult> Upload(IList<IFormFile> files)
Run Code Online (Sandbox Code Playgroud)

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

public void Post([FromBody]SomeObject value)
Run Code Online (Sandbox Code Playgroud)

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

Bru*_*ell 36

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

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

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

履行

代码背后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;
    }
}
Run Code Online (Sandbox Code Playgroud)

示例请求

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

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

绑定的文件IFormFile还需要filename在请求的第二部分中指定as.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--
Run Code Online (Sandbox Code Playgroud)

用邮递员测试

Postman可用于调用操作并测试服务器端代码.这很简单,主要是UI驱动.创建一个新请求并在Body -Tab中选择表单数据.现在,您可以在reqeust的每个部分中选择文本文件.

在此输入图像描述

  • 这不会生成正确的 swagger 模型。并且 json 不会作为正文发送,而是作为查询参数发送... (4认同)
  • 伟大的解决方案,谢谢!我现在唯一的问题是如何调用邮递员的上载路由进行集成测试?如何用JSON表示IFormFile? (2认同)
  • 非常感谢,这正是我正在寻找的。但是,也许一个简单的 FromForm 而不是 FromBody 就可以达到我想的效果。 (2认同)
  • 这正是我正在寻找的,但我不知道如何使用 HttpClient 发布请求:/。请提供任何帮助:) (2认同)

and*_*rob 24

我在前端使用 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'
}
Run Code Online (Sandbox Code Playgroud)

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

  • 比这里的其他例子容易得多。效果很好。根据佐夫的?只需对每个具有相同名称的附加文件进行另一个追加即可。 (2认同)

And*_*ius 14

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

自定义ModelBinder将搜索使用属性修饰的FromJson属性,并将来自多部分请求的字符串反序列化为JSON.我将我的模型包装在另一个具有模型和IFormFile属性的类(包装器)中.

IJsonAttribute.cs:

public interface IJsonAttribute
{
    object TryConvert(string modelValue, Type targertType, out bool success);
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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());
});
Run Code Online (Sandbox Code Playgroud)


Pat*_*ote 6

遵循@ 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));
}
Run Code Online (Sandbox Code Playgroud)

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

  • 太好了,如果尝试执行 HttpClient.PostAsync 调用来上传文件,C# 客户端代码会是什么样子? (2认同)
  • 我使用 [FromForm] 进行了一些测试,得到了正确类型的对象,但未设置参数。 (2认同)