WF 4.5 新機能 : Versioning (Side-by-Side, Dynamic Update)

環境 :
Visual Studio 2012 RC (.NET Framework 4.5 RC)

WF 4.5 新機能

こんにちは。

イチロー (Ichiro) が Seattle から居なくなってしまいました。実は、先々週のまさにその時、Seattle 出張だったのですが (しかも、はじめての夏の Seattle 訪問でした . . .)、結局、生イチローを見ることなく終わってしまいました。子供にイチローの背番号のついた服を買っていったのですがサイズがあわず、少し残念な出張になってしまいました . . . (次回からは、Hernandez を応援したいと思います . . .)

ところで、間があいてしまい すみません。WF 4.5 の新機能の紹介の途中でしたので、続きを掲載したいと思います。
今回は、WF 4.5 の Versioning 機能を、実際のコードを含め解説します。

 

Side-by-Side (SxS)

ワークフローがサービス インされて実行された後で、そのワークフロー (ビジネス プロセス) の修正が必要になった場合、通常のビジネス要件では、既に実行中の古いワークフローは古いバージョンのまま実行し、これから実行するワークフローは、新しいバージョンのワークフロー定義で動作させるのが普通でしょう。
これまでの WF では、こうした複数のバージョンのワークフローの同時実行を制御する仕組みを持っていませんでした。

補足 : SharePoint のワークフローでは、独自の方法で、こうしたビジネス要件に対応するための仕組みを持っています。

WF 4.5 では、こうした制御をおこなえるよう、永続化データベースにワークフロー定義の ID とバージョン番号を登録するようになっており、開発者は、この登録された情報を元に、動的に必要なワークフロー定義をロードして実行できるようになっています。(これを、ワークフローの Side-by-Side 実行と呼びます。)

この Side-by-Side 実行は、特に Workflow Service を使用した場合、非常に簡易に活用できるようになっていますが (後述)、今回は、まず、内部構造を理解するため、コンソール アプリケーションを使って動きを見てみましょう。
なお、以下では、WF の永続化 (Persistent) についての事前知識が必要です。WF の永続化についてご存じない方は、あらかじめ、「WF 4 : Workflow Extensions と永続化、トラッキング」などで予習しておいてください。

では早速、動作を見てみましょう。
まずは、ワークフローを永続化するデータベースを作成するため、SQL Server のデータベースを作成し、下記のスクリプトを実行してください。

%windir%Microsoft.NETFrameworkv4.0.30319SQLjaSqlWorkflowInstanceStoreSchema.sql
%windir%Microsoft.NETFrameworkv4.0.30319SQLjaSqlWorkflowInstanceStoreLogic.sql

SQL Server にログインし、System.Activities.DurableInstancing.Instances のビューなどを見ると、下図の通り、IdentityName, Major, Minor など、バージョン関連の列 (カラム) が定義されているのが確認できます。(下図は、SQL Server Management Studio を使って確認しています。)

では、Visual Studio 2012 で .NET Framework 4.5 の Workflow Console Application を作成してみましょう。
今回は、いったんワークフロー インスタンスを永続化する必要があるため、下記の通り、ブックマーク を持つ簡単なカスタム アクティビティを作成し、これを使用します。

. . .
using System.Activities;
. . .

public class WaitInputActivity : NativeActivity
{
  protected override bool CanInduceIdle
  {
    get { return true; }
  }

  protected override void Execute(
    NativeActivityContext context)
  {
    Console.WriteLine("Instance Id : {0}",
      context.WorkflowInstanceId.ToString());
    context.CreateBookmark("Bookmark1-MyWorkflow",
      new BookmarkCallback(OnResume));
  }

  void OnResume(NativeActivityContext context,
    Bookmark bookmark, object value)
  {
    Console.WriteLine("OnResume Called ! : {0}",
      value.ToString());
  }
}
. . .

Workflow1.xaml を開き、今回は、下図の通り、簡単なワークフローを作成します。(下図の WaitInputActivity は、上記のカスタム アクティビティです。)

では、このワークフローを実行します。
WF のバージョン コントロールをおこなう場合、WF の自己ホストでは WorkflowApplication を使用してください。(後述しますが、WorkflowApplication 以外に、WorkflowServiceHost でもバージョン コントロールが可能です。)
プロジェクトに System.Runtime.DurableInstancing.dll、System.Activities.DurableInstancing.dll を参照追加し、Program.cs を開いて、下記の通り実装します。

. . .
using System.Runtime.DurableInstancing;
using System.Activities.DurableInstancing;
. . .

static void Main(string[] args)
{
  Console.WriteLine("Input instance id, or empty (new instance).");
  string instanceId = Console.ReadLine();

  // Set instancestore (persistence settings)
  InstanceStore store = new SqlWorkflowInstanceStore(
    @"Data Source=.sqlexpress;Initial Catalog=WFPersistDB;Integrated Security=True");
  InstanceView view = store.Execute(store.CreateInstanceHandle(),
    new CreateWorkflowOwnerCommand(),
    TimeSpan.FromSeconds(30));
  store.DefaultInstanceOwner = view.InstanceOwner;

  if (string.IsNullOrEmpty(instanceId))
  {
    //
    // Create a new instance
    //

    AutoResetEvent evt = new AutoResetEvent(false);
    WorkflowIdentity wid = new WorkflowIdentity()
    {
      Name = "SampleWF",
      Version = new Version(1, 0, 0, 0)
    };
    WorkflowApplication app = new WorkflowApplication(
      new Workflow1(), wid);
    app.InstanceStore = store;
    app.PersistableIdle = (e) => PersistableIdleAction.Unload;
    app.Unloaded = (e) =>
    {
      Console.WriteLine("Unloaded !");
      evt.Set();
    };
    app.Run();
    evt.WaitOne();
  }
  else
  {
    //
    // Get and continue a persisted instance
    //

    AutoResetEvent evt = new AutoResetEvent(false);
    WorkflowApplicationInstance ins = WorkflowApplication.GetInstance(
      new Guid(instanceId), store);
    WorkflowApplication app = new WorkflowApplication(
      GetWorkflowActivity(ins.DefinitionIdentity),
      ins.DefinitionIdentity);
    app.InstanceStore = store;
    app.Unloaded = (e) =>
    {
      Console.WriteLine("Unloaded !");
      evt.Set();
    };
    app.Load(ins);
    app.ResumeBookmark("Bookmark1-MyWorkflow", "test data");
    evt.WaitOne();
  }

  Console.ReadLine();
}

static Activity GetWorkflowActivity(WorkflowIdentity wid)
{
  if ((wid.Name == "SampleWF") &&
    (wid.Version.Major == 1))
    return new Workflow1();
  else
    throw new Exception("Unknown workflow identity");
}
. . .

上記のコードを解説します。
このアプリケーションでは、まず、コンソールから文字列を読み込み (ReadLine)、もし入力が空文字の場合 (文字が入力されなかった場合) には、ワークフロー インスタンスを新規作成し、実行 (開始) します。
開始された Workflow1 のインスタンスは、上記の WaitInputActivity (ブックマークを持つカスタム アクティビティ) のブックマークの箇所で Idle 状態となり、インスタンスはいったんメモリーから Unload されて SQL Server に保存されます。(つまり、”This is version1″ という文字列がコンソールに表示される前に、いったん SQL Server に永続化されます。)
この実行結果は、下図の通りになります。

SQL Server の中を見てみると、実際に、永続化されたインスタンスが保持されているのがわかります。

つぎに、上記のコンソール アプリケーションを再度実行し、コンソールに Instance Id の文字列 (上図の出力結果) を入力すると、今度は、SQL Server に永続化されていたインスタンスがメモリーにロードされ、続きから実行します。WaitInputActivity アクティビティのブックマークの処理がおこなわれ (上記コードの ResumeBookmark 参照)、”This is version1″ の文字列がコンソールに出力されてワークフローは完了します。(完了すると、メモリーから Unload されます。)

特に、上記の GetWorkflowActivity メソッドに注目してください。ワークフローのデータベースからのロードの際に、永続化データベース (SQL Server) に登録された WorkflowIdentity の Name や Version 番号を確認し、もしバージョン 1 なら Workflow1 のアクティビティを実行するように制御しています。今回は、まだ Workflow1 しかありませんので、それ以外の場合は例外を発生させています。

さて、今回は、上記の永続化されたバージョン 1 のインスタンスをそのまま放置して (データベースに残したままにして)、Visual Studio でバージョン 2 のワークフロー (Workflow2.xaml) を新規作成してみましょう。(つまり、バージョン 1 のワークフロー インスタンスを残したまま、下記の手順でバージョン 2 のワークフローを使用します。)
Visual Studio で Workflow2.xaml を新規追加し、下図の通りワークフローを作成します。(Workflow1 の Sequence アクティビティごとコピー / ペーストできます。下記では、出力する文字列を “This is version 2” に変更しただけです。)

さらに、Program.cs のコードを下記太字の通り変更・追加します。

static void Main(string[] args)
{
  Console.WriteLine("Input instance id, or empty (new instance).");
  string instanceId = Console.ReadLine();

  // Set instancestore (persistence settings)
  InstanceStore store = new SqlWorkflowInstanceStore(
    @"Data Source=.sqlexpress;Initial Catalog=WFPersistDB;
      Integrated Security=True");
  InstanceView view = store.Execute(store.CreateInstanceHandle(),
    new CreateWorkflowOwnerCommand(),
    TimeSpan.FromSeconds(30));
  store.DefaultInstanceOwner = view.InstanceOwner;

  if (string.IsNullOrEmpty(instanceId))
  {
    //
    // Create a new instance
    //

    AutoResetEvent evt = new AutoResetEvent(false);
    WorkflowIdentity wid = new WorkflowIdentity()
    {
      Name = "SampleWF",
      Version = new Version(2, 0, 0, 0)
    };
    WorkflowApplication app = new WorkflowApplication(
      new Workflow2(), wid);
    app.InstanceStore = store;
    app.PersistableIdle = (e) => PersistableIdleAction.Unload;
    app.Unloaded = (e) =>
    {
      Console.WriteLine("Unloaded !");
      evt.Set();
    };
    app.Run();
    evt.WaitOne();
  }
  else
  {
    //
    // Get and continue a persisted instance
    //

    AutoResetEvent evt = new AutoResetEvent(false);
    WorkflowApplicationInstance ins = WorkflowApplication.GetInstance(
      new Guid(instanceId), store);
    WorkflowApplication app = new WorkflowApplication(
      GetWorkflowActivity(ins.DefinitionIdentity),
      ins.DefinitionIdentity);
    app.InstanceStore = store;
    app.Unloaded = (e) =>
    {
      Console.WriteLine("Unloaded !");
      evt.Set();
    };
    app.Load(ins);
    app.ResumeBookmark("Bookmark1-MyWorkflow", "test data");
    evt.WaitOne();
  }

  Console.ReadLine();
}

static Activity GetWorkflowActivity(WorkflowIdentity wid)
{
  if ((wid.Name == "SampleWF") &&
    (wid.Version.Major == 1))
    return new Workflow1();
  else if ((wid.Name == "SampleWF") &&
    (wid.Version.Major == 2))
    return new Workflow2();
  else
    throw new Exception("Unknown workflow identity");
}
. . .

以上で修正は完了です。

今度は、ワークフロー インスタンスの新規作成時は Workflow2 を作成するように変更し、SQL Server に永続化されたワークフロー インスタンスのロードの際は、登録されたインスタンスのバージョンを確認して、Workflow1、Workflow2 を選択してロードするように変更しました。

このアプリケーションを実行して、まずは、さきほどと同様、何も入力せず Enter キーを押すと、今度は内部で Workflow2 が実行されます。試しに、コンソールに出力された Instance Id をコピーして、再度、コンソール アプリケーションを開始し、コピーした Instance Id を入力してワークフローを継続すると、下図の通り、Workflow2 (バージョン 2 のワークフロー) が実行 (および、完了) されるのが確認できます。

しかし、このワークフロー コンソール アプリケーションでは、先ほど実行した Workflow1 (バージョン 1 のワークフロー) もサポートされています。
このアプリケーションを実行し、今度は、上記であらかじめ永続化しておいた Workflow1 の Instance Id を入力してみてください。下図の通り、Workflow1 (バージョン 1 のワークフロー) が実行されているのがわかります。

このように、データベースに保存されているバージョン番号などの情報を元に、新しいバージョンのワークフロー (Workflow2) を扱いながら、永続化されている古いバージョンのワークフロー (Workflow1) も同時に実行できます。

 

Side-by-Side (SxS) – Workflow Service の場合

もちろん、Workflow Service で Side-by-Side をおこなうこともできます。

WF の自己ホストのアプリケーションの場合 (WorkflowApplication を使用した場合) では、上記の通り、バージョンの制御を開発者自らが構築しました。実は、Workflow Service の場合は、もっと簡単に複数バージョンのサポートを実装できます。(WorkflowServiceHost が、内部で、上記のような処理をおこなってくれます。)

まず、準備として、Workflow Service を新規作成し、Web.config を開いて、下図の通り、InstanceStore の設定をおこないます。(Workflow instance の Idle 時に SQL Server に Unload をおこなうように構成します。なお、Workflow Service は IIS の Application Pool で実行されるので、SQL Server のログイン情報には注意してください。)

. . .

<system.serviceModel>
  <behaviors>
  <serviceBehaviors>
    <behavior>
    . . .

    <sqlWorkflowInstanceStore
      connectionString="Data Source=.sqlexpress;Initial Catalog=WFPersistDB;
        User Id=test;Password=password"
      instanceCompletionAction="DeleteAll"/>
    <workflowIdle timeToUnload="0"/>
    </behavior>
  </serviceBehaviors>
  </behaviors>
  . . .

</system.serviceModel>
. . .

つぎに、ワークフローを作成します。Workflow Service におけるワークフローの基本的な構築手順については、「ワークフローを使用したサービスの作成 (ワークフロー サービス)」、「ワークフロー サービスにおける状態の維持 (Correlation の使用)」を参照してください。(ここでは、構築手順の説明は省略します。)

つぎに WorkflowIdentity を設定しますが、Workflow Service で WorkflowIdentity を設定するには、ワークフロー (.xamlx) のプロパティ ウィンドウを開いて、[DifinitionIdentity] プロパティを設定します。(下図)

複数バージョンをサポートしたい場合は、App_Code フォルダーにサービス名と同じ名前をサブフォルダーを作成し、そこに古いバージョンの .xamlx ファイル (古い DefinitionIdentity の定義された .xamlx ファイル) をコピーします。(ファイル名は、適当に設定しておきます。)

補足 : なお、ワークフロー サービスの自己ホストの場合 (IIS を使わず、WorkflowServiceHost を使った自己ホストの場合) には、App_Code に入れる代わりに、WorkflowServiceHost の SupportedVersions プロパティを使って複数バージョンのサポートをコードで指定することもできます。

以上で完了です。あとは、WorkflowServiceHost が上記の設定を参照して、複数バージョンのコントロールをしてくれます。例えば、永続化されたワークフローが古いバージョンのワークフローの場合は、App_Code に入っている古いバージョンのワークフロー定義 (.xamlx) が使用され、続きから実行できます。

なお、使用している変数 (DataContract) などを変更したい場合は、簡単なメンバーの追加などであれば、WCF の Version Resiliency の仕組みによって、下記の通り定義すれば OK です。

[DataContract]
public class TestData
{
  . . .

  [DataMember(IsRequired = false)]
  public int AddedFlag { get; set; }
}

 

Dynamic Update

Side-by-Side は複数のバージョンのワークフローを同時に扱うケースですが、さらに応用的なケースとして、バージョン 1 のワークフローを実行後にバージョン 2 を作成し、永続化されたバージョン 1 のワークフローをバージョン 2 に変更して継続させたい場合があります。こうした場合には、WF 4.5 の Dynamic Update が使用できます。

Dynamic Update は、下記の流れで実施します。

  1. ワークフロー定義 (.xaml, .xamlx) を作成します。
  2. ワークフローを実行して (ワークフロー インスタンスを作成して)、永続化をおこないます。
  3. ワークフロー定義を変更しますが、この際、変更前に、このワークフローの変更開始を宣言します (DynamicUpdateServices.PrepareForUpdate メソッド)
  4. ワークフロー定義を変更します
  5. 変更が完了したら、変更内容を記録した Update Map (.map ファイル) を作成します。(DynamicUpdateServices.CreateUpdateMap メソッド)
    また、変更後のワークフロー定義 (.xaml, .xamlx) を保存しておきます。
  6. 永続化されているインスタンス (上記) に、この Update Map を適用します。
  7. 永続化されているインスタンスを、変更後の新しいワークフローとして実行 (継続) できます。

今回は、上記で作成した Workflow1.xaml を下図の Workflow1_new.xaml に変更する場合を例に解説します。(WriteLine の文字列を “This is version1” から “This is version2” に変更した簡単なワークフローです。)

Workflow1.xaml

Workflow1_new.xaml

まず準備として、前述のプログラム (上記の SxS のサンプル コードを参照) をそのまま使って、Workflow1 のワークフロー インスタンスを開始し、WaitInputActivity の箇所でインスタンスを永続化 (Unload) しておきます。(SQL Server にバージョン 1 のインスタンスが保存されます。)

以降のコードで、この Workflow1.xaml を Workflow1_new.xaml (上図) の定義に変更 (Update) し、この変更内容を (SQL Server に) 永続化されたワークフロー インスタンスに反映します。

まず、別のコンソール アプリケーションを新規作成し、下記の通り実装します。(System.Runtime.Serialization.dll、System.Activities.dll、System.Xaml.dll を参照追加します。)
このコードでは、プログラム コードを使ってワークフローの変更をおこない、変更の内容を Update Map (test.map) として保存し、変更後のワークフロー定義を新しいファイル名 (Workflow1_new.xaml) で保存しています。

なお、元の Workflow1.xaml は、この後も使用するので、上書きしないようにしてください。

注意 : 下記のコンソール アプリケーションでは上記の WaitInputActivity (カスタム アクティビティ) を使用するため、WaitInputActivity をライブラリー (.dll) などで作成しておき、下記のプロジェクト (コンソール アプリケーション) でこのライブラリーを参照追加しておいてください。

. . .
using System.IO;
using System.Runtime.Serialization;
using System.Xaml;
using System.Activities;
using System.Activities.DynamicUpdate;
using System.Activities.XamlIntegration;
using System.Activities.Statements;
. . .

static void Main(string[] args)
{
  // prepare Xaml before update
  ActivityBuilder ab;
  using (var rd = new StreamReader(
    @"C:DemoWorkflowConsoleApplication1Workflow1.xaml"))
  {
    ab = (ActivityBuilder)XamlServices.Load(
      ActivityXamlServices.CreateBuilderReader(
        new XamlXmlReader(rd, new XamlSchemaContext())));
  }
  DynamicUpdateServices.PrepareForUpdate(ab);

  // Update workflow !
  // 1. Remove a last WriteLine activity
  // 2. Insert a new WriteLine activity
  Sequence sq = (Sequence)ab.Implementation;
  sq.Activities.RemoveAt(1);
  WriteLine wl = new WriteLine()
  {
    Text = "This is version2"
  };
  sq.Activities.Insert(1, wl);

  // Create Update Map
  DynamicUpdateMap map =
    DynamicUpdateServices.CreateUpdateMap(ab);
  DataContractSerializer sr =
    new DataContractSerializer(typeof(DynamicUpdateMap));
  using (FileStream fs = System.IO.File.Open(
    @"C:Demotest.map",
    FileMode.Create))
  {
    sr.WriteObject(fs, map);
  }

  // Save new workflow (Workflow1_new.xaml)
  StreamWriter wr = File.CreateText(
    @"C:DemoWorkflowConsoleApplication1Workflow1_new.xaml");
  XamlWriter xw = ActivityXamlServices.CreateBuilderWriter(
    new XamlXmlWriter(wr, new XamlSchemaContext()));
  XamlServices.Save(xw, ab);
  wr.Close();
}
. . .

test.map には、XML 形式で変更内容が保存されます。

つぎに、作成した Update Map (上記の test.map) を永続化しておいたバージョン 1 のワークフロー インスタンスに適用します。
別のコンソール アプリケーションを新規作成し、下記の通り実装します。(System.Runtime.Serialization.dll、System.Activities.dll、System.Runtime.DurableInstancing.dll、System.Activities.DurableInstancing.dll を参照追加します。)
なお、下記で使用する Workflow1 は、変更前の古いワークフロー定義 (Workflow1.xaml) を使用してください。

. . .
using System.Activities;
using System.Runtime.DurableInstancing;
using System.Activities.DurableInstancing;
using System.Activities.DynamicUpdate;
using System.IO;
using System.Runtime.Serialization;
. . .

static void Main(string[] args)
{
  Console.WriteLine("Input instance id.");
  Guid instanceId = new Guid(Console.ReadLine());

  //
  // Load update map in memory
  //
  DynamicUpdateMap map;
  using (FileStream fs =
    File.Open(@"C:Demotest.map", FileMode.Open))
  {
    DataContractSerializer sr =
      new DataContractSerializer(typeof(DynamicUpdateMap));
    map = (DynamicUpdateMap)sr.ReadObject(fs);
  }

  //
  // Apply map and save
  //
  SqlWorkflowInstanceStore store = new SqlWorkflowInstanceStore(
    @"Data Source=.sqlexpress;Initial Catalog=WFPersistDB;
      Integrated Security=True");
  WorkflowApplicationInstance ins =
    WorkflowApplication.GetInstance(instanceId, store);
  WorkflowIdentity wid = new WorkflowIdentity
  {
    Name = "SampleWF",
    Version = new Version(2, 0, 0, 0)
  };
  WorkflowApplication app =
    new WorkflowApplication(new Workflow1(), wid);
  app.Load(ins, map);
  app.Unload();
}
. . .

以上で Update 完了です。

あとは、上記で作成した新しい Workflow1_new.xaml を使って、永続化されたワークフロー インスタンスの続きから実行 (Load) できます。永続化されたインスタンスを Resume すると、実行結果として、コンソールに “This is version2” が表示されるはずです。
なお、Dynamic Update では、元の Workflow1.xaml も、新しく作成された Workflow1_new.xaml も、同じ Workflow1 というクラス名になっているので、いったん、Workflow1.xaml をプロジェクトから削除するなどして、新しい Workflow1_new.xaml をプロジェクトに取り込んで使用してください。(.xaml ファイルで作成されるクラス名は Name 属性で変更できます。今回の場合、既に実行済みのインスタンスがあるため、この Name 属性は勝手に変更しないでください。)

Dynamic Update も、SxS と同様、WorkflowApplication と WorkflowServiceHost の双方で使用できます。

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s