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=""/> </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で送信できるみたい)
参考サイト
Web API
ストアアプリ
- 連載:Windowsストア・アプリ開発入門
- セルフホストWeb APIをWindows ストア アプリから呼び出すサンプル
- クイック スタート: ListView コントロールと GridView コントロールの追加
- 良いWindowsストア アプリの作り方 vol.02 ~アプリバーの使い方~
(※10/10 ストアアプリのデザインとコードを修正。)