Tag Archives: ASP.NET Web API

Helios を使うとワーカースレッド数が制限されていた話

まだ、Version 1.0.0 – alpha 1 ですが、Project Helios を使った Web アプリケーションでは、リクエストの実行に使用するワーカースレッドと完了ポートスレッドの数が制限されていました。

気が付いたきっかけは、Azure Web サイトにデプロイして動かした際に、「System.InvalidOperationException:There were not enough free threads in the ThreadPool to complete the operation.」の例外が発生したことでした。

次のようなテスト用の Web API を Azure Web サイトにデプロイして、確認してみました。

public class Test
{
    public int MaxWorkerThreads { get; set; }
    public int MaxCompletionPortThreads { get; set; }
    public int ProcessorCount { get; set; }
}

[Route("api/test")]
public class TestController : ApiController
{
    public IHttpActionResult Get()
    {
        int workerThreads = 0;
        int completionPortThreads = 0;
        ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);

        int processorCount = Environment.ProcessorCount;

        return base.Ok(new Test {
                            MaxWorkerThreads = workerThreads,
                            MaxCompletionPortThreads = completionPortThreads,
                            ProcessorCount = processorCount });
    }
}
無料・共有(Heliosなし) 無料・共有(Heliosあり) 基本・標準(Heliosなし) 基本・標準(Heliosあり)
MaxWorkerThreads 8191 16 8191 2
MaxIoThreads 1000 16 1000 2
ProcessorCount 8 8 1 1

基本と標準は、Sインスタンスです。上記の結果の通り、Helios を使うと、論理コア数あたり2個が設定されています。なお、Visual Studio 14 CTP 4 の ASP.NET vNext でも、Helios と同様の値が設定されていました。

ワーカースレッド数を変更するとしたら、こちらの推奨値を参考にすると、このような感じでしょうか?

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        int processorCount = Environment.ProcessorCount;
        ThreadPool.SetMaxThreads(100 * processorCount, 100 * processorCount);

        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();
        app.UseWebApi(config);

        app.UseWelcomePage();
    }
}

Helios の正式版がリリースされる可能性は低いですが、ASP.NET vNext は今後のアップデートで変わってくると思いますので、あくまで現時点の情報ということになります。

 

 

ASP.NET Web API で Azure Redis Cache を利用する

クラウドデザインパターンの Cache-Aside Pattern を ASP.NET Web API で実装してみました。Cache-Aside Pattern は、オンデマンドでデータをキャッシュに効率的に読み込むパターンです。キャッシュ機構には、Azure Redis Cache(Preview)を利用しています。

Azure Redis Cache の作成

事前準備として、新しいAzure 管理ポータルで Redis Cache を作成しておきます。任意の名前を入力し、プランとDCのリージョンを選択します。なお、フル機能の旧ポータルからは作成できません。後ほど使うので、Redis Cache の Keys(接続文字列)をメモしておきます。

プロジェクトの作成

Visual Studio 2013 で、WebApplication のプロジェクトを選択し、Web API のテンプレートで作成します。Redis Cache の C# 用ライブラリの NuGet パッケージをインストールします。

  • Install-Package StackExchange.Redis

Person クラスを操作する Web API を作成します。ID と Name だけの Person クラスを定義し、スキャフォールディングで「Entity Framework を使用したアクションがある Web API 2 コントローラー」の PersonController を追加します。

Azure Redis Cache への接続

プログラムから Azure Redis Cache のインスタンスに接続するには、ConnectionMultiplexer クラスを使います。ConnectionMultiplexer のインスタンスは、アプリケーションで共有できるので、スタティックなプロパティとして実装します。Azure 管理ポータルから取得できる Redis Cache の Keys(接続文字列)を渡して接続します。

public class PersonController : ApiController
{
    private static ConnectionMultiplexer connection;
    private static ConnectionMultiplexer Connection
    {
        get
        {
            if (connection == null || !connection.IsConnected)
            {
                connection = ConnectionMultiplexer.Connect("xxx.redis.cache.windows.net,ssl=true,password=xxx");
            }
            return connection;
        }
    }
}

.NET オブジェクト用の拡張メソッド

String や int などのプリミティブなデータ型であれば、StackExchange.Redis クライアントの StringSet と StringGet のメソッドでアクセスできますが、Person クラスのような任意の型の場合、オブジェクトをシリアル化する必要があります。今回は、Json.NET を使った JSON 形式のシリアル化を行う拡張メソッドを作成します。

public static class JsonNetRedisExtensions
{
    public static async Task<T> GetAsync<T>(this IDatabase cache, string key)
    {
        return await DeserializeAsync<T>(cache.StringGet(key));
    }

    public static async Task SetAsync(this IDatabase cache, string key, object value, TimeSpan? expiry = null)
    {
        await cache.StringSetAsync(key, await SerializeAsync(value), expiry);
    }

    static async Task<string> SerializeAsync(object target)
    {
        if (target == null)
        {
            return null;
        }
        return await JsonConvert.SerializeObjectAsync(target);
    }

    static async Task<T> DeserializeAsync<T>(string json)
    {
        if (json == null)
        {
            return default(T);
        }
        return await JsonConvert.DeserializeObjectAsync<T>(json);
    }
}

データの取得

GetPerson メソッドにおいて、Cache-Aside Pattern のロジックを実装します。ConnectionMultiplexer.GetDatabase メソッドを呼び出すことで、Redis Cache への参照が返されます。キャッシュにデータがあればそのまま返却し、なければデータベースから取得したデータをキャッシュに追加してから返却します。キャッシュのキーはPerson の ID 、有効期間は10分を設定しています。

public async Task<IHttpActionResult> GetPerson(int id)
{
    // キャッシュからデータを取得
    IDatabase cache = Connection.GetDatabase();
    var person = await cache.GetAsync<Person>(CacheKey(id));
    if (person == null)
    {
        // キャッシュになければ、データベースから取得した結果を保存
        person = await db.People.FindAsync(id);
        if (person == null)
        {
            return NotFound();
        }
        await cache.SetAsync(CacheKey(id), person, TimeSpan.FromMinutes(10));
    }
    return Ok(person);
}

static string CacheKey(int id)
{
    return String.Format("PersonId:{0}", id);
}

データの更新と削除

PutPerson メソッドにおいて、データベースの情報が更新されたら、キャッシュを削除します。複数クライアントからの同時更新を考えると、キャッシュを更新するよりも削除したほうが、キャッシュとデータベースの一貫性を確保し易くなります。

public async Task<IHttpActionResult> PutPerson(int id, Person person)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (id != person.Id)
    {
        return BadRequest();
    }

    db.Entry(person).State = EntityState.Modified;

    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!PersonExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    // キャッシュを削除
    IDatabase cache = Connection.GetDatabase();
    await cache.KeyDeleteAsync(CacheKey(id));

    return StatusCode(HttpStatusCode.NoContent);
}

DeletePerson メソッドにおいても、キャッシュを削除しておます。

public async Task<IHttpActionResult> DeletePerson(int id)
{
    Person person = await db.People.FindAsync(id);
    if (person == null)
    {
        return NotFound();
    }

    db.People.Remove(person);
    await db.SaveChangesAsync();

    // キャッシュを削除
    IDatabase cache = Connection.GetDatabase();
    await cache.KeyDeleteAsync(CacheKey(id));

    return Ok(person);
}

まとめ

より多くのリクエストを捌く必要が出てきた場合、Azure SQL Database はボトルネックになるケースが多いです。データベースに頼り過ぎず、静的データをキャッシュするなら、Cache-Aside Pattern は有効なパターンだと思います。Azure Redis Cache が東日本リージョンにも来たことですし、 Preview から GA することを待つばかりです。

OWIN の Middleware をサブディレクトリに適用する

先週、OWINって何?Microsoft MVPに聞いてみよう! に参加してきました。サブディレクトリに、OWIN の Middleware を適用する方法を教えて頂いたので、試してみました。Web API へのリクエストだけに ロギング Middleware を追加するシナリオで実装しました。

ロギングの Middleware をクラスライブラリに実装する

ClassLibrary プロジェクトに NuGet パッケージをインストールします。

  • Install-Package Microsoft.Owin

ロギングの Middleware を実装します。

public class CustomLoggerMiddleware : OwinMiddleware
{
    public CustomLoggerMiddleware(OwinMiddleware next)
        : base(next)
    {

    }

    public override async Task Invoke(IOwinContext context)
    {
        Trace.TraceInformation("Web API Start");
        await base.Next.Invoke(context);
        Trace.TraceInformation("Web API End");
    }
}

Invoke の前後にトレースを入れるだけのシンプルなロギングです。

Webアプリケーションを実装する

空のWebApplication プロジェクトに NuGet パッケージをインストールします。

  • Install-Package Microsoft.AspNet.WebApi.Owin
  • Install-package Microsoft.Owin.Host.SystemWeb

OWIN パイプラインに Middleware を追加します。

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Map("/api", subApp =>
        {
            subApp.Use<CustomLoggerMiddleware>();

            var config = new HttpConfiguration();
            config.MapHttpAttributeRoutes();
            subApp.UseWebApi(config);
        });
    }
}

引数の app の Use メソッドに Middleware を追加すると、すべてのリクエストに対して Middleware が動きます。IAppBuilder の拡張メソッドである Map メソッドを使っているのがポイントです。subApp の Use メソッドに Web API と CustomLogger の Middleware を追加することで、”/api” から始まる URL のリクエストに対して Middleware が動きます。

Web API の Controller を追加します。

// φ(..) "api/test" では、404 になるので注意!
[Route("test")]
public class TestController : ApiController
{
    // GET api/test
    public string Get()
    {
        Trace.TraceInformation("TestController.Get()");
        return "Hello OWIN !";
    }
}

GETで “api/test” にリクエストすると、「Hello OWIN !」を返すシンプルな Web API です。Route 属性で “api/test” と設定すると、404 エラーが発生して Get メソッドを呼び出せません。“/api” から始まる URL のリクエストに対して Web API が追加されているので、”/api” を省略して “test” と設定する必要があります。

実行結果

GETで “api/test” にリクエストすると、Visual Studio の出力ウィンドウには、以下のようにログが表示されます。

logger

まとめ

OWIN の複数の Middleware を組み合わせたときに、機能の重複を回避したり、特定のリクエストのみに限定したい場合などは、この方法が使えそうです。ただ、今回のシナリオのように、Web API のリクエストだけをロギングするのであれば、OWIN の Middleware でなく、Web API の ActionFilter で作るのもアリです。

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

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

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

クライアント側

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

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 のコンテンツを引数にバインディングする型を用意します。

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 クラスの派生クラスを実装します。

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 クラスを登録します。

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

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

[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 メソッドでカスタムの結果を返すことも可能です。

ASP.NET Web API で multipart / form-data を使ってファイルをアップロードする

ASP.NET Web API でファイルをアップロードする際に、Content-Type の異なるデータを送信したかったので、multipart / form-data を使う方法を試してみました。画像ファイルのバイナリデータとファイル名を送信して、Azure Blob Storage にアップロードするシナリオで実装しました。

クライアント側

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

private async void Button_Click(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");
            multipart.Add(fileContent, JsonConvert.SerializeObject("buffer"));

            var addContent = new StringContent(JsonConvert.SerializeObject("test.jpeg"));
            addContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            multipart.Add(addContent, JsonConvert.SerializeObject("fileName"));

            var response = await new HttpClient().PostAsync("http://localhost:23436/api/blob/", multipart);
            response.EnsureSuccessStatusCode();
            await new MessageDialog("成功!").ShowAsync();
        }
    }
}

MultipartFormDataContent に ByteArrayContent(画像ファイル)と StringContent(ファイル名)を含めて、HttpClient クラスで POST しています。Add メソッドで指定している名前(buffer と fileName)は、サーバー側で値を取り出すときに使います。

サーバー側

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

[HttpPost]
public async Task<IHttpActionResult> Upload()
{
    if (Request.Content.IsMimeMultipartContent() == false)
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }

    var provider = await Request.Content.ReadAsMultipartAsync();
    var fileContent = provider.Contents.First(x => x.Headers.ContentDisposition.Name == JsonConvert.SerializeObject("buffer"));
    var buffer = await fileContent.ReadAsByteArrayAsync();
    var json = await provider.Contents.First(x => x.Headers.ContentDisposition.Name == JsonConvert.SerializeObject("fileName")).ReadAsStringAsync();
    var fileName = JsonConvert.DeserializeObject<string>(json);

    var blob = this.container.GetBlockBlobReference(fileName);
    blob.Properties.ContentType = fileContent.Headers.ContentType.MediaType;
    await blob.UploadFromByteArrayAsync(buffer, 0, buffer.Length);

    return Ok();
}

ReadAsMultipartAsync メソッドで、マルチパートのコンテンツを読み取るプロバイダークラスを取得します。このプロバイダークラスを使って、クライアント側で指定した名前(buffer と fileName)をキーに、ByteArrayContent(画像ファイル)と StringContent(ファイル名)を取り出すことができます。

まとめ

ASP.NET Web API で Content-Type の異なるデータを送信するには、multipart / form-data を使います。複数のファイルをまとめてアップロードすることも可能です。ちなみに、ファイル名であれば、multipart / form-data ではなく、Content-Disposition ヘッダーの FileName を使う方法もあります。