[英]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可用于调用操作并测试您的服务器端代码。 这非常简单,主要是 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 应用程序中的file和userData分别更改为File和UserData 。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.