SharePoint アドイン製品一覧
SharePoint 2010 開発のステップ・バイ・ステップ
Windows Azure 入門
Windows Azure How-To 集
WCF / WF 入門
赤字 -> 2008/03/28 追記
環境:Visual Studio 2005.NET Framework 3.0WCF LOB Adapter SDK
こんにちは。
今回は、前回おさらいした Binding, Channel の概念を踏まえ、実際に WCF LOB Adapter SDK を使って WCF のカスタムチャネルに相当するカスタムのアダプターを実装してみます。
まず開発環境についてですが、BizTalk Server 2006 R2 は .NET Framework 3.0 に対応していますが現状開発ツールは Visual Studio 2005 がベースとなっています。無論、Visual Studio 2008 への対応なども現在検討されていますので、今後のアナウンスなどお待ちください。(BizTalk を使う場合、当面は、Visual Studio 2005 が混在した開発環境となります。)
【Adapter Framework の概要】
まずは、いきなりアダプター作成する前に、第 1 回 でもご説明した WCF の上にかぶさっている .NET 3.0 Adapter Framework というものについて 実際の UI を見ながら簡単にご説明します。WCF LOB Adapter SDK をインストールすると、.NET 3.0 Adapter Framework のインストールと Visual Studio への Add-in が追加されます。第1回で記載したように、WCF LOB Adapter に BizTalk は必要ありませが、.NET 3.0 Adapter Framework は BizTalk (というか、SOA のモデルです) を強く意識したフレームワークになっています。
まず、このフレームワークを組み込むと、「アダプターを使用する開発者、もしくは BizTalk の管理者」 (注 : アダプターを『作る』 開発者ではありません) は、以下のように Visual Studio 上から通常の WCF のサービスリファレンスとは別に、Adapter Service Reference というものを追加できるようになります。(これは、BizTalk Server 2006 R2 上からも起動可能です。)カスタムアダプターを作成して組み込むと、『アダプターを使用する開発者』 は、このメニューを使って、簡単にアダプターを組み込んだ処理を実装することができるようになっています。
これを選択すると、以下のような画面が表示され、ここで選択・設定したアダプタの情報は、通常の WCF サービスの構成情報として .config に記述されます。
(アダプター参照の設定画面)
つまり、WCF の上に 1 枚「Adapter」という概念がかぶさった構成となっています。上記の設定画面を図入りで掲載しましたが、この画面を理解しておくことはカスタムアダプターを構築する上で重要です。というのは、後述する WCF LOB Adapter の開発では、この概念を意識して必要な機能を実装していくためです。上記の画面でおこなうことは以下です。
【カスタムアダプターの構築】
では、いよいよ上記のフレームワークにカスタムアダプターを構築して組み込んでいきましょう。
カスタムアダプター作成については以下にチュートリアルがありますが、そこそこちゃんとしたものを構築している関係もあり、はじめて構築する人にとっては半日 (は言いすぎ?でも 3 - 4 時間程度) はかかってしまうことでしょう。また、「そもそも何をやっているのか」を理解することも時間がかかるかもしれません。チュートリアルhttp://msdn2.microsoft.com/en-us/library/bb798080.aspx
そこで、以降では、より簡単なサンプルで主要なエッセンスのみをご紹介して感じをつかんで頂きたいと思います。今回作成するサンプルでは、文字列をプロパティで指定した回数だけ繰り返して答えを返すという簡単なオペレーションの実装と、inbound シナリオの例として定期的な間隔で決められた文字列を出力するという 2 つオペレーションを持ったカスタムアダプターを作成してみましょう。
まず、Visual Studio 2005 で、[WCF LOB Adapter] のプロジェクトを新規作成します。
するとウィザードが起動してきますので、以下の通り設定していきます。
ウィザードを完了すると、コードファイルが一式の入ったプロジェクトが自動作成されます。(下図右)
ここで、作成されたコードファイルについて簡単にご説明しておきましょう (そろそろ、前回記載したブログの内容に関する知識が必要ですのでご注意ください)。
まず、Binding の本体を定義しているのは、<Project Name>Binding.cs というファイル (上図の CustomAdapter1Binding.cs) です。この中では、第 2 回 (前回) に述べた BindingElement の Collection (<Project Name>BindingCollectionElement.cs) を作成していますが、使用されている BindingElement は実は 1 つだけで、<Project Name>.cs (上図の CustomAdapter1.cs) がその BindingElement になります。この CustomAdapter1.cs クラスはコード上では Adapter というクラスを継承していますが、この Adapter クラスが前回「必須である」と記載した TransportBindingElement 抽象クラスを継承したクラスになっています。つまりこの Binding は、TransportBindingElement の BindingElement ただ 1 つだけが使用された Binding ということになります。もし Binding の Collection 自体をカスタマイズしたいなら、この <Project Name>Bindings.cs (CustomAdapter1Binding.cs) を編集すればOK ですが、コンフィグレーションによってカスタムの BindingElement が挿入できるよう <Project Name>BindingElement.cs (CustomAdapter1BindingElement.cs) というカスタム開発用の予備の BindingElement も用意されていますので、この実装をおこなって所定の箇所のコメントアウトを外すと独自の BindingElement を組み込むことができるようになっています。(つまり、この <Project Name>BindingElement.cs は予備であり、デフォルトでは使用されていません。)
outbound や inbound の処理本体 (WCF における Channel で処理されるべき部分) は、上図の <Project Name>OutboundHandler.cs、<Project Name>InboundHandler.cs の中で分離して記述できるようになっていて、それぞれ Adapter Framework がシナリオにあった方法で呼び出してくれます。
では、まず、この Adapter (以降では、CustomAdapter1 とします) の Dispose 処理を修正しましょう。CustomAdapter1HandlerBase.cs の Dispose メソッドの中の処理 (生成されたコードでは NotImplementedException を throw しています) を削除してください。
[CustomAdapter1HandlerBase.cs]
protected virtual void Dispose(bool disposing){ // Exception は throw しません !}
つぎに、アダプター参照の設定画面でユーザ (このアダプターを利用する開発者、BizTalk 管理者) が入力する「スキーム」文字列や URI Properties (今回は、上述の通り hostname というものを設定しました) に応じて URI 文字列 (myscheme://hostname という文字列) を構成する処理を作成していきましょう。この処理では、アダプターの Connection オブジェクト (CustomAdapter1Connection.cs) を使用しますが、その前に、Connection の組み立ての際の Open や Close 処理にも暫定のコードが入ってしまっていますので、CustomAdapter1Connection.cs を開いて、以下の各メソッドを下記の通り実装しなおします。
[CustomAdapter1Connection.cs]
public void Open(TimeSpan timeout){ // Exception は throw しません !}
public void Close(TimeSpan timeout){ // Exception は throw しません !}
public void ClearContext(){ // Exception は throw しません !}
public bool IsValid(TimeSpan timeout){ return true;}
では、URI 文字列を組み立てましょう。CustomAdapter1ConnectionUri.cs を開き、以下の通り CustomAdapter1ConnectionUri クラスのコンストラクタと Uri の getter/setter を実装します。
[CustomAdapter1ConnectionUri.cs]
public CustomAdapter1ConnectionUri(){ Uri = new Uri("myschema://hostname");}
public CustomAdapter1ConnectionUri(Uri uri) : base(){ Uri = uri;}
public override Uri Uri{ get { if (String.IsNullOrEmpty(this.hostname)) throw new InvalidUriException("Invalid target system host name."); return new Uri(CustomAdapter1.SCHEME + "://" + Hostname); } set { this.hostname = value.Host; }}
このように、URI 自身も CustomAdapter1ConnectionUri というカスタムのクラスになっていて、例えば、URI の中から上述の hostname 部分だけ取り出したいという場合には、このクラスのプロパティを参照するだけで取り出せるような構造になっています。(そのような処理が必要になった場合には、便利です。)
さて、つぎに、上述した Adapter 参照の設定画面のブラウズ (Browse) 機能 (画面下部) を実装しましょう。CustomAdapter1MetadataBrowseHandler.cs を開き、Browse メソッドを以下の通り実装します。今回は、繰り返し文字列を返す outbound オペレーションを「EchoTimes」、inbound オペレーションを「DoSomeWork」としておきましょう。
[CustomAdapter1MetadataBrowseHandler.cs]
public MetadataRetrievalNode[] Browse(string nodeId , int childStartIndex , int maxChildNodes, TimeSpan timeout){ // 左のツリー部分に、ルートのカテゴリを追加 if (MetadataRetrievalNode.Root.NodeId.Equals(nodeId)) { MetadataRetrievalNode node = new MetadataRetrievalNode("CustomMainCategory"); node.DisplayName = "Main Category"; node.IsOperation = false; node.Description = "This category contains inbound and outbound categories."; node.Direction = MetadataRetrievalNodeDirections.Inbound | MetadataRetrievalNodeDirections.Outbound; return new MetadataRetrievalNode[] { node }; }
if (nodeId == "CustomMainCategory") { // Outbound オペレーションを設定 MetadataRetrievalNode outNode = new MetadataRetrievalNode("Custom1/EchoTimes"); outNode.DisplayName = "EchoTimes"; outNode.Description = "This operation echoes the incoming string COUNT number of times in a string array."; outNode.Direction = MetadataRetrievalNodeDirections.Outbound; outNode.IsOperation = true;
// Inbound オペレーションを設定 MetadataRetrievalNode inNode = new MetadataRetrievalNode("Custom1/DoSomeWork"); inNode.DisplayName = "DoSomeWork"; inNode.Description = "This operation is used for inbound operation test."; inNode.Direction = MetadataRetrievalNodeDirections.Inbound; inNode.IsOperation = true;
return new MetadataRetrievalNode[] { inNode, outNode }; } return null;}
なお、Node ID は、BizTalk から使用する際 (XML 変換の際) にも重要な文字列となりますので、考えてネーミングをしておいてください。
つぎに、上述した Adapter 参照の Retrieve の処理を実装しますが、その前に、サービスやオペレーションのメタデータというものについて簡単に概念を記述しておきます。WCF を使われたことがある方は、「メタデータ交換」(Metadata Exchange) という言葉を聞いたことがあるかと思いますが、メタデータとは、そのサービスやオペレーションが、どのような名前で、どのような引数・返値の型で、などの基礎情報のことを言います。当然のことですが、このメタデータの情報がなければクライアントはサーバのメソッドを呼ぶことはできません (どこの何に、どのような形式で呼び出せば良いのかわかりません)。
では、この Retrieve の処理を実装していきましょう。上記の EchoTimes オペレーションは、文字列の引数 param を受け取って文字列を返す必要があります。また、DoSomeWork は、処理する文字列を引数 param として受け取り、特に戻り値はありません。また、今回のサンプルではユーザ定義型の交換はしないので、TypeResolver は必要ありません。よって、CustomAdapter1MetadataResolverHandler.cs を開き、以下の通り実装します。
[CustomAdapter1MetadataResolverHandler.cs]
public bool IsOperationMetadataValid(string operationId, DateTime lastUpdatedTimestamp, TimeSpan timeout){ return true;}
public bool IsTypeMetadataValid(string typeId, DateTime lastUpdatedTimestamp, TimeSpan timeout){ return true;}
public OperationMetadata ResolveOperationMetadata(string operationId, TimeSpan timeout, out TypeMetadataCollection extraTypeMetadataResolved){ extraTypeMetadataResolved = null; // ここでしか判断しない
ParameterizedOperationMetadata om; OperationParameter parmData; switch (operationId) { case "Custom1/EchoTimes": om = new ParameterizedOperationMetadata(operationId, "EchoTimes"); om.OriginalName = "lobEchoTimes"; om.Description = "This operation echoes the incoming string COUNT number of times in a string array."; om.OperationGroup = "Custom1OutboundContract"; om.OperationNamespace = CustomAdapter1.SERVICENAMESPACE; parmData = new OperationParameter("param", OperationParameterDirection.In, QualifiedType.StringType, false); parmData.Description = "Input string"; om.Parameters.Add(parmData); om.OperationResult = new OperationResult(QualifiedType.StringType, false); return om; case "Custom1/DoSomeWork": om = new ParameterizedOperationMetadata(operationId, "DoSomeWork"); om.OriginalName = "lobDoSomeWork"; om.Description = "This operation is used for inbound operation test."; om.OperationGroup = "Custom1InboundContract"; om.OperationNamespace = CustomAdapter1.SERVICENAMESPACE; parmData = new OperationParameter("param", OperationParameterDirection.In, QualifiedType.StringType, false); parmData.Description = "Input string"; om.Parameters.Add(parmData); om.OperationResult = OperationResult.Empty; return om; default: throw new AdapterException("Cannot resolve metadata for operation."); }}
public TypeMetadata ResolveTypeMetadata(string typeId, TimeSpan timeout, out TypeMetadataCollection extraTypeMetadataResolved){ extraTypeMetadataResolved = null; // ここでしか判断しない
throw new AdapterException("No custom type is used.");}
一般の LOB システムでは、答えとして配列 (array) を返すことが多いと思いますが、その場合には上記の OperationResult の第 2 引数を true に設定します。
では、以下では、本体の処理部である Outbound と Inbound のオペレーションの処理を記述していきましょう。
まずは Outbound からです。簡単です!CustomAdapter1OutboundHandler.cs を開き、Execute メソッドを実装するのみです。以下の通り記述してみましょう。
[CustomAdapter1OutboundHandler.cs]
public Message Execute(Message message, TimeSpan timeout){ if (timeout.Equals(TimeSpan.Zero)) throw new AdapterException("operation is not executed, because timeout is zero");
OperationMetadata om = this.MetadataLookup.GetOperationDefinitionFromInputMessageAction(message.Headers.Action, timeout);
switch (message.Headers.Action) { case "Custom1/EchoTimes": // 引数書式 <EchoTimes><param>...</param></EchoTimes> XmlDictionaryReader inputReader = message.GetReaderAtBodyContents(); while (inputReader.Read()) { if ((String.IsNullOrEmpty(inputReader.Prefix) && inputReader.Name.Equals("param")) || inputReader.Name.Equals(inputReader.Prefix + ":" + "param")) break; } inputReader.Read(); string inputValue = inputReader.Value; string resultValue = ""; for (int i = 0; i < this.Connection.ConnectionFactory.Adapter.Count; i++) resultValue += inputValue;
// 戻値書式 <EchoTimesResponse><EchoTimesResult>...</EchoTimesResult></EchoTimesResponse > XmlWriterSettings settings = new XmlWriterSettings(); settings.OmitXmlDeclaration = true; StringBuilder outputString = new StringBuilder(); XmlWriter resultWriter = XmlWriter.Create(outputString, settings); resultWriter.WriteStartElement(om.DisplayName + "Response", CustomAdapter1.SERVICENAMESPACE); resultWriter.WriteElementString(om.DisplayName + "Result", resultValue); resultWriter.WriteEndElement(); resultWriter.Close(); XmlReader replyReader = XmlReader.Create(new StringReader(outputString.ToString())); return Message.CreateMessage(message.Version, om.OutputMessageAction, replyReader);
default : return null; }}
このコーディングスタイルですが、かなり面倒な文字列の構築をおこなっていますが、前回記述したようにチャネルの実装はプロトコルそのものの本体を記述することに等しいので、どうしてもこうした実装スタイルになります (ここで .NET 固有のオブジェクトをガンガン使ってしまっては、.NET 以外の LOB システムとの接続に配慮されなくなってしまいます)。
続いて、Inbound の処理を実装します。Inbound シナリオでは、StartListner, StopListner, TryReceive の 3 つを実装します。まず、Listen の開始時に StartListner が呼ばれます。Listen の際の初期化処理がある場合にはここに処理を記載します。また inbound シナリオでは、定期的に TryReceive が呼ばれるので、ここで監視対象のオブジェクトなどをチェックし、メッセージ (Message) オブジェクトを作成して処理を呼び出します (ここが処理本体になります)。以降は、この返されたメッセージは、このアダプターを使ったサービス側で処理されます (ここの処理のコードは、このサービスを使用する際に記載しましょう)。クラス内には StartListner, StopListner, TryReceive の他に WaitForMessage というメソッドがありますが、この処理は外部から明示的に呼び出されない限り (Channel クラスの WaitForMessage メソッドを使用) 呼び出されません。WaitForMessage メソッドは、Message を返さずにメッセージが到着したかどうかだけを bool で返すもので、例えば「監視対象のオブジェクトが到着するまで処理を開始しない」といった場合に、今回のように TryReceive の中で待機するのではなく、明示的に「待機!」を指定する目的で (待機時間を指定して) WaitForMessage メソッドを呼び出し、その後で Receive メソッドを呼ぶといったことが外からできるようになっています。(つまり WaitForMessage メソッドでは処理がブロックされます。もしブロックせずに同じことをしたい場合には、BeginWaitForMessage メソッドを呼び出します。)今回はこの WaitForMessage メソッドは使いませんので、そのまま (未実装) で結構です。
では、StartListner, StopListner, TryReceive の各メソッドを以下の通り実装してみましょう。
[CustomAdapter1InboundHandler.cs]
public void StartListener(string[] actions, TimeSpan timeout){ foreach (string action in actions) { if ("Custom1/DoSomeWork".Equals(action)) { // 何か初期化処理 (今回はなし !) } }}
public void StopListener(TimeSpan timeout){ // 何か終了処理 (ここもなし !)}
public bool TryReceive(TimeSpan timeout, out System.ServiceModel.Channels.Message message, out IInboundReply reply){ // timeout の処理は省略します ...
reply = new CustomAdapter1InboundReply();
String xmlData = String.Format(@"<DoSomeWork xmlns=""{0}""><param>{1}</param></DoSomeWork>", CustomAdapter1.SERVICENAMESPACE, "testdata"); XmlReader reader = XmlReader.Create(new StringReader(xmlData)); message = Message.CreateMessage(MessageVersion.Default , "Custom1/DoSomeWork" , reader);
System.Threading.Thread.Sleep(5000); // ここで 5 秒間待機
return true;}
また、この inbound ハンドラクラスの Abort と Reply メソッドの暫定コード(未実装のコードが入っています)を削除しておきましょう。
public override void Abort(){ // Exception は throw しません !}
public override void Reply(System.ServiceModel.Channels.Message message , TimeSpan timeout){ // Exception は throw しません !}
以上で、コードの記述は終了です。第 2 回で述べたカスタムチャネルの実装というのは製品ベンダの方などが扱う非常にプロ級!なコーディングなのですが、このカスタムアダプターでは必要な処理を自動で生成してくれますので、こんなに簡単に処理を作成することができます!
では、ビルドをおこない、WCF のフレームワークに登録をおこなっていきます。まず、このプロジェクトを署名 (プロジェクトのプロパティの [署名] タブで key ファイルを設定) してビルドします。そして、ビルドされた dll を GAC に登録し、<Windows install path>\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.configに、以下の要素を追加します。(以下で Public Key Token は署名の内容にあわせて変更してください。また、BizTalk Adapter Pack などを入れている場合には既に SAP 用、Siebel 用などの設定が入っているはずですので、必要な要素のみを追加するようにしてください。)
<system.serviceModel> <client> <endpoint binding="myCustomBinding1" contract="IMetadataExchange" name="myscheme" /> </client> <extensions> <bindingElementExtensions> <add name="myCustomAdapter1" type="myadapter.CustomAdapter1BindingElementExtensionElement,CustomAdapter1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2f3f25ee448858df" /> </bindingElementExtensions> <bindingExtensions> <add name="myCustomBinding1" type="myadapter.CustomAdapter1BindingCollectionElement,CustomAdapter1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2f3f25ee448858df" /> </bindingExtensions> </extensions></system.serviceModel>
以上で、登録もすべて完了しました!
次回では、まず基本的な動きを理解してもらうため、作成したこのアダプターを WCF から呼び出してその動きを確認してみましょう。(さらに次の回では、アダプターを BizTalk から使用してみましょう!といっても、この inbound 処理ではちょっと無意味なので、BizTalk Adapter Pack に入っている Oracle 用のアダプターなどを使用してみたいと思っています、、、)
尚、先ほど少しむずかしいと書いた MSDN のチュートリアルの URL を掲載しましたが、そちらのチュートリアルを見て頂くと、ここで記載したヘボいサンプルと違って、以下のような処理も学習することができるようになっています。
そして、この MSDN のサンプルの完成プロジェクトは、WCF LOB Adapter SDK のインストールディレクトリの以下に置かれています。
%Program Files%\WCF LOB Adapter\Documents\Samples
ここで作成したサンプルプロジェクト -> Download
環境: Visual Studio 2005 .NET Framework 3.0 WCF LOB Adapter SDK 前回 -> 「 WCF LOB Adapter SDK によるカスタムアダプター実装
環境: .NET Framework 3.0 BizTalk Server 2006 R2 Visual Studio 2005 WCF LOB Adapter SDK BizTalk LOB Adapter