miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET Web API 2 Request Batching

ASP.NET Web API 2 新機能シリーズ、次は「Request Batching」です。

Request Batching(バッチリクエスト) とは、複数の HTTP リクエストを1つの HTTP リクエストにまとめて、1回の通信で複数の処理を実行するというものです。

メリットは

  • ネットワークトラフィックの低減
  • サーバー側が公開する API (インターフェイス)が少なくなる

があります。

ASP.NET Web API 2 はこの Request Batching に対応し、主に以下の機能を持ちます。

  • OData と 標準の HTTP の両方のリクエストに対応。
  • 独自の形式で複数のリクエストが格納された HTTP リクエストを解析。(要拡張)
  • 1つ1つのリクエストの実行を制御。(同期・非同期で行える)

また、ApiController の方は、通常通りの API の実装を行います。

つくってみた

バッチリクエストを試すため、こんなコメントを追加&削除する Windows ストアアプリを作ってみました。

Web API は 2 つだけ用意します。

  • POST api/comments コメント追加
  • DELETE api/comments/{id} Id が一致するコメント削除

このアプリは複数選択されたコメントを削除できるのですが、バッチリクエストを使って1回の送信で済むようにしてみます。

HTTP リクエスト

削除する時のリクエストはこのようになります。
1つの HTTP リクエストが複数格納されています。

POST 'http://localhost:33591/api/batch HTTP/1.1
Content-Type: multipart/mixed; boundary="f798dbdc-90f1-43f0-a058-117c5c3823f5"

--f798dbdc-90f1-43f0-a058-117c5c3823f5
Content-Type: application/http; msgtype=request

DELETE /api/comments/1 HTTP/1.1
Host: localhost:33591


--f798dbdc-90f1-43f0-a058-117c5c3823f5
Content-Type: application/http; msgtype=request

DELETE /api/comments/2 HTTP/1.1
Host: localhost:33591


--f798dbdc-90f1-43f0-a058-117c5c3823f5--

バッチリクエストは、POST で送信します。
また、これは HTTP の例であり OData の場合はまた違う形式になります。

HTTP レスポンス

返ってくるレスポンスも、複数の HTTP レスポンスが格納されています。

HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary="bf9ee5fe-1c89-4fa9-a6c7-b32f2fe6d180"

--bf9ee5fe-1c89-4fa9-a6c7-b32f2fe6d180
Content-Type: application/http; msgtype=response

HTTP/1.1 200 OK


--bf9ee5fe-1c89-4fa9-a6c7-b32f2fe6d180
Content-Type: application/http; msgtype=response

HTTP/1.1 200 OK


--bf9ee5fe-1c89-4fa9-a6c7-b32f2fe6d180--

実装 - Web API 側

まずは、ASP.NET Web API で Web API を公開します。
最初に、2 つだけ Web API を定義するといいましたが、面倒なのでスキャフォールディング機能で
一式コードを生成してしまいます。

★Visual Studio 2013 で、プロジェクトを作成します。

ファイル>新規作成>プロジェクト>ASP.NET Web アプリケーション、
.NET Framework のバージョンは、4.5 を選択。

「テンプレートの選択」:Empty、「以下にフォルダーおよびコア参照を追加」:Web API にチェック、プロジェクト作成。(プロジェクト名は何でもいいですがここでは「RequestBatching」としました。)

★コメントのモデルクラスを作成します。

「Models」フォルダを右クリック>追加>新しい項目>クラス>Comment.cs

namespace RequestBatching.Models
{
    public class Comment
    {
        public int Id { get; set; }
        public string Text { get; set; }
    }
}

★ソリューションをビルドします
ビルド>ソリューションのビルド

★ApiController を作成します

「Controllers」フォルダを右クリック>追加>スキャフォールディングビュー、
Entity Framework を使用した読み取り/書き込みアクションがある Web API 2 コントローラー、

表示されるダイアログにて、
コントローラー名:CommentsController
Use async controller actions にチェック
モデルクラス:Comment (RequestBatching.Models)
データコンテキストクラス:<新しいデータコンテキスト>→「RequestBatching.Models.RequestBatchingContext」

CommentsController クラスが生成されるのを待ちます。

★ルーティングの設定
「App_Start」フォルダにある WebApiConfig.cs に、以下のようにコードを追加します。

using System.Web.Http;
using System.Web.Http.Batch;

namespace RequestBatching
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            //ここから
            var batchHandler = new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer)
            {
                ExecutionOrder = BatchExecutionOrder.NonSequential
            };
            config.Routes.MapHttpBatchRoute(
                routeName: "WebApiBatch",
                routeTemplate: "api/batch",
                batchHandler: batchHandler);
            //ここまでのコードを追加する

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}

コードで指定した URL「~/api/batch」 で、バッチリクエストに対応できるようになります。
「ExecutionOrder = BatchExecutionOrder.NonSequential」は、バッチリクエストで指定した 各 API を非同期で実行することを指定しています。

以上で、Web API 側の実装は終わりです。

実装 - Windows Store アプリ側

先ほどは Visual Studio 2013 RC でしたが、OS が Windows 8 だとアプリが作れないので、Visual Studio 2012 を起動します。
(参考:第2回 初めてのWindowsストア・アプリ開発 (1/2)

★プロジェクトを作成します。

ファイル>新しいプロジェクト>Windows ストアの「新しいアプリケーション」、
.NET Framework 4.5 を選択し、「OK」。

プロジェクトが作成されたら、HttpClient を使うために、NuGet から取得します。

パッケージマネージャコンソールで、以下のコマンドを打ち込みます。

PM> Install-Package Microsoft.AspNet.WebApi.Client

★コメントのモデルクラスを作成します。
Web API 側でも作成した、コメントクラスを作成します。
Web API の通信で、受け取った値をコメントクラスで扱うためです。
(同じライブラリを参照できるのなら2度も作成する必要ないのですが。)
場所は、プロジェクトの直下でかまいません。「Comment.cs」を作成します。

★スタイルを定義
アイコンボタンのスタイルを使用します。
「Common」フォルダにある StandardStyles.xaml の中に、下のコードがコメントされているので、コメントアウトします。(DeleteAppBarButtonStyle で検索かけるとよいです。)

<Style x:Key="DeleteAppBarButtonStyle" TargetType="ButtonBase" BasedOn="{StaticResource AppBarButtonStyle}">
    <Setter Property="AutomationProperties.AutomationId" Value="DeleteAppBarButton"/>
    <Setter Property="AutomationProperties.Name" Value="Delete"/>
    <Setter Property="Content" Value="&#xE106;"/>
</Style>

これは削除ボタンのスタイルです。

★MainPage.xaml

MainPage.xaml のコードを、以下を参照してコピペします。

<Page
    x:Class="App2.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App2"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
	
	<Page.Resources>
		<x:String x:Key="AppName">My Application</x:String>
	</Page.Resources>
	
	<Page.BottomAppBar>
		<AppBar x:Name="BottomAppBar1" Padding="10,0,10,0">
			<Grid>
				<Grid.ColumnDefinitions>
					<ColumnDefinition Width="*"/>
				</Grid.ColumnDefinitions>
				<StackPanel x:Name="LeftPanel" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
					<Button x:Name="Delete"  Style="{StaticResource DeleteAppBarButtonStyle}" Tag="Delete" Click="Delete_Click"/>
				</StackPanel>
			</Grid>
		</AppBar>
	</Page.BottomAppBar>
	
	<StackPanel Orientation="Vertical" Grid.Column="0" HorizontalAlignment="Left" Width="1366">

		<Grid Margin="120,0">
			<Grid.RowDefinitions>
				<RowDefinition Height="100"/>
			</Grid.RowDefinitions>
			<Grid.ColumnDefinitions>
				<ColumnDefinition Width="400"/>
				<ColumnDefinition Width="*"/>
			</Grid.ColumnDefinitions>
			<TextBox x:Name="newCommentTextBox" Grid.Column="0" FontSize="18" VerticalAlignment="Bottom" Height="42"/>
			<Button  Tag="Add" Click="Add_Click" Grid.Column="1"  FontSize="18" VerticalAlignment="Bottom" Content="Add" BorderThickness="2" Margin="20,0,0,0"></Button>
		</Grid>

		<ListView x:Name="itemListView" Width="800"
			  Margin="120,40,0,50" SelectionMode="Multiple" HorizontalAlignment="Left">
			<ListView.ItemTemplate>
				<DataTemplate>
					<Grid>
						<Grid.RowDefinitions>
							<RowDefinition Height="50"/>
						</Grid.RowDefinitions>
						<Grid.ColumnDefinitions>
							<ColumnDefinition Width="50"/>
							<ColumnDefinition Width="*"/>
						</Grid.ColumnDefinitions>
						<Border Background="DimGray">
						</Border>
						<StackPanel Grid.Column="1" VerticalAlignment="Center"  Margin="10,0,0,0">
							<TextBlock Text="{Binding Text}" Style="{StaticResource TitleTextStyle}" TextWrapping="NoWrap" FontSize="20" VerticalAlignment="Center" />
						</StackPanel>
					</Grid>
				</DataTemplate>
			</ListView.ItemTemplate>
		</ListView>

	</StackPanel>
</Page>

※プロジェクト名を「App2」にした場合のコードなので、「App2」をプロジェクト名に置換する必要があります。
※StackPanel と Grid の使い方も合っているか自信ないです。orz

★MainPage.xaml.cs

MainPage.xaml.cs のコードを以下を参考に追加します。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

// 空白ページのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=234238 を参照してください

namespace App2
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
		private readonly string baseAddress = "http://localhost:33591/";

        public MainPage()
        {
            this.InitializeComponent();
        }

        /// <summary>
        /// このページがフレームに表示されるときに呼び出されます。
        /// </summary>
        /// <param name="e">このページにどのように到達したかを説明するイベント データ。Parameter 
        /// プロパティは、通常、ページを構成するために使用します。</param>
        protected override async void OnNavigatedTo(NavigationEventArgs e)
		{
			base.OnNavigatedTo(e);
	        await LoadComment();
		}

		private async void Delete_Click(object sender, RoutedEventArgs e)
		{
			await DeleteComments();
		}
		
		private async void Add_Click(object sender, RoutedEventArgs e)
		{
			await AddComment(this.newCommentTextBox.Text);
			this.newCommentTextBox.Text = String.Empty;
		}
		
		private async Task LoadComment()
		{
			var client = new HttpClient() { BaseAddress = new Uri(baseAddress) };
			using (var result = await client.GetAsync("api/comments"))
			{
				if (!result.IsSuccessStatusCode)
				{
					return;
				}
				var comments = await result.Content.ReadAsAsync<IEnumerable<Comment>>();
				this.itemListView.ItemsSource = new ObservableCollection<Comment>(comments);
			}
		}

		private async Task AddComment(string text)
		{
			var client = new HttpClient() { BaseAddress = new Uri(baseAddress) };
			using (var result = await client.PostAsJsonAsync("api/comments", new { text = text }))
			{
				if (!result.IsSuccessStatusCode)
				{
					return;
				}
				await LoadComment();
			}
		}

		private async Task DeleteComments()
		{
			var selectedItems = this.itemListView.SelectedItems;

			using (var client = new HttpClient() {BaseAddress = new Uri(baseAddress)})
			{
				var multiContent = new MultipartContent("mixed");
				foreach (object selectedItem in selectedItems)
				{
					Comment selectedComment = selectedItem as Comment;
					var deleteCommentContent = new HttpMessageContent(
						new HttpRequestMessage(
							HttpMethod.Delete, baseAddress + "api/comments/" + selectedComment.Id.ToString()));
					multiContent.Add(deleteCommentContent);
				}
				var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "api/batch")
					{
						Content = multiContent
					};

				await client.SendAsync(batchRequest);
				await LoadComment();
			}
		}
    }
}

baseAddress は、Web API 側の起動 URL を記述します。
DeleteComments() メソッド内が、バッチリクエストを構築している部分になります。
※非同期の使い方、たぶん変な実装してます。orz
※追加ボタンは、場所がアプリバーではないので、AddAppBarButtonStyle は使わないほうが良いです。

以上

以上で終了です。
Web API とストアアプリのプロジェクトを実行すると、コメントの追加&削除が行えると思います。

ストアアプリ側の実装が下手な部分がありますが、これでバッチリクエストが試せます。
(始めは Web から操作しようと思いましたが、バッチリクエストを送信する方法が、JavaScript で HTTP リクエストをゴリゴリ書く方法しかなかったので諦めました。)
(OData であれば、datajsで送信できるみたい)