ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

ASP.NET Web API で MediaTypeFormatter を追加する

gooner.hateblo.jp

前回の投稿では、multipart / form-data を使ってファイルをアップロードしました。ASP.NET Web API において、クライアントから POST されたコンテンツが POST や PUT メソッドの引数にバインディングされるのは、JSON や XML 用の MediaTypeFormatter がデフォルトで実装されているからです。

カスタムの MediaTypeFormatter クラスを実装して、multipart / form-data のコンテンツを引数にバインディングしてみました。前回と同じく、画像ファイルのバイナリデータとファイル名を送信して、Azure Blob Storage にアップロードするシナリオです。

クライアント側

Windows ストア アプリ で、FileOpenPicker から選択した画像ファイルをアップロードします。

#MainPage.xaml.cs
private async void Button_Click_2(object sender, RoutedEventArgs e)
{
    var picker = new FileOpenPicker();
    picker.ViewMode = PickerViewMode.Thumbnail;
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    picker.FileTypeFilter.Add(".jpg");
    picker.FileTypeFilter.Add(".jpeg");
    var storageFile = await picker.PickSingleFileAsync();
    if (storageFile != null)
    {
        var dialog = new MessageDialog("アップロードする?");
        dialog.Commands.Add(new UICommand("OK"));
        dialog.Commands.Add(new UICommand("Cansel"));
        var stream = await storageFile.OpenStreamForReadAsync();
        var byteArray = new byte[stream.Length];
        var result = await dialog.ShowAsync();
        if (result.Label == "OK")
        {
            var multipart = new MultipartFormDataContent();
 
            stream.Read(byteArray, 0, (int)stream.Length);
            var fileContent = new ByteArrayContent(byteArray);
            fileContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
            fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
            fileContent.Headers.ContentDisposition.FileName = "test.jpeg";
            multipart.Add(fileContent);
 
            var response = await new HttpClient().PostAsync("http://localhost:23436/api/blob/", multipart);
            response.EnsureSuccessStatusCode();
            await new MessageDialog("成功!").ShowAsync();
        }
    }
}

MultipartFormDataContent に ByteArrayContent(画像ファイル)を含めて、HttpClient クラスで POST しています。ファイル名は、Content-Disposition ヘッダーの FileName にセットしています。

サーバー側

multipart / form-data のコンテンツを引数にバインディングする型を用意します。

#ImageMedia.cs
public class ImageMedia
{
    public byte[] Buffer { get; set; }
    public string FileName { get; set; }
    public string MediaType { get; set; }
 
    public ImageMedia(byte[] buffer, string fileName, string mediaType)
    {
        this.Buffer = buffer;
        this.FileName = fileName;
        this.MediaType = mediaType;
    }
}

MediaTypeFormatter クラスの派生クラスを実装します。

#MultipartMediaTypeFormatter.cs
public class MultipartMediaTypeFormatter : BufferedMediaTypeFormatter
{
    public MultipartMediaTypeFormatter()
    {
        base.SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg"));
        base.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
    }
 
    public override bool CanReadType(Type type)
    {
        return (type == typeof(ImageMedia));
    }
 
    public override bool CanWriteType(Type type)
    {
        return false;
    }
 
    public override object ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        if (content.IsMimeMultipartContent() == false)
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }
 
        var provider = content.ReadAsMultipartAsync().Result;
 
        var fileContent = provider.Contents.First(x => base.SupportedMediaTypes.Contains(x.Headers.ContentType));
        var buffer = fileContent.ReadAsByteArrayAsync().Result;
        var fileName = fileContent.Headers.ContentDisposition.FileName;
        var mediaType = fileContent.Headers.ContentType.MediaType;
 
        return new ImageMedia(buffer, fileName, mediaType);
    }
}

ReadAsMultipartAsync メソッドで、マルチパートのコンテンツを読み取るプロバイダークラスを取得します。このプロバイダークラスを使って、ByteArrayContent(画像ファイル)を取り出し、ImageMedia クラスに変換します。

WebApiConfig クラスで、カスタムの MediaTypeFormatter クラスを登録します。

#WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
    config.Formatters.Add(new MultipartMediaTypeFormatter());
    config.MapHttpAttributeRoutes();
}

Azure Blob Storage にファイルをアップロードする BlobController を用意します。

#BlobController.cs
[HttpPost]
public async Task<IHttpActionResult> Upload([FromBody]ImageMedia imageMedia)
{
    var blob = this.container.GetBlockBlobReference(imageMedia.FileName);
    blob.Properties.ContentType = imageMedia.MediaType;
    await blob.UploadFromByteArrayAsync(imageMedia.Buffer, 0, imageMedia.Buffer.Length);
    return Ok();
}

引数のImageMedia クラスから画像ファイルのデータを受け取って、Blob にアップロードします。

まとめ

ASP.NET Web API では、カスタムの MediaTypeFormatter クラスを実装することで、さまざまなフォーマットのデータを POST や PUT メソッドの引数にマッピングできます。コンテンツのフォーマットやシリアライズを意識することなく、Controller クラスを実装できるようになります。

また、MediaTypeFormatter クラスの WriteToStream メソッドを実装することで、GET メソッドでカスタムの結果を返すことも可能です。