OLE Transactionを使用して、クライアントとWCFサービスが同じトランザクションコンテキスト上で、処理を行うプログラムを実装します。分散環境のトランザクションではDistributed Transaction Coordinatar(DTC)を使用します。[スタートメニュー]→[Administrative Tools]→[Services]をクリックして、Distributed Transaction Coodinatorが開始していることを確認しておいて下さい。トランザクションの実装として、WS-AtomicTransactionを使用した方法もあります。

プログラム自体はWCFサンプル再作成(ブラッシュアップ) をベースにして作成していますので、参考にして下さい。

動作確認環境

  • Vista Enterprise(スタンドアロン,クライアント,サービスともに同一マシン上)
  • 開発環境 VIsual Studio 2008 Professional
  • .NET 3.5

1. 空のソリューションの作成

いつもどおり、Visual Studioのメニュー[File]→[New]→[Project]からProject TypesでVisual Studio Solutionsを選択肢、右欄のBlank Solutionを選択して空のソリューションを作成します。ソリューション名はWCFSample007としました。

次に、WCFサンプル再作成(ブラッシュアップ) で作成したWCFSample.ProductService,WCFSample.WPFHost,WCFSample.ConsoleClientプロジェクトの格納されているフォルダをWCFSample007のソリューションの直下にコピーして、WCFSample007に既存のプロジェクトとして追加します。WCFSample.ProductService,WCFSample.ConsoleClientプロジェクトにはSystem.Transactions.dllの参照を追加しておいて下さい。

2.WCFサービスプロジェクトの修正

WCFサービスコントラクトIProductService.csを次のように変更します。

[ServiceContract(Namespace="http://handcraft.wcfsample.org/2008/04/26", Name="ProductService", SessionMode=SessionMode.Required)]
    public interface IProductService
    {
        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        List<int> GetProductIDs();

        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        bool ChangeListPrice(int productID, decimal newListPrice);

        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        Product GetProductByID(int productID);

        [OperationContract]
        [TransactionFlow(TransactionFlowOption.Mandatory)]
        void Commit();
    }

TransactionFlowAttributeをTransactionFlowOption.Mandatoryとすることで、クライアントからトランザクションの伝播が必須であることを定義しています。またCommitメソッドを追加して、トランザクションの確定処理を行えるようにしています。

トランザクションを伝播させるにはバインディングのレイヤにSystem.ServiceModel.Channels.TransactionFlowBindingElement が含まれなければなりません。そのため定義済みのBasicHttpBindingではなく、WSHttpBindingを本サンプルでは使用しています。

WCFサービスクラスProductService.csを次のように変更します。

[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession, 
                TransactionIsolationLevel=System.Transactions.IsolationLevel.ReadCommitted)]
    public class ProductService : IProductService
    {
        private DbConnection CreateConnection()
        {
            return new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["AdventureWorksConnection"].ConnectionString);
        }

        #region IProductService Members
        [OperationBehavior(TransactionScopeRequired=true, TransactionAutoComplete=false)]
        public List<int> GetProductIDs()
        {
            List<int> productIDs = new List<int>();
            using (DbConnection cn = CreateConnection())
            {
                DbCommand cmd = cn.CreateCommand();
                cmd.CommandText = @"SELECT ProductID FROM Production.Product";
                cmd.CommandType = CommandType.Text;

                cn.Open();
                DbDataReader reader = cmd.ExecuteReader();
                while (reader.Read())
                {
                    productIDs.Add(reader.GetInt32(0));
                }
                reader.Close();
                cn.Close();

                return productIDs;
            }
        }

        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
        public bool ChangeListPrice(int productID, decimal newListPrice)
        {
            using (DbConnection cn = CreateConnection())
            {
                StringBuilder builder = new StringBuilder();
                builder.Append(" UPDATE Production.Product ");
                builder.Append("    SET ListPrice = " + newListPrice.ToString());
                builder.Append(" WHERE ProductID = " + productID.ToString());

                DbCommand cmd = cn.CreateCommand();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = builder.ToString();

                cn.Open();

                int result = cmd.ExecuteNonQuery();

                return (result == 1);
            }
        }

        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
        public Product GetProductByID(int productID)
        {
            using (DbConnection cn = CreateConnection())
            {
                DbCommand cmd = cn.CreateCommand();
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = @"SELECT ProductID, Name, Color, StandardCost, ListPrice, Weight
                                      FROM  Production.Product
                                     WHERE ProductID = " + productID.ToString();
                cn.Open();
                DbDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
                Product product = new Product();
                if (reader.Read())
                {
                    product.ProductID = reader.GetInt32(0);
                    product.Name = reader.GetString(1);
                    if (reader.IsDBNull(2))
                    {
                        product.Color = string.Empty;
                    }
                    else
                    {
                        product.Color = reader.GetString(2);
                    }
                    product.StandardCost = reader.GetDecimal(3);
                    product.ListPrice = reader.GetDecimal(4);
                    if (reader.IsDBNull(5))
                    {
                        product.Weight = 0.0m;
                    }
                    else
                    {
                        product.Weight = reader.GetDecimal(5);
                    }
                }
                reader.Close();
                cn.Close();

                return product;
            }
        }
        [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
        public void Commit()
        {
            OperationContext.Current.SetTransactionComplete();
        }
        #endregion
    }

各メソッドのOperationBehavior.TransactionScopeRequiredをtrue(デフォルトfalse)に設定することで、トランザクションが必要であることを宣言します。trueの場合、トランザクションが存在しない場合は新規作成されますが、本プロジェクトではクライアントからトランザクションが伝播されるます。TransactionAutoCompleteをfalse(デフォルトtrue)とすることで、サービスメソッドの処理完了時に自動でコミットが行われないようにします。コミット処理はCommitメソッド内で行います。

OperationBehaviorAttribute.TransactionScopreRequiredの値と、TransactionFlowAttributeの値の組み合わせで、トランザクションの伝播、作成がどのように行われるかは以下のリンクを参照して下さい。
OperationBehaviorAttribute..::.TransactionScopeRequired プロパティ
http://msdn.microsoft.com/ja-jp/library/system.servicemodel.operationbehaviorattribute.transactionscoperequired.aspx
トランザクションがクライアントから伝播した場合、Commitサービスメソッドを呼び出したとしても、トランザクションの発生元であるクライアントのTransactionScope.Completeメソッドが呼び出されない限り、コミット自体はAbortとなることを注意して下さい(2フェーズコミット)。逆の場合も同様で、TransactionScope.Completeがクライアント側で呼び出されても、Commitサービスメソッドが呼び出されない(OperationContect.Current.SetTransactionComplete()が呼び出されない)場合はトランザクションは失敗(Abort)します。

3. WCFホストプロジェクトの修正

WCFホスト用のWCFSample.WPFHostプロジェクトのApp.configを選択して次のように設定します。WCFサンプル再作成(ブラッシュアップ) との変更点は、endpointタグのbindingをwsHttpBindingに変更し、bindingConfigurationにWSTransactionFlowBindingを指定し、wsHttpBindingの設定で、transactionFlowをtrueにしています。この設定で、トランザクションがクライアントから伝播できるようになります。いつものごく、接続文字列のID,パスワードは適当に変更して下さい。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="AdventureWorksConnection" connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=AdventureWorks;User ID=【ユーザID】;Password=【パスワード】"/>
  </connectionStrings>
  <system.web>
    <compilation debug="true" />
  </system.web>
  <system.serviceModel>
    <services>
      <service name="WCFSample.ProductService.ProductService" behaviorConfiguration="ProductServiceBehavior">
        <endpoint address="http://localhost:8056/ProductService" binding="wsHttpBinding"
          bindingConfiguration="WSTransactionFlowBinding" contract="WCFSample.ProductService.IProductService" />
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <binding transactionFlow="true" name="WSTransactionFlowBinding" />
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ProductServiceBehavior">
          <serviceMetadata httpGetEnabled="true" httpGetUrl="http://localhost:8056/ProductService/Mex" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
手持ちの参考資料によると、WCFクライアントからDTC(Distributed Transaction Coodinar)を使用して、トランザクションを伝播させる場合でかつSQL Server 2005を使用している場合、接続文字列にMARSを有効にする文字列を追加する必要があるかもしれません。確認環境では動作しているようなので、掲載している接続文字列にはMulti Active Result Setsを有効にする処理を追加していません。MARSを有効にする場合は接続文字列に"MultipleActiveResultSets=True;"を追加します。

この段階でWCFホストプログラムは動作します。

Vista上でHttpポートをオープンする場合はアクセス権限が必要になります。対処方法はWCFサンプル再作成(ブラッシュアップ) の3節の最後に記載していますが、再掲します。

netsh http add urlacl url=http://+:8056/ProductService user=マシン名\ユーザ名

-Configuring HTTP and HTTPS
http://msdn2.microsoft.com/en-us/library/ms733768.aspx

4.WCFクライアントの修正

デバッグ実行をしてWCFホストを開始して、プロキシクラスを再作成します。コマンドは下記+下図参照。

svcutil /namespace:*,WCFSample.Client.Proxy http://localhost:8056/ProductService/Mex /out:Proxy.cs

作成されたProxy.csクラスをWCFSample.ConsoleClient直下のProxy.csに上書きコピーします。

つぎに、Program.csのMainメソッドを修正します。

class Program
    {
        static void Main(string[] args)
        {
            System.Console.WriteLine("WCFサービスホストを起動したら、キーを入力して下さい。");
            System.Console.ReadLine();

            TransactionOptions option = new TransactionOptions();
            option.IsolationLevel = IsolationLevel.ReadCommitted;
            using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew, option))
            {
                ProductServiceClient proxy = new WCFSample.Client.Proxy.ProductServiceClient("WSHttpBinding_ProductService");
                Product product = proxy.GetProductByID(514);
                Console.WriteLine("ProductID:{0}の変更前の価格:{1}", product.ProductID, product.ListPrice);

                proxy.ChangeListPrice(514, product.ListPrice - 1);

                product = proxy.GetProductByID(514);
                Console.WriteLine("ProductID:{0}の変更後の価格:{1}", product.ProductID, product.ListPrice);

                proxy.Commit();  // これを呼び出さないとAbortする
                ts.Complete();   // これを呼び出さないとAbortする
                proxy.Close();
            }

            Console.WriteLine("終了するにはなにかキーを押してください。");
            Console.ReadLine();
        }
    }

構成ファイルを開き、次のようになっていることを確認します。構成ファイルが設定されていない場合は、以下のように記述します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint address="http://localhost:8056/ProductService" binding="wsHttpBinding"
          bindingConfiguration="WSHttpBinding_ProductService" contract="WCFSample.Client.Proxy.ProductService"
          name="WSHttpBinding_ProductService" />
    </client>
    <bindings>
      <wsHttpBinding>
        <binding name="WSHttpBinding_ProductService" transactionFlow="true" />
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

プログラムを実行して、更新が行われているかをSQL Server Management Studioで確認して下さい。proxy.Commit()や、tx.Complete()を呼び出さないと、トランザクションはAbortされます。

5. チップス:Component Services MMCスナップインで分散トランザクション結果の確認

Vistaを使用している場合は、C:\Windows\System32\comexp.mscをダブルクリック(もしくはファイル名を指定して実行で、dcomcnfg.exeを実行)して、Component Services MMCスナップインを表示できます。Windows 2003 Serverの場合は[管理ツール]→[コンポーネントサービス]で起動できます。
いままでの統計がスナップインには表示されるので、統計をクリアしたい場合はServicesスナップインのDistributed Transaction Coordinatorをリスタートして下さい。プログラムが正常終了した場合は次のように表示されます。Committedが0から1に、Totalが0から1にインクリメントされます。

 

 

4のクライアントプログラムの修正で、tx.Commit()をコメントアウトした場合は下図のようになり、統計を開始してからのTotalが1から2に、Abort数が0から1になっています。

 

 

 以上で完了です。ご指摘事項、問題点がありましたら、ご連絡ください。