MSDN マガジン > Home > 発行物 > 2008 > October >  基礎 : 永続性のあるサービスで状態を管理する
基礎
永続性のあるサービスで状態を管理する
Juval Lowy
分散コンピューティングでは、長い時間にわたって実行されるプロセスがよく見られます。複数の実行シーケンスから構成され、数日間または数週間も続くビジネス プロセスもあります。そのようなプロセスに関係しているクライアント (場合によってはエンド ユーザー) の中には、アプリケーションに接続して少量の作業を実行し、ワークフローを新しい状態に遷移させた後、不確定な期間にわたって切断してから、再度接続してワークフローを続行するようなものがあります。クライアントは任意の時点でワークフローを終了して新しいワークフローを開始するよう決定したり、または、バックエンドのワークフロー サービスがワークフローを終了させたりすることもあります。そのような間欠的なフローに対して、クライアントからの呼び出しを待つ間、プロキシやサービスをメモリに保持しておく必要はあるでしょうか。
このコラムでは、そのような長時間実行されるサービスをサポートするいくつかの技法について説明し、一般的な疑問のいくつかに回答します。また、それに付随する Windows Communication Foundation (WCF) の機能、および著者が開発したいくつかのヘルパ クラスも紹介します。

インスタンス ID と永続性のあるストレージ
長時間実行されるサービスの場合、待っているプロキシやサービスをメモリに保持し続けることにほとんど意味がないのは明らかです。そのようなアプローチは、時の試練に耐えられません。最低でも、タイムアウトの問題によって接続は最終的に終了してしまい、いずれの側のコンピュータも簡単に再起動またはログオフできる方法がありません。解決策は、サービスの状態をメモリに保持することを避け、新しいインスタンスの各呼び出しをそれぞれ独自の一時的なメモリ内状態によって処理することです。すべての操作に対して、サービスはその状態を何らかの永続性のあるストレージ (ファイルやデータベースなど) から取得し、その操作に対して要求された作業の単位を実行した後、呼び出しの終わりでその永続的なストレージに状態を再度保存します。このように動作するサービスは、永続性のあるサービスと呼ばれます。また、このストレージは共有できるため、永続性のあるサービスは、スケーラビリティ、冗長性、保守など任意の目的で、複数のコンピュータ間でスケールを調整できます。
長時間続くプロセスの実際の作業が開始される前に、サービスはその状態を永続性のあるストレージに書き込んで、後続の操作でそれを参照できるようにする必要があります。ワークフローが終了したら、サービスはその状態を削除する必要があります。そうしないと、時間の経過と共にストレージが不要な状態情報でいっぱいになってしまいます。
各操作に対して新しいサービス インスタンスが作成されるため、インスタンスは、永続性のあるストレージから状態を検索して読み込む方法を備えている必要があります。したがって、クライアントは、インスタンスに対して何らかの状態識別子を提供する必要があります。この識別子はインスタンス ID と呼ばれます。サービスに間欠的に接続するクライアントや、呼び出し間で初期状態に戻るクライアント アプリケーションまたはコンピュータをサポートするために、一般にクライアントは、インスタンスID を独自の永続性のあるストレージに保存し、その ID をすべての呼び出しに対して提供します。ワークフローが終了すると、クライアントは ID を破棄できます。インスタンス ID は、シリアル化可能で同等視可能であることが重要です。サービスではその状態と共に ID を永続性のあるストレージに保存する必要があるため、ID はシリアル化可能である必要があります。サービスがストレージから状態を検索できるようにするため、ID は同等視可能である必要があります。
通常、永続性のあるストレージは、インスタンス ID をインスタンスの状態と対応付ける一種の辞書です。それを使用するサービスは一般に、個々のインスタンス メンバを固有の ID と共に格納するのではなく、1 つのデータ構造を使用してその状態のすべてを格納します。多くの場合、次にサービスは専用のヘルパ クラスまたは構造を使用してそのすべてのメンバ変数を集約し、その型を永続性のあるストレージから 1 回の操作で格納および取得します。複数のインスタンスが同時にストレージにアクセスして変更しようとする可能性があるため、永続性のあるストレージ自体へのアクセスは、スレッド セーフで同期されている必要があります。
単純な永続性のあるサービスの実装とサポートを支援するために、図 1 に示す FileInstanceStore<ID,T> クラスを作成しました。FileInstanceStore<ID,T> は、汎用のファイル ベースのインスタンス ストアであり、2 つの型パラメータを受け取ります。1 つは ID 型パラメータであり、これは制約として一様な型である必要があります。もう 1 つは、インスタンス状態を表す T 型パラメータです。これらの型は両方ともシリアル化可能である必要があります。
public interface IInstanceStore<ID,T> where ID : IEquatable<ID>
{
   void RemoveInstance(ID instanceId);
   bool ContainsInstance(ID instanceId);
   T this[ID instanceId]
   {get;set;}
}

public class FileInstanceStore<ID,T> : IInstanceStore<ID,T> 
                                       where ID : IEquatable<ID>
{

   public FileInstanceStore(string fileName);

   //Rest of the implementation
}
FileInstanceStore<ID,T> は、単純なインデクサを提供し、インスタンス状態をファイルから読み込んだりファイルに書き込んだりできるようにします。また、ファイルにインスタンス状態が存在するかどうかを確認したり、それを削除したりもできます。

明示的なインスタンス ID
クライアントがサービスにインスタンス ID を提供するための最も単純な方法は、状態にアクセスするすべての操作に対してそれぞれ明示的なパラメータを使用することです。そのようなクライアントおよびサービスを図 2 に示します。このサービスは、呼び出しごとに動作するポケット計算機であり、永続性のあるメモリがファイルに格納されます。
[ServiceContract]
interface ICalculator
{
   [OperationContract]
   double Add(double number1,double number2);

   /* More arithmetic operations */  

   //Memory management operations 

   [OperationContract]
   void MemoryStore(string instanceId,double number);

   [OperationContract]
   void MemoryClear(string instanceId);

}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<string,double> Memory = new
     FileInstanceStore<string,double> (Settings.Default.MemoryFileName);

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(string instanceId,double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[instanceId] = number;
      }
   }
   public void MemoryClear(string instanceId)
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(instanceId);
      }
   }
   //Rest of the implementation  
}
計算機のすべてのインスタンスが、FileInstanceStore<string,double> という形式の同じ静的メモリを使用します。計算機は、サービス型に対するロックを使用して、すべてのインスタンスにわたる各操作でメモリへのアクセスを同期します。メモリをクリアするとワークフローの終了が通知されるため、計算機はストレージから状態をパージします。

インスタンス ID に対するコンテキスト バインディング
明示的なパラメータの代わりに、Microsoft .NET Framework 3.5 で導入された専用のコンテキスト バインディングを使用することもできます。これらのバインディング、BasicHttpContextBinding、NetTcpContextBinding、および WSHttpContextBinding は、System.WorkflowServices.dll アセンブリに含まれています。これらの各コンテキスト バインディングは、それぞれ通常のプロトコル固有のバインディングから派生していますが、専用のコンテキスト管理プロトコルに対するサポートを追加します。これらのバインディングは、コンテキストなしでも使用できます。
コンテキスト管理プロトコルを使用すると、(メッセージ ヘッダー内で) キー/値文字列のペアの辞書を渡すことができ、これがコンテキストと呼ばれます。これは、アプリケーション固有の実行コンテキストを表し、組み込みのセキュリティ コンテキストや操作コンテキストと同様なものです。コンテキスト バインディングでは、プロキシを開く (またはそれを初めて使用する) 前に、サービスに渡すコンテキストを 1 回だけ設定できます。その後は、辞書がキャッシュされ、それを変更しようとするとエラーになります。クライアントは、IContextManager インターフェイスを使用して、サービスに送信するコンテキストを設定します。これは次のように定義されます。
public interface IContextManager
{
   IDictionary<string,string> GetContext();
   void SetContext(IDictionary<string,string> context);
   //More members
}
クライアントは、プロキシの InnerChannel プロパティにアクセスすることで IContextManager インターフェイスへの参照を取得します。このプロパティは、GetProperty<T>() メソッドにより IChannel インターフェイスをサポートします。
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
class MyContractClient : ClientBase<IMyContract>,IMyContract
{...}

MyContractClient proxy = new MyContractClient();
IContextManager contextManager =  
    proxy.InnerChannel.GetProperty<IContextManager>();
クライアントは、IContextManager を取得すると、GetContext メソッドを呼び出して現在のコンテキストを複製できます。GetContext から返される辞書は実際のコンテキストのコピーであるため、クライアントはそれを使用してコンテキストを変更することはできません。その代わり、クライアントは SetContext メソッドを呼び出し、新しいコンテキストを提供する必要があります。クライアントは古いコンテキストを無効にするか、単にそれに値を追加してから、図 3 に示すように再度コンテキストを設定します。
MyContractClient proxy = new MyContractClient();
IContextManager contextManager = 
    proxy.InnerChannel.GetProperty<IContextManager>();

IDictionary<string,string> context = contextManager.GetContext();
context["NumberContext"] = "123";
contextManager.SetContext(context);

proxy.MyMethod();

proxy.Close();
サービスは、受信したメッセージ プロパティからコンテキスト値を読み取ります。このプロパティには、操作コンテキストを通じてアクセスします。
public sealed class OperationContext : ...
{
   public MessageProperties IncomingMessageProperties
   {
      get;
   }
   //More members
}
MessageProperties は、タイプ セーフではない辞書であり、文字列キーを受け取って、一致するオブジェクト値を返します。
public sealed class MessageProperties : IDictionary<string,object>
{...}
コンテキスト プロパティを取得するために、サービスは静的文字列 ContextMessageProperty.Name を使用します。これは、次のように定義された ContextMessageProperty 型のオブジェクトを返します。
[Serializable]
public class ContextMessageProperty : IMessageProperty
{
   public IDictionary<string,string> Context
   {get;}
   public static string Name
   {get;}

   //More members
}
ContextMessageProperty の Context プロパティは、クライアントから渡されるものと同じパラメータの辞書です。図 3 で渡された数値コンテキストを読み取るために必要なサービス側の手順を図 4 に示します。クライアントがコンテキストに書き込むために必要となる手順は、図 5 に示す ContextManager 静的ヘルパ クラスを使用して合理化できます。
class MyService : IMyContract
{
   public void MyMethod()
   {
      ContextMessageProperty contextProperty = 
          OperationContext.Current
          .IncomingMessageProperties[ContextMessageProperty.Name]
          as ContextMessageProperty;

      string number = contextProperty.Context["NumberContext"];

      Debug.Assert(number == 123);
   }
}
public static class ContextManager
{
   public static void SetContext(IClientChannel innerChannel,
                                 string key, string value) {
      SetContext(innerChannel, UpdateContext(innerChannel, key, value));
   }

   public static void SetContext(IClientChannel innerChannel,
                                 IDictionary<string, string> context) {
      Debug.Assert((innerChannel as ICommunicationObject).State !=
                   CommunicationState.Opened);
      IContextManager contextManager =
         innerChannel.GetProperty<IContextManager>();
      contextManager.SetContext(context);
   }

   public static IDictionary<string, string> UpdateContext(
      IClientChannel innerChannel, string key, string value) {
      IContextManager contextManager =
         innerChannel.GetProperty<IContextManager>();
      IDictionary<string, string> context =
         new Dictionary<string, string>(contextManager.GetContext());
      context[key] = value;
      return context;
   }

   //Proxy extensions

   public static void SetContext<T>(this ClientBase<T> proxy,
             IDictionary<string, string> context) where T : class {
      SetContext(proxy.InnerChannel, context);
   }

   public static void SetContext<T>(this ClientBase<T> proxy,
                        string key, string value) where T : class {
      SetContext(proxy.InnerChannel, key, value);
   }

   //More helper methods
}
ContextManager は、SetContext メソッドをオーバーロードしたいくつかのメソッドを提供し、クライアントが 1 つのキー/値ペアを使用して、または辞書からペアのコレクションを使用して、プロキシの内部チャネルに新しいコンテキストを設定できるようにします。これらのメソッドは、プロキシ クラスとチャネル ファクトリの両方に対して有用です。また、ContextManager により、コンテキストをプロキシ クラスの拡張として設定することもできます。CreateContext メソッドを使用して新しい辞書を作成したり、既存のコンテキストにキー/値ペアを追加したりできます。ContextManager を使用すると、図 3 は次のように縮小されます。
MyContractClient proxy = new MyContractClient();
proxy.SetContext("NumberContext","123");
proxy.MyMethod();
proxy.Close();
ただし、このように SetContext を利用するには、プロキシのすべてのインスタンス化時にそれを明示的に使用する必要があります。それよりも、ContextManager を ContextClientBase<T> プロキシ クラスのような専用のプロキシ クラスにカプセル化する方が適切です。
public abstract class ContextClientBase<T> : ClientBase<T> 
where T : class
{
   public ContextClientBase();
   public ContextClientBase(string endpointName);
   public ContextClientBase(string key,string value);
   public ContextClientBase(IDictionary<string,string> context);
   public ContextClientBase(string key,string value,
                                           string endpointName);
   public ContextClientBase(IDictionary<string,string> context,
                                           string endpointName);
   //More constructors    
}
ContextClientBase<T> のコンストラクタは、エンドポイント名やバンディングおよびアドレスなどの通常のプロキシ パラメータを受け取りますが、それに加えて、サービスに送信するコンテキスト パラメータも受け取ります。これは、1 つのキー/値ペア、または辞書を使用したペアのコレクションです。プロキシは、ContextClientBase<T> から直接派生できます。
class MyContractClient : ContextClientBase<IMyContract>,IMyContract
{
   public MyContractClient(string key,string value) : base(key,value)
   {}
   /* More constructors */ 
   public void MyMethod()
   {
      Channel.MyMethod();
   }
}
ContextClientBase<T> を使用すると、図 3 は次のように縮小されます。
MyContractClient proxy = new MyContractClient("NumberContext","123");
proxy.MyMethod();
proxy.Close();
図 6 に、ContextClientBase<T> の実装を示します。
public abstract class ContextClientBase<T> : ClientBase<T> 
                                             where T : class
{
   public ContextClientBase(string key,string value,string endpointName):
              this(ContextManager.CreateContext(key,value),endpointName){}

   public ContextClientBase(IDictionary<string,string> context, string endpointName) : base(endpointName){
      SetContext(context);
   }

   /* More constructors */

   void SetContext(IDictionary<string,string> context){
      VerifyContextBinding();
      ContextManager.SetContext(InnerChannel,context);
   }

   void VerifyContextBinding(){
      BindingElementCollection elements = Endpoint.Binding.CreateBindingElements();

      if(elements.Contains(typeof(ContextBindingElement))){
         return;
      }
      throw new InvalidOperationException("Can only use context binding");
   }
}
ContextClientBase<T> のいくつかのコンストラクタは、ContextManager を使用して新しいコンテキストを作成し、それを別のコンストラクタに渡します。渡されたコンストラクタは、SetContext ヘルパ メソッドを呼び出します。SetContext は最初に、使用されているバインディングが本当にコンテキスト バインディングであることを確認してから、ContextManager を使用してコンテキストを設定します。バインディングがコンテキスト プロトコルをサポートすることの確認は、バインディング要素のコレクションに ContextBindingElement が含まれるかどうか検索することで行います。この確認方法は、バインディングの型を調べる方法よりも優れています。カスタム コンテキスト バインディングでも自動的に機能するためです。
サービスに対して、ContextManager ヘルパ クラスは、操作コンテキストとメッセージ プロパティの間のやり取りをカプセル化できます。ContextManager は、GetValue メソッドを提供します。
public static class ContextManager
{
   public static string GetValue(string key);

   //More members
}
GetValue を使用すると、図 4 のサービス コードは次のように縮小されます。
class MyService : IMyContract
{
   public void MyMethod()
   {
      string number = ContextManager.GetValue("NumberContext");

      Debug.Assert(number == "123");
   }
}
いくつかのエラー処理コードを削除した GetValue の実装を図 7 に示します。
public static class ContextManager
{
   public static string GetValue(string key)
   {  
      if(OperationContext.Current == null)
         return null;

      ContextMessageProperty contextProperty =       
             OperationContext.Current.IncomingMessageProperties
                                  [ContextMessageProperty.Name] 
                                     as ContextMessageProperty;

      if(contextProperty.Context.ContainsKey(key) == false)
         return null;

      return contextProperty.Context[key]; 
   }
}
GetValue は、状態およびエラー管理を追加することを除いて、図 4 に示した明示的な手順と似ています。コンテキストに要求キーが含まれていない場合 (またはコンテキストがまったく見つからない場合) には、GetValue は null を返します。
クライアントは ContextClientBase<T> を使用して、クライアントのコンテキスト バインディング プロトコル上でインスタンス ID を渡します。コンテキスト バインディングではすべてのコンテキスト パラメータに対してキーと値が必要であるため、クライアントはその両方をプロキシに提供する必要があります。キーの値はあらかじめサービスに知られている必要があるため、クライアントは、キーをプロキシ自体にハード コーディングしてもよいでしょう。サービスは ContextManager を使用した関連する操作で、インスタンス ID を取得します。
図 8 に、コンテキスト バインディングを使用した計算機サービスを示します。この図では、サービスが ContextManager とのやり取りを専用のコンテキスト クラスにカプセル化しています。
//Client Side:
[ServiceContract]
interface ICalculator
{
   [OperationContract]
   double Add(double number1,double number2);

   [OperationContract]
   void MemoryStore(double number);

   [OperationContract]
   void MemoryClear();
}
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
  public MyCalculatorClient(string instanceId) :  base("CalculatorId",instanceId)
   {}
  public MyCalculatorClient(string instanceId,string endpointName):
                             base("CalculatorId",instanceId,endpointName)
   {}

   //More constructors

   public double Add(double number1,double number2)
   {
      return Channel.Add(number1,number2);
   }

   public double MemoryStore (double number)
   {
      return Channel.MemoryStore (number);
   }

   //Rest of the implementation  
}


//Service Side: 
class CalculatorContext 
{
   public static string Id
   {
      get
      {
         return ContextManager.GetValue("CalculatorId") ?? String.Empty;
      }
   }      
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<string,double> Memory = new 
      FileInstanceStore<string,double>(Settings.Default.MemoryFileName);

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[CalculatorContext.Id] = number;
      }
   }
   public void MemoryClear()
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(CalculatorContext.Id);
      }
   }
   //Rest of the implementation  
}

コンテキスト ID に対する標準キー
インスタンス ID に使用されるキーをハード コーディングする必要性と、それをあらかじめ知っておく必要性は、それぞれ義務です。そのため、永続性のあるサービスのディスパッチャ ロジックは、Guid の形 (文字列形式) でインスタンス ID を自動的に作成します。これは、instanceId という名前の予約キーによってアクセスできます。クライアントとサービスは、インスタンス ID として同じ値を取得します。バインディングがクライアントとサービスの間でインスタンス ID を相関付ける機会を得た後、プロキシの最初の呼び出しが戻ると、インスタンス ID の値は初期化されます。コンテキスト バインディングで渡される他のパラメータと同様に、インスタンス ID の値は、プロキシの有効期間全体を通じて不変です。標準インスタンス ID とのやり取りを合理化するため、図 9 に示すように、ContextManager を ID 管理メソッド、プロパティ、およびプロキシ拡張によって拡張しました。
public static class ContextManager
{
   public const string InstanceIdKey = "instanceId";

   public static Guid InstanceId
   {
      get 
      {  
         string id = GetValue(InstanceIdKey) ?? Guid.Empty.ToString();
         return new Guid(id); 
      }    
   }
   public static Guid GetInstanceId(IClientChannel innerChannel)
   {
      try
      {
        string instanceId = innerChannel.GetProperty<IContextManager>()
         .GetContext()[InstanceIdKey];
         return new Guid(instanceId);
      }
      catch(KeyNotFoundException)
      {
         return Guid.Empty;
      }
   }
   public static void SetInstanceId(IClientChannel innerChannel,
                                                Guid instanceId)
   {
      SetContext(innerChannel,InstanceIdKey,instanceId.ToString());
   }

   //More members 
}
ContextManager は、クライアントがコンテキストに対してインスタンス ID を読み取りまたは書き込みできるように、GetInstanceId および SetInstanceId メソッドを提供します。読み取り専用の InstanceId プロパティは、サービスが ID を取得する際に使用されます。ContextManager は、インスタンス ID を文字列ではなく Guid として扱うことでタイプ セーフとし、エラー処理も追加しています。
クライアントは図 8 のように ContextClientBase<T> を使用して標準 ID を渡すことができますが、図 10 に示されるように、それをさらに厳しくし、標準インスタンス ID に対する ContextClientBase<T> の組み込みサポートを提供する方が適切です。図 11 に、標準 ID を使用した永続性のある計算機クライアントおよびサービスを示します。
public abstract class ContextClientBase<T> : ClientBase<T> 
                                             where T : class
{
   public Guid InstanceId
   {
      get
      {
         return ContextManager.GetInstanceId(InnerChannel);
      }
   }
   public ContextClientBase(Guid instanceId) :
     this(ContextManager.InstanceIdKey,instanceId.ToString())
   {}

   public ContextClientBase(Guid instanceId,string endpointName) :
     this(ContextManager.InstanceIdKey,instanceId.
                                       ToString(),endpointName)
   {}

   //More constructors
}
//Client Side:
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
   public MyCalculatorClient() 
   {}
   public MyCalculatorClient(Guid instanceIdId) : base(instanceId)
   {}
   public MyCalculatorClient(Guid instanceId,string endpointName) : 
       base(instanceId,endpointName)
   {}

   //Rest same as Figure 8
}

//Service Side:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<Guid,double> Memory = new 
       FileInstanceStore<Guid,double>(Settings.Default.MemoryFileName);
   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[ContextManager.InstanceId] = number;
      }
   }
   public void MemoryClear()
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(ContextManager.InstanceId);
      }
   }
   //Rest of the implementation  
}

自動的な永続性のある動作
永続性のあるサービスに対してここまでに示したすべての手法では、サービスにかなりの量の作業が要求されます。そのほとんどは、永続性のある状態ストレージを提供し、操作ごとにそのストレージに対してインスタンス状態を明示的に管理するための作業です。この作業は反復的な性質を持つため、WCF では標準コンテキスト ID を使用して、各操作でのサービス状態を指定された状態ストレージとの間でシリアル化および逆シリアル化することにより、作業を自動化できます。クライアントが有効な ID を提供すると、各操作について WCF はインスタンスをストレージから逆シリアル化し、操作を呼び出した後、操作によって変更された新しい状態をストレージに再度シリアル化します。クライアントが ID を提供しない場合、WCF はコンストラクタを呼び出して新しいサービス インスタンスを作成しますが、その後すぐにそのインスタンスを状態ストレージにシリアル化します。
クライアントからプロキシに提供された ID に一致する状態が既にストレージに含まれている場合、WCF はインスタンス コンストラクタを呼び出すことはありません。その代わり、状態ストレージから逆シリアル化された新しいインスタンスに対して呼び出しが処理されます。状態ストレージに含まれない ID がクライアントから提供された場合、WCF は例外をスローします。
WCF は、DurableService 動作属性を提供します。これは次のように定義されます。
public sealed class DurableServiceAttribute :  
    Attribute,IServiceBehavior,...
{...}
この属性は、サービス クラス上で直接適用します。最も重要なこととして、サービス クラス自体は Serializable としてマークするか、または、永続性のある状態管理を必要とするすべてのメンバにそれぞれの DataMember 属性を持つデータ コントラクトとして、マークする必要があります。
このインスタンスは、通常のインスタンスの場合と同様に、その状態をメンバ変数で管理でき、図 12 に示すように、WCF がそれらのメンバを管理してくれます。
[Serializable]
[DurableService]
class MyCalculator : ICalculator
{
   double Memory
   {get;set;}

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      Memory = number;
   }
   [DurableOperation(CompletesInstance = true)]
   public void MemoryClear()
   {
      Memory = 0;
  }   
   //Rest of the implementation  
}
自動的な永続性のある状態管理を利用するすべてのサービスは、セッションごとに構成される必要がありますが、動作は常に呼び出しごとに行われます (WCF は各呼び出し後にコンテキストの非アクティブ化を実行します)。また、サービスはすべてのエンドポイントでコンテキスト バインディングの 1 つを使用して標準インスタンス ID を有効にする必要があります。コントラクトはセッションを許可または要求する必要があり、禁止することはできません。この 2 つの制約は、サービスの読み込み時に確認されます。
サービスはオプションで DurableOperation 操作動作属性を使用して、WCF がワークフローの終了時にストレージから状態をパージするよう指示できます。
[AttributeUsage(AttributeTargets.Method)]
public sealed class DurableOperationAttribute : Attribute,...
{
   public bool CanCreateInstance
   {get;set;}

   public bool CompletesInstance
   {get;set;}
}
CompletesInstance プロパティを true に設定することで、WCF は操作呼び出しの終了後にインスタンス ID をストレージから削除します。CompletesInstance プロパティの既定値は false です。クライアントがインスタンス ID を提供しない場合に備えて、CanCreateInstance プロパティを false に設定することで、操作によって新しいインスタンスが作成されるのを防ぐことができます。図 12 に、計算機の MemoryClear 操作に対して CompletesInstance プロパティを使用する例を示します。
CompletesInstance を利用する際の問題点は、コンテキスト ID が不変であることです。クライアントが、CompletesInstance が true に設定された操作を呼び出した後でプロキシに対して別の呼び出しを実行しようとすると、ストレージにはそのインスタンス ID がもう含まれていないため、呼び出しは失敗します。
クライアントは、プロキシを引き続き使用できないことを認識する必要があります。クライアントがサービスに対してさらに呼び出しを実行する必要がある場合は、まだインスタンス ID を持っていない新しいプロキシで行う必要があります。それにより、クライアントは新しいワークフローを開始します。これを行うには、単にワークフローの完了後にクライアント プログラムを閉じるか、または新しいプロキシ参照を作成します。

永続化プロバイダ
DurableService 属性は WCF に対して、インスタンスをいつシリアル化および逆シリアル化すればよいかを指示しますが、それをどこで行うかについては何も指示しません。その代わり、WCF はプロバイダ モデルという形のブリッジ パターンを使用して、状態ストレージをこの属性とは別に指定できるようにしています。それにより、属性がストレージから切り離されるため、自動的な永続性のある動作を任意の互換性のある記憶媒体と共に使用できます。
サービスが DurableService 属性で構成されている場合は、永続化プロバイダ ファクトリを使用してホストを構成する必要があります。このファクトリは、図 13 に示すように、抽象クラス PersistenceProviderFactory から派生し、抽象クラス PersistenceProvider のサブクラスを作成します。
public abstract class PersistenceProviderFactory : CommunicationObject
{
   protected PersistenceProviderFactory();
   public abstract PersistenceProvider CreateProvider(Guid id);
}

public abstract class PersistenceProvider : CommunicationObject
{
   protected PersistenceProvider(Guid id);

   public Guid Id
   {get;}

   public abstract object Create(object instance,TimeSpan timeout);
   public abstract void   Delete(object instance,TimeSpan timeout)
   public abstract object Load(TimeSpan timeout);
   public abstract object Update(object instance,TimeSpan timeout);

   //Additional members
}
永続化プロバイダ ファクトリを指定する最も一般的な方法は、それをホストの .config ファイルに (サービス動作として) 指定し、その動作をサービス定義で次のように参照することです。
<behaviors>
   <serviceBehaviors>
      <behavior name = "DurableService">
         <persistenceProvider
            type = "...type...,...assembly ..."
            <!--  Provider-specific parameters  -->
         />
      </behavior>
   </serviceBehaviors>
</behaviors>
永続化プロバイダ ファクトリが指定されていない場合、WCF はサービス ホストの作成を中止します。ホストに永続化プロバイダ ファクトリが指定されると、WCF は作成された PersistenceProvider を呼び出しごとに使用して、インスタンスをシリアル化および逆シリアル化します。単純な永続化プロバイダ ファクトリとして、次に示す FilePersistenceProviderFactory を作成しました。
public class FilePersistenceProviderFactory : PersistenceProviderFactory 
{
   public FilePersistenceProviderFactory();
   public FilePersistenceProviderFactory(string fileName);
   public FilePersistenceProviderFactory(
                                        NameValueCollection parameters);
}
public class FilePersistenceProvider : PersistenceProvider
{
   public FilePersistenceProvider(Guid id,string fileName);
}
FilePersistenceProvider は、FileInstanceStore<ID,T> をラップします。FilePersistenceProviderFactory のコンストラクタでは、目的のファイル名を指定する必要があります。ファイル名が指定されないと、FilePersistenceProviderFactory は既定で Instances.bin を使用します。
.config ファイルでカスタム永続化ファクトリを使用する際の鍵は、パラメータの NameValueCollection を受け取るコンストラクタを定義することです。これらのパラメータは、.config ファイルのプロバイダ ファクトリ動作セクションに指定されたキーおよび値のペアを単純なテキスト形式にしたものです。ほとんど任意の自由な形式のキーおよび値が有効です。たとえば、ファイル名が何であるかを指定するには、次のように記述します。
<persistenceProvider type =
      "FilePersistenceProviderFactory,ServiceModelEx" 
         fileName = "MyService.bin"
 />
これによりコンストラクタは、パラメータ コレクションを使用してこれらのパラメータにアクセスできます。
string fileName = parameters["fileName"];
WCF には、インスタンス状態を専用の SQL Server テーブルに格納する永続化プロバイダが付属しています。Visual Studio 2008 の既定のインストール後には、データベースのインストール スクリプトが C:\Windows\Microsoft.NET\Framework\v3.5\SQL\EN に格納されています。状態ストレージには SQL Server 2005 または SQL Server 2008 のみを使用できることに注意してください。
SQL プロバイダは、SqlPersistenceProviderFactory および SqlPersistenceProvider の形で用意され、System.ServiceModel.Persistence 名前空間の System.WorkflowServices アセンブリに含まれています。必要なことは、図 14 に示されるように、アプリケーション設定から SQL プロバイダ ファクトリと接続文字列名を指定することだけです。
<connectionStrings>
   <add name = "DurableServices" 
      connectionString = "..."
      providerName = "System.Data.SqlClient" 
   />
</connectionStrings>

<persistenceProvider
   type = "System.ServiceModel.Persistence.SqlPersistenceProviderFactory,
                System.WorkflowServices,Version=3.5.0.0,Culture=neutral, 
                 PublicKeyToken=31bf3856ad364e35"
                 connectionStringName = "DurableServices"
/>
serializeAsText パラメータを true に設定することにより、診断や分析の目的などで、(既定のバイナリ シリアル化を使用する代わりに) インスタンスをテキストとしてシリアル化するよう WCF に指示することもできます。
<persistenceProvider
   serializeAsText = "True"
  ...
/>

ご意見やご質問は mmnet30@microsoft.com まで英語でお送りください。

Juval Lowy は、WCF のトレーニングとアーキテクチャ コンサルティングを行う IDesign 社に所属するソフトウェア アーキテクトです。最新の著書は、『Programming WCF Services, Second Edition』です。彼は、シリコン バレー地域の Microsoft Regional Director も務めています。連絡先は www.idesign.net です。

コミュニティ コンテンツ   コミュニティ コンテンツとは
新しいコンテンツの追加 RSS  注釈
Processing
Page view tracker