次の方法で共有


基礎

ローカル通信向けのワークフロー サービス

Matt Milner

サンプル コードのダウンロード

以前のコラム (MSDN Magazine 2007 年 9 月号の「ワークフロー通信」(msdn.microsoft.com/ja-jp/magazine/cc163365.aspx) 参照) では、Windows Workflow Foundation 3 (WF3) の主要通信アーキテクチャについて説明しました。そのとき説明しなかった 1 つのトピックが、この通信アーキテクチャの上位に抽象化されるローカル通信のアクティビティです。.NET Framework 4 Beta 1 を見てみると、HandleExternalEvent アクティビティが存在しないことに気付きます。実際には、WF4 に含まれる通信アクティビティは、Windows Communication Foundation (WCF) 上に構築されます。今月は、Windows Workflow Foundation 3 のワークフローとホスト アプリケーション間で WCF を使用して通信する方法について説明します。この知識を習得すると、WF3 を使用する開発作業や、WF4 に向けての準備に役立ちます。WF4 では、WCF が唯一キューを抽象化するもの (WF4 では "ブックマーク" と呼ばれます) で、フレームワークに付属して出荷されます。WF3 でのワークフロー サービスの基礎については、MSDN Magazine の Launch 号の「基礎」コラム (msdn.microsoft.com/ja-jp/magazine/cc164251.aspx) を参照してください。

概要

ホスト アプリケーションとワークフロー間の通信は、ワークフローとホストが異なるスレッドで実行されることが多く、このことをつい見落としがちなので、一部の開発者にとっては課題となることが実証されています。通信アーキテクチャを設計するときは、開発者が、スレッド コンテキストの管理やデータのマーシャリングなどの低レベルの詳細にとらわれる必要がなくなることを考えます。WF のキュー アーキテクチャを抽象化するために .NET Framework 3.5 から導入されたのが、WCF メッセージング統合です。多くのサンプルやラボで、WCF に対するアクティビティや拡張機能を使用して、ホスティング プロセスの外部に存在するクライアントにワークフローを公開する方法が示されていますが、同じプロセス内の通信にもこのような通信フレームワークを使用できます。

通信を実装するには複数の手順が必要ですが、ローカル通信アクティビティで実行する必要がある作業量とそれほど変わりません。

他の作業に先立って、まず、WCF サービス コントラクトを使用して、通信のコントラクトを定義する必要があります (また、少なくとも、反復的アプローチで定義に着手する必要があります)。次に、定義したコントラクトをワークフローで使用して、ロジックの通信ポイントをモデル化します。最後に、すべてを繋ぎ合わせ、構成済みのエンドポイントを備えた WCF サービスとして、ワークフローおよびその他のサービスをホストします。

通信のモデル化

通信をモデル化するには、まず、ホスト アプリケーションとワークフロー間のコントラクトを定義します。WCF サービスではこのコントラクトを使用して、サービスを構成する操作のコレクションと、送受信するメッセージを定義します。この場合、ホストからワークフロー、ワークフローからホストと双方向に通信するため、2 つのサービス コントラクトと、関連するデータ コントラクトを定義する必要があります (図 1 参照)。

図 1 通信のコントラクト

[ServiceContract(
    Namespace = "urn:MSDN/Foundations/LocalCommunications/WCF")]
public interface IHostInterface
{
[OperationContract]
void OrderStatusChange(Order order, string newStatus, string oldStatus);
}

[ServiceContract(
    Namespace="urn:MSDN/Foundations/LocalCommunications/WCF")]
public interface IWorkflowInterface
{
    [OperationContract]
    void SubmitOrder(Order newOrder);

    [OperationContract]
    bool UpdateOrder(Order updatedOrder);
}

[DataContract]
public class Order
{
    [DataMember]
    public int OrderID { get; set; }
    [DataMember]
    public string CustomerName { get; set; }
    [DataMember]
    public double OrderTotal { get; set; }
    [DataMember]
    public string OrderStatus { get; set; }
    }

コントラクトの定義が完了したら、リモート通信と同様に機能する Send アクティビティと Receive アクティビティを使用して、ワークフローをモデル化します。リモートでもローカルでもプログラミング モデルが同じなのは、WCF の優れた機能の 1 つです。簡単な例として、図 2 に、ワークフローとホスト間の通信をモデル化する、2 つの Receive アクティビティと 1 つの Send アクティビティを含むワークフローを示します。Receive アクティビティは IWorkflowInterface サービス コントラクトを使用して構成され、Send アクティビティでは IHostInterface コントラクトが使用されています。

ここまでは、ローカル通信に WCF を使用することとリモート通信に WCF を使用することにはそれほど違いはなく、使用するアクティビティやサービスもよく似ています。主な違いは、ワークフローを開始して、ワークフローからの通信を処理するホスト コードの記述方法です。

図 2 コントラクトに照らしてモデル化したワークフロー

サービスのホスト

WCF を使用して双方向に通信するため、ワークフローを実行するワークフロー サービスと、ホスト アプリケーション内でワークフローからメッセージを受信するサービスの 2 つのサービスをホストする必要があります。この例では、ホストとして機能する簡単な Windows Presentation Foundation (WPF) アプリケーションを構築して、App クラスの OnStartup メソッドと OnExit メソッドを使用してホストを管理しました。まず、WorkflowServiceHost クラスを作成し、OnStartup メソッドでホストを開こうとしがちです。ホストが開かれた後は Open メソッドがブロックしないため、処理を続行して、ユーザー インターフェイスを読み込み、ワークフローとの通信を開始できます。WPF (およびその他のクライアント テクノロジ) では、1 つのスレッドで処理が行われるため、この手順ではすぐに問題が発生します。これは、サービスとクライアントの両方の呼び出しで同じスレッドを使用できないことが原因で、クライアントがタイムアウトするためです。これを回避するため、ThreadPool を使用して、別のスレッドに WorkflowServiceHost を作成します (図 3 参照)。

図 3 ワークフロー サービスのホスト

ThreadPool.QueueUserWorkItem((o) =>
{

//host the workflow
workflowHost = new WorkflowServiceHost(typeof(
    WorkflowsAndActivities.OrderWorkflow));
workflowHost.AddServiceEndpoint(
    "Contracts.IWorkflowInterface", LocalBinding, WFAddress);
try
{
    workflowHost.Open();
}
catch (Exception ex)
{
    workflowHost.Abort();
    MessageBox.Show(String.Format(
        "There was an error hosting the workflow as a service: {0}",
    ex.Message));
}
});

次に、ローカル通信に適切なバインディングを選択するときに、新たな課題に直面します。現時点では、この種のシナリオで使用できる、非常に軽量なインメモリまたはインプロセスのバインディングはありません。軽量チャネルにする最適なオプションは、セキュリティを無効にした状態の NetNamedPipeBinding を使用することです。残念ながら、このバインディングを使用してワークフローをサービスとしてホストしようとすると、エラーが発生します。このエラーでは、サービス コントラクトでセッションが必要になることがあるため、ホストではコンテキスト チャネルを備えたバインディングが必要であることが示されます。さらに、NetNamedPipeContextBinding は .NET Framework に付属していません。.NET Framework に付属するコンテキスト バインディングは、BasicHttpContextBinding、NetTcpContextBinding、および WSHttpContextBinding の 3 つだけです。さいわい、独自のカスタム バインディングを作成して、コンテキスト チャネルを含めることができます。図 4 は、NetNamedPipeBinding クラスから派生して、ContextBindingElement をバインディングに挿入する、カスタム バインドを示しています。これで、さまざまなアドレスを使用して登録されたエンドポイントでの双方向通信に、このバインディングを使用できるようになります。

図 4 NetNamedPipeContextBinding

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
    public NetNamedPipeContextBinding() : base(){}

    public NetNamedPipeContextBinding(
        NetNamedPipeSecurityMode securityMode):
        base(securityMode) {}

    public NetNamedPipeContextBinding(string configurationName) :
        base(configurationName) {}

    public override BindingElementCollection CreateBindingElements()
    {
        BindingElementCollection baseElements = base.CreateBindingElements();
        baseElements.Insert(0, new ContextBindingElement(
            ProtectionLevel.EncryptAndSign,
            ContextExchangeMechanism.ContextSoapHeader));

        return baseElements;
    }
}

この新しいバインディングでは、WorkflowServiceHost 上にエンドポイントを作成して、エラーを発生させることなくホストを開くことができます。ワークフローは、サービス コントラクトを使用して、ホストからデータを受信する準備が整っています。このデータを送信するには、プロキシを作成して、操作を呼び出す必要があります (図 5 参照)。

図 5 ワークフローを開始するホスト コード

App a = (App)Application.Current;
    IWorkflowInterface proxy = new ChannelFactory<IWorkflowInterface>(
    a.LocalBinding, a.WFAddress).CreateChannel();

    proxy.SubmitOrder(
        new Order
        {
            CustomerName = "Matt",
            OrderID = 0,
            OrderTotal = 250.00
        });

コントラクトを共有しているため、プロキシ クラスは存在しません。そこで、ChannelFactory<TChannel> を使用してクライアント プロキシを作成する必要があります。

ワークフローをホストし、メッセージを受信できるようになりましたが、ホストにメッセージを送信するためにはさらに構成が必要です。最も重要なことは、Send アクティビティを使用する際に、ワークフローからクライアント エンドポイントを取得できるようにすることです。Send アクティビティでは、エンドポイント名を指定できます。このエンドポイント名は、通常、構成ファイル内の名前付きエンドポイントへのマッピングです。エンドポイント情報を構成ファイル内に配置してもうまくいきますが、筆者が 2008 年 8 月のコラム (msdn.microsoft.com/ja-jp/magazine/cc721606.aspx) で説明した ChannelManagerService を使用して、ワークフロー内の Send アクティビティが使用するクライアント エンドポイントを保持することもできます。図 6 は、サービスを作成し、このサービスで名前付きエンドポイントを指定してから、WorkflowServiceHost でホストされている WorkflowRuntime にサービスを追加するホスティング コードを示しています。

図 6 ランタイムへの ChannelManagerService の追加

ServiceEndpoint endpoint = new ServiceEndpoint
(
    ContractDescription.GetContract(typeof(Contracts.IHostInterface)),
        LocalBinding, new EndpointAddress(HostAddress)
);
endpoint.Name = "HostEndpoint";

WorkflowRuntime runtime =
    workflowHost.Description.Behaviors.Find<WorkflowRuntimeBehavior>().
WorkflowRuntime;

ChannelManagerService chanMan =
    new ChannelManagerService(
        new List<ServiceEndpoint>
        {
            endpoint
        });

runtime.AddService(chanMan);

ワークフロー サービスがホストされると、ホストからワークフローにメッセージを送信する機能が提供されますが、ホストにメッセージを返すためには、ワークフローからのメッセージを受信できる WCF サービスが必要です。このサービスは、アプリケーション内で自己ホストされる標準の WCF サービスです。このサービスはワークフロー サービスではないため、標準の NetNamedPipeBinding を使用するか、先ほど説明した NetNamedPipeContextBinding を再利用できます。最後に、このサービスはワークフローから呼び出されるため、UI スレッドでホストして、UI 要素との通信をより単純にできます。図 7 に、このサービスのホスティング コードを示します。

図 7 ホスト サービスのホスティング

ServiceHost appHost = new ServiceHost(new HostService());
appHost.AddServiceEndpoint("Contracts.IHostInterface",
LocalBinding, HostAddress);

try
{
    appHost.Open();
}
catch (Exception ex)
{
    appHost.Abort();
    MessageBox.Show(String.Format(
        "There was an error hosting the local service: {0}",
    ex.Message));
}

両方のサービスがホストされたので、ワークフローを実行して、メッセージを送受信できるようになります。しかし、このコードを使用して、ワークフロー内の 2 つ目の Receive アクティビティに 2 つ目のメッセージを送信しようとすると、コンテキストに関するエラーが返されます。

インスタンスの関連付けの処理

コンテキストに関する問題に対処する 1 つの方法として、すべてのサービス呼び出しに同じクライアント プロキシを使用する方法があります。これにより、(NetNamedPipeContextBinding を使用して) クライアント プロキシでコンテキスト ID を管理し、それ以降の要求ではそのコンテキスト ID をサービスに送信することができます。

シナリオによっては、すべての要求に同一のプロキシを使用し続けることはできないことがあります。たとえば、ワークフローを開始して、データベースに保存し、クライアント アプリケーションを閉じる場合を考えてみましょう。クライアント アプリケーションを再度開始するときには、その特定のインスタンスに別のメッセージを送信することによってワークフローを再開する方法が必要です。ほかにもよくある使用例としては、1 つのクライアント プロキシを使用することを考えていても、それぞれが一意の ID を持つ複数のワークフロー インスタンスと通信する必要がある場合が挙げられます。たとえば、ユーザー インターフェイスに注文の一覧を表示し、各注文にそれぞれワークフローが対応している場合、ユーザーが注文を選択してその操作を呼び出したときは、そのワークフロー インスタンスにメッセージを送信する必要があります。この場合、バインディングでは最後に通信したワークフロー ID を常に使用するため、バインディングではコンテキスト ID を管理することができません。

呼び出しごとに新しいプロキシを使用する 1 つ目のシナリオでは、IContextManager インターフェイスを使用して、ワークフロー ID をコンテキストに手動で設定する必要があります。IContextManager には、IClientChannel インターフェイスの GetProperty<TProperty> メソッドを使用してアクセスできます。IContextManager を取得したら、これを使用してコンテキストを取得または設定できます。

コンテキスト自体は、名前と値の組み合わせで構成される辞書にすぎません。最も重要なのはインスタンス ID 値です。次のコードは、コンテキストから ID を取得して、クライアント アプリケーションでその ID を保存する方法を示しています。後で、同じワークフロー インスタンスと通信する必要があるときに、この保存した ID を使用します。この例では、ID をデータベースに保存するのではなく、クライアントのユーザー インターフェイスに表示しています。

IContextManager mgr = ((IClientChannel)proxy).GetProperty<IContextManager>();
      
string wfID = mgr.GetContext()["instanceId"];
wfIdText.Text = wfID;

ワークフロー サービスを最初に呼び出すときに、サービス エンドポイントのコンテキスト バインディングにより、コンテキストにワークフローのインスタンス ID が自動的に設定されます。

新しく作成したプロキシを使用して、先ほど作成したワークフロー インスタンスと通信する場合は、同様の方法を使用してコンテキスト内で ID を設定し、メッセージを適切なワークフロー インスタンスにルーティングします。

IContextManager mgr = ((IClientChannel)proxy).GetProperty<IContextManager>();
  mgr.SetContext(new Dictionary<string, string>{
    {"instanceId", wfIdText.Text}
  });

プロキシを新しく作成すると、最初はうまく機能します。しかし、2 回目にコンテキストを設定して別のワークフロー インスタンスを呼び出そうとすると、うまくいきません。返されるエラーでは、自動コンテキスト管理が有効になっているのに、コンテキストを変更できないことが通知されます。原則的に、一挙両得はできないということです。コンテキストを自動管理しているときに、手動で操作することはできません。残念ながら、コンテキストを手動で管理する場合は、自動管理は実行できません。つまり、先ほど説明したように、コンテキストからワークフロー インスタンス ID を取得することはできません。

このように相容れないことを行うには、それぞれ個別に処理します。つまり、ワークフローの最初の呼び出しには新しいプロキシを使用し、それ以降のすべての既存のワークフロー インスタンスの呼び出しには、1 つのクライアント プロキシを使用してコンテキストを手動で管理します。

最初の呼び出しでは、1 つの ChannelFactory<TChannel> を使用してすべてのプロキシを作成します。ChannelFactoryの作成にはオーバーヘッドが伴いますが、毎回の呼び出しで作成が繰り返されなくなるため、結果的にパフォーマンスが向上します。図 5 で示したようなコードを使用すると、1 つの ChannelFactory<TChannel> を使用して最初のプロキシを作成できます。コードの呼び出しでは、プロキシの使用後に、Close メソッドを呼び出してプロキシを解放するベスト プラクティスに従います。

これは、チャネル ファクトリ方式を使用してプロキシを作成する際の標準の WCF コードです。バインディングがコンテキスト バインディングであるため、既定でコンテキストが自動管理されます。つまり、ワークフローを最初に呼び出した後に、コンテキストからワークフロー インスタンス ID を抽出できます。

それ以降の呼び出しを行う際は、コンテキストを自身で管理する必要があります。この作業では、開発者があまり頻繁には使わない WCF クライアント コードを使用することになります。コンテキストを手動で設定するには、OperationContextScope を使用して、MessageContextProperty を自身で作成する必要があります。MessageContextProperty は、送信するメッセージに設定します。これは、IContextManager を使用してコンテキストを設定することに似ていますが、コンテキスト管理が無効になっている場合でもプロパティを直接使用できる点が異なります。図 8 に、最初のプロキシで使用したのと同じ ChannelFactory<TChannel> を使用して、プロキシを作成するコードを示します。この場合は、IContextManager を使用して自動コンテキスト管理機能を無効にする点と、要求ごとに新しいプロキシを作成するのではなくキャッシュされたプロキシを使用すると点が以前と異なります。

図 8 自動コンテキスト管理の無効化

App a = (App)Application.Current;

if (updateProxy == null)
{
    if (factory == null)
        factory = new ChannelFactory<IWorkflowInterface>(
            a.LocalBinding, a.WFAddress);

        updateProxy = factory.CreateChannel();
        IContextManager mgr =
            ((IClientChannel)updateProxy).GetProperty<IContextManager>();
        mgr.Enabled = false;
        ((IClientChannel)updateProxy).Open();
}

プロキシを作成したら、OperationContextScope を作成して、スコープ内の送信メッセージのプロパティに MessageContextProperty を追加する必要があります。これにより、スコープの期間中はプロパティを送信メッセージに含めることができます。図 9 に、OperationContextScope を使用してメッセージのプロパティを作成および設定するコードを示します。

図 9 OperationContextScope の使用

using (OperationContextScope scope =
    new OperationContextScope((IContextChannel)proxy))
{
    ContextMessageProperty property = new ContextMessageProperty(
        new Dictionary<string, string>
        {
            {“instanceId”, wfIdText.Text}
        });

OperationContext.Current.OutgoingMessageProperties.Add(
    "ContextMessageProperty", property);

proxy.UpdateOrder(
    new Order
        {
            CustomerName = "Matt",
            OrderID = 2,
            OrderTotal = 250.00,
            OrderStatus = "Updated"
        });
}

ホストとワークフロー間に限って言えば、これはかなりの作業のように思えるかもしれません。さいわい、このロジックの大部分と ID の管理は、いくつかのクラスにカプセル化できます。ただし、この作業では、ワークフロー インスタンスに複数のメッセージを送信する必要がある場合に、コンテキストが適切に管理されるようにする特別な方法でクライアントをコーディングする必要があります。今回のダウンロード コードには、こうした複雑さの大部分をカプセル化した、ローカル通信に使用するワークフローのサンプル ホストと、ホストの使用方法を示すサンプル アプリケーションが含まれています。

ユーザー インターフェイスの対話について

ワークフローからホストにデータを送信する主な理由の 1 つは、アプリケーション インターフェイスでユーザーにデータを表示するためです。さいわい、このモデルでは、WPF でのデータ バインディングを含め、ユーザー インターフェイス機能を活用するためのオプションがいくつかあります。単純な例としては、ユーザー インターフェイスでデータ バインディングを使用し、ワークフローからのデータ受信時にユーザー インターフェイスを更新する場合に、ユーザー インターフェイスをホストのサービス インスタンスに直接バインドできます。

サービス インスタンスをウィンドウのデータ コンテキストとして使用する際の重要なポイントは、インスタンスをシングルトンとしてホストする必要があることです。サービスをシングルトンとしてホストすると、そのインスタンスにアクセスでき、UI 内でそのインスタンスを使用できます。図 10 に示す単純なホスト サービスでは、ワークフローからの情報受信時にプロパティを更新し、データ バインディング インフラストラクチャが変更をすぐに検出できるように INotifyPropertyChangedInterface を使用しています。ServiceBehavior 属性は、このクラスがシングルトンとしてホストされることを示しています。もう一度図 7 を参照すると、型ではなくクラスのインスタンスを使用して ServiceHost のインスタンスが作成されていることがわかります。

図 10 INotifyPropertyChanged を備えたサービスの実装

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
internal class HostService : IHostInterface, INotifyPropertyChanged
{
    public void OrderStatusChange(Order order, string newStatus,
        string oldStatus)
    {
        CurrentMessage = String.Format("Order status changed to {0}",
            newStatus);
    }

private string msg;

public string CurrentMessage {
get { return msg; }
set
    {
        msg = value;
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(
                "CurrentMessage"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

この値にデータバインドするために、ウィンドウの DataContext、またはウィンドウの特定のコントロールが、インスタンスに設定されます。このインスタンスは、ServiceHost クラスの SingletonInstance プロパティを使用することによって取得できます。

HostService host = ((App)Application.Current).appHost.SingletonInstance as HostService;
  if (host != null)
    this.DataContext = host;

これで、ウィンドウ内の要素とオブジェクトのプロパティをバインドできます (次の TextBlock 参照)。

<TextBlock Text="{Binding CurrentMessage}" Grid.Row="3" />

前述のように、これは実行できる処理の単純な例です。実際のアプリケーションでは、サービス インスタンスに直接バインドすることはあまりなく、ウィンドウとサービスの両方の実装からアクセスできるいくつかのオブジェクトにバインドします。

WF4 に向けての準備

WF4 では、WCF を通じてローカル通信をさらに簡単にする機能がいくつか導入されます。主な機能は、プロトコルに依存しないメッセージの関連付けです。つまり、ワークフロー インスタンス ID を使用するオプションは依然として存在しますが、新しいオプションを使用することで、メッセージのコンテンツに基づいてメッセージを関連付けることが可能になります。そのため、各メッセージに注文 ID、顧客 ID などのデータが含まれている場合は、これらのメッセージ間で関連付けを定義することができ、その際にコンテキスト管理をサポートするバインディングを使用する必要はありません。

また、WPF と WF の両方が .NET Framework 4 の同じコア XAML API 上に構築されることにより、テクノロジをまったく新しい方法で統合する興味深い可能性が開けるかもしれません。.NET Framework 4 のリリースが近づくにつれて、WF と WCF や WPF との統合に関する詳細に加えて、WF4 内部のしくみに関するその他のコンテンツも提供していきます。            

Matt Milner は、Pluralsight の技術スタッフのメンバーとして主に接続システム テクノロジ (WCF、Windows Workflow Foundation、BizTalk、"Dublin" および Azure Services Platform) に携わっています。また、Microsoft .NET アプリケーションの設計および開発を専門とするフリーランスのコンサルタントでもあります。Matt は、地元または地域での会議や国際的なカンファレンス (Tech Ed など) での発言を通してテクノロジへの情熱を分かち合っています。接続システム テクノロジに基づく地域への貢献に対して、マイクロソフトから MVP として認められました。Matt には、彼のブログ (pluralsight.com/community/blogs/matt、英語) から連絡することができます。