ROMANCE DAWN for the new world

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

ASP.NET Web API を経由して Azure Blob Storage にアクセスする

実際に試してみたら、少しハマってしまったので、備忘も兼ねて投稿しておきます。

Azure Storage Client を使って Blob にアクセスできますが、

  • クライアントに Azure Storage のキーや署名情報を公開したくない
  • アクセスログを取りたい

といった要件があった場合、ASP.NET Web API を経由して Blob にアクセスすることで、データが Webサーバー上にあるかのように扱うことができます。

Blob の test コンテナにファイルをアップロード/ダウンロードできる Web API を実装してみました。

#BlobController.cs
[RoutePrefix("api/blob/{blobName}")]
public class BlobController : ApiController
{
    private CloudBlobContainer container;
 
    public BlobController()
    {
        var storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString"));
        var blobClient = storageAccount.CreateCloudBlobClient();
        this.container = blobClient.GetContainerReference("test");
        this.container.CreateIfNotExists();
    }
 
    [Route]
    // GET api/blob/sample.jpg
    public async Task<IHttpActionResult> Get(string blobName)
    {
        var blob = await this.container.GetBlobReferenceFromServerAsync(blobName);
        var content = new byte[blob.Properties.Length];
        await blob.DownloadToByteArrayAsync(content, 0);
 
        var response = new HttpResponseMessage(HttpStatusCode.OK);
        response.Content = new ByteArrayContent(content);
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(blob.Properties.ContentType);
        return ResponseMessage(response);
    }
 
    [Route]
    // POST api/blob/sample.jpg
    public async Task<IHttpActionResult> Post([FromBody]byte[] content, [FromUri]string blobName)
    {
        var blob = this.container.GetBlockBlobReference(blobName);
        blob.Properties.ContentType = Request.Content.Headers.ContentType.MediaType;
        await blob.UploadFromByteArrayAsync(content, 0, content.Length);
 
        return Created(Request.RequestUri, "");
    }
}

HTTP の GET メソッドで「api/blob/sample.jpg」にリクエストすると、ファイルをダウンロードできます。

ハマったのはアップロードです。クライアント側は、WPFアプリでリクエストを投げています。HTTP の POST メソッドで「api/blob/sample.jpg」にリクエストすると、HTTP ステータスコードの 415(Unsupported Media Type)が返されます。

#MainWindow.xaml.cs
private async void Button_Click(object sender, RoutedEventArgs e)
{
 var client = new HttpClient();
 var content = new ByteArrayContent(File.ReadAllBytes(@"C:\Sample.jpg"));
 content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
 await client.PostAsync("http://localhost:2839/api/blob/sample.jpg/", content);
}

いろいろと試してみたところ、BlobController の実装を変更することで解決しました。

#BlobController.cs
[Route]
// POST api/blob/sample.jpg
public async Task<IHttpActionResult> Post([FromUri]string blobName)
{
    var content = await Request.Content.ReadAsByteArrayAsync();
 
    var blob = this.container.GetBlockBlobReference(blobName);
    blob.Properties.ContentType = Request.Content.Headers.ContentType.MediaType;
    await blob.UploadFromByteArrayAsync(content, 0, content.Length);
 
    return Created(Request.RequestUri, "");
}

byte のデータを引数ではなく、Request.Content プロパティから ReadAsByteArrayAsync メソッドで受け取る形に変更したところ、無事にアップロードできました。

これまでは、POSTメソッドの Body で string や int などのプリミティブ型や独自のクラス型を JSON でやり取りする分には問題ありませんでした。何故 byte や Stream のような型だとうまくいかないのかの原因までは分からずにスッキリしなかったので、また時間が取れたら確認してみようと思います。

追記:原因が分かったので、こちらの記事にまとめました。

gooner.hateblo.jp

追記:ASP.NET Core の場合

gooner.hateblo.jp