ASP.Net Core PostリクエストでJsonと複数ファイルを同時に送信

ASP.Net Coreで用意したAPIにJsonのリクエストのほかに、ファイルも同時にアップしたい要件があったので、
小さなプロジェクトに雑に載せました。

二つのライブラリをnugetで落としています。
Microsoft.AspNetCore.StaticFiles;
Newtonsoft.Json;

ソースコード
https://github.com/shinnosukekubo/ASP.NetCoreMultipleFileUpload

サーバープロジェクトを用意します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/286be645-6796-64f5-bb6a-ef15a656ab91.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/7303c32c-8558-c4a0-d9a7-5705c0b2394f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/7966536e-95b5-56bb-84c9-f1d4bb51d44a.png)
適当にコントローラーを追加して
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/97cd17c8-8ebe-9e19-50ae-b518a2f4ded2.png)

```C#
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MulipleFileUpload.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class JsonWithFileRequestController
    {
        private const long MaxFileSize = 1024L * 1024L * 1024L; // 1GB

        [HttpPost]
        [RequestSizeLimit(MaxFileSize)]
        [RequestFormLimits(MultipartBodyLengthLimit = MaxFileSize)]
        public async Task Upload(
            // json,fileList1などの引数名と、後述するクライアントでの指定名が一致する必要がある
            [FromForm] string json,
            [FromForm] IEnumerable<IFormFile> fileList1,
            [FromForm] IEnumerable<IFormFile> fileList2)
        {
            // Jsonパース
            TextModel model = JsonConvert.DeserializeObject<TextModel>(json);
            // 保存など
            foreach (var file in fileList1) 
            {

                var stream = file.OpenReadStream();
            }
        }

        class TextModel
        {
            public string Text { get; set; }
        }
    }
}

```

クライアントプロジェクトを用意
リクエストを投げるだけなのでコンソールアプリにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139166/7369d585-a993-c899-39be-593e26df0db4.png)

```C#
using Microsoft.AspNetCore.StaticFiles;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;


namespace MultipleFileUpload.Client
{
    class Program
    {
        static HttpClient client;
        static async Task Main(string[] args)
        {
            client = new HttpClient();
            // ポートは書き換えてください
            client.BaseAddress = new Uri("https://localhost:44357/");

            // アップロードするファイルパス
            var filePath = @"C:\Users\shinn\Downloads\images.jpg";
    
                await PostWithFilesAsync(
                "api/JsonWithFileRequest/Upload",
                new { Test = "AAA" },
                new Dictionary<string, List<string>>()
                {
                    // 名前を付ければいくつでも送れるし、リストにしても送れる
                    // 適当に2つにリストに分けていれた
                    { "fileList1", new List<string>{ filePath, filePath } },
                    { "fileList2", new List<string>{ filePath, filePath } }
                });
        }


        public static async Task PostWithFilesAsync(string api, object request, Dictionary<string, List<string>> filePathsDictionary)
        {
            using var content = new MultipartFormDataContent();
            List<FileStream> streams = new List<FileStream>();
            try
            {
                // 先ずJsonをContentに含めた
                var requestJson = JsonConvert.SerializeObject(request);
                var jsonContent = new StringContent(requestJson, Encoding.UTF8, "application/json");
                jsonContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
                {
                    Size = requestJson.Length,
                    // コントローラーの関数の引数と一致させる
                    Name = "json",
                };
                content.Add(jsonContent);

                // 指定パスのファイルをContentに含める
                foreach (var filePaths in filePathsDictionary)
                {
                    var partName = filePaths.Key;
                    foreach (var filePath in filePaths.Value)
                    {
                        FileStream fs = File.OpenRead(filePath);
                        string fileName = Path.GetFileName(filePath);
                        content.Add(CreateFileContent(fs, partName, fileName));
                        streams.Add(fs);
                    }
                }
                // 送信
                await client.PostAsync(api, content);
            }
            finally
            {
                streams.ForEach(x => x.Close());
            }
        }

        private static StreamContent CreateFileContent(Stream stream, string partName, string fileName)
        {
            var fileContent = new StreamContent(stream);
            fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
            {
                Size = stream.Length,
                // コントローラーの関数の引数と一致させる
                Name = partName,
                FileName = fileName
            };

            // 拡張子から自動でcontent-typeを割り出してくれる
            // Microsoft.AspNetCore.StaticFilesのインポートが必要
            if (new FileExtensionContentTypeProvider().TryGetContentType(fileName, out string contentType))
            {
                fileContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
                return fileContent;
            }
            else
            {
                throw new Exception($"拡張子が不明なファイルが指定されました。ファイル名{fileName}");
            }
        }
    }
}
```

サーバーを起動した状態で、コンソールアプリを起動すれば、リクエストが飛ぶと思います。