繁体   English   中英

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

[英]Upload files and JSON in ASP.NET Core Web API

如何使用分段上传将文件(图像)列表和 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 数据并将它们绑定到我的对象?

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

有一个更简单的解决方案,深受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;
    }
}

示例请求

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

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

绑定到IFormFile的文件还需要在请求的第二部分中指定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 进行测试

Postman可用于调用操作并测试您的服务器端代码。 这非常简单,主要是 UI 驱动的。 创建一个新请求并在Body -Tab 中选择form-data 现在您可以为请求的每个部分在文本文件之间进行选择。

在此处输入图像描述

我在前端使用 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对象时提供的名称相匹配。

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

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

遵循@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 方法来调用您的控制器。

我有一个类似的问题,我通过在函数中使用[FromForm]属性和FileUploadModelView解决了这个问题,如下所示:

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

我想使用 Vue 前端和 .net core api 来做同样的事情。 但由于某些奇怪的原因, IFormFile总是返回 null。 因此,我不得不将其更改为 IFormCollection 并对其进行整理。 这是面临相同问题的任何人的代码:)

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

.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;
    }
}

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

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

从角度发布到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();
    }
}

虽然这种方式不受欢迎,但它奏效了。

你不需要“JsonModelBinder”和其他自定义的东西,我有模型

public class UpdateAdminProfileInfoRequest
{
    
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Mobile { get; set; }
    public IFormFile Image { get; set; }
}

并且在控制器动作方法中只有一个参数

[FromForm]UpdateAdminProfileInfoRequest request

一切正常。 如果您需要多个文件上传,只需更改

IFormFile 

List<IFormFile> files {get; set}

小心我不知道为什么,但如果你用

[JsonProperty("imageFile")] 

属性或类似 asp.net 的东西不会将客户端“imageFile”字段映射到“Image”属性。

我只是加上我的两分钱

使用 json 上传文件的Angular 应用功能

 uploadFileWithJson = (files) => {
if (files.length === 0) {
  return;
}

let fileToUpload = <File>files[0];
const formData = new FormData();
formData.append('file', fileToUpload, fileToUpload.name);

const user = {
  "name":"Vikram",
  "age":35
}
const userTxt = JSON.stringify(user);
formData.append('userData',userTxt);    

this.http.post('https://localhost:5001/api/upload/UploadFileWithJson', formData, {reportProgress: true, observe: 'events'})
  .subscribe({
    next: (event) => {
    if (event.type === HttpEventType.UploadProgress)
      this.progress = Math.round(100 * event.loaded / event.total);
    else if (event.type === HttpEventType.Response) {
      this.message = 'Upload success.';
      this.onUploadFinished.emit(event.body);
    }
  },
  error: (err: HttpErrorResponse) => console.log(err)
});

}

.NET Core API

[HttpPost("UploadFileWithJson"), DisableRequestSizeLimit]        
    public IActionResult UploadFileWithJson([FromForm]UserWithFile model)
    {
        try
        {
            if (model == null)
                throw new Exception($"{nameof(model)} is null");

            if (model.File == null)
                throw new Exception("File is null");

            var folderName = Path.Combine("Resources", "Images");
            var pathToSave = Path.Combine(Directory.GetCurrentDirectory(), folderName);
            if (model.File.Length > 0)
            {
                var fileName = ContentDispositionHeaderValue.Parse(model.File.ContentDisposition).FileName.Trim('"');
                var fullPath = Path.Combine(pathToSave, fileName);
                var dbPath = Path.Combine(folderName, fileName);
                using (var stream = new FileStream(fullPath, FileMode.Create))
                {
                    model.File.CopyTo(stream);
                }
                return Ok(new { dbPath });
            }
            else
            {
                return BadRequest();
            }                
        }
        catch (Exception ex)
        {
            return StatusCode(500, $"Internal server error: {ex}");
        }
    }

以及模型类。

public class UserWithFile
{
    public string UserData { get; set; }

    public IFormFile File { get; set; }
}

现在请注意,在 Angular 应用程序中, FormData属性文件名的第一个字母f小写的 但是,在 .NET Core 中相同的是File (或大写的)。 userData也是如此。

我只是将 Angular 应用程序中的fileuserData分别更改为FileUserData

瞧! 一切正常 .

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM