web-dev-qa-db-ja.com

ASP.NET Core Web APIでファイルとJSONをアップロードする

マルチパートアップロードを使用して、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データの両方をアップロードしてオブジェクトにバインドするにはどうすればよいですか?

42
Andrius

どうやら私がしたいことをする方法が組み込まれていないようです。そのため、私はこの状況を処理するために独自の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());
});
14
Andrius

シンプルで少ないコード、ラッパーモデルなし

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でテストする

Postman を使用してアクションを呼び出し、サーバー側のコードをテストできます。これは非常にシンプルで、ほとんどがUI駆動です。新しいリクエストを作成し、Body-Tabでform-dataを選択します。これで、要求の各部分に対してtextfileのどちらかを選択できます。

enter image description here

34
Bruno Zell

@ 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メソッドを使用して、コントローラーを呼び出すことができます。

6
Patrice Cote

フロントエンドで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オブジェクトに追加するときに指定する名前と一致することに注意してください。

3
andreisrob

1つのステップで2つのことを実行できるかどうかはわかりません。

過去にこれを達成した方法は、ajaxを介してファイルをアップロードし、応答でファイルのURLを返し、それを実際のレコードを保存するためのポストリクエストと共に渡すことです。

0
Chirdeep Tomar

同様の問題があり、次のように関数で[FromForm]属性とFileUploadModelViewを使用して問題を解決しました。

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

Vue frontend and .net core api。を使用して同じことをしたかったのですが、何らかの奇妙な理由でIFormFileは常にnullを返しました。それでIFormCollectionに変更する必要がありました同じ問題に直面している人のためのコードは次のとおりです:)

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