.NET リモート処理から Windows Communication Foundation (WCF) へ

 

Ingo Rammer
thinktecture

January 2005

適用対象:
Visual Studio .NET
Windows Communication Foundation (WCF)
移行

概要: この記事では、サービス指向アプリケーションを構築するために新規インフラストラクチャの利点を活用できるよう、既存の .NET リモート処理ベースのアプリケーションから Windows Communication Foundation (WCF。旧称は Indigo) への統合および移行に関して考え得るシナリオを取り上げています。 必要な主要変更点と、考慮に入れるべき設計上の決定事項について学習します。

ここをクリックして、この記事のコード サンプルをダウンロードしてください。

目次

統合か移行か
移行のためのベースラインを設定しましょう
ステップ 1 - インターフェイス契約をさらに明示的なものにします
インターフェイスに障害情報を組み込みます
ステップ 2 - 明示的データ型定義に対して DataContract を使用します
ステップ 3 - クライアント起動オブジェクトの代わりにセッションを使用します
コールバックおよびイベントを簡略化します
リモート処理が行ったのと同じやり方でオブジェクト グラフを引き渡します
移行の拡張性の重要点
まとめ

統合か移行か

.NET リモート処理ベースのアプリケーションを WCF に移行する方法の詳細を述べる前に、確実な点を申し上げておきます。つまり、その必要性すらないかもしれません。 同一のアプリケーション内または同一の AppDomain 内でさえ、WCF とリモート処理を一緒に使用することができます。WCF とリモート処理を自由に組み合わせることができ、この記事中のデモ アプリケーション コードを見ればお分かりのとおり、WCF およびリモート処理と一緒に同時に使用できるサーバー側オブジェクトでさえ作成することができます。

それでも、場合によっては、やむを得ず WCF を移行する必要が生じることもあります。この新しいプラットフォームの機能の利点を活用したい場合やその必要がある場合は特にそうです。WCF が関心の的になるのは、それぞれ異なるプラットフォームにサービスを公開したい場合や、相互運用可能なセキュリティおよびトランザクションを使用することにした場合、あるいは、TCP や HTTP からの通信チャネルでの選択肢を増やして、MSMQ およびより高速化された名前付きパイプ チャネルを組み込みたい場合などです。

この記事の後半でお分かりになりますが、.NET リモート処理から WCF への移行は、思った以上に簡単かもしれません。 ただし、アプリケーションを直ちに移行しないことに決めた場合においても、後で .NET リモート処理のどの機能を簡単に WCF に移行できるか、どの機能セットを慎重に使用する必要があるかを学習することができます。

移行のためのベースラインを設定しましょう

ここ数年、Web サービスに似た .NET リモート処理の使用が一般的に推奨されてきました。それには主として、サーバーで起動されるコンポーネントを SingleCall または Singleton モードで使用します。 これもまた、かなり簡単に移行できるシナリオです。

たいていのリモート処理ベース アプリケーションでは、通常は次のように、少なくとも 3 種類の Visual Studio プロジェクトがリモート対話を処理することになります。

  • クライアント側アプリケーション (EXE)
  • クライアントとサーバーとで共用される DLL。 これには、インターフェイス定義および [Serializable] オブジェクトが入っています。
  • サービスの実装。 これは、ホストされているスタンドアロンの場合は EXE 内に置かれていますが、IIS (Internet Information Server) 内でホストされている場合は DLL 内に置かれています。

たとえば、ユーザー情報の保管用のサービスのサブセットは、次のような共用インターフェイスおよびデータ型定義を使用することができます。 (ここでは、[Serializable]ISerializable を混合して使用し、包括的な移行シナリオを示しています。)

public interface ICustomerManager
{
  void StoreCustomer(Customer cust);
}

[Serializable]
public class Customer
{
  public string Firstname;
  public string Lastname;
  public Address DefaultDeliveryAddress;
  public Address DefaultBillingAddress;
}

[Serializable]
public class Address: ISerializable
{
  public string Street;
  public string Zipcode;
  public string City;
  public string State;
  public string Country;

  public Address() { }

  public Address(SerializationInfo info, StreamingContext context)
  {
    Street = info.GetString("Street");
    Zipcode = info.GetString("Zipcode");
    City = info.GetString("City");
    State = info.GetString("State");
    Country = info.GetString("Country");
  }

  public void GetObjectData(SerializationInfo info,
          StreamingContext context)
  {
    info.AddValue("Street", Street);
    info.AddValue("Zipcode", Zipcode);
    info.AddValue("City", City);
    info.AddValue("State", State);
    info.AddValue("Country", Country);
  }
}

このサービスの定形文面の実装コードは、次のように MarshalByRefObject から派生して ICustomerManager を実装するクラス内に置かれます。

public class CustomerService: MarshalByRefObject, ICustomerManager
{
  public void StoreCustomer(Customer cust)
  {
    // 実際の操作は省かれています
    Console.WriteLine("Storing customer ...");
  }
}

このクラスを .NET リモート処理コンポーネントとして公開するには、以下のような構成ファイルを作成し、http://<servername>:8080/CustomerManager.rem という URL をクラス CustomerService に関連付けます。

<configuration >
  <system.runtime.remoting>
    <application>
      <channels>
        <channel ref="http" port="8080" />
      </channels>
      <service>
        <wellknown mode="Singleton"
          type="Server.CustomerService, Server"
          objectUri="CustomerManager.rem" />
      </service>
    </application>
  </system.runtime.remoting>
</configuration>

この .NET リモート処理ベースのアプリケーションを作成する最後のステップでは (IIS 内部でこれを実行しないことにした場合)、RemotingConfiguration.Configure() を呼び出して着信要求を待ち合わせるカスタムのホスト アプリケーションを実装します。

using System;
using System.Runtime.Remoting;
using Shared;

class ServerProgram
{
  public static void Main(string[] args)
  {
    StartRemotingServer();
    Console.ReadLine();
  }

  private static void StartRemotingServer()
  {
    string configFile =
         AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
    RemotingConfiguration.Configure(configFile);
    Console.WriteLine("Remoting-Server is running ...");
  }
}

それに対応するクライアント アプリケーションは次に Activator.GetObject() を使用して、サーバー側の CustomerService への参照を作成し、そのメソッドを呼び出します。

class ClientProgram
{
  public static void Main(string[] args)
  {
    CallSimpleServiceWithRemoting();

    Console.WriteLine("Done");
    Console.ReadLine();
  }

  private static void CallSimpleServiceWithRemoting()
  {
    Console.WriteLine("Calling a simple service with .NET Remoting");
    Customer cust = new Customer();
    cust.Firstname = "John";
    cust.Lastname = "Doe";
    cust.DefaultBillingAddress = new Address();
    cust.DefaultBillingAddress.Street = "One Microsoft Way";
    cust.DefaultDeliveryAddress = cust.DefaultBillingAddress;

    ICustomerManager mgr =
         (ICustomerManager) Activator.GetObject(typeof(ICustomerManager),
         "http://localhost:8080/CustomerManager.rem");
    mgr.StoreCustomer(cust);
  }
}

サンプルのベースライン アプリケーションを作成し終わったので、次に WCF へのその移行を見てみましょう。

ステップ 1 - インターフェイス契約をさらに明示的なものにします

.NET リモート処理ベースのアプリケーションを移行する場合、通常はシンプルな 3 つのステップから実施することができます。 最初のステップでは、インターフェイス定義を変更して、[ServiceContract] および [OperationContract] を使用するさらに明示的な WCF の方式を採用する必要があります。 アプリケーションおよび相互運用性の要件によっては、変更の必要があるのはこの点のみの場合もあります。(.NET リモート処理のクライアント起動オブジェクトを使用しなかった場合に限り、他の 2 つのステップは完全にオプションになることもあります。)

インターフェイス契約をさらに明示的にするため、リモート インターフェイスを [ServiceContract] で修飾し、公開する各メソッドを [OperationContract] で修飾する必要があります。(これらの属性は、ライブラリ System.ServiceModel.DLL の一部であり、WCF コンポーネントの大半とともに名前空間 System.ServiceModel に収容されています。)

したがって、上記のインターフェイスを次のように変更する必要があります。

[ServiceContract]
public interface ICustomerManager
{
  [OperationContract]
  void StoreCustomer(Customer cust);
}

サーバー側の実装は未変更のままにすることができ、この最初のステップでは、[Serializable] または ISerializable オブジェクトのいずれの詳細も変更する必要はありません。 変更する必要があるのは、クライアントとサーバー側の構成ファイルだけです。それは、.NET リモート処理の代わりに WCF を使用するようにするためです。

セクション <system.serviceModel> を使用して、サーバー側の構成ファイルを拡張する必要があります。このセクションには、アプリケーションが公開する WCF サービスに関する情報が入っています。

<configuration>
  <system.runtime.remoting>
    <!-- you can leave this as-is -->
  </system.runtime.remoting>

  <system.serviceModel>
    <services>
      <service name="Server.CustomerService">
        <endpoint
          address="net.tcp://localhost:8081/CustomerManager"
          binding="netTcpBinding"
          contract="Shared.ICustomerManager" />
      </service>
    </services>
  </system.serviceModel>
</configuration>

この場合、アドレス net.tcp://localhost:8081/CustomerManager での WCF の tcp バインディングを使用して、サーバー側コンポーネントが公開されます。 サービス ホストを開始するために、次のようにアプリケーションを変更して、サービスの ServiceHost を作成して開く必要があります。

class ServerProgram
{
  private static ServiceHost _customerServiceHost;

  public static void Main(string[] args)
  {
    StartWCFServer();
    Console.ReadLine();
  }

  private static void StartWCFServer()
  {
    _customerServiceHost = new ServiceHost(typeof(CustomerService));
    _customerServiceHost.Open();
    Console.WriteLine("WCF Server is running ...");
  }
}

これは、解説用の小さなサーバー ホストでしかないことに注意してください。 実際には、独自のホスト アプリケーションではなく、Internet Information Server などの既存のホストを使用する可能性が高いと思われます。

次にクライアント側で、対応する構成ファイルを作成する必要があります。 以下のスニペットは、事前に定義した宛先 URL に構成名 customermanager を関連付けます。

<configuration>
  <system.serviceModel>
    <client>
      <endpoint
        name="customermanager"
        address="net.tcp://localhost:8081/CustomerManager"
        binding="netTcpBinding"
        contract="Shared.ICustomerManager"/>
    </client>
  </system.serviceModel>
</configuration>

また、クライアント側アプリケーション コードを変更して、Activator.GetObject() ではなく、WCF の ChannelFactory が使用されるようにする必要もあります。

class ClientProgram
{
  public static void Main(string[] args)
  {
    CallSimpleServiceWithWCF();
  }

  private static void CallSimpleServiceWithWCF()
  {
    Console.WriteLine("Calling a simple service with WCF");
    Customer cust = new Customer();
    cust.Firstname = "John";
    cust.Lastname = "Doe";
    cust.DefaultBillingAddress = new Address();
    cust.DefaultBillingAddress.Street = "One Microsoft Way";
    cust.DefaultDeliveryAddress = cust.DefaultBillingAddress;

    ChannelFactory<ICustomerManager> fact = 
        new ChannelFactory<ICustomerManager>("customermanager");
    ICustomerManager mgr = fact.CreateChannel();
    mgr.StoreCustomer(cust);
  }
}

上記のサンプルでお分かりのとおり、WCF への .NET リモート処理アプリケーションの移行はきわめてシンプルです。行う必要があるのは、[ServiceContract] および [OperationContract] を使用してインターフェイスに注釈を付け、構成ファイルにセクションを追加し、サーバー側のホスティング コードの 2 行を変更し、WCF で実行するクライアント側起動コードの 2 行を変更するだけです。

インターフェイスに障害情報を組み込みます

インターフェイスをさらに明示的にするときは、生じ得るエラー結果に関する情報を、メソッド宣言に追加する必要もあります。 ここでの最初の反復処理では、WCF 境界を越えて例外情報を転送できるようにするだけで十分です。 そのためには次のように、フラグ ReturnUnknownExceptionsAsFaultstrue に設定した属性 [ServiceBehavior] を使用して、サービス実装を修飾する必要があります。

[ServiceBehavior(ReturnUnknownExceptionsAsFaults=true)]
public class CustomerService: ICustomerManager
{
   //}

この属性を指定しなかった場合に、サーバーとの通信が正常に完了しないと (サーバー側の例外がスローされた場合も含む)、常に CommunicationException が WCF から呼び出し元に戻されます。 サービスに対してこの属性を定義すると、キャッチされなかったすべての例外は直ちに FaultException として転送されます。これをクライアント側でキャッチし、サーバーの例外情報を取り出すことができます。 また、インターフェイス メソッドの定義上で [FaultContract] 属性を使用して、ここのカスタム障害の転送を可能にすることもできます。 これを行う方法の詳細は、WinFX SDK のオンライン ヘルプにあるトピック「FaultContractAttribute Class」を参照してください。

おめでとうございます。 作成しようとしているアプリケーションの種類、相互運用性の要件、および予定しているサービスの対象によっては、これが変更する必要のあるすべてです。 リモート コンポーネントに渡すパラメータに対しては、[Serializable] および ISerializable が自動的に WCF によって適用されます。 ただし、複雑なオブジェクト グラフの引き渡しでは、.NET リモート処理とまったく同じ動作は WCF では公開されていないことに注意してください。WCF は、シリアライゼーション解除後にメモリ内オブジェクト参照子を透過的に復元するリモート処理モデルに準拠するのではなく、既定として ASP.NET Web サービスに似たモデルに準拠します。後者のモデルは、シリアライズ化オブジェクトから階層構造へのレンダリングを常に試みます。 その意味するところは、たとえば内部参照または巡回参照を持ったオブジェクト グラフは、正しくシリアライズ化されないということです。 詳細は、後述のリモート処理が行ったのと同じ方法でオブジェクト グラフを引き渡しますの項を参照してください。

ステップ 2 - 明示的データ タイプ定義に対して DataContract を使用します

引き続き [Serializable] を使用する (ISerializable の使用時にはさらに当てはまります) 場合、サービス指向の原則のうちの 1 つに違反する可能性があります。それは、クライアントとサーバーの間の境界は、非常に明示的でなければならないという原則です。 [Serializable] を使用すると、クラスのすべてのプライベート メンバは自動的にシリアライズされます。 したがって、プライベート フィールドの追加、移動、または名前変更のプロセスによって、インターフェイス契約が暗黙で変更されることになり、それによって既存のクライアントが破壊されます。

.NET Framework バージョン 1.1 の [Serializable] もまた、データ構造をバージョン管理する簡潔な方式をサポートしていませんでした。そのため、複数バージョンのクライアントが確実に既存サーバーと対話できるようにするためだけに、ISerializable の使用を余儀なくされる可能性が非常に高くなっていました。(そうしないと、サーバー バージョンのデータ構造内に新規フィールドを追加した場合、既存のクライアントは使用不能になってしまいます。)

クラスを [Serializable] から完全な WCF バージョンに変更するには、[DataContract] と、[DataMember] に組み込まれているはずのすべてのフィールド メンバ (プライベート、パブリック、およびプロパティ) を使用して、クラスにマークを付ける必要があります。 その後、以前の .NET リモート処理環境の同じクラスを使用する予定がなければ、選択しだいで [Serializable] 属性を除去しても構いません。

したがって、変更後の Customer クラスは次のようになります。

[DataContract]
public class Customer
{
  [DataMember]
  public string Firstname;
  [DataMember]
  public string Lastname;
  [DataMember]
  public Address DefaultDeliveryAddress;
  [DataMember]
  public Address DefaultBillingAddress;
}

この最初の変換は非常に単純明快です。 必要なのは、バージョン管理のサポートのために ISerializable を実装したクラスの使用時には特に注意を払うことだけです。 その理由は、このような目的での ISerializable の使用は、多くの場合、一時対策でしかないからです (データ構造のバージョン管理のネイティブ サポートは .NET リモート処理には備えられていないので)。 ただし、WCF では、[DataMember] 用の組み込みバージョン管理サポートを活用できます。それには、フィールドを追加されたバージョン番号を指定し、それがオプションかどうかを指定します。

以下の .NET リモート処理 ベースの ISerializable 実装は、この種のバージョン管理を示しています。 この例では、アプリケーションの 2 番目のバージョンにフィールド AttentionOf が追加されています。 シリアライゼーション解除コンストラクタでは、GetString() の呼び出しが try/catch ブロック内にカプセル化されています。それは、入力メッセージ中にこのフィールドがない場合のエラーを無視するためです。

[Serializable]
public class Address: ISerializable
{
  public string Street;
  public string AttentionOf;
  public string Zipcode;
  public string City;
  public string State;
  public string Country;

  public Address() { }

  public Address(SerializationInfo info, StreamingContext context)
  {
    Street = info.GetString("Street");
    Zipcode = info.GetString("Zipcode");
    City = info.GetString("City");
    State = info.GetString("State");
    Country = info.GetString("Country");

    // バージョン 2
    try
    {
      AttentionOf = info.GetString("AttentionOf");
    } catch {}
  }

  public void GetObjectData(SerializationInfo info,
     StreamingContext context)
  {
    info.AddValue("Street", Street);
    info.AddValue("Zipcode", Zipcode);
    info.AddValue("City", City);
    info.AddValue("State", State);
    info.AddValue("Country", Country);

    // バージョン 2
    info.AddValue("AttentionOf", AttentionOf);
  }
}

この実装を WCF の [DataContract] に変更すると、はるかにシンプルになります。それは、特定のフィールドが必須またはオプションのどちらであるかを、この属性を使って指定できるからです。(オプションが既定です。)

[DataContract]
public class Address
{
  [DataMember(IsRequired=true)]
  public string Street;
  [DataMember(IsRequired=true)]
  public string Zipcode;
  [DataMember(IsRequired=true)]
  public string City;
  [DataMember(IsRequired=true)]
  public string State;
  [DataMember(IsRequired=true)]
  public string Country;

  [DataMember]
  public string AttentionOf;
}

IsRequired フラグを指定しないで [DataMember] 属性を使用すると、フィールドは自動的にオプション フィールドとして扱われるので、変更に対して耐性のあるシリアライゼーションが可能になります。

ステップ 3 - クライアント起動オブジェクトの代わりにセッションを使用します

.NET リモート処理には、クライアント起動オブジェクト (CAO) という概念があります。これにより、サーバー側インスタンスへの参照を作成し、それをローカル オブジェクトであるものとして取り扱うことができます。 たとえば、複数のそれぞれ個別のクライアント側インスタンス参照を作成した場合、それらは、同じ数のそれぞれ個別のサーバー側オブジェクトを指し示します。

それと同じこと (個別のサーバー側オブジェクトを個別のプロキシが指し示す) が、MarshalByRefObject から派生したオブジェクトをリモート処理境界を越えて引き渡すときにも必ず生じます。 この場合、オブジェクト参照 (シリアライズされた ObjRef オブジェクト) のみが実際に回線を通して送信されて、元のオブジェクトを指し示すプロキシが、宛先サイド上で作成されます。 その後のメソッド呼び出しはすべて、元のオブジェクトに返送されます。[Serializable] オブジェクトとは対照的に、これらの CAO は、その作成場所である AppDomain から送出されることはありません。

.NET リモート処理では、以下の例に示されているとおり、MarshalByRefObject を戻すメソッドを作成するだけで、このような動作を実装することができます。

public interface IRemoteFactory
{
  IMySessionBoundObject GetInstance();
}

public interface IMySessionBoundObject
{
  string GetCurrentValue();
  void SetCurrentValue(string val);
  void PrintCurrentValue();
}

次に、以下のコード中に示されている IRemoteFactory の実装が、通常のサーバー起動オブジェクトとして登録されます。

public class RemoteFactory : MarshalByRefObject, IRemoteFactory
{
  public IMySessionBoundObject GetInstance()
  {
    return new MySessionBoundObject();
  }
}

public class MySessionBoundObject : MarshalByRefObject, IMySessionBoundObject
{
  private string _value;

  public void PrintCurrentValue()
  {
    Console.WriteLine("Current value is " + _value);
  }

  public string GetCurrentValue() {return _value;}
  public void SetCurrentValue(string val) {_value = val;}
}

お分かりのとおり、.NET リモート処理では特別なマーシャリングまたは準備作業を行う必要はありません。 単に MySessionBoundObject の新しいインスタンスをクライアントに返送して、他のすべての通常オブジェクトと同じように使用することができます。(以下の例では、URL http://localhost:8080/Factory.rem の .NET リモート処理に RemoteFactory を登録済みであると想定されています。)

private static void CallRemoteObjectWithRemoting()
{
  IRemoteFactory fact =
     (IRemoteFactory )Activator.GetObject(typeof(IRemoteFactory ),
     "http://localhost:8080/Factory.rem");

  IMySessionBoundObject o1 = fact.GetInstance();
  IMySessionBoundObject o2 = fact.GetInstance();

  o1.SetCurrentValue("Hello");
  o2.SetCurrentValue("World");

  if (o1.GetCurrentValue() == "Hello" && o2.GetCurrentValue() == "World")
  {
    Console.WriteLine("Remote instance management works as expected");
  }
}

このコードを実行すると、変数 o1 および o2 は、それぞれ異なる 2 つのサーバー側オブジェクトを指し示します。 各オブジェクトごとに文字列値の設定が完了すると、その値を再度取り出すことができるようになり、他の対話や他のセッションによって影響を受けることはありません。 それらのオブジェクトは、通常のオブジェクトと同様に稼働します。

このようなリモート参照の引き渡しのシンプルさは、.NET リモート処理における最大の危険性のうちの 1 つでもあります。 多くのアーキテクチャでは、きわめて制約的でしかも制御下にあるケースでない限り、リモート参照の転送は望ましくありません。[Serializable] オブジェクトではなく MarshalByRefObject を誤って渡した場合、通常はパフォーマンスおよびスケーラビリティが極端に低下することになります。 結局のところ、どのようなメソッド呼び出しも、ネットワークを通して送信される必要があるので、複数の都市、国、または大陸を横断する可能性があります。

WCF では、参照をリモート サービスに渡すことはできますが、その引き渡しはきわめて明示的に行う必要があります。 引き渡しが、あまり明確な意図もなしにたまたま行われることがあってはありません。

まず、[OperationContract] を拡張する必要があります。それには、そのフィールド Sessiontrue に設定し、セッションをサポートするトランスポート チャネルが必要であることを示します。

[ServiceContract(Session=true)]
public interface IMySessionBoundObject
{
  [OperationContract]
  string GetCurrentValue();

  [OperationContract]
  void SetCurrentValue(string val);
  [OperationContract]
  void PrintCurrentValue();
}

注意する点として、WCF の見地から見たセッションは、ASP.NET またはその他の Web アプリケーション フレームワークの見地から見たセッションとは本質的に異なります。 通常は、セッションが複数のサービスを対象とすることはなく、単一のサービス インスタンスとの通信でのみ有効となります。 それは、クライアント側プロキシの存続期間中、同じサーバー側オブジェクトと対話することを意味します。

実装クラスでは、[ServiceBehavior] 属性を追加して、どのような種類のセッション分離を使用したいかを示す必要があります。(セッションは、プライベートまたは共用にすることができます。 共用セッション サービスへの参照は、他の通信パートナーに渡すことができます。)

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Shareable)]
public class MySessionBoundObject : MarshalByRefObject,
IMySessionBoundObject
{
  private string _value;

  public void PrintCurrentValue()
  {
    Console.WriteLine("Current value is " + _value);
  }

  public string GetCurrentValue() {return _value;}
  public void SetCurrentValue(string val) {_value = val;}
}

WCF 用のファクトリの作成は必ずしも必要ではありません。なぜなら単に、クライアント側とサーバー側の構成ファイル内にセッション バインドのオブジェクトを登録できるからです。 そのためには、次のように <service> 項目をサーバー側構成に追加します。

<service name="Server.MySessionBoundObject">
  <endpoint
    address="net.tcp://localhost:8081/MySessionBound"
    binding="netTcpBinding"
    contract="Shared.IMySessionBoundObject" />
</service>

さらに、次のような <endpoint> 項目をクライアント側構成ファイルにも追加します。

<endpoint
    name="sessionbound"
    address="net.tcp://localhost:8081/MySessionBound"
    binding="netTcpBinding"
    contract="Shared.IMySessionBoundObject" />

次に、ChannelFactory を使用して、このサービスへのチャネルを作成することができます。 その背後では、セッションを対象とした接続が確立されるので、以下の o1o2 の 2 つの参照は、それぞれ別々の 2 つのサーバー側オブジェクトを指し示すことになります。

private static void CallRemoteObjectWithWCFWithoutFactory()
{
  ChannelFactory<IMySessionBoundObject> chfact =
     new ChannelFactory<IMySessionBoundObject>("sessionbound");
  IMySessionBoundObject o1 = chfact.CreateChannel();
  IMySessionBoundObject o2 = chfact.CreateChannel();

  o1.SetCurrentValue("Hello");
  o2.SetCurrentValue("World");

  if (o1.GetCurrentValue() == "Hello" && o2.GetCurrentValue() == "World")
  {
    Console.WriteLine("Remote instance management works as expected");
  }
}

サーバー側ファクトリを使用することにした (おそらく、特定の外的条件に応じて、別々のサービスへの参照を戻すのが望ましいと考えたことが原因で) 場合、クライアント側でセッション バインド オブジェクト用の <endpoint> を登録する必要はありません。

それに代えて、.NET リモート処理ベースの IRemoteFactory インターフェイスを再利用して、これに注釈を付け、WCF との互換性をもたせることができます。 主要な変更点として、IMyRemoteObject のインスタンスを直接戻すことはできなくなり、タイプ EndpointAddress10 (これは、WS-Addressing 1.0 との互換性のあるエンドポイント アドレスです) のオブジェクトを戻す必要があります。 クライアントは後で、こうして戻されるアドレスへのチャネルを手動で作成する必要があります。それは、このオブジェクトに対する各メソッド呼び出しごとに、リモート サービス起動が行われたことがクライアント開発者に必ず通知されるようにするためです。

[ServiceContract]
public interface IRemoteFactory
{
  [OperationContract]
  EndpointAddress10 GetInstanceAddress();
}

リモート ファクトリの GetInstanceAddress() メソッドの実装では、次のように、登録済みのオブジェクトへのサーバー側チャネルを作成してはじめて、EndpointAddress10 オブジェクトを戻すことができます。

public class RemoteFactory : MarshalByRefObject, IRemoteFactory
{
  public static ChannelFactory<IMySessionBoundObject> _fact = 
     new ChannelFactory<IMySessionBoundObject>("sessionbound");

  public EndpointAddress10 GetInstanceAddress()
  {
    IClientChannel chnl = (IClientChannel) _fact.CreateChannel();
    return EndpointAddress10.FromEndpointAddress(chnl.RemoteAddress);
  }
}

このコードが功を奏するためには、サーバー側構成ファイル内で、<service> および <client> の両方としてセッション バインド オブジェクトを登録する必要があります。 これが必要なのは、サーバー側ファクトリは、新規のサービス インスタンスへのセッション ベースのチャネルを作成する必要があるために、自身のサービスに対するクライアントとして稼働する必要があるからです。

<client>
  <endpoint
    name="sessionbound"
    address="net.tcp://localhost:8081/MySessionBound"
    binding="netTcpBinding"
    contract="Shared.IMySessionBoundObject " />
</client>
<services>
  <service name="Server.RemoteFactory ">
    <endpoint
      address="net.tcp://localhost:8081/MyRemoteFactory"
      binding="netTcpBinding"
      contract="Shared.IRemoteFactory " />
    </service>
</services>

クライアント側では、セッション バインド オブジェクトを登録する必要はなく、次のようにファクトリだけを登録します。

<endpoint
      name="factory"
      address="net.tcp://localhost:8081/MyRemoteFactory"
      binding="netTcpBinding"
      contract="Shared.IRemoteFactory " />

セッション ベースのサービスへのチャネルを作成するためには、まずファクトリへのチャネルを作成し、その GetInstanceAddress() メソッドを呼び出し、その後新規の ChannelFactory と、セッション バインド オブジェクトへのチャネルを作成します。そのためには、EndpointAddress10 (ファクトリから受け取ったもの) を通常の WCF EndpointAddress オブジェクトに変換します。

private static void CallRemoteObjectWithWCFWithFactory()
{
  ChannelFactory<IRemoteFactory> chfact =
    new ChannelFactory<IRemoteFactory>("factory");
  IRemoteFactory fact = chfact.CreateChannel();

  EndpointAddress10 adr1 = fact.GetInstanceAddress();
  EndpointAddress10 adr2 = fact.GetInstanceAddress();

  ChannelFactory<IMySessionBoundObject> f1 =
    new ChannelFactory<IMySessionBoundObject>(
       new NetTcpBinding(),
       adr1.ToEndpointAddress());

  ChannelFactory<IMySessionBoundObject> f2 =
    new ChannelFactory<IMySessionBoundObject>(
       new NetTcpBinding(),
       adr2.ToEndpointAddress());

  IMySessionBoundObject o1 = f1.CreateChannel();
  IMySessionBoundObject o2 = f2.CreateChannel();

  o1.SetCurrentValue("Hello");
  o2.SetCurrentValue("World");

  if (o1.GetCurrentValue() == "Hello" && o2.GetCurrentValue() == "World")
  {
    Console.WriteLine("Remote instance management works as expected");
  }
}

WCF を使用したこの 2 種類のアプローチでは、.NET リモート処理の参照の引き渡しをさらに明示的なやり方で模倣することができます。 この場合の存続期間管理システムは、セッション バインド チャネルをベースとします。つまり、オブジェクトにつながるチャネルがもうなくなって、タイマの期限が切れると、そのオブジェクトは直ちに解放されます。

コールバックおよびイベントを簡略化します

.NET リモート処理での比較的複雑な領域として、コールバックおよびイベントの使用があります。 特に後者の場合、イベントを受信するときには、中間シム オブジェクトを使用する必要があります。 このシムは次に、実際のクライアント側実装にそのイベントを送信します。 概してこのプロセスは複雑になるので、長時間実行操作 (究極的に非同期で呼び出される) の多くは、.NET リモート処理の使用時には同期実行されることになります。

他方 WCF では、選択によっては、いわゆる「二重」メッセージ交換パターンを利用することができます。 二重交換は、一連の一方向対話をベースにし、それらの対話が会話全体を構成します。 たとえば、長時間実行操作を 1 回のメソッド呼び出しで開始することができ、その操作が完了するまでクライアントが待っている必要はありません。 その後サーバーは、完了した作業のパーセントを呼び出し元に定期的に通知することができます。

たとえば、複数の外部サービスおよびデータベースをチェックして、企業の顧客の存続期間値を計算する長時間実行操作の場合などは、全面的に非同期のインターフェイスを用意するのが理にかなっています。 その場合、2 種類の [ServiceContracts] を作成する必要があります。1 つは、クライアントからサーバーに通信するために使用され、もう 1 つは、バック チャネル上での通知のために使用されます。

コールバック契約の種類が [ServiceContract] で指定されるように、クライアントからサーバーへの契約を作成する必要があります。 さらに、[OperationContract] の場合、IsOneWaytrue に設定して、完全に非同期の動作が可能になるようにする必要もあります。 コールバック契約そのものは通常の契約にすぎず、この契約に対しては [OperationContract] のメンバも、やはり一方向対話のマークを付けられています。 これを、以下の例に示してあります。

[ServiceContract(CallbackContract=typeof(ICustomerCalculationCallback))]
public interface ICustomerCalculation
{
  [OperationContract(IsOneWay=true)]
  void CalculateLifetimeValue(int customerID);
}

[ServiceContract]
public interface ICustomerCalculationCallback
{
  [OperationContract(IsOneWay=true)]
  void PercentCompleted(int percentCompleted);

  [OperationContract(IsOneWay = true)]
  void CalculationCompleted(decimal lifetimeValue);
}

クライアント側では、コールバック インターフェイス ICustomerCalculationCallback を実装するクラスを作成する必要があります。 リモート サービスへの参照の作成では、タイプ DuplexChannelFactory (通常のチャネルの場合の ChannelFactory ではなく) のオブジェクトを使用します。 コールバックを最初のパラメータとして処理するオブジェクトを引き渡すことによって、この二重チャネル ファクトリを初期化します。

public class Calculation : ICustomerCalculationCallback
{
  private ICustomerCalculation _calc;

  public void CalculateLifetimeValue(int customerID)
  {
    DuplexChannelFactory<ICustomerCalculation> calcFactory =
      new DuplexChannelFactory<ICustomerCalculation>(
          this, "customercalculation");

    Console.WriteLine("Starting customer calculation");
    _calc = calcFactory.CreateChannel();
    _calc.CalculateLifetimeValue(customerID);
    Console.WriteLine("Calculation started");
  }

  public void PercentCompleted(int percentCompleted)
  {
    Console.WriteLine("{0} % completed", percentCompleted);
  }

  public void CalculationCompleted(decimal lifetimeValue)
  {
    Console.WriteLine("Lifetime value is {0}.", lifetimeValue);
    ((IChannel)_calc).Close();
  }
}

サーバー側では、現在の OperationContextGetCallbackChannel() メソッドを使用して、バック チャネルを獲得することができます。これは、コールバック ハンドラのクライアント側実装を指し示しています。 通常の WCF 通信インターフェイスと同じように、このチャネルを使用することができます。

public class CustomerCalculationService : ICustomerCalculation
{
  public void CalculateLifetimeValue(int customerID)
  {
    ICustomerCalculationCallback _callback = 
      OperationContext.Current.
         GetCallbackChannel<ICustomerCalculationCallback>();

    System.Threading.Thread.Sleep(1000);
    _callback.PercentCompleted(25);
    System.Threading.Thread.Sleep(1000);
    _callback.PercentCompleted(50);
    System.Threading.Thread.Sleep(1000);
    _callback.PercentCompleted(75);
    System.Threading.Thread.Sleep(1000);
    _callback.PercentCompleted(100);
    _callback.CalculationCompleted(19344);
  }
}

このコードを実行すると、CalculateLifetimeValue() の呼び出しはすぐに完了するので、長時間実行操作の完了をクライアントが待機する必要はないことがお分かりになります。 サーバーは、新しい情報を受け取ると、その都度コールバック チャネルに対してメソッドを起動するので、クライアントはそれに対して非同期で対応することができます。

リモート処理が行ったのと同じやり方でオブジェクト グラフを引き渡します

既に述べたとおり、.NET リモート処理の動作には、WCF の作動方法とはかなり違った重要な一面があります。それは、オブジェクト グラフの引き渡しです。 その説明として、上記のサンプル全体を通して使用されている Customer オブジェクトが、クライアント側でどのように取り込まれたかを見てみましょう。

Customer cust = new Customer();
cust.Firstname = "John";
cust.Lastname = "Doe";
cust.DefaultBillingAddress = new Address();
cust.DefaultBillingAddress.Street = "One Microsoft Way";
cust.DefaultDeliveryAddress = cust.DefaultBillingAddress;

クライアント側ではフィールド DefaultBillingAddress および DefaultDeliveryAddress は、同じ Address オブジェクトを参照します。 このオブジェクトを .NET リモート処理境界をまたいで渡した場合、サーバー側でも同じことが起きます。つまり、タイプ Address のインスタンスは 1 つだけ存在することになり、この顧客の住所指定フィールドはどちらも、この同じインスタンスを参照することになります。

しかし WCF を使用した場合は、これは当てはまりません。 その理由は、単にオブジェクト グラフ内の内部参照を、相互運用可能なやり方で引き渡すために標準化された方法はないからです。Java クライアントから見れば、その意図を測りかねるかもしれません。なぜなら、基盤の XML 転送フォーマットは本質的に階層方式であってグラフおよびネットワークに主眼を置いたものではないからです。 プラットフォームを選ばない相互運用性が WCF の設計目標であるため、すぐに使用できる .NET リモート処理の「旧式の」セマンティクスはサポートされていません。

さいわいにも柔軟性を念頭に置いて、基盤となるシリアライザが構築されました。.NET リモート処理 ベースのアプリケーションの大半がそうであるように、相互運用性を一大関心事としない場合、WCF の拡張ポイントの 1 つである DataContractSerializerOperationBehavior を使用して、この動作を変更することができます。 この拡張ポイントが内部でどのように働くかの詳細を取り上げる前に、まず 1 つの考え得る実装 (これは、WCF チーム メンバ Sowmy Srinivasan の Web ログで最初に知りました) の完全ソース コードをご覧にいれます。

public class PreserveReferencesOperationBehavior:
   DataContractSerializerOperationBehavior
{

  public PreserveReferencesOperationBehavior (
    OperationDescription operationDescription)
    : base(operationDescription) { }

  public override XmlObjectSerializer CreateSerializer(
    Type type, string name, string ns, IList<Type> knownTypes)
  {
    return CreateDataContractSerializer(type, name, ns, knownTypes);
  }

  private static XmlObjectSerializer CreateDataContractSerializer(
    Type type, string name, string ns, IList<Type> knownTypes)
  {
    return CreateDataContractSerializer(type, name, ns, knownTypes);
  }

  public override XmlObjectSerializer CreateSerializer(
    Type type, XmlDictionaryString name, XmlDictionaryString ns,
    IList<Type> knownTypes)
  {
    return new DataContractSerializer(type, name, ns, knownTypes,
        0x7FFF,
        false,
        true /* preserveObjectReferences */,
        null);
  }
}

これの主な秘策はオーバーライドされたメソッド CreateSerializer() にあります。これは、内部オブジェクト参照をサポートするための基盤となるシリアライザを構成するために、preserveObjectReferences の値として true を渡します。

このような機能をアプリケーション内で有効化するには、また別の拡張性実装が必要になります。それを操作動作属性と呼びます。 この属性をサービス インターフェイス上で後で使用して、参照保存の新旧のスタイルを切り替えることができます。

public class PreserveReferencesAttribute : Attribute, IOperationBehavior
{
  public void AddBindingParameters(OperationDescription description,
    BindingParameterCollection parameters)
  {}

  public void ApplyClientBehavior(OperationDescription description,
    ClientOperation proxy)
  {
    IOperationBehavior innerBehavior =
      new PreserveReferencesOperationBehavior(description);
    innerBehavior.ApplyClientBehavior(description, proxy);
  }

  public void ApplyDispatchBehavior(OperationDescription description,
    DispatchOperation dispatch)
  {
    IOperationBehavior innerBehavior =
      new PreserveReferencesOperationBehavior(description);
    innerBehavior.ApplyDispatchBehavior(description, dispatch);
  }

  public void Validate(OperationDescription description)
  { }
}

サービス契約インターフェイスのメソッド上にこの属性を置くと、常に WCF は、カスタマイズされたシリアライザを自動的に使用すると共に、.NET リモート処理が行ったのと同じグラフの引き渡し動作を使用するようになります。 以下の例のメソッド StoreCustomerWithReferences() は、そのように構成されます。

[ServiceContract]
public interface ICustomerManager
{
  [OperationContract]
  void StoreCustomer(Customer cust);

  [OperationContract]
  [PreserveReferences]
  void StoreCustomerWithReferences(Customer cust);

  [OperationContract]
  void TestException();
}

移行の拡張性の重要点

.NET リモート処理および WCF は両方とも、広大かつ包括的な拡張性モデルを提供します。 どちらのシステムでも、クライアントからサーバーへの完全な処理チェーンの変更も構成も可能です。 また、WCF の拡張性モデルのほうが、.NET リモート処理のものよりもはるかに簡単に使用できます。 ただし、インターフェイスにおいてこのような変更を行えるということは、拡張性コードの移行 はそれほど複雑ではないことも意味しています。(結論を言えば、拡張性ロジックの大半に対して再考を加えてから、それを新規インターフェイスに移動する必要があると思われます。)

これは望ましくない一面です。 望ましい一面としては、コードの大半は、移行する必要さえないと考えられます。 ここ数年間で目にした拡張性実装の多くにおいて、.NET リモート処理の特定の制限事項に対処するための対策がとられています。 目に付いたものには、セキュリティ用のカスタム シンクやチャネル、トランザクション フロー、負荷分散、ログ記録および追跡記録、MSMQ および名前付きパイプのサポートなどがあります。 現在、これらの機能はすべて WCF および Windows Server システムの一部となっているので、カスタム シンクやチャネルを移行する必要さえありません。 そのような場面ではすべて、WCF のすぐに使える機能を使用することができます。

拡張性シンクおよびチャネルを WCF に実際に移動する必要が生じた場合でも、カスタム動作用としてはるかに簡単になった API の使用を介して、そのようなタスクが新規モデルでサポートされます。 その詳細はこの記事では取り上げませんが、このトピックに関する別の白書や記事が間もなく MSDN に掲載される運びになっています。

まとめ

ご覧になったとおり、.NET リモート処理から WCF への移行は、恐れるに足りないタスクです。 たいていのアプリケーションの場合、3 つのステップから成るシンプルなプロセスで、アプリケーションを新しいプラットフォームに移動することができます。 たいていの場合、インターフェイス契約に [ServiceContract] および [OperationContract] のマークを付け、データ構造に [DataContract] および [DataMember] のマークを付けると共に、クライアント起動オブジェクトではなくセッションをベースとするように起動モデルを部分的に変更する必要があるだけです。

Windows Communication Foundation の諸機能の利点を活用することにした場合、たいていのアプリケーションにとって、.NET リモート処理から WCF への完全移行は比較的簡単なタスクになるはずです。

著者の紹介

Ingo Rammer は、ソフトウェアの設計者および開発者に対して一歩踏み込んだ技術上のコンサルティングおよびサービスを提供する会社である thinktecture の共同設立者です。 同氏は、分散アプリケーションの設計および開発のエキスパートであり、あらゆる規模の団体に対してアーキテクチャ、設計、および構築上の検討サービスを提供しています。 同氏が主に重視しているのは、基幹の .NET アプリケーションのパフォーマンス、スケーラビリティ、および信頼性の向上です。 同氏の連絡先は、ingo.rammer@thinktecture.com であり、同氏について詳しくは、http://www.thinktecture.com/staff/ingo を参照してください。