Silverlight から、Windows Azure の Table ストレージ サービスを利用する方法を検討してみました。考えられる方法は、以下が考えられます。
- Web サービスを経由してアクセスする
RIA サービス(内部で Table ストレージを使用する) -- Silverlight クライアント
Web サービス側では、Azure SDK のストレージ クライアント ライブラリを使用できる - ダイレクトにアクセスする
Table ストレージ サービス -- Silverlight クライアント
Table ストレージは OData 形式なので、 WCF Data サービス クライアントが使えるかも
RIA サービスを使った Table ストレージの利用方法は、探せばサンプルが見つかることでしょう。ここでは、残りの WCF Data サービス クライアントを使う方法を検討します。最初に検討しないといけないのが、リクエスト ヘッダーに対して情報を追加することが可能かという点です。これに関しては、System.Data.Services.Client.DataServiceContext クラスが SendingRequest イベントを公開しているので、このイベントを使用すればできそうです。 SendingRequest イベントは、 引数として sender (DataServiceContextクラス) と e (SendingRequestEventArgs クラス) を取ります。以下にイベントの シグネチャと SendingRequest EventArgs クラスのシグネチャを示します。
// SendignRequest イベントのシグネチャ private void DataContextSendingRequest(object sender, SendingRequestEventArgs e) // SendingRequestEventArgs クラスのシグネチャ public class SendingRequestEventArgs : EventArgs { public WebHeaderCollection RequestHeaders { get; } public virtual bool Equals(object obj) protected virtual void Finalize() public virtual int GetHashCode() public Type GetType() protected object MemberwiseClone() public virtual string ToString() }
SendingRequestEventArgs クラスのメンバーをデスクトップ用のライブラリと較べると、リクエスト オブジェクトを取得するプロパティが無いことに気が付きます。 Azure の Storage サービスでは、SAS を public にしない限りは認証スキームに従って Authorization ヘッダーを付与する必要があります。Authorization ヘッダーに設定する署名を作成するための CanonicalizedResource を作るには リクエスト URI が必要になります。リクエスト オブジェクトが取得できないため、別の方法でリクエスト URI を取得するか設定しなければなりません。この部分は、Azure Toolkit for Windows Phoneでは、 独自にData Server Client を実装することで解決しています。この辺りの実装方法を Azure Toolkit for Windows Phone を参考にしながら、考えていきましょう。どのように実装するかと言えば、DataServiceContext を継承した TableServiceContext クラスを実装することで、リクエスト URI を取り出せないかを模索してみます(デスクトップ用のライブラリでも TableServiceContext クラスが提供されています)。
public class TableServiceContext : DataServiceContext { public IStorageCredentials StorageCredentials { get; set; } // Azure Toolkit の StorageCredentialsAccountAndKey クラスを引数にします // StorageCredentialsAccountAndKey.GetEndpoint メソッドは、追加メソッドで // 該当する Uri 文字列を返すものです public TableServiceContext(StorageCredentialsAccountAndKey credentials) :base(new Uri(credentials.GetEndpoint(ServiceType.table))) { // SendingRequest イベント ハンドラー を設定します this.SendingRequest += this.DataContextSendingRequest; this.IgnoreMissingProperties = true; this.MergeOption = MergeOption.PreserveChanges; this.StorageCredentials = credentials; } private void DataContextSendingRequest(object sender, SendingRequestEventArgs e) { // SendingRequestEventArgs には、リクエスト オブジェクトが含まれません // この実装をこれから考えます } }
DataService クライアントの使用方法を考えると、考慮しないといけない点が何点かあります。それは、以下のような使い方です。
- DataServiceContext.CreateQuery メソッドでクエリーを作成して、DataServiceCollection<T>.LoadAsync メソッドでの読み込み
- DataServiceContext クラスの AddObject、DeleteObject メソッドなどの使用。
- バッチ トランザクション
クエリーを使用するパターンで考えると、クエリーオブジェクト(DataServiceQuery)を取得することでリクエスト URI を取り出すことができますから、DataServiceCollection<T> を継承した TableServiceCollection<T> クラスを定義して、LoadAsync メソッドをオーバーライドしてみます。
public class TableServiceCollection<t> : DataServiceCollection<t> { public void LoadAsync(IQueryable<t> query) { // TableDataContext を取得して クエリーオブジェクトを設定する var field = query.Provider.GetType().GetField("Context", BindingFlags.NonPublic | BindingFlags.Instance); var context = (TableServiceContext)field.GetValue(query.Provider); context.requestQuery = (DataServiceQuery) query; base.LoadAsync(query); } }
DataServiceQuery.Provider オブジェクトは、プライベート フィールド Context に DataServiceContext オブジェクトを保持しています(CreateQuery メソッドが内部的に行っています)。この DataServiceContext オブジェクトをリフレクションで取り出して、TableServiceContext クラスに requestQuery フィールドを追加して DataServiceQuery オブジェクトを設定します。こうすることで、TableServiceContext 側で DataServiceQuery オブジェクトを使用することでリクエスト URI を取り出すことができます。
次に、DataServiceContext クラスの AddObject メソッドなどから リクエスト URI を取り出す方法を考えて、TableServiceContext クラスに以下のようなメソッドを記述します。
// DataServiceQuery 用のフィールド internal DataServiceQuery requestQuery; // 処理している EntityDescriptor オブジェクトを取得 private EntityDescriptor GetProcessEntity() { foreach (var item in this.Entities) { if (GetEntitySaveStatus(item)) { return item; } } return null; } // EntityDescriptor が処理済みかどうか private bool GetEntitySaveStatus(EntityDescriptor descriptor) { var value = (bool)descriptor.GetType().InvokeMember( "ContentGeneratedForSave", BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic, null, descriptor, null); return !value; } // EntityDescriptor が保持している entitySetName(テーブル)名を取得 private string GetEntitySetName(EntityDescriptor descriptor) { var field = descriptor.GetType().GetField("entitySetName", BindingFlags.NonPublic | BindingFlags.Instance); var value = field.GetValue(descriptor).ToString(); return value; }
GetProcessEntity メソッドは、処理しようとしている EntityDescriptor オブジェクトを取得するメソッドです。EntityDescriptor オブジェクトが処理されると、プライベート である ContentGeneratedForSave プロパティが true になりますので、このプロパティをリフレクションで取り出しています(これは、バッチトランザクションを指定しない場合の動作になります)。次に、GetEntitySetName メソッドで EntityDescriptor オブジェクトから エンティティセット名(テーブル名)をリフレクションで取り出しています。これは、テーブルに対する操作のリクエスト URI を組み立てるために用意したメソッドになります。ここまで出来たら、TableServiceContext クラスの SendingRequest イベントハンドラを記述します。
private void DataContextSendingRequest(object sender, SendingRequestEventArgs e) { // リクエスト URI を格納する RequestData 構造体を作成します var context = (TableServiceContext) sender; var data = new StorageCredentialsAccountAndKey.RequestData(); if (context.requestQuery != null) { // DataServiceQuery を使ったパターン data = new StorageCredentialsAccountAndKey.RequestData() { RequestUri = context.requestQuery.RequestUri }; context.requestQuery = null; } else if (context.SaveChangesDefaultOptions == SaveChangesOptions.Batch) { // バッチトランザクション を��ったパターン data.RequestUri = new Uri(this.BaseUri, "$batch"); } else { var entity = GetProcessEntity(); // AddObject などの個別のトランザクション if (entity != null) { var name = GetEntitySetName(entity); // EditLink プロパティが有効であれば、更新か削除の操作 if (entity.EditLink != null) data.RequestUri = entity.EditLink; else if (entity.State == EntityStates.Deleted || entity.State == EntityStates.Modified) { // 更新か削除操作のリクエスト URI を作成 var query = string.Format("{0}(PartitionKey=\"{1}\",RowKey=\"{2}\")", name, ((TableServiceEntity)entity.Entity).PartitionKey, ((TableServiceEntity)entity.Entity).RowKey); data.RequestUri = new Uri(context.BaseUri, query); } else // 追加操作のリクエスト URI を作成 data.RequestUri = new Uri(context.BaseUri, name); } else // EntityDescriptor オブジェクトが null なので、テーブル サービスへの操作へ data.RequestUri = new Uri(this.BaseUri, "Tables"); } // 認証ヘッダーの作成 ((StorageCredentialsAccountAndKey)this.StorageCredentials) .AddAuthenticationHeadersLite( data, e.RequestHeaders, true); }
SendingRequest イベント ハンドラができたら、テーブル サービスに対する操作は以下のようにします。
[DataServiceKey("TableName")] class TableServiceTable { private string tableName; public TableServiceTable() { } public TableServiceTable(string name) { this.TableName = name; } public override bool Equals(object obj) { if (obj == null) { return false; } TableServiceTable table = obj as TableServiceTable; if (table == null) { return false; } return this.TableName.Equals( table.TableName, StringComparison.InvariantCultureIgnoreCase); } public override int GetHashCode() { return this.TableName.GetHashCode(); } public string TableName { get { return this.tableName; } set { this.tableName = value; } } }
// テーブルを新規に作成する public void CreateTable(string tableName, Action<CloudOperationResponse<bool>> callback) { var context = this.GetServiceContext(); var entity = new TableServiceTable() { TableName = tableName }; context.AddObject("Tables", entity); context.BeginSaveChanges( asyncResult => { try { var response = context.EndSaveChanges(asyncResult); callback.Invoke(new CloudOperationResponse<bool>(true, null)); } catch (Exception exception) { callback.Invoke(new CloudOperationResponse<bool>(false, StorageClientExceptionParser.ParseDataServiceException(exception))); } }, null);
// テーブルを削除する public void DeleteTable(string tableName, Action<CloudOperationResponse<bool>> callback) { var context = this.GetServiceContext(); var entity = new TableServiceTable() { TableName = tableName }; context.AttachTo("Tables", entity); context.DeleteObject(entity); context.BeginSaveChanges( asyncResult => { try { var response = context.EndSaveChanges(asyncResult); callback.Invoke(new CloudOperationResponse<bool>(true, null)); } catch (Exception exception) { callback.Invoke(new CloudOperationResponse<bool>(true, StorageClientExceptionParser.ParseDataServiceException(exception))); } }, null) }
コールバックの実装は示していませんが、テーブルサービスに対する扱い方を理解する ことができるでしょう。テーブルを作成する場合は、テーブル名を設定したTableServiceTable クラスのインスタンスを作成してから、AddObject メソッドで追加してから BeginSaveChanges メソッドを呼び出します。一方でテーブルを削除するには、TableServiceTable クラスのインスタンスを作成してから、AttachTo メソッドでオブジェクトへアッタチしてから DeleteObject メソッドを呼び出して削除にマークしてから、BeginSaveChanges メソッドを呼び出すのです。この操作は、テーブルに含まれるエンティティでも同様になります。クエリーを行うには、以下のようにします。
var storageCredential = new StorageCredentialsAccountAndKey(); try { var tableClient = new CloudTableClient(storageCredential); _serviceContext = new TableServiceContext(storageCredential); var query = _serviceContext.CreateQuery<nougakudo>("nougakudo"); query.AddQueryOption("$filter", "PrimaryKey='キー' , RowKey='ロー'"); // AddQueryOption メソッド、もしくは以下のような拡張メソッドでも良い //.Where(x => x.PartitionKey == "キー" && x.RowKey == "ロー"); var datas = new TableServiceCollection<テーブル名>(); datas.LoadCompleted += new EventHandler<loadcompletedeventargs>(datas_LoadCompleted); datas.LoadAsync(query); } catch (Exception ex) { // エラー処理 }
LoadCompleted イベントハンドラを示していませんが、クエリーを扱うのも DataServiceClient と同様に扱えるのがわかることでしょう。全てのパターンを網羅したわけではありませんが、ここに示したような方法を使うことで Silverlight から DataServiceClient のライブラリを使用して Windows Azure の Table Storage サービスへアクセスすることが可能になります。サンプルではリフレクションを使用してプライベート メンバーを取り出していることに注意してください。DataServiceQuery が内部で DataServiceContext を保持することは変わらないと考えられますが、メンバー名などが変更される可能性があるからです。
最後に、Silverlight の DataServiceContext オブジェクトの SendingRequest イベントで リクエスト オブジェクトを提供しない理由ですが、現状の実装がクライアント http スタックと XMLHttpRequest オブジェクトを使用するハイブリッドになっているためだと考えられます。Ajax などで使用する XMLHttpRequest オブジェクトを使用する条件は、XMLHttpRequest オブジェクトを作成できる(イン ブラウザー)状態で、かつ 同一のホストと通信を行う時です。異なるホストと通信を行う時は、クライアント http スタック(WebRequest.CreateHttp)を使用します。Silverlight 5 では、クライアント http スタックを使用する場合は、プロジェクト プロパティで「ブラウザー内での実行に昇格された信頼を要求する」を設定する必要があります。Azure Toolkit for Windows Phone は、OOB のモードしかありませんので クライアント http スタックを使用していることは言うまでもありません。