Build Custom Connector on Microsoft Flow and PowerApps with Authentication

The custom connector (API connector) enables you to connect your own web api (REST api) in Microsoft Flow (including SharePoint workflow) and PowerApps. You can connect Microsoft Flow and PowerApps with your in-house applications or unknown 3rd party (ISV) applications.

In this post I show you how to build and use the custom connector (API connector), and in most cases the authentication is needed, then I also explain with real authentication scenario. (First I explain using Azure AD, and next I show you the other cases, such as Google account.)

Note : Now you can submit your connector for certification and publication. (All users can access your connector.) See “Submit your connectors for Microsoft certification“. (Added on May 2017)

Note : You can now build and publish your API connector with Postman (see here), but here we create with custom swagger configuration. (Added on May 2017)

Build your own web api

In the first example, we use the Azure Active Directory (Azure AD)  as the authentication provider with custom api connector.
In this case, your web api must handle the OAuth access token.

I don’t describe how to build the web api secured by the Azure AD, but if you’re using ASP.NET Web API, you just click [Change Authentication] button in the project creation wizard and set-up the Azure AD information. (See the following screen.)

If you’re using other programming language, see “How to build API secured by Azure AD” (Japanese) in my previous post.

Note : You can also use the Azure App Service (API App) and [Authentication / Authorization] settings (so called “Easy Auth”) for every programming languages.
See “Azure App Service Authentication Deep Dive” (Japanese) for details.

Next you must prepare the swagger specification file (json file).
If you use ASP.NET Web API, you can insert “Swashbuckle” Nuget package and download this json by accessing http://{your root URL}/swagger/docs/v1.

The next is one of the swagger specification example. Especially, you must remember operationId value (the following “Values_Get”), because we use this operation in the PowerApps later.

In the swagger specification, you must add the following “securityDefinitions” section, and set Azure AD authentication information as follows.

{
  "swagger": "2.0",
  "info": {
    "version": "v1",
    "title": "TestApi01"
  },
  "host": "demo.azurewebsites.net",
  "schemes": [
    "https"
  ],
  "paths": {
    "/api/Values/{id}": {
      "get": {
        "tags": [
          "Values"
        ],
        "operationId": "Values_Get",
        "consumes": [
          
        ],
        "produces": [
          "application/json",
          "text/json",
          "application/xml",
          "text/xml"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "type": "integer",
            "format": "int32"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      . . .

    }
  },
  "definitions": {
  },
  "securityDefinitions": {
    "oauth2": {
      "type": "oauth2",
      "flow": "implicit",
      "authorizationUrl": "https://login.windows.net/common/oauth2/authorize",
      "scopes": {
        
      }
    }
  }
}

If you use Swashbuckle in your ASP.NET Web API project, you just insert the following code (bold font) in App_StartSwaggerConfig.cs.

public class SwaggerConfig
{
  public static void Register()
  {
    var thisAssembly = typeof(SwaggerConfig).Assembly;

    GlobalConfiguration.Configuration 
      .EnableSwagger(c =>
        {
          c.SingleApiVersion("v1", "TestApi01");
          . . .

          c.OAuth2("oauth2")
            .AuthorizationUrl("https://login.windows.net/common/oauth2/authorize")
            .Flow("implicit")
            .Scopes(scopes =>
            {
              //scopes.Add("xxxxx", "Some access to protected resources");
            });

        })
      .EnableSwaggerUi(c =>
        {
          . . .

        });
  }
}

Note (added Feb 2017) : If you use Azure App Services (including Azure Functions) for hosting your api, you can easily create your swagger definition with security configurations. See “PowerApps team blog : Making it easier to use Azure APIs in PowerApps” for more details.

Register APIs in Azure AD

In the case of Azure AD, the custom connector proxy in the Microsoft Flow or PowerApps retrieves the access token for your web api resource, and calls your web api by setting this token in the http header.
i.e, you must register both the custom connector proxy app and your web api app in the Azure AD, and set the permission between custom connector proxy and your web api.

Note : If you have used the previous [Change Authentication] button in ASP.NET Web API, the web api app is already registered in Azure AD.

The following illustrates this.

When you register the custom connector proxy (Azure AD app of Microsoft Flow or PowerApps side), you must add the following url (fixed value) as the redirect url. Both Microsoft Flow and PowerApps uses this redirect url, when processing OAuth.

https://msmanaged-na.consent.azure-apim.net/redirect

Currently (in Nov 2016), the Azure AD v2 endpoint is not supported (but v1 only) for this scenario, and you must use the Azure Classic Portal (https://manage.windowsazure.com/), not Azure Ibiza Portal (https://portal.azure.com).
And you must set the custom connector proxy’s permissions for accessing your web api. (You cannot set this permission and cannot see the resource id in the Ibiza portal today.)

See the following screenshot.

Note : Strictly speaking, v2.0 endpoint (Azure AD v2 endpoint) is supported in the custom api connector. But, this proxy and web api flow (see the illustration above) is not supported for v2.0 endpoint.
Please refer the next Google scenario (flow) for the v2.0 endpoint.

How to work (or use) in PowerApps

Now you’re ready to use the custom api connector in Microsoft Flow and PowerApps. Here I show you the step of setting PowerApps.

Note : For the details of this procedure, please refer the official document “Register Custom Connectors in PowerApps“.

First you login to PowerApps (https://web.powerapps.com/), and select [Connections] in the navigation, and click [New connection].

Select [Custom] tab and click [New custom API].

In the next window, upload the swagger specification file (json) previously created.

The swagger specification is parsed and the identity provider is auto-detected by the PowerApps.
You must set some api information in the next window like the following screenshot. Note that this client id and secret is for the previous custom connector proxy, not your web api. The Resource URL (the accessing scopes) is the ID/URI of your web api, not the custom connector proxy.

The connection settings of the custom api connector has done. Next you start to create the app in the PowerApps.

Click [New App] button, and select [Phone layout] in the [Blank app].

In the design window, select [Content] – [Data sources] menu.

The data source window is displayed in the right pane, and click [Add data source] button, and [New connection] button.
You can find the previously created custom api connector, and select (connect) that.

Then the Azure AD sign-in UI is displayed, and you must enter your credential.

The custom api connector is inserted as the data source.

Let’s test this custom connector !

Please insert the button control into your app, and input the following expression as the button “OnSelect” function (fx).
This is calling the “ValuesGet” method (see the previous “operationId” in the swagger file) in the custom connector named “TestApi01”, and setting the string result in the context variable named “ResultValue”.

UpdateContext( { ResultValue : TestApi01.ValuesGet(3) } )

Note : You can use auto-correct or suggestions when you’re writing functions.

Next you insert the text box, and set “ResultValue” (the context variable previously created) in the text box function.

Push run button, and this app is launched in the web browser.
When you push the button in the app, your web api is called by the PowerApps and the returned value is displayed in the text box.

In the backend, the following http request is called against your web api.
Then your web api can verify the token (the following “Authorization” header value) and retrieve the claims, as I described before in the post “How to build API secured by Azure AD” (Japanese).

GET /api/Values/3
Accept: */*
Authorization: Bearer eyJ0eXAiOi...
Origin: https://us.create.powerapps.com
X-Ms-Apim-Tokens: ew0KICAidG...
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

"you entered 3"

Note : The “X-Ms-Apim-Tokens” header is also important, and I explain this later. (In the case of Azure AD, there’s no need to use this token.)

This token (“Authorization” header value) is the Azure AD access token iteself. Then you can also get the access token for anothor resources in your web api by calling the following OAuth on_behalf_of flow.
That is, your web api can collaborate another Azure AD resources like Office 365 API, Azure ARM REST, Power BI REST, etc.

POST https://login.microsoftonline.com/common/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion={received access token}&requested_token_use=on_behalf_of&resource={resource id that your api wants to access}&scope=openid&client_id={client id of your web api}&client_secret={client secret of your web api}

You can share your app to other people in the same organization.
The sign-in for this custom connector, i.e, Azure AD sign-in is needed, when the user launch this app for the first time.  (see the following screenshot)

Other Providers (the case of Google)

You can also use the OAuth 2.0 of Google, Facebook, Salesforce and other SaaS applications including the generic OAuth 2.0 providers. (see the official document “Register Custom Connectors in Microsoft Flow“. The flow by API Key and Basic Authentication are also supported.)

Let’s see the case of Google account.

Google and most providers are not having api registration, only client registration. (Except for the app context like api key.)
Therefore, you register only the custom connector proxy as OAuth client into Google Developer Console, get access token for pre-defined Google scopes only (profile, email, etc), and pass this token to your web api. (Or you could use the api key instead.)

Next is the swagger example for Google account settings. (see the bold font)

{
  "swagger": "2.0",
  "info": {
    "version": "v1",
    "title": "TestApi01"
  },
  "host": "demo.azurewebsites.net",
  "schemes": [
    "https"
  ],
  "paths": {
    "/api/Values/{id}": {
      "get": {
        "tags": [
          "Values"
        ],
        "operationId": "Values_Get",
        "consumes": [
          
        ],
        "produces": [
          "application/json",
          "text/json",
          "application/xml",
          "text/xml"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "type": "integer",
            "format": "int32"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      . . .

    }
  },
  "definitions": {    
  },
  "securityDefinitions": {
    "oauth2": {
      "type": "oauth2",
      "flow": "accessCode",
      "authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
      "tokenUrl": "https://www.googleapis.com/oauth2/v4/token",
      "scopes": {
        
      }
    }
  }
}

PowerApps (or Microsoft Flow) automatically detects Google account, and when you connect to the custom connector, the Google account login is displayed as the following screenshot.

Next is the http request for your web api.
It is the same like Azure AD, but not. The “Authorization” header value is the access token for Google scopes, not for your web api.

GET /api/Values/3
Accept: */*
Authorization: Bearer ya29.Ci-aAy...
Origin: https://us.create.powerapps.com
X-Ms-Apim-Tokens: ew0KICAidG...

Therefore you cannot verify this access token in your web api, but you can verify the login user instead of using X-Ms-Apim-Tokens. This token (X-Ms-Apim-Tokens) is the Base64 Url encoded value (see RFC 4648) of the following json string, and as you can see, the value includes the refresh token and id token of Google account. As a result, you can decode the id token value, and retrieve the user claims, verify the digital signature.

{
  "token": {
    "AccessToken": "ya29.Ci-aAy...",
    "ExpiresIn": "3600",
    "IdToken": "eyJhbGciOi...",
    "RefreshToken": "1/udVjULwb...",
    "TokenType": "Bearer",
    "OAuth2TokenEndPointCredentialLocation": "Body",
    "ExpiresOn": "636150505441694110",
    "LoginSettingId": "msmanaged-na_customapidemo02.5f989...",
    "TokenAcquireTime": "11/18/2016 6:22:24 AM"
  },
  "sku": "Enterprise",
  "$connectionCreator": {
    "objectId": "cf258756-2623-47cb-be46-c85d436265bb",
    "tenantId": "3c839350-a414-442a-9585-8db0b0f5f300",
    "userPrincipalName": "tsmatsuz@o365directory.onmicrosoft.com"
  },
  "$ConnectionKey": "Key eyJ0eXAiOi...",
  "$callerIdentity": {
    "objectid": "cf258756-2623-47cb-be46-c85d436265bb",
    "prinicipaltype": "ActiveDirectory",
    "tenantid": "3c839350-a414-442a-9585-8db0b0f5f300",
    "email": "tsmatsuz@o365directory.onmicrosoft.com"
  }
}

If your web api doesn’t need the login user, you could use the api key instead.

 

Change logs :

2017/05  renamed “custom api” to “custom connector”

 

SharePoint Add-ins : Workflow の開発

SharePoint Add-ins 開発

こんにちは。

今回は、SharePoint Add-ins (SharePoint アドイン, 旧 App for SharePoint) のワークフロー開発について解説します。

 

New Workflow !

まず、今回から、Workflow の Platform (Engine) は、以下の通り新しくなっています。

  • WF 4 (Windows Workflow Foundation 4) がベースです。(WF 4 については、こちら を参照してください。)
    このため、XAML による表現力の向上 (Code を使わずに多くの処理を実現可能)、FlowChart などを使ったより直観的な表現、パフォーマンス向上、デザイン時のリッチなアクティビティ デザイナーの実現など、多くの点が改善されています。
  • SharePoint 2013 のワークフローでは、WF 4 に加えて、Workflow Manager 1.0 (及び、これと接続する Workflow Manager Client 1.0) と呼ばれる Middle-tier のエンジンが使用されています。
    このエンジンは、SharePoint とは異なるプロセスで実行され、SharePoint と接続して動作します。また、これまで CodePlex などで提供されていた HttpGet などの REST 関連のアクティビティも、この Workflow Manager のアクティビティとして統合されています。

補足 : SharePoint Server 2013 (On-Premise) を使用している場合は、SharePoint Server とは別に、Workflow Manager のセットアップをおこなってください。(既定では、サービス アプリケーションとして Workflow Service Application がインストールされていますが、Workflow Manager と接続されていません。) このインストール方法等については、MVP の山崎愛さんが下記で紹介してくれています。
http://shanqiai.weblogs.jp/sharepoint_technical_note/2013/02/sp2013-workflow-manager-10-%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB.html

補足 : また、On-Premise 用の開発などで Code の Custom Activity を作成した場合には、従来の SharePoint への登録だけでなく、Workflow Manager へも登録が必要なので注意してください。(SharePoint Online では、Code を使った Custom Activity の配置は不可能です。)

補足 : Microsoft Office Developer Tools for Visual Studio 2012 を導入すると、開発環境にも Workflow Manager がインストールされます。(Debug については、後述します。)

また、SharePoint では、Workflow Service Manager と呼ばれる中間層を通して、ワークフローの発行・変更などの管理 (Deployment Service)、インスタンスの管理 (Instance Service)、リモートの呼び出し (Messaging Service)、過去の SharePoint 2010 ベースのワークフローの呼び出し (Interop Service) などの処理が可能です。

SharePoint 2010 までのワークフローでは、Task (Workflow Task) を中心とした外部連携をしました。例えば、ワークフローでカスタム UI を提供する場合は、Custom Content Type を使って Task Form と関連付けたり、外部システム連携も Workflow Task を媒介として Web Service で連携をおこないました。(「SharePoint : Workflow 用 Web Service を使ってシステム連携をおこなう」を参照。)
SharePoint 2013 からは、こうした、ある意味「不自然な連携」は不要です。Task は、あくまでも Task 割り当ての目的だけで使えばよく、外部システムと連携したい場合は Web API を呼び出したり、UI 系のカスタム機能を構築したい場合は JavaScript Object Model (JSOM) を使ってワークフロー関連の UI を構築するなど、自然な連携が可能です。
以降では、まずは、これまで通りの Task を使った処理を構築し、つぎに、こうした新しい Workflow 開発を簡単に確認してみたいと思います。

なお、「MSDN : SharePoint 2013 workflow fundamentals」の説明を読むと、Azure Service Bus を介して Windows Azure 上の Workflow を呼び出せると書いていますが、「MSDN : What is Workflow Manager 1.0」には下記の通り記載されています。すなわち、現状 (2013 年 03 月時点) は、作成した Workflow は SharePoint にホストして使用してください。(将来、環境が整ったら、ドキュメントなども出てくることでしょう。)

Over time, we intend to provide both a Windows Azure service capability, as well as a user-installed service capability, thereby providing user flexibility and symmetry across on-premises and Azure offerings. Initially, the capability is being made available publically as a user-installed service (for on-premises installation or installation on Azure Virtual Machines).

(2013/09 削除 : 現在、予定はありません。。。)

特に、上述の Workflow Manager では、簡単な構成によってマルチ テナントによる実行が可能で (さらに、SharePoint からは、個別のテナントに接続可能)、こうした仕組みと組み合わせることで、将来的に、Azure 上にホストされたワークフローとの柔軟な連携も可能となるでしょう。

ただし、現時点でも、この後、見ていくように、外部とのメッセージングが容易なため、必要な処理を Azure など外に出して簡単に連携できます。特に、Office 365 では、Custom の Code Activity をホストできないため、コード レベルの処理が必要な場合には、Web API などの形で処理を分離して Azure 上に実装する意義は充分にあります。

 

構築前の事前知識 (準備など)

Workflow を追加するには、[Apps for SharePoint 2013] (SharePoint Add-ins) のプロジェクトを新規作成して、いつものように [追加] – [新しい項目] でワークフローを追加できます。(Visual Studio のソリューション エクスプローラーで、プロジェクトをマウスで右クリックして、[追加] – [新しい項目] メニューを選択します。)

追加をおこなうと Workflow の Desiger が表示されますが、上述の通り、今回から、WF 4 の Desiger が表示されます。このデザイナーの基本的な使い方は、以前紹介した「WF 4 (Windows Workflow Foundation 4) シリーズ」に記載していますので、はじめて WF 4 を使用する方は、あらかじめ参照しておいてください。(ここでは、WF 4 の基本的な操作などの解説は省略します。)

また、使用できるアクティビティは、WF 4 の標準アクティビティと、Workflow Manager と共に追加されているアクティビティ (HttpSend など)、さらに、SharePoint 用のアクティビティの、大きく 3 種類のアクティビティがあります。
注意していただきたいのが、ツールボックス (Toolbox) には、表示されていないアクティビティもあるという点です。例えば、下図の通り、メッセージング関連のアクティビティとして HttpSend のみが表示されていますが、実際には、HttpGet、HttpPost、HttpDelete なども内部で存在し、使用できます。(HttpSend でこれらの処理の代用が可能なので、大きな問題とはなりませんが。)

こうした隠れたアクティビティを使用するには、ツールボックス上をマウスで右クリックし、[アイテムの選択…] を選択してください。(Workflow Manager のアクティビティや SharePoint のアクティビティは、表示される下図の画面で [参照] ボタンを押して、Microsoft.Activities.dll、Microsoft.SharePoint.DesignTime.Activities.dll を追加します。)

補足 : Microsoft.Activities.dll、Microsoft.SharePoint.DesignTime.Activities.dll は、それぞれ下記にあります。
%programfiles%Reference AssembliesMicrosoftWorkflow Manager1.0Microsoft.Activities.dll
%programfiles(x86)%Microsoft Visual Studio 11.0Common7IDEPublicAssembliesMicrosoft.SharePoint.DesignTime.Activities.dll

実際、このあとでも、こうしたツールボックスにないアクティビティをいくつか使用すると思います。

 

構築手順 – 基礎

では、実際の構築をおこなってみましょう。
今回から、Microsoft Office Developer Tools for Visual Studio 2012 の RTM 版を使用します。

まず、Visual Studio 2012 を起動して、[Apps for SharePoint 2013] (SharePoint Add-ins) のプロジェクトを新規作成します。今回は、SharePoint-hosted (SharePoint ホスト型) のプロジェクトを新規作成します。

Add-ins の Workflow では、リストに紐づいた「リスト ワークフロー」と、リストに紐づかない「サイト ワークフロー」が作成できますが、ここではリスト ワークフローを使用します。
このため、まず準備として、あらかじめ カスタム リストを作成しておいてください。(カスタム リストの作成方法については、「SharePoint Add-ins : List の開発と CSR」を参照してください。今回は、「List1」の名前のリストを作成したと仮定します。)

つぎに、Visual Studio のソリューション エクスプローラーで、プロジェクトをマウスで右クリックして、[追加] – [新しい項目] を選択します。
表示される画面で、[ワークフロー] (Workflow) を選択してワークフローを追加します。

ウィザードが表示されます。
まず、[リスト ワークフロー] か [サイト ワークフロー] か選択する画面が表示されるので、今回は [リスト ワークフロー] を選択します。

補足 : なお、[サイト ワークフロー] (Site Workflow) を選択した場合には、App web の /_layouts/15/workflow.aspx の画面からサイト ワークフローのインスタンスを追加 (ワークフローを実行) できます。(Add-ins で提供する画面などに、このページへのリンクなどを設定しておくと良いでしょう。)

つぎに、ワークフローを関連付けるリストを選択する画面が表示されるので、上記で作成したリスト (List1) を選択します。
また、今回、後述の通り、Task を使った処理を構築するため、関連付ける Task List として [新規作成] (New) を選択し (下図の赤枠を参照)、ワークフローの作成と同時に Task List も作成します。(これにより、「WorkflowTaskList」という名前のリスト インスタンスが作成されます。)
なお、あらかじめ、「タスク」(Task) のリスト インスタンスを新規追加して、これを Task List として設定しても構いません。

つぎに、ワークフローの開始方法を確認する画面が表示されます。
今回、リスト アイテムが新規作成された際にワークフローを自動開始するため、[項目が作成されたときにワークフローを自動的に開始] にチェックを付けておきます。(以降のサンプルでも、同様に自動開始するようにしておきましょう。)

以上でウィザードの設定が完了です。(ワークフローが新規追加されます。) なお、このウィザードで設定した内容は、作成された Workflow のプロパティ ウィンドウを使って、あとから変更 (設定) できます。

では、ワークフローを構築していきましょう !
まずは、SharePoint 2010 時代のように、リスト アイテムが作成されたら、Task をアサインするようなワークフローを構築してみましょう。(SharePoint 2013 で、どのように変わったか実感してください。)

今回、デモ (サンプル) として、リスト アイテムの作成者 (Author) に Task をアサインしますので、まずは、リスト アイテムの作成者を取得します。

まず、作成された Workflow1.xaml で [Sequence] アクティビティを選択し、Designer の左下の [変数] (Variables) をクリックします。そして、「authorId」という名前の Int32 型の変数を追加します。

つぎに、[LookupSPListItemInt32Property] アクティビティを Drag & Drop して [Sequence] アクティビティの内部に挿入します。

挿入した [LookupSPListItemInt32Property] アクティビティのプロパティ ウィンドウを表示し、下記の通りプロパティを設定します。
これにより、現在のリスト アイテムの登録者の Id 情報が、変数 authorId に挿入されます。

ListId : 現在のリスト (Current List)

ItemId : 現在の項目 (Current Item)

PropertyName : 登録者 (authorId)

Result : authorId (上記で作成した変数)

以降も、同様に、「変数作成」、「アクティビティ挿入」、「プロパティ設定」を繰り返していきます。Code の記述はいっさいおこないません。(「C# 式」と呼ばれる定義式の挿入はおこなえますが。)

つぎに、上記で取得した authorId からユーザー名を取得するため、[Sequence] アクティビティに下記の変数を追加します。なお、DynamicValue 型は、デザイン時に「型」が決まっていない値を受け取ることができる特別な型だと思ってください。

userValue : DynamicValue 型

userName : String 型

[LookupSPUser] アクティビティを挿入して、下記の通りプロパティを設定します。これにより、ユーザーに関するさまざまな情報が userValue に設定されます。

PrincipalId : authorId (上記の変数)

Result : userValue (上記の変数)

[GetDynamicValueProperties] アクティビティを挿入して、下記の通りプロパティを設定します。これにより、userValue から LoginName のプロパティだけを抽出して、変数 userName に設定します。

Source : userValue

Properties – エンティティの種類 : ユーザー (User)

Properties – 辞書 : LoginName userName

以上で、リスト アイテムの登録者のユーザー名 (i:0#.w|contosodemouser1 形式の文字列) が userName に設定されます。

では、このユーザーに、Task の割り当てをおこないましょう。
今回は、標準のタスクを割り当て、タスクの承認・却下の情報 (Int 型の情報) を受け取ります。(Approved が 0、Rejected が 1 です。)
まず、この結果を設定する変数として、以下の変数を追加します。

taskResult : Int32 型

[SingleTask] アクティビティを挿入してプロパティを設定します。
実は、[SingleTask] アクティビティを挿入すると、下図のように、ほとんどのプロパティは設定済みの状態になっているはずです。そこで、必要に応じて、プロパティを変更します。

まず、[AssignTo] プロパティが空になっていると思いますので (ここは必須です)、上記の userName 変数を設定します。
また、[Outcome] プロパティに、上記で作成した変数 taskResult を設定します。

AssignTo : userName

Outcome : taskResult

なお、今回は SingleTask で単一タスクのみを割り当てますが、CompositeTask アクティビティを使って複数名にタスクを割り当てることも可能です。(Parallel と Serial の双方が選択できます。)

また、ここでは、タスクが完了するまで待って、「タスクの結果」(TaskOutcome) を取得していますが、Workflow Task の Custom Content Type を構築して、独自なプロパティを結果として取得することもできます。
また、出力結果として、TaskList の Item Id (TaskItemId) も取得できるので、あとからタスクのプロパティ (結果) を自由に参照して、結果に応じた柔軟な処理も構築できます。

さいごに、taskResult の内容を判定して、Workflow Status を設定します。
[If] アクティビティを挿入して、taskResult の結果に応じて、[SetWorkflowStatus] アクティビティでステータスを設定します。(すみません、そろそろ面倒になってきたので、詳細は省略します。下図のような感じです。)

以上で完了です。(さいごに、Permission の設定も忘れずに実施しておきましょう。)

ここでは、Primitive なアクティビティをそのまま使用しましたが、Custom Activity を (XAML で) 作成し、これらの Activity を 1 まとまりの処理として組み合わせることができます。(実際の開発では、こうした設計もちゃんとおこなっておきましょう。)

 

Debug と動作確認

2013/10 追記 : Visual Studio 2013 では、Office 365 においても、Azure Service Bus を使用したワークフローのデバッグが可能になりました。

あらかじめ、Azure Service Bus の名前空間を新規作成して接続文字列を取得しておきます。プロジェクトのプロパティ ウィンドウの [SharePoint] タブを選択し、下図の通り、[ワークフロー デバッグを有効にする] (Enable Workflow debugging) と [Windows Azure サービス バスによるデバッグを有効にする] (Enable debugging via Windows Azure Service Bus) をチェックして、[Windows Azure Service Bus の接続文字列] (Windows Azure Service Bus connecting string) にコピーした接続文字列を入力します。(あとは、ワークフローのアクティビティに F9 でブレークポイントを設定して、デバッグ実行をおこなってみてください。)

Azure Service Bus を介して、SharePoint Online (Office 365) から、localhost で実行されているワークフローを呼び出します。

以前、「SharePoint Add-ins : Remote Event Receiver の開発と Debug」でぼやいたように、Workflow や Event Receiver 開発において、デバッグは悩みの種です。

上述の通り、実行に際しては Workflow Manager が必要ですが、実は、On-Premises (SharePoint Server 2013) であれば、開発環境にインストールされた Microsoft.Workflow.TestServiceHost.exe を起動してワークフローをホストし、デバッグ (トラックなど) をおこなうことができるようになっています。
しかし、Office 365 (SharePoint Online) を使用している場合は、無論、こうした逆向きの (localhost への) 呼び出しはできません。

申し訳ありませんが、Office 365 をお使いの方は、プロジェクトのプロパティ画面を開き、[SharePoint] タブを選択して、下図の [ワークフロー デバッグを有効にする] のチェックを外しておいてください。

なお、デバッグをおこないたい場合は、今のところ、「オンプレミスを使って動作確認してください」という答えになります。(どなたか、良い方法を思いついた方は、アイデアください!)

では、動作を確認してみましょう。

F5 でデバッグ実行をおこない、App web の /Lists/List1 (~appWebUrl/Lists/List1) を表示します。
リスト (List1) が表示されるので、リスト アイテムを追加 (新規作成) します。追加後、/Lists/WorkflowTaskList を表示すると、ワークフロー タスクが作成されているのが確認できます。(下図)

このタスクをクリックしてタスクの編集画面を表示し、承認や却下をおこなうと、下図の通り、Workflow Status が設定されているのが確認できます。

補足 : .xaml ファイルは、配置タイプが「AppPackage」となっています。(「モジュール」など、App web のフィーチャーとして作成されるのではありません。) この .xaml ファイルは、App web の wfsvc フォルダーに配置されます。

 

Web API (REST サービス) との連携

つぎに、前述の通り、SharePoint 2013 らしい外部連携のシナリオを確認してみましょう。
まず、Workflow Manager で追加された REST API (Web API) 関連のアクティビティ (メッセージング アクティビティ) を使ったサンプルを構築します。

補足 : SharePoint 2010 のサンドボックス ソリューションでは外部ネットワークの接続は制限されていましたが、ここで紹介する Workflow (SharePoint Add-ins の Workflow) は Office 365 でも使用可能です。

Web API の作成手順から書いていると時間がかかるので (もう 1 時間以上、ブログを書き続けています。。。とほほ)、今回は、リクルート株式会社の ATND API を使用して外部サービス連携します。もちろん、実際の開発 (プログラミング) では、開発者の皆さんが作成した独自な Web API (SharePoint の外の API) と連携して動作させることも可能です。

今回のサンプルでは、リストに設定された eventid の内容を取得して、その owner の情報を ATND API から取得するワークフロー サンプルを構築してみましょう。

まず、準備として、あらかじめ、下図の eventid (文字列型)、owner (文字列型) の列 (フィールド) を持つカスタム リストを作成します。(カスタム リストの作成方法については、「SharePoint Add-ins : List の開発と CSR」を参照してください。)

上記と同様に、このリストに紐づいたリスト ワークフローを作成 (追加) します。(今回は、タスク リストは不要です。)

つぎに、ワークフローを構築します。
まず、上記同様、リスト アイテムから eventid の列 (フィールド) の値を取得するため、[Sequence] アクティビティに変数 eventId (String 型) を作成し、[LookupSPListItemStringProperty] アクティビティを使って、この変数に eventid 列の値を設定します。(詳細の手順は上記と同様ですので、省略します。PropertyName には、”eventid” を設定します。)

つぎに、Web API を呼び出して、結果の Json 文字列を DynamicValue 型の変数に設定します。
変数 eventValue (DynamicValue 型) を作成し、つぎに [HttpSend] アクティビティを挿入して、[HttpSend] アクティビティのプロパティを下記の通り設定します。

Method : GET

Uri : “http://api.atnd.org/events/?event_id=” + eventId + “&format=json”

ResponseContent : eventValue

ATND API のリファレンス」に記載されているように、結果は、下記のフォーマットの Json 文字列として返ってきます。(今回、結果は 1 件なので、イベント データは、要素が 1 つの配列として返ってきます。)
今回は、この中の owner_nickname (エンコードされています) を取得して、リスト アイテムの owner 列 (フィールド) に設定します。

{
  "results_returned":1,
  "results_available":1,
  "events":
  [
    {
      "event_url":"http://atnd.org/events/27457",
      "title":"Office365 LTu5927u4f1a u7b2cu4e00u56de",
      "started_at":"2012-05-18T18:30:00+09:00",
      "ended_at":"2012-05-18T21:00:00+09:00",
      "updated_at":"2012-05-18T08:21:47+09:00",
      "deleted_at":null,
      "event_id":27457,
      "url":null,
      "owner_nickname":"u76eeu4ee3u660cu5e78",
      . . .
    }
  ],
  "results_start":1
}

そこで、変数 ownerNickname (String 型) を作成し、[GetDynamicValueProperty<T>] アクティビティを挿入して、下記の通りプロパティを設定します。(Generic の T として「String」を選択してください。)
なお、今回、データが抽出できなかったときのことはまったく考慮してませんので、実際の開発では、返ってくる件数の情報を取得して、きめ細かな処理を作成してください。(今回は、さぼります。)

Source : eventValue

PropertyName : “events(0)/owner_nickname”

Result : ownerNickname

さいごに、[SetFiled] アクティビティを挿入して、下記の通りプロパティを設定します。

FieldName : owner

FieldValue : ownerNickname

以上で完了です。

デバッグ実行をおこない、上記で作成したリストを表示してアイテムを追加します。
この際、eventid に「27457」と設定します。(http://api.atnd.org/events/?event_id=27457&format=json で、実際に値が取得できることを確認してみてください。)

しばらくすると、下図の通り、owner 列に目代さんの名前が設定されます。(目代さん、サンプルに使ってすみません !)

なお、Workflow Manager が提供するメッセージング アクティビティ (HttpSend、HttpGet、など) では、プロパティを使って HTTP ヘッダーの設定も可能なため、簡単な認証処理などもアクティビティで処理できます。
また、メッセージング アクティビティには SecurityToken プロパティも存在しています。このプロパティを GetS2SSecurityToken アクティビティと組み合わせて使うことで、server-to-server 認証をおこなって SharePoint に接続することもできるようです。(S2S 認証については、「.NET CSOM を使ったプログラミングと認証」を参照してください。) もちろん、SharePoint に接続するだけなら、そのためのアクティビティは既に存在するので、一般には、こうした処理をしなくても SharePoint 連携が可能です。

 

Custom UI と Object Model (JSOM) との連携

SharePoint 2013 の Object Model (または REST API) を使うと、Workflow Service Manager を使用してワークフロー関連のさまざまな処理を実行できます。このため、例えば、JSOM (JavaScript OM) を使って、カスタム UI (ページ) も簡単に構築できます。

今回は、カスタム リストにリボン ボタンを配置し、このボタンを押して承認行為をおこなう簡単な UI (画面) を構築してみましょう。

今回も、上記同様、カスタム リストを作成します。(作成手順については、省略します。)
さらに、上記同様、このリストに紐づいたリスト ワークフローを作成 (追加) します。(今回も、タスク リストは不要です。)

では、ワークフローを構築します。
まず、リボンから渡される結果を格納するための変数 res (String 型) を作成します。

つぎに、[WaitForCustomEvent] アクティビティを追加し、下記の通りプロパティを設定します。
このアクティビティでは、カスタムの画面から JSOM を通して送られてくるカスタムのイベントを待機し、イベントが送信されると、その結果を変数 res に設定します。

EventName : “MyTestEvent”

Result : res (上記の変数)

[SetWorkflowStatus] アクティビティを挿入し、下記のプロパティを設定します。
ここでは、上記で取得した res を Workflow Status として そのまま設定しています。

Status : res

つぎに、上記で追加したリスト (リスト アイテム) にリボンの Custom Action を追加します。追加方法は、「SharePoint Add-ins : UI Custom Action の開発」を参照してください。

今回、リスト アイテムが 1 つだけ選択されている場合のみボタンを有効にし、ボタンを押すと /Pages/MyWorkflowAction.aspx に遷移するようにします。このため、Elements.xml に、下記の通り記述します。

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="..."
    RegistrationType="List"
    RegistrationId="10002"
    Location="CommandUI.Ribbon"
    Sequence="10001"
    Title="...">
    <CommandUIExtension>
      <CommandUIDefinitions>
        <CommandUIDefinition Location="Ribbon.ListItem.Actions.Controls._children">
          <Button Id="Ribbon.ListItem.Actions.RibbonCustomAction1Button"
            Alt="RibbonCustomAction1 の要求"
            Sequence="100"
            Command="Invoke_RibbonCustomAction1"
            LabelText="RibbonCustomAction1 の要求"
            TemplateAlias="o1"
            Image32by32="_layouts/15/images/placeholder32x32.png"
            Image16by16="_layouts/15/images/placeholder16x16.png" />
        </CommandUIDefinition>
      </CommandUIDefinitions>
      <CommandUIHandlers>
        <CommandUIHandler Command="Invoke_RibbonCustomAction1"
          CommandAction="~site/Pages/MyWorkflowAction.aspx?source={Source}&amp;list={SelectedListId}&amp;item={SelectedItemId}"
          EnabledScript="javascript:function OneItemSelected()
            {
              var items = SP.ListOperation.Selection.getSelectedItems();
              var ci = CountDictionary(items);
              return (ci == 1);
            }
            OneItemSelected();"/>
      </CommandUIHandlers>
    </CommandUIExtension >
  </CustomAction>
</Elements>

さいごに、MyWorkflowAction.aspx のページを (Pages フォルダーに) 追加し、下記の通り JavaScript のコードを記述します。(非同期チェーンでネストしまくってますので、実際のプログラミングでは関数を分けるなどしてください。すみません、どんどんいい加減になってきてます。。。)

この処理では、ボタンを押すと、Workflow Service Manager の Workflow Instance Service を使って、このリスト アイテムに設定されている Workflow Instance の一覧を取得し、すべての Workflow Instance にカスタムのイベントを投げます。(そして、Workflow 側で、このイベントを受信して、処理が進むはずです。)

. . .
<asp:Content ContentPlaceHolderId="PlaceHolderAdditionalPageHead" runat="server">
  /_layouts/15/sp.core.js
  /_layouts/15/sp.runtime.js
  /_layouts/15/sp.init.js
  /_layouts/15/sp.js
  /_layouts/15/sp.workflowservices.js
  http://../Scripts/jquery-1.7.1.min.js
  
    $(document).ready(function () {
      $('#btnApprove').click(
        { 'payload': 'approved' },
        pubEvent);
      $('#btnReject').click(
        { 'payload': 'rejected' },
        pubEvent);
    });

    function pubEvent(e) {
      var params = getQueryParams();
      var ctx = SP.ClientContext.get_current();
      var man =
        SP.WorkflowServices.WorkflowServicesManager.newObject(
          ctx, ctx.get_web());
      ctx.load(man);
      ctx.executeQueryAsync(function () { // load WorkflowServicesManager
        var isv = man.getWorkflowInstanceService();
        ctx.load(isv);
        ctx.executeQueryAsync(function () { // load WorkflowInstanceService
          var ins = isv.enumerateInstancesForListItem(
            params['list'],
            params['item']);
          ctx.load(ins);
          ctx.executeQueryAsync(function () { // load Workflow instances
            var enu = ins.getEnumerator();
            while (enu.moveNext()) {
              isv.publishCustomEvent(
                enu.get_current(),
                'MyTestEvent',
                e.data.payload);
            }
            ctx.executeQueryAsync(function () { // publishCustomEvent
              location.href = params['source'];
            }, failf); // publishCustomEvent
          }, failf); // load Workflow instances
        }, failf); // load WorkflowInstanceService
      }, failf); // load WorkflowServicesManager
    }

    function failf(sender, arg) {
      alert('failed: ' + arg.get_message());
    }

    function getQueryParams() {
      var params = [];
      var arrays = location.search.substr(1).split('&');
      for (var i = 0; i 
</asp:Content>

<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">
  Please select !
  <br />
  <input id="btnApprove"
    type="button"
    value="Approve" />
  <input id="btnReject"
    type="button"
    value="Reject" />
</asp:Content>

以上で完了です。

デバッグ実行をおこない、App web の上記リストを表示します。
リスト アイテムを新規作成します。(Workflow が開始され、上記の WaitForCustomEvent で待機状態になります。)
リスト アイテムを選択すると、下図の通り、リボンのボタンが押せるようになるので、このボタンをクリックします。

ボタンを押すと、下図の画面が表示されます。

上図の画面で [Reject] ボタンを押すと、下図の通り、リスト アイテムの Workflow Status が「rejected」に設定されます。

ここでは、インスタンスにイベントを渡す処理を作成しましたが、Object Model (CSOM, JSOM) を使って、リボンのコマンドや ECB メニューで Workflow の開始も制御できるでしょう。(こうした On-Demand な実行や制御は、Remote Event Receiver では不可能です。) また、SharePoint Designer がおこなっているような発行などの機能も実装できるらしいので、以前、Web Service (SOAP) を使って苦労して実装した「SharePoint の Custom Workflow Editor」なども、今回から、この Object Model を使ってもっと簡単に実装できるかもしれませんね。(すみません、試してませんので、どこまでできるか分かりませんが。)
また、ここではページ上から JSOM を使いましたが、REST API を直接呼び出すことも可能なため、リモートで実行されているサービスなどから Workflow を制御することもできるでしょう。(「SharePoint Add-ins 概要」で解説したように、新しい REST API を使うと、Object Model で提供されている多くの処理を実行できます。)

 

以上、ざっとですが、SharePoint Add-ins の Workflow 開発における特徴的な側面をみてきました。

なお、Workflow Manager は、単体でインストールして使うこともできます。
この Workflow Manager については、以前、SharePoint User Group で登壇されていた “てすとぶろぐ” さんが詳しく書いてくれていますので、勝手にリンクしてしまいますが、是非、下記も確認してみてください。

もっといろいろ書こうかと思いましたが、そろそろ疲れてきたので、このシリーズもいったんお開きにしようかと思います。(いろいろと試してみてください)

 

※ 変更履歴 :

2015/05/05  App for SharePoint (SharePoint 用アプリ) を SharePoint Add-ins (SharePoint アドイン) に名称変更

 

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 の双方で使用できます。

 

WF 4.5 新機能 (Design Experience 向上)

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

WF 4.5 新機能

こんにちは。

思い出したような投稿ですみません。先日 Caching のネタを書いたときに、WF 4.5 (.NET Framework 4.5 の WF) の話も書いてなかったことを後悔しましたので、今回 記載しておきたいと思います。(こちらも、これまで連載で書いてきてましたので。。。)

キャッチアップされている方にとっては「今さら」感 満載なのですが、WF 4.5 については、昨年の BUILD でもセッションで紹介されていましたし、最近だと Tech Ed 2012 (US) のセッションでも紹介されていますので、英語に抵抗がない方は、是非、ストリーミングなどを参照してください。
また、日本のリソースを検索してみると、てすとぶろぐ さんも素早く情報提供されていますので、参考にしてみてください。 

さて、WF 4.5 の新機能ですが、ざっと私なりに新機能を概観すると、次の 3 つに大別されるでしょう。

まず、StateMachine Workflow のサポート、Workflow における部分信頼 (Partial Trust) のサポートなど、これまで .NET 4 Platform Update (4.0.1 – 4.0.3) として追加で提供されてきた更新がすべて含まれているという点です。(.NET 4 Platform Update ですが、私のブログは .NET 4 Platform Update 1 しか紹介していませんでしたが、実は、.NET 4 Platform Update 3 まであります。)
つぎが、今日紹介する Design 系の進化の話です。後述するように、Designer そのものの使い勝手も良くなっていますし、新しい Design 手法への対応もあります。
最後が、WF 4.5 の一番大事な新機能だと思いますが、バージョン操作ができるようになっているという点です。これは、ちょっと今回まとめて書くのはあまりに長文になってしまいそうなので、次回にわけて記載したいと思います。

ということで、今週は、WF 4.5 の Design 系の進化について紹介したいと思います。もちろん、Visual Studio 2012 RC をダウンロードして すぐ試すことができます。

 

Workflow Designer の新機能

では、まず、Workflow Designer がどのように使いやすくなったか見てみましょう。
結構ありますので、箇条書きします。

  • 従来、Activity に挿入する Expression として VB 式しか設定できませんでしたが (「WF 4: コードいらずのワークフロー」を参照)、今回から、C# 式 (C# Expression) が入力できるようになっています。これについては、若干注意点があるので、このあと補足します。
  • Workflow のアウトライン表示が可能です (つまり、Tree 形式で階層表示できます。Designer Rehosting でも使用できます)
  • Workflow Designer 上で検索 (Find) ができます
  • アクティビティへコメント (annotation) を挿入できます
  • 一部アクティビティが追加されています (RC 版では、NoPersistScope, InvokeDelegate)
  • その他、activity の複数選択、activity どうしの自動接続、Sequence アクティビティの自動挿入、など、いろいろと細やかな改善がおこなわれています (ここは、実際、さわってみてください)

C# 式を挿入すると、.xaml ファイルには専用のタグが付与された式が設定されます。(互換性に配慮し、VB 式の場合は、.xaml に、これまでと同じフォーマットで記述されます。) Visual Studio の IDE では、既定で、Visual Basic のプロジェクトの場合は VB 式 (VB Expression)、C# のプロジェクトの場合は C# 式 (C# Expression) を使うようになっています。

なお、Designer Rehosting (デザイナー リホスティング) を活用している方は、上述の新機能に注意してください。(てすとぶろぐさんも、「WF 4.5 における新機能のオンオフ」に注意点として記載されています。)
互換性に配慮して、新機能の利用を明示しない限り、Designer Rehosting で新機能を使うことはできないようになっています。例えば、上述の Annotation を Designer Rehosting で使えるようにするには、下記 太字の通りコードを追加します。
また、アクティビティ検索や C# Expression は、Designer Rehosting ではサポートされていませんので注意してください。

...

// register designer metadata of Activities
DesignerMetadata metadata = new DesignerMetadata();
metadata.Register();

// create workflow designer
WorkflowDesigner designer = new WorkflowDesigner();

// use annotation (wf 4.5 new feature !)
designer.Context.Services.
  GetService<DesignerConfigurationService>().AnnotationEnabled

    = true;
designer.Context.Services.
  GetService<DesignerConfigurationService>().TargetFrameworkName

    = new System.Runtime.Versioning.FrameworkName(".NETFramework",
          new Version(4, 5));


// insert "Sequence" activity
// (we must call "Load" once !)
designer.Load(new Sequence());

// insert UIElement (designer view, property view)
DesignerArea.Child = designer.View;
PropertyArea.Child = designer.PropertyInspectorView;

...

 

Contract-First

もう 1 つ、Design 関連で見逃せない新機能として、ワークフロー サービス (Workflow Service) における Contract-First があります。従来の WF 4 でワークフロー サービスを構築する場合、ワークフロー (アクティビティ) の実装をしながら、Service Contract 名 (ServiceContractName) やオペレーション名 (OperationName) を設定しました。つまり、Contract を定義しながらワークフローを実装するという、定義と実装を混在しながら構築するスタイルでした。
WF 4.5 では、先に Contract を定義して、この Contract を使ったワークフローをあとから実装できます。

Visual Studio で、WCF ワークフロー サービス アプリケーションを作成します。例えば、プロジェクトに [クラス] ファイルを追加し、下記の通りコードを実装します。(つまり、コントラクトを定義します。)

. . .
using System.ServiceModel;
using System.Runtime.Serialization;
. . .

[ServiceContract]
public interface IOrderService
{
  [OperationContract]
  OrderItem StartOrder(string ProductId, int Count);
  [OperationContract(IsOneWay=true)]
  void ReceiveOrder(OrderItem Order);
}

[DataContract(Namespace = "")]
public class OrderItem
{
  [DataMember]
  public Guid OrderId;
  [DataMember]
  public string ProductId;
  [DataMember]
  public int Count;
  [DataMember]
  public string Process;
}
. . .

いったんリビルドをおこなってから、プロジェクトを右クリックして、[サービス コントラクトのインポート] (Import Service Contract) を選択します。

表示される画面で、下図の通り、上記で作成した IOrderService を選択して [OK] ボタンを押します。

プロジェクトをビルドしなおし、.xamlx ファイル (ワークフロー デザイナー) を開くと、下図の通り、上記で作成したコントラクトがアクティビティとして選択可能になります。

このようにデザイナーにコントラクトをインポートした場合、その実体は、アクティビティ クラス (.xaml ファイル) として、プロジェクトの「Service Contracts」の名前のサブ フォルダーに展開されます。デザイナーに追加されたこのコントラクトを削除するには、Visual Studio のソリューション エクスプローラーで、プロジェクトの「Service Contracts」フォルダーにあるコントラクト (下図) を削除すれば OK です。

また、ツールボックス上に追加されたアクティビティの名前に注目してください。(上図の StartOrder_ReceiveAndSendReply と ReceiveOrder_Receive です。)
例えば、StartOrder オペレーションは、上述のコードの通り、引数と返り値を持つ Request-Reply 型のオペレーションです。このため、Receive アクティビティと SendReply アクティビティのシーケンスとして展開されます。一方、ReceiveOrder オペレーションは、上述のコードの通り oneway オペレーションであるため、Receive アクティビティのみに展開されます。
このように、性質に応じた適切なメッセージ タイプに展開してくれるので、Contract を作成する際には、上記の通り、IsOneWay などの属性をちゃんと設定しておいてください。

さいごに、ワークフロー (.xamlx) のプロパティ ウィンドウを表示して、[ImplementedContracts] のボタン (下図) をクリックし、上記の IOrderService を追加します。これで、上図 ツールボックスの ReceiveOrder_Receive と StartOrder_ReceiveAndSendReply を使用したワークフロー作成が可能になります。

以降の使い方は、これまでの WF 4 における Receive アクティビティ、SendReply アクティビティなどの使い方と同じです。上図 ツールボックスの StartOrder_ReceiveAndSendReply や ReceiveOrder_Receive をドラッグ アンド ドロップして挿入し、引数や返り値、Correlation の設定などをおこないます。手順の詳細は、WF 入門 (10 行シリーズ) の「ワークフローを使用したサービスの作成」、「ワークフロー サービスにおける状態の維持 (Correlation の使用)」に記載されているので参照してください。(ここでは、手順の説明は割愛します。)

無論、定義されているオペレーションをすべて実装する必要があるので注意してください。(例えば、上記の場合、StartOrder_ReceiveAndSendReply アクティビティと ReceiveOrder_Receive アクティビティの双方を使ってワークフローを作成する必要があります。) また、Contract-First で作成したワークフローに、従来の Receive、SendReply アクティビティなど使ってオペレーションを追加することもできますが、ServiceContractName 名を変更し、別の Contract (つまり、別のインターフェイスのオペレーション) として追加する必要があるので注意してください。Contract-First で作成する場合は、一貫して、できるだけ最初にすべてのオペレーションを定義してから、実装するのが良いでしょう。

なお、ここではサンプルのため、同じプロジェクト内にコントラクトを定義しましたが、もちろん、Contract は別の dll (ライブラリー) として作成し、その dll を参照追加して構築することもできます。(というか、現実の開発では、こういうスタイルでの開発になるでしょう。) また、WCF 4.5 の新機能 の 1 つである wsdl からの Contract 作成と組み合わせると、wsdl の定義を元にワークフロー サービスを構築することも可能です。

 

関連ナンバー

 

 

SharePoint 開発の本質 (3) : ワークフロー フィーチャー

本内容は、SharePoint Server 2007 をベースに記載しています。(2008 年に記載したコラムです。コラム終了のため、こちらに移動しました . . .)
最新の SharePoint 2010 を使用した製品開発手法については、「SharePoint Server 開発 サンプル コード集 : 10 行コードで作る .NET アプリケーション」を参照してください。

前回まではユーザーインタフェース (UI) 構築を中心に話をしてきました。今回は、SharePoint 上でのプロセス実装としてワークフローを学び、この章では、その基本となる「考え方」を中心に説明をします。これまで述べてきた発想はここでも役に立ちます。つまり、ベースは「.NET」であり、そこに SharePoint 独自のフレームワークをかぶせてエンドユーザーに使いやすくしているという基本思想です。

SharePoint 開発の本質

 

WF の理解

.NET Framework では、バージョン 3.0 以降、Windows Workflow Foundation (以下 WF と記載します) という新しいフレームワークが追加されました。SharePoint の UI が ASP.NET のフレームワークをベースとして構築されていたように、SharePoint のワークフローも、この WF という .NET 上のフレームワークをベースに構築されています。一般のエンドユーザーはここに記載している WF を理解する必要はありませんが、製品レベル開発などをおこなうプロフェッショナル開発者がこの SharePoint ワークフローを快適に乗りこなしていくためには、WF への理解が必要不可欠となってきます。
WF は、例えて言うなら、注文組立型のパソコンのようなものです。こうしたパソコンでは、「プロセッサが必要で、メモリが必要で、、、」などその動きの大きなスペックは決まっていますが、そこにどのようなデバイスを入れるかは自由に組み合わせができます。WF では、自分だけの独自な (世界にただ 1 つだけの) デバイスを作って組み込むことさえもできます。ミッションクリティカルな業務で使うなら独自部品も組み合わせて思い切り豪華に、また作って遊ぶだけなら必要な最低限のものだけを使って組み立てるといった具合です。
では、基本的な動きを理解していただくため、WF の構造を SharePoint の実装と照らし合わせながら見ていきましょう。

補足 : .NET Framework のバージョン 3.0 が登場した当初の開発環境は、Visual Studio 2005 の拡張機能 (アドイン) として提供されていました。このバージョンをあらかじめ含んだ Visual Studio の製品リリースとしては、Visual Studio 2008 が該当します。(Visual Studio 2008には、さらに上位の .NET Framework 3.5 が含まれます)

補足 : なお、WF は、WF 4 (.NET Framework 4 の WF) で、大きくベースが変更されているので、ご注意ください。

まず、WF のワークフローで使用するオブジェクトは、すべて、「ワークフロー ランタイム」 (WorkflowRuntime) と呼ばれる .NET のオブジェクトの上で動作をさせます。そのため、必ずこのランタイムオブジェクトを作成して開始 (StartRuntime) させる必要があります。しかし、SharePoint では、このランタイムオブジェクトは SharePoint 内部で暗黙裡に実行されており、この内部のランタイムオブジェクトを使った勝手な操作は禁止されていますので注意してください。
WF 上のワークフロー全体の動きを制御するには、「ワークフロー ランタイム サービス」 (WorkflowRuntimeService) と呼ばれる .NET のオブジェクトを上述のワークフロー ランタイムにどんどん追加していきます。ワークフローが実行される場面を想像してみてください。通常は、いくつかのワークフローがバックエンドで動作し、必要な処理に到達したときに何らかのアクションを要求するといった動作が求められるでしょう。この一般的な動作を実現するためには、「スケジューラーサービス」 (WorkflowSchedulerService) と呼ばれる「ワークフローランタイムサービス」を追加します。デフォルトでは、1 つ 1 つのワークフローが並列に実行できるように、それぞれのワークフローが異なるスレッドで実行されるようなスケジューラーサービスが追加されています。 (無論、SharePoint でも、このスケジューラーサービスは挿入されています。) また承認ワークフローなどのケースを想像してみてください。日をまたいで実行されるこのようなワークフローでは、ワークフローがインメモリに長く常駐している状態は望まないことでしょう。こうした動きを制御するには、ワークフローランタイムに「永続化サービス」 (WorkflowPersistenceService) と呼ばれる同じく「ワークフローランタイムサービス」の一種を挿入することによって、例えばアイドル時に自動的にワークフローをデータベースなどへ永続化 (メモリから退避) し、実行時は自動的にこれらストアからメモリ上にロードさせることができます。そして SharePoint においても、SharePoint の動作仕様に適合した独自の永続化サービス (SPWinOePersistenceService) が挿入されています。さらに、基本的なデータのやりとりにおいても、この「ワークフローランタイムサービス」が必要です。先ほどスケジューラーサービスや永続化サービスなどを説明しましたが、ワークフローはマルチスレッドである場合や、メモリからアンロードされている場合などがありますので、普通の .NET のクラスのようにメソッドやプロパティに直接アクセスするのではなく、「データ交換サービス」 (ExternalDataExchangeService) と呼ばれる「ワークフロー ランタイム サービス」を使って、ワークフローと、ワークフローの外の世界とのデータ交換 (連携) をおこなう必要があるのです。
先ほど WF を「組立型のパソコン」に例えましたが、WF は、何も入れないと、「基本的な配線しか入っていないただの箱」でしかないという点がご理解いただけるでしょう。

ここで注意すべきポイントがあります。上述の通り、これら数々の「ワークフローランタイムサービス」は、ワークフローランタイムに挿入 (追加) され、ワークフローラインタイムを使った独自な処理は SharePoint では禁止されていました。つまり、SharePoint ワークフローの開発では、例えば、既存の永続化サービスを独自なサービスに変更したり、独自にトラッキング (追跡) 用のサービスを追加したり、といった処理はできません。

補足 : 厳密には、中身をよく理解している開発者はこうした実装が可能かもしれませんが、ルール上、こうした実装をしてはいけません。(例えば、ホット フィックス / サービス パックの適用の際などに、動作が保証されません。)

SharePoint ワークフローのカスタマイズでは、あらかじめ準備されたワークフローランタイムサービスしか使用できず、上述の「データ交換サービス」においても、「SharePoint 上のタスクとワークフローの連携」、「SharePoint 上のリストアイテムとワークフローの連携」などの限られた種類 (全部で 4 種類です) のデータ交換 (連携) しかおこなうことはできません。これはほとんどの場合には問題ありませんが、例えば、Web サービス/WCF サービスとワークフローを連携させながら動かしたい場合などにはしばしば問題になります。

補足 : こうしたケースでは、外部システム連携用の SharePoint タスクを作成して、このタスクをワークフローと連携させて、Web サービスを使って外部からこのタスクを変更する、などの実装が必要となります。また、このようにワークフローの中の個々のアクティビティと外部のシステムを連携させるのではなく、SharePoint のワークフローどうしを上位の別のプロセス (あるいはシステム) で統合連携させたい場合には、BizTalk Server が使用できます。
なお、SharePoint 2010 では、ローカル サービス を使って、こうした外部連携の機能を (開発者が) プラグインできるように拡張されています。

さて、ワークフローの流れ (フロー) の組み立てについてはまだ言及していませんでした。ワークフロー上で動作する個々の「振る舞い」は、すべて「アクティビティ」と呼ばれる .NET のオブジェクトが担当しています。このアクティビティには、その中にアクティビティを含める (ネストさせる) ことができる Composite アクティビティ (複合アクティビティ) というクラスもあり、実は、アクティビティの逐次実行や、While、If/Else などの制御、さらには並列実行なども、すべてこの Composite アクティビティ (厳密には、Composite アクティビティから継承されたクラス) を使って、これらのアクティビティの中にアクティビティをネストさせることでさまざまな制御を実装できるようになっています。つまり、ワークフローそのものも「アクティビティ」の 1 種であり、フローの組み立て自体もシンプルな概念の「組み合わせ」になっていることがおわかりいただけるでしょう。
WF には、開発者が必要とするアクティビティのいくつかが標準アクティビティとしてあらかじめ用意されています。例えば、ワークフローを停止するためのアクティビティや、前述のデータ交換サービスを使って処理をおこなうためのアクティビティなどさまざまです。SharePoint では、この WF が標準で提供している基本的なアクティビティ (Base Activity Library) に、SharePoint 独自の「タスクの作成」アクティビティ (CreateTask アクティビティ) 、「E メールの送信」アクティビティ (SendEMail アクティビティ) などのアクティビティが追加されているだけです。ワークフローの組み立ては、WF が提供している標準アクティビティとこれらの SharePoint 用のアクティビティを組み合わせて実装するという極めてシンプルなものです。

 

SharePoint 独自のフレームワーク

つぎに、SharePoint ワークフローだけがもつ独自のフレームワークについても説明をおこなっておきましょう。

実は SharePoint の世界では、同じ WF を使って構築されたワークフローであっても、その利用用途にあわせ 2 種類のワークフローが存在しています。 (これは、SharePoint 独自の概念です)

  • ワークフロー (Workflow)
    SharePoint のワークフローは、必ず、特定のリスト (またはコンテンツタイプ) に関連付いています。SharePoint Designer 2007 でワークフローを作成する場合は、ワークフローを作成すると同時にどのリストに関連付けるかを指定して構築をおこないます。このようにして作成したワークフローは、現場の業務管理者などが必要に応じ (リモートなどから) 作成と配置をおこなって迅速に利用することができますが、その一方で、他のリストへの設定や移動などはおこなえません。
  • ワークフロー テンプレート (Workflow Template)
    SharePoint では、「承認」ワークフローなどの頻繁に利用されるワークフローがデフォルトで入っていて、エンドユーザーは、好みのリスト (またはコンテンツタイプ) でこのインストールされているワークフローを自由に設定することができるようになっています。
    こうしたワークフローは、上記とは異なり、SharePoint に組み込まれたフィーチャー (機能) としてインストールされていて、開発者によるコーディングと、管理コマンドによるインストール作業が必要となりますが、SharePoint 上の自由なリスト (またはコンテンツタイプ) で設定 (再利用) することができるようになっています。

これを踏まえ、SharePoint を使用してワークフローを作成したい場合には、目的に応じて以下の 3 つの方法のいずれかを選択することができることをおぼえておいてください。

方法 ツール(その利用者) 内容
ワークフローの作成 SharePoint Designer
(現場業務の管理者)
特定のリストなどに関連付いたワークフローを SharePoint Designer を使って、ウィザード形式で、コードを記述することなく作成できます。
ワークフローテンプレートの作成 Visual Studio
(開発者)
Visual Studio を使用して、再利用可能なワークフローテンプレートを開発し、SharePoint 上に配置できます。
アクティビティの作成 Visual Studio
(開発者)
SharePoint Designer の利用者がワークフロー作成時に選択可能なカスタムのアクティビティを Visual Studio を使用して開発し、SharePoint 上に配置できます。

表のように、一般のエンドユーザー (現場業務の管理者) は 1 番目の方法だけをおぼえておけば問題ありません。そもそも彼らは、SharePoint に「ワークフロー」と「ワークフローテンプレート」が存在するという事実も知る必要はありません。一方でプロフェッショナル開発者は、WF の理解、「ワークフロー」と「ワークフローテンプレート」の理解など、仕組みの根本的な理解が必要とされます。

しかし、プロフェッショナル開発者は、エンドユーザーをより自由な世界へ導くことができる強力な武器を手にしています。例えば、SharePoint Designer 2007 によるワークフロー作成 (表の 1 番目の方法) では、事前に SharePoint Designer に登録されているアクティビティの中から必要なものを選択して組み合わせていくことしかできません (条件分岐は可能です)。よくあるシナリオとして、承認行為をおこなうために上長の取得をおこないたくなりますが、こうしたアクティビティも SharePoint Designer では標準で用意されていません。どの人事データベースを見れば良いのか、Active Directory を確認すれば良いのか、などがわからないためです。こうした点を回避する手段として、3 番目のカスタムのアクティビティ開発があります。Visual Studio で、ループの処理や上長の取得処理などを含むアクティビティを開発して SharePoint サーバーに配置をすれば、SharePoint Designer を使用する一般のエンドユーザーは、こうしたプロフェッショナル開発者が構築したアクティビティを使用して今まで不可能であったワークフローを構築することができます。また複雑なワークフローで、なおかつ頻繁に扱われるものは、あらかじめ 2 番目の方法で作成とインストールをしておき、エンドユーザーに、彼らが使いたい箇所で設定してもらうことが可能です。

 

SharePoint ワークフローの “開発”

前述の通り、SharePoint のワークフローは WF (Windows Workflow Foundation) をベースに構築されており、大袈裟に表現すれば、そこに SharePoint 用の仕組みがかぶさっているだけというシンプルな構造であることがおわかり頂けるでしょう。では、「ワークフローテンプレート」の構築を例に、そうした点を皆さんに実感して頂こうと思います。
なお、以降で述べるサンプルは、「製品レベル」の 高度な開発者がその中身 (Inside) を理解できるようにするため実施しているサンプルです。実際の開発では Visual Studio が標準で持っている SharePoint 用のテンプレートやアクティビティなどを使用して構築します ので注意してください。(現実の開発では、下記の真似はしないようにしてください。)

くどいようですが SharePoint のワークフローは、結局のところ WF (Windows Workflow Foundation) のワークフローにすぎません。今回は、あえてそのことを示すため Visual Studio 2008 の[シーケンシャルワークフローライブラリ]のプロジェクトテンプレートを選択してプロジェクトを新規作成します。そして、SharePoint が提供するクラスやインタフェースが使用できるように Microsoft.SharePoint.dll へ参照を追加しておきます。

つぎに、アクティビティを追加して処理を記述していきますが、その際に使用されるのが前回説明した「データ交換サービス」です。ワークフローの中から SharePoint に対してタスク作成をおこなったり、逆に SharePoint 側でタスクが更新されたらワークフローの処理を進めるなど、SharePoint サーバーと作成したワークフローの間で処理を連携させる必要がありますが、こうしたワークフローと外部の連携では「データ交換サービス」を使用する必要がありました。そして、このデータ交換サービスを使ってワークフロー内部から外のシステム (今回は SharePoint サーバー) に処理を要求する場合、WF では CallExternalMethod アクティビティを使用します。逆に、外のシステム (今回は SharePoint Server) からの要求をワークフロー内部で処理するには HandleExternalEvent アクティビティを使用します。

補足 : WF のデータ交換サービスの使い方 (CallExtenalMethod や HandleExternalEvent の使用方法) については、「MSDN : ソリューションサンプル WF の使用」でサンプルを例に理解することができます。

SharePoint では、まず、ワークフローの開始と共に「ワークフローが開始された!」ことを必ず通知してきますので (この通知の中に、「どのサイトで開始されたか」、「どのドキュメントアイテムのワークフローか」などの情報が入っています)、ワークフローの最初の処理として、この外のイベントを HandleExternalEvent アクティビティで処理することからはじまることになります。
そこで、まず、ツールボックスから HandleExternalEvent アクティビティをドラッグアンドドロップします (下図)。

つぎに、SharePoint サーバーから送られているメッセージがインタフェースとして定義されていますので、このインタフェースを HandleExternalEvent アクティビティで受け取れるようにします。上図で、右下のプロパティウィンドウを使用して、[InterfaceType]として Microsoft.SharePoint.dll の ISharePointService を選択し (下図)、[EventName]として OnWorkflowActiveted (このイベントは選択した ISharePointService の中で定義されています)を選択します。

補足 : この際、CorrelationToken というプロパティも表示されますが、ここには適当な文字列を設定しておいてください。例えば、タスクを複数作成して、それらのタスクの変更状況を HandleExternalEvent でリスンしたい場合などに、複数のタスクのうちのどのタスクをリスンするか指定する必要があるでしょう。CorrelationToken は、こうした場合に使用する識別子ですので、今回は適当な値を設定しておけば充分です。

設定されたプロパティは、以下の通りになります。

このアクティビティでは、受け取ったイベントの詳細の情報は、上図のイベントのパラメータ e の中に入っています。上図でパラメータ e の箇所を選択することで、このパラメータを受け取る変数を作成することができますので、今回は、「workflowEventArgs」という変数を作成して、この変数にこの値を格納しておきます。

また、この HandleExternalEvent アクティビティをダブルクリックすると、このアクティビティが実行される際の処理をプログラムコードで記述できます。今回は、以下のコードを記述して、ワークフローを開始したユーザー ID と、このワークフローが設定されているアイテムのタイトルを変数に格納しておきます。

private string itemTitle;
private string creatorUser;
private void handleExternalEventActivity1_Invoked(object sender, ExternalDataEventArgs e)
{
 itemTitle = workflowEventArgs.properties.Item.Title;
 creatorUser = workflowEventArgs.properties.Originator;
}

つぎに、SharePoint のタスクを作成してみましょう。今度は、ワークフローインスタンスの内部から外のシステム (SharePoint サーバー) に処理を要求するため、CallExternalMethod アクティビティの出番です。

上記同様に、CallExternalMethod アクティビティをツールボックスからドラッグアンドドロップし、今度は、InterfaceType として Microsoft.SharePoint.dll の ITaskService を選択し、MethodName として CreateTask (ITaskService の中のメソッドです) を選択します。(また先ほどと同様に CorrelationToken の値も適当な文字列を設定しておきます。)

上図のように、今度は、指定するプロパティとして taskId や properties といった値が指定可能になっています。これらは、ここで選択した CreateTask メソッドの引数として SharePoint 側に渡されます。taskId には作成するタスクの ID を GUID 型で指定し、properties には、タスクのその他の属性 (例:タスクのタイトルや、アサイン担当者、カスタムの属性、など) を SharePoint の SPWorkflowTaskProperties 型で指定します。そこで、先ほどの HandleExternalEvent アクティビティ同様に、これらのプロパティを設定する変数をそれぞれ taskId、taskProperties という名前で作成し、さらに、先ほど同様にこのアクティビティをダブルクリックして、アクティビティ実行時のコードとして以下の通り記述します。(CallExternalMethod のコードは、先ほどの HandleExternalEvent のコードと異なりメソッドを呼び出す前に実行されます。)

private void callExternalMethodActivity1_MethodInvoking(object sender, EventArgs e)
{
 taskId = Guid.NewGuid();
 taskProperties.Title = itemTitle + "のタスクが割り当てられました";
 taskProperties.AssignedTo = creatorUser;
}

最後に、このプロジェクトに署名を添付してリビルドをおこないます。すると、上記で構築したワークフローの処理がアセンブリ (.dll ファイル) として作成されます。

さて、このワークフローを SharePoint 上にどのように配置すれば良いのでしょうか?
前回の説明を思い出してください。SharePoint のワークフローには「ワークフロー」と「ワークフローテンプレート」という 2 種類の設定方法があり、後者は「フィーチャー」としてインストールする必要がありました。2 章で、.NET Framework のオブジェクトと SharePoint 独自の「フィーチャー」の違いについて説明しました。フィーチャーとして配置すると利用者にとっては使いやすいですが、開発者にとってはそのフィーチャーに関するさまざまな情報を XML 形式で定義しておく必要がありました。2 章では Visual Studio が自動でこの XML ファイルを作成してくれていましたが、今回はこの定義ファイルを自作してみましょう。

以下の 2 種類のファイルを作成します。

feature.xml

<?xml version="1.0" encoding="utf-8" ?>
<FeatureId="7075DF62-387B-4165-8B7C-D528578FCDD9"
  Title="デモ用のスクラッチワークフロー機能"
  Description="Tech Fielders のデモとして試しに作成しました"
  Version="1.0.0.0"
  Scope="Site"
  ReceiverAssembly="Microsoft.Office.Workflow.Feature, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
  ReceiverClass="Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="MyWorkflow.xml" />
  </ElementManifests>
  <Properties>
    <Property Key="GloballyAvailable" Value="true" />
    <Property Key="RegisterForms" Value="*.xsn" />
  </Properties>
</Feature>

MyWorkflow.xml

<Elements
  Id="5319F617-FC0A-4d52-A61C-0FEADA08F36D"
  xmlns="http://schemas.microsoft.com/sharepoint/">
  <Workflow
    Name="デモ用のスクラッチワークフロー"
    Description="Tech Fielders のデモとして試しに作成しました"
    Id="1C2B8DA4-05D0-46e0-8C19-EFA53F7DF191"
    CodeBesideClass="MySPWorkflowDemo.Workflow1"
    CodeBesideAssembly="MySPWorkflowDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a5f5336fed11daa1">
    <Categories/>
    <MetaData>
      <StatusPageUrl>_layouts/WrkStat.aspx</StatusPageUrl>
    </MetaData>
  </Workflow>
</Elements>

補足 : 上記で、Feature 要素の Id 属性、Elements 要素の Id 属性、Workflow 要素の Id 属性のそれぞれには新しい GUID を作成して設定します。また、Workflow 要素の CodeBesideClass、CodeBesideAssembly には、上記で作成 (ビルド) したアセンブリにあわせて設定をおこないます。

これで準備は整いましたので、フィーチャーとして配置をしていきましょう。
作成されたアセンブリ (dll) を SharePoint から参照できるようにグローバルアセンブリキャッシュ (GAC) に登録し、SharePoint の管理コマンド (stsadm.exe) を使用して、以下の通りワークフローをフィーチャーとして SharePoint に組み込みます。

:: マニフェストのコピー
mkdir "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESMySPWorkflowDemo"
copy feature.xml  "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESMySPWorkflowDemo"
copy MyWorkflow.xml "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESMySPWorkflowDemo"

:: フィーチャーのインストール
pushd %programfiles%common filesmicrosoft sharedweb server extensions12bin

stsadm -o installfeature -filename MySPWorkflowDemofeature.xml
stsadm -o activatefeature -filename MySPWorkflowDemofeature.xml -url http://localhost

popd

:: IIS の再起動
iisreset

これで配置は完了です。リストを作成し、登録された上記のワークフローを設定して、リストアイテムに対してワークフローを開始すると、実行したユーザーにタスクが新規作成されます。ワークフローは、アイテムの作成時などに自動的に開始させることも可能です。

ここでは、ワークフローの仕組みを理解していただくため敢えてそのベースとなっている WF (Windows Workflow Foundation) のみを使用して構築をおこないましたが、実際には、これらの設定を簡素化するために SharePoint 用の便利なテンプレートやアクティビティなどがいくつか用意されています。例えば、最初に設定した HandleExternalEvent アクティビティは、SharePoint の OnWorkflowActivated アクティビティを使用するともっと簡単に同じ処理を実現することができます (このアクティビティは、Microsoft.SharePoint.WorkflowActions.dll に含まれています)。
また、実際のワークフローの構築では、ここで述べた簡単なワークフローとは異なり While や IfElse などと組み合わせた高度なワークフローを構築する必要があるでしょう。ワークフロー開始時やタスクの実行時に独自なユーザーインタフェースを使用することも必要となるでしょう。あとはアクティビティや周辺コンポーネントを組み合わせて構成していくのみです。

また、今回は「ワークフローテンプレート」を構築しましたが、SharePoint Designer が作成する「ワークフロー」では、WF のアクティビティを使用してワークフローのコードを構築し、このコードを SharePoint サーバーに送信して、Web サービスを介して SharePoint サーバー側でコンパイルされています。(つまり、ここでも WF を使用してワークフローが構築されています)。より進んだ開発者は、こうした内部の動きを理解しておけば、自作のワークフローエディターまでも構築することが可能でしょう。

補足 : なお、SharePoint Designer が作成する WF のワークフローは、XML 形式のマークアップコードとして作成されます。

ここで見てきたように、WF を使用して構築したワークフローに SharePoint 用のフィーチャー構成をおこなうだけで SharePoint のワークフローテンプレートは完成します。WF を含む .NET Framework の知識があれば、あとはこのフレームワークをベースに SharePoint の追加の機能が実装されているだけであるという基本的な構造がおわかり頂けるでしょう。

補足 : SharePoint Workflow におけるより詳細のメカニズムと開発技術については、書籍「VSTOとSharePoint Server 2007 による開発技術」(翔泳社) に記載しました。

 

Workflow Service の Azure SQL Database への永続化 (Persistence)

環境 : Visual Studio 2010 Service Pack 1 (SP1), .NET Framework 4 Platform Update 1 – Design-time Update for Visual Studio 2010 SP1 (日本語), Windows Azure Tools for Visual Studio 2010 v1.5 (September 2011), Windows Azure SDK 1.5 (September 2011)

.NET 4 Platform Update 1 の WF 新機能

こんにちは。

上記 3 回連載の最終回ですが、お待たせして すみません。

実は、.NET Framework 4 Platform Update 1 (.NET 4 PU1) が Windows Azure のプロジェクトに対応するのを待っていたのですが、ようやくご紹介できるようになりましたので、最終回を記載したいと思います。

もう、何の話をしていたか忘れてしまったかもしれませんが、.NET Framework 4 PU 1 では、Windows Azure SQL Database (旧 SQL Azure) を使って WF のワークフローの永続化が可能になっています。

MSDN : What’s New in Windows Azure」に記載されているように、既に、2011 年 7 月 15 日 (July 15, 2011) のアップデートで、Windows Azure の Guest OS で .NET Framework 4 Platform Update 1 (.NET 4.0.1) が対応されています。そして、更新情報には載っていないですが、現在は、SDK や CSUpload などでも配置可能になっているようです。

つまり、この .NET Framework 4 PU 1 によって、以下が可能になっています。

  • Windows Azure SQL Database 上にワークフロー (WF) の永続化データベースを作成して利用できるようになりました。
  • Windows Azure 上にホストされるワークフロー サービスで、ステートマシン ワークフロー (第 1 回 を参照) など、Platform Update 1 の機能が使用できるようになりました。

さっそく、サンプル コードを使って見ていきましょう。

 

Windows Azure SQL Database へのデータベースの作成

では、まず、WF の永続化をおこなうためのデータベースを Windows Azure SQL Database に構築してみましょう。

Windows Azure SQL Database (SQL Azure) の管理画面を開き、データベースを新規作成します。(今回は、「WFPersistDB」という名前のデータベースを作成します。)

ご存じの方も多いかと思いますが、Windows Azure SQL Database は、SQL Server と同一ではなく、いくつかの機能が制限されています。その中の 1 つに、allow_page_locks があり、.NET 4 の永続化データベースでは、この allow_page_locks を使用していましたが、新しい .NET 4 PU1 のスクリプトでは、この allow_page_locks を使用しないようになっています。(.NET 4 PU1 がインストールされた環境の SqlWorkflowInstanceStoreSchema.sql には、こうしたコメントが記載されていますので確認してみてください。)
このため、この新しいスクリプト (.sql) を使って、Windows Azure SQL Database に永続化データベースを作成することが可能です。

コマンド プロンプトを管理者権限で開き、%windir%Microsoft.NETFrameworkv4.0.30319SQL<言語> のディレクトリーに移動して、以下のコマンドを実行します。
(お使いの環境にあわせ、AAAAAAAAAA には Windows Azure SQL Database の DNS 名、testuser / XXXXXXX には Windows Azure SQL Database の ログイン ユーザー ID / パスワード を設定してください。)

sqlcmd -S AAAAAAAAAA.database.windows.net -d WFPersistDB -U testuser@AAAAAAAAAA -P XXXXXXX -i SqlWorkflowInstanceStoreSchema.sql
sqlcmd -S AAAAAAAAAA.database.windows.net -d WFPersistDB -U testuser@AAAAAAAAAA -P XXXXXXX -i SqlWorkflowInstanceStoreLogic.sql

補足 : Windows Azure SQL Database への接続では、TCP が使用されます。企業のプロキシー設定などで、TCP をブロックしている場合は、上記の方法では接続できないので注意してください。(この場合、Windows Azure SQL Database の管理ポータルを使用して、スクリプトを実行してください。)

 

アプリケーションの構築

SP1 と .NET 4 PU1 のインストールされた Visual Studio 2010 を、管理者権限で実行します。

ターゲットフレームワークとして [.NET Framework 4 Platform Update 1] を選択して、[Windows Azure Project] を新規作成し、ウィザードで [WCF Service Web Role] を追加します。

今回は、ワークフロー サービス (.xamlx) を構築するので、既存で作成された以下のコード ファイルを削除します。

IService1.cs

Service1.svc (及び、Service1.svc.cs)

WCF Service Web Role のプロジェクトを右クリックして、[追加] – [新しい項目] で、([Workflow] グループの) [WCF ワークフロー サービス] を追加します。(Service1.xamlx が追加されます。)

今回は、永続化の確認をするだけなので、下図のように、GetData1、GetData2 の 2 回の要求 (Request) を処理して答え (Response) を返す単純なワークフローを構築しましょう。(今回は、普通に、シーケンシャル ワークフローを作成します。もちろん、ステートマシン ワークフローを構築していただいても構いません。)

なお、上図のように、2 つのメッセージを処理する場合には、必ず、Correlation の設定を忘れずに実施してください。(Correlation を設定しないと、1 番目のメッセージと 2 番目のメッセージの間で、ワークフロー インスタンスのマッピングができません。)
ここでは、この概念や設定方法は説明しませんが、以前 投稿した「.NET 4 の WCF / WF 入門」で説明していますので、是非 参照してください。(最初の Receive Request アクティビティの [CanCreateInstance] プロパティも、忘れずに、true に設定してください。)

このワークフローを、上記で作成した Windows Azure SQL Database のデータベースに永続化可能にするため、WCF Service Web Role のプロジェクトの Web.config を開き、下記の太字の通り追記します。

. . .

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

        <workflowIdle timeToUnload="0"/>
        <sqlWorkflowInstanceStore
          connectionString="Data Source=AAAAAAAAAA.database.windows.net;Initial Catalog=WFPersistDB;User Id=testuser@AAAAAAAAAA;Password=XXXXXXX"
          instanceEncodingOption="None"
          instanceCompletionAction="DeleteAll"
          instanceLockedExceptionAction="BasicRetry"
          hostLockRenewalPeriod="00:00:30"
          runnableInstancesDetectionPeriod="00:00:05"/>
        </behavior>
    </serviceBehaviors>
  </behaviors>
  . . .

</system.serviceModel>
. . .

上記では、ワークフローがアイドル状態になった際に、ワークフローをアンロード (Unload) して、データベースに永続化するように指定しています。このため、上図のワークフローで、最初の Receive アクティビティ (GetData1) が呼ばれると、次の Receive アクティビティ (GetData2) が呼ばれるまで、ワークフローは Windows Azure SQL Database に永続化されます。
また、上記の instanceCompletionAction 属性により、GetData2 の呼び出しが成功してワークフローが完了すると、データベースに永続化されていたインスタンス情報は削除されます。

 

Windows Azure への配置と動作確認

では、実際に配置して動作を確認してみましょう。

ソリューション エクスプローラーで、クラウドのプロジェクトをマウスで右クリックして、[Package] を選択すると、パッケージが作成されます。そして、Windows Azure ポータル画面 を使用して、このパッケージを、あらかじめ作成しておいた Hosted Service にアップロードします。

クライアントを作成して、動作を確認してみましょう。

補足 : 今回、クライアントの作成手順の説明は省略します。構築手順については、上記で記載した「.NET 4 の WCF / WF 入門」の「ワークフローを使用したサービスの作成 (ワークフロー サービス)」を参照してください。

まず、Windows Azure SQL Database (SQL Azure) のポータル画面で、上記のデータベース (WFPersistDB) にログインして InstancesTable テーブルを確認すると、下図の通り、データは入っていない (0 件) ことがわかります。

作成したクライアントから、このワークフロー サービスに接続し、上図 (ワークフロー) の GetData1 メソッドを呼び出します。
この段階で、Windows Azure SQL Database (SQL Azure) のポータル画面で、InstancesTable テーブルを確認すると、下図の通り、データが 1 件追加されているのが確認できます。

ワークフローはメモリ上から退避されて、データベースに永続化されているため、このサービスをいったん停止 (Windows Azure ポータル画面で、Hosted Service の Deployment を停止) して、再度、開始をおこなっても、このワークフロー インスタンスでは、引き続き、GetData2 を呼び出すことが可能です。

また上述の通り、GetData2 を呼び出してこのワークフロー インスタンスを完了すると、InstancesTable テーブルからデータが削除されます。

 

その他の留意点

Microsoft Suppport : Microsoft .NET Framework 4 Platform Update 1 – Runtime Update」に依ると、.NET Framework 4 Platform Update 1 (.NET 4.0.1) では、下図の通り、MaxConnectionRetries 属性も使用可能になっています。この属性を設定することで、Windows Azure SQL Database への接続に失敗した場合にリトライする回数を指定できます。 (既定値は、3 になっています。)

. . .

  <sqlWorkflowInstanceStore maxConnectionRetries="15" . . ./>
. . .

これで、いよいよ本格的に Windows Azure 上で動くカスタムのワークロー サービスを構築できますが、まだ、単なる永続化のため、Windows Server AppFabric Host Services (Dublin) のような リカバリー機能、高度な管理機能などの機能はありませんので注意してください。(例えば、Windows Server AppFabric のように、バックグラウンドで実行中のワークフロー インスタンスが実行途中で停止した場合に、Windows サービスにより自動リカバリーする機能は持っていません。)

なお、Windows Azure 上のさまざまな機能 (Azure Blob Storage、Azure Table Storage、Azure AppFabric Cache など) と連携したワークフローを構築する場合、まだ CTP 版ですが、「Workflow Foundation Activity Pack for Windows Azure」なども使用可能です。(この Workflow Foundation Activity Pack for Windows Azure については、Windows Azure チームのブログ を参考にしてください。)

 

WF 4 の補正処理 (トランザクション) と CompensationExtension

環境 :
Visual Studio 2010 Service Pack 1 (SP1 – 日本語)
.NET Framework 4 Platform Update 1 – Design-time Update for Visual Studio 2010 SP1 (日本語)

.NET 4 Platform Update 1 の WF 新機能

こんにちは。すみません、こちらの連載を、すっかり放置していました。。。

今回は、.NET Framework 4 Platform Update 1 の WF (Windows Workflow Foundation) で使用可能になった CompensationExtension について説明します。
しかし、その前に、そもそも WF 4 の補正処理 (Compensation) を知らない方のために、まずは、その概要と使い方をちゃんと説明しておきたいと思います。(というか、ほとんどその話で終わりです . . .)

なお、 WF 3.5 にもこの補正 (Compensation) の考え方はありましたが、一部、WF 4 で概念が変わっている箇所もありますので、WF 3.5 までの補正処理をご存じな方も、是非 復習として理解しておいてください。、

補足 : WF 3.5 の頃の補正処理については、以前 投稿した「WF 標準アクティビティの歩き方 (Base Activity Library)」を参考にしてください。

補正処理 (Compensation) とは ?

ワークフローでは、承認処理のように、何日にもまたがって実行される処理が多数考えられます。このようなケースでトランザクション処理をおこないたい場合、データベースなどで一般的に使われるアトミック トランザクション (Atomic Transaction) や分散トランザクションを使用するのは現実的ではありません。数日間もワークフローをメモリ中にロードしておくのは、好ましくないでしょう。

そこで、こうした long-running なワークフローで活躍するのが、この補正処理です。補正処理は、ざっくりと説明すると、ロールバック用の処理を開発者自らが定義し、例外 (エラー) 発生時に、この定義された処理を呼び出すというものです。

想像していただくとわかりますが、補正処理では、複数のブロックが繰り返し処理されたり、ブロックどうしがネストするような複雑な関係になると、すべての処理をワークフローで定義するのは困難です。WF の補正処理では、WF 3.5 の頃から、こうした複雑な補正処理を扱えるような仕組み (メカニズム) が提供されています。

なお、WF 3.5 では、補正処理を扱う場合に、必ず、永続化サービスと一緒に使う必要がありましたが、WF 4 以降では、永続化 (Persistence) を設定しなくても補正処理が可能です。(無論、この場合、常にメモリ中にインスタンスが存在しているので、アトミック トランザクションなどで代用できる場合もありますが。)

WF 4 の補正処理のアクティビティ (超入門)

では、まず、使用するアクティビティと、それぞれの使い方を簡単に説明しましょう。

まず、WF 4 のトランザクション関連のアクティビティには、以下 (下図) があります。これらのうち、TransactionScope アクティビティ以外のものが、すべて補正処理に関連したアクティビティです。

まず、最も基本的な (必須の) アクティビティが、CompensableActivity アクティビティ (下図) になります。

CompensableActivity アクティビティには、上図の通り、処理本体の Body と、CompensationHandler、ConfirmationHandler、CancellationHandler の 3 つのハンドラーがあります。(これら 3 つのハンドラーをすべて設定する必要はありません。)

まず、CompensableActivity アクティビティでは、上図の Body に、このブロックにおける処理本体を入れます。

そして、Body が完了すると、この処理が終了したとみなされて、以降の補正処理の対象となります。Body がすべて終了し、その後、どこかで Compensate アクティビティが呼び出されると、上図の CompensationHandler が呼び出されます。
例えば、Body の中でアイテムの登録処理をおこない、CompensationHandler でこのアイテムの削除の処理を入れておきます。Body が終了してこのブロックを出た後、承認処理で却下された場合に Compensate アクティビティを呼ぶようにしておくと、却下された際にアイテムが削除されます。

もし、例外 (エラー) 発生時にこの CompensationHandler を呼びたいなら、Try – Catch (TryCatch アクティビティなど) をおこなって、Catch ブロックの中で Compensate アクティビティを呼び出せば CompensationHandler が呼ばれます。

つぎに、CancellationHandler (上図) は、Body の処理が途中でキャンセルされた場合に呼ばれる処理です。例えば、ある Body の処理の途中で例外 (エラー) を Throw し、TryCatch アクティビティによって外で終了処理がおこなわれる場合を想像してください。この場合、Body の処理が途中でキャンセルされるため、この CancellationHandler が呼び出されます。

ConfirmationHandler は、データベースのコミットのように、処理が確定した段階で呼び出させます。
例えば、A と B の 2 つの CompensableActivity アクティビティを逐次 (Sequential) に実行して終了する簡単なワークフローを想像してみましょう。この場合、B のアクティビティが終了してワークフロー全体が完了した段階で、双方 (A, B) の ConfirmationHandler が呼び出されます。(Confirm アクティビティを明示的に呼び出さなくても、ワークフローの完了時に自動的に呼び出されます。)
データベースにおけるコミット処理のように、処理を途中で確定させたい場合には、Confirm アクティビティを使います。例えば、A と B の 2 つの CompensableActivity アクティビティを逐次 (Sequential) に実行する場合、A の終了後に Confirm アクティビティを呼び出すと、B の実行の前に、いったん A が完了 (確定) され、A の ConfirmationHandler が呼び出されます。

なお、補正処理 (Compensation) を扱わず、キャンセル処理 (Cancellation) だけを扱いたい場合は、CancellationScope アクティビティ (下図) が使用できます。(このアクティビティの使い方は、上記の CancellationHandler と同様です。)

参考 : TransactionScope アクティビティは、ここで説明している「補正処理」ではなく、アトミック トランザクションや分散トランザクション (例 : データベースに対するロールバック処理、など) を処理するためのブロックです。.NET の TransactionScope クラスと同じ振る舞いをします。

補正処理のブロック設計

ここから、少し難しくなります。
WF 4 では、ブロックどうしの細かな制御ができるように設計されているため、CompensableActivity アクティビティの各ブロックの関係を細かく設計しておく必要があります。

まず、A と B の 2 つの CompensableActivity アクティビティを逐次 (Sequential) に実行し、終了後に Compensate アクティビティを呼び出す場合を考えてみましょう。この場合、期待される動きとして、B と A の CompensationHandler が呼ばれると思うかもしれませんが、実は、WF 4 では、そのようには動作しません。(WF 3.5 の頃は、そのように動作していました。)

WF 4 では、Compensate アクティビティが呼ばれると、対応する直前の CompensableActivity アクティビティの CompensationHandler のみが呼び出されます。このため、上述のように 2 つの CompensableActivity アクティビティに対する補正処理をおこなう場合は、下図のように、CompensableActivity アクティビティの CompensationHandler の中で、再度 Compensate アクティビティを実行して、Body 内部にネストされた CompensableActivity アクティビティの CompensationHandler を呼び出すようにします。(下図の場合、CompensableActivity 3、CompensableActivity 2 の CompensationHandler が呼び出されます。)

上図で、各ハンドラーの呼ばれる順番 (実行結果) は以下の通りです。

  1. CompensableActivity 2 の Body
  2. CompensableActivity 3 の Body
  3. CompensableActivity 1 の CompensationHandler
  4. CompensableActivity 3 の CompensationHandler
  5. CompensableActivity 2 の CompensationHandler

補足 : 上図のトークン (token) は、CompensableActivity アクティビティと Compensate アクティビティの対応関係を設定するためのものです。下図のように、ワークフロー変数 (Variables) として System.Activities.Statements.CompensationToken クラスの変数を作成し (複数可能)、対応するアクティビティどうしで設定をおこないます。
ネストされた CompensationHandler を処理するには、上図のように、ネストされたアクティビティでトークン (token) を Null に設定します。

上図で、CompensableActivity 1 の CompensationHandler で何かの処理を実行してから Compensate アクティビティを呼び出すことで、ネストされた CompensableActivity (CompensableActivity 2、CompensableActivity 3) の CompensableHandler もあわせて処理することができます。(全体の補正処理と、部分的な補正処理を、順番に処理できます。)

なお、CompensationHandler に処理が含まれていない場合、ネストされた CompensableActivity アクティビティの CompensationHandler に処理が遷移します。このため、下図のように処理しても、上記と同じ実行結果になります。

賢い ! (Cool !)

上記で、CompensableActivity 2 と CompensableActivity 3 の CompensationHandler が呼ばれる順番に注目してください。
Body では、CompensableActivity 2、CompensableActivity 3 の順番で処理がおこなわれているため、逆に、CompensableActivity 3、CompensableActivity 2 の順番で CompensationHandler が呼ばれているのがわかります。WF 4 では、WF 3.5 同様、あたかも、インスタンスの実行履歴が参照されているかのように、適切な順番と回数で補正処理が呼び出されます。

例えば、下図の通り処理された場合を想像してみてください。

この場合、最後の Compensate アクティビティによって、CompensableActivity 1 の補正処理 (CompensationHandler) のみが呼び出されます。

つぎの場合はどうでしょう ? (While の中で、CompensableActivity が 3 回呼ばれています。)

この場合は、最後の Compensate アクティビティによって、CompensableActivity 2 の補正処理 (CompensationHandler) が 3 度呼び出されます。また、CompensableActivity 2 のスコープの変数 (Variables) がある場合、それぞれの CompensationHandler では、対応する Body が実行されたときの変数の値が維持されます。(例えば、Id 情報などを保持して、対応する Id ごとに補正処理をおこなうことなどができます。)

公開された CompensationExtension !

WF 4 では、CompensationToken に関する情報や、ConfirmationHandler / CompensattionHandler などの各ハンドラー (Activity) のブックマーク (Bookmark) の情報などは、すべて、内部で作成される CompensationExtension という Workflow Extension の中に格納されていて、上記の Confirm アクティビティ、Compensate アクティビティなども、この情報を使って補正処理をおこなっています。(ブックマークが使用されているため、上記の While の例で説明したように、そのコンテキストにおける変数の情報なども、ちゃんと保持されています。)

補足 : ここでは、Workflow Extension や、永続化のメカニズム (下記の PersistenceParticipant など) についての説明は省略します。詳細は、以前投稿した「WF 4 : Workflow Extension と 永続化、トラッキング」を参照してください。
なお、この CompensationExtension は、CompensableActivity が内部で作成 (追加) しています。(ただし、既に、CompensationExtension がワークフローに設定されている場合は、追加されません。)

また、この CompensationExtension は PersistenceParticipant を継承したクラスです。このため、永続化と共に使用しても、上述の補正処理 (Compensation) に関する情報は正しく永続化ストア (データベースなど) に保存され、必要に応じてメモリへのロードがおこなわれます。

そして、.NET Framework 4 まで、この CompensationExtension は、internal、かつ sealed のクラスでしたが、.NET Framework 4 Platform Update 1 では、このクラスは開発者に公開されていて、このクラス (CompensationExtension) を継承することも可能です。(これが、Platform Update 1 の進化のポイントです !)

おまけ . . .

しかし、ここまで書いていて何ですが、これ (CompensationExtension)、どう使うんでしょうね ? CompensationExtension のメンバーのほとんどは private か internal です。しかし、CollectValues、PublishValues が override 可能ですので、補正処理 (Compensation) そのものに独自のカスタマイズと永続化をおこないたい場合などに使用できるかもしれませんね . . . (今後、.NET Endpoint チーム ブログなどで、こうしたサンプルを教えてくれると嬉しいですね . . .)

まあ、いずれにせよ、こうした複雑な補正処理を扱えるという点だけでも、WF を使用する価値があることがお分かり頂けたでしょう。