前提条件としてWCF Sample 018その1 : WS-Atomic Transactionを有効にする で行ったのと同じようにWS-ATが有効に設定されているとします。バインディングはWSHttpBindingを使用します。wsHttpBindingでは分散トランザクションにOLE TransactionとWS-Atomic Transactionのどちらを使用するかは透過的に決定されます。OLE Transactionはhttp通信を行うときに使用され、https通信を行うときはWS-ATが使用されます。netTcpBindingでは構成ファイルでどちらを使用するかを設定します。

確認環境は次の通り

  • Windows Vista Enterprise(WCFホスト,WCFクライアント同一OS上)
  • 開発環境 Visual Studio 2008 Professional(英語版)
  • .NET 3.5
  • バインディング:wsHttpBinding

1. サンプルのソリューションの作成

Visual Studioを起動して空のソリューションを作成します。ソリューション名はWCFSample018として作成しました。次に、WCFサンプル再作成(ブラッシュアップ) で作成した次の3つのプロジェクトをWCFSample018ソリューション直下にコピーし、既存のプロジェクトとして追加します。(リンク先でサンプルプロジェクトをダウンロードできます)

  • WCFSample.ProductService
  • WCFSample.WPFHost
  • WCFSample.ConsoleClient

ソリューションを右クリック→プロパティを選択して、WCFSample.WPFHost,WCFSample.ConsoleClientをマルチスタートアッププロジェクトとして設定します。

2.WCFサービスプロジェクトの変更

WCFSample.ProductServiceプロジェクトのコントラクト,サービスクラスを修正します。

IProductService.csを開き、次のように変更します。クライアントからトランザクションを開始するために、ServiceContract.SessionModeをSessionMode.Requiredにしています。サービスオペレーションにCommitを追加し、各オペレーションにTransactionFlowAttributeを設定し、TransactionFlowOption.Mandatoryとして、トランザクションの伝播を必須にしています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace WCFSample.ProductService
{

    [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();
    }
}

ProductService.csを開き、次のように変更します。ServiceBehaviorにトランザクションのアイソレーションレベルをReadCommittedにし、各サービスオペレーション実装メソッドのOperationBehavior.TransactionRequiredをtrueにTransactionAutoCompleteをfalseに設定しています。TransactionRequiredをtrueにすることで、トランザクションを使用することを設定し、TransactionAutoCompleteをfalseにすることでメソッド終了時に自動でトランザクションがコミットされないようにしています。Commitメソッド内で2フェーズコミット時にコミット可能であることをマーク設定をするようにしています。

using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.SqlClient;
using System.Configuration;
using System.Data;
using System.ServiceModel;
using System.Text;
using System.Transactions;

namespace WCFSample.ProductService
{
    [ServiceBehavior(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
    }
}

 3 WCFホストの修正

WCFSample.WPFHostプロジェクトを編集します。まず、App.configのsystem.serviceModelタグ内をすべて削除し、WCF Service Configuration EditorでApp.configを編集します。エディタの左ペインのServicesを選択し、右ペインのCreate New ServiceをクリックしてWizardを開始します。Wizardで、次のように設定し、エンドポイントを作成します。

  • サービスタイプ:WCFSample.ProductService.ProductService
  • コントラクト:WCFSample.ProductService.IProductService
  • 接続モード:http
  • 接続の互換性:Advanced Web Services Interoperability,Simplex communication
  • Address:https://localhost:8057/ProductService
  • ウィザード確認ダイアログで下図のようになっていることを確認してFinishボタンクリック

 

 

つづけて、左ペインのBindingsを選択し、右ペインのNew Binding Configurationをクリックし、名前をWSATTransactionFlowBindingとしTransactionFlowプロパティをTrueに設定、続いてSecurityタブを選択し、ModeをTransportWithMessageCredentialを設定、TransportClientCredetialTypeをNoneに設定します。設定後、上記で作成したバインディングのコンフィギュレーションをtrueにします。

 

 

WCFサービスプロジェクトから直接Proxyを作成すると、ProxyのサービスコントラクトになぜかTransactionFlowAttributeが付与されて作成されないので、サービスメタデータを公開するようにServiceBehaviorを設定しています。最終的には以下のように構成ファイルを編集しました。(なんでsvcutilで直接dllからプロキシを作成するとTransactionFlowAttributeがプロキシのコントラクトから削除されるのかしっていたら教えて下さいm(_ _)m。)

<?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>
    <bindings>
      <wsHttpBinding>
        <binding name="WSATTransactionFlowBinding" transactionFlow="true">
          <security mode="TransportWithMessageCredential">
            <transport clientCredentialType="None" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <services>
      <service name="WCFSample.ProductService.ProductService" behaviorConfiguration="WSATTransactionFlowBehavior">
        <endpoint address="https://localhost:8057/ProductService" binding="wsHttpBinding"
          bindingConfiguration="WSATTransactionFlowBinding" contract="WCFSample.ProductService.IProductService" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WSATTransactionFlowBehavior">
          <serviceMetadata  httpGetEnabled="true" httpGetUrl="http://localhost:8056/ProductService/Mex"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

デバッグ実行してWCFホストが動作することを確認します。

Vista上でデスクチッププログラムからHttp,Httpsを使用してポートをオープンする場合はアクセス権限が必要になります。対処方法はWCFサンプル再作成(ブラッシュアップ) の3節の最後に記載していますが、再掲します。httpsも同様の処理が必要です。

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

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

 4 WCFクライアントの修正

WCFSample.ConsoleClientプロジェクトを編集します。プロジェクトにSystem.Threading.dllの参照を追加しておきます。

4.1 プロキシの作成

ソリューションをデバッグ実行してWCFホストを開始したあと、次のコマンドを入力して、プロキシクラスを作成します。

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

 

 

作成されたProxy.csをWCFSample.COnsoleClientのProxy.csに上書きコピーします。

4.2 構成ファイルの編集

4.1で作成されたoutput.configを使って、クライアントの構成を以下のように編集します。プロキシ作成時にデフォルト値も構成値として作成されるのでクライアントのApp.confi編集時には必要のないデフォルト値は削除しています。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint address="https://localhost:8057/ProductService" binding="wsHttpBinding"
          bindingConfiguration="WSATTransactionFlowBinding" contract="WCFSample.Client.ProductService"
          name="WSHttpBinding_ProductService" />
    </client>
    <bindings>
      <wsHttpBinding>
        <binding name="WSATTransactionFlowBinding" transactionFlow="true">
          <security mode="TransportWithMessageCredential">
            <transport clientCredentialType="None" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

4.3 プログラムの修正

httpsでWCFホストが使用する証明書は信頼されていません。Program.csを開き信頼されていない証明書でも開発テスト時に例外が発生しないように証明書が信頼されていなくても証明書を使用するConfirmCertificatePolicyクラスを作成します。Mainメソッド内で、このクラスのインスタンスを作成すると、信頼されていない証明書でもその証明書を使用するようになります。

/// <summary>
    /// 証明書に問題がある場合は確認するポリシークラス
    /// </summary>
    class ConfirmCertificatePolicy
    {
        public ConfirmCertificatePolicy()
        {
            ServicePointManager.ServerCertificateValidationCallback += RemoteCertValidation;
        }
        bool RemoteCertValidation(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
        {
            return true;   // 証明書に問題があっても、なくても証明書を信用する
        }
    }

次にProgram.cs内のMainメソッドを次のように入力してトランザクション内でプロキシを作成し、サービスを呼び出すようにします。トランザクションをAbortさせないために、Commitメソッドを呼び出した後、TransactionScope.Complete()を呼び出すようにしています。

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

            ConfirmCertificatePolicy policy = new ConfirmCertificatePolicy(); // 証明書続行確認用

            TransactionOptions option = new TransactionOptions();
            option.IsolationLevel = IsolationLevel.ReadCommitted;
            using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew, option))
            {
                ProductServiceClient proxy = new WCFSample.Client.ProductServiceClient("WSHttpBinding_ProductService");
                int[] productIDs = proxy.GetProductIDs();

                foreach (int productID in productIDs)
                {
                    Console.WriteLine("Product ID: " + productID);
                    Product product = proxy.GetProductByID(productID);
                    Console.WriteLine("Name: " + product.Name);
                    Console.WriteLine("Color: " + product.Color);
                    Console.WriteLine("ListPrice: " + product.ListPrice);

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

5. 動作確認

Visual Studio上でデバッグ実行し、wCFホストを開始し、WCFクライアントを開始するとプログラムが正常終了します。dcomcnfg.exeを実行してComponentServices管理ツールを表示し、Component Services→My Computer→Distributed Transaction Coordinator→Local DTCと選択して、Transactionの統計を表示すると、正常にトランザクションがコミットされていることを確認できます。

 

 

管理ツール→サービスコンソールを起動し、Distributed Transaction Coordinatorを再起動すると結果が確認しやすくなります。

説明は以上です。間違い、指摘等がありましたら連絡ください。