マルチパートアップロードを使用して、ASP.NET Core Web APIコントローラーにファイル(画像)とJSONデータのリストをアップロードするにはどうすればよいですか?
次のようなmultipart/form-data
コンテンツタイプでアップロードされたファイルのリストを正常に受信できます。
public async Task<IActionResult> Upload(IList<IFormFile> files)
そしてもちろん、次のようなデフォルトのJSONフォーマッターを使用して、オブジェクトにフォーマットされたHTTPリクエスト本文を正常に受信できます。
public void Post([FromBody]SomeObject value)
しかし、これら2つを1つのコントローラーアクションにどのように組み合わせることができますか?画像とJSONデータの両方をアップロードしてオブジェクトにバインドするにはどうすればよいですか?
どうやら私がしたいことをする方法が組み込まれていないようです。そのため、私はこの状況を処理するために独自の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());
});
Andrius 'answer に強く触発された、よりシンプルなソリューションがあります。 ModelBinderAttribute
を使用すると、モデルまたはバインダープロバイダーを指定する必要がありません。これにより、多くのコードを節約できます。コントローラーのアクションは次のようになります。
public IActionResult Upload(
[ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value,
IList<IFormFile> files)
{
// Use serialized json object 'value'
// Use uploaded 'files'
}
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;
}
}
上記のコントローラーアクションUpload
で受け入れられる生のhttpリクエストの例を次に示します。
multipart/form-data
リクエストは、指定されたboundary=12345
で区切られた複数の部分に分割されます。各パートには、Content-Disposition
- headerで名前が割り当てられています。これらの名前を使用すると、デフォルトでASP.Net-Core
はコントローラーアクションのどのパラメーターにどの部分がバインドされているかを認識します。
IFormFile
にバインドされているファイルは、リクエストの2番目の部分のようにfilename
を追加で指定する必要があります。 Content-Type
は不要です。
もう1つ注意すべきことは、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駆動です。新しいリクエストを作成し、Body-Tabでform-dataを選択します。これで、要求の各部分に対してtextとfileのどちらかを選択できます。
@ bruno-zellによる優れた回答に続いて、ファイルが1つしかない場合(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メソッドを使用して、コントローラーを呼び出すことができます。
フロントエンドでAngular 7を使用しているので、フォームに文字列またはblobを追加できる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リストで指定する名前mustは、フロントエンドのFormData
オブジェクトに追加するときに指定する名前と一致することに注意してください。
1つのステップで2つのことを実行できるかどうかはわかりません。
過去にこれを達成した方法は、ajaxを介してファイルをアップロードし、応答でファイルのURLを返し、それを実際のレコードを保存するためのポストリクエストと共に渡すことです。
同様の問題があり、次のように関数で[FromForm]
属性とFileUploadModelView
を使用して問題を解決しました。
[HttpPost("Save")]
public async Task<IActionResult> Save([FromForm] ProfileEditViewModel model)
{
return null;
}
Vue frontend and .net core api。を使用して同じことをしたかったのですが、何らかの奇妙な理由でIFormFileは常にnullを返しました。それでIFormCollectionに変更する必要がありました同じ問題に直面している人のためのコードは次のとおりです:)
public async Task<IActionResult> Post([FromForm]IFormCollection files)