C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2007/12/02 01:42
開発した WCF サービスのちょっとしたテストを行うのに、一々 Visual Studio でプロジェクト作ってサービス参照追加して C# でコード書いてビルドして・・・なんて面倒すぎです。
でも PowerShell を使えば、そんな面倒なことをせずとも、WCF サービスを思いのままに呼び出せます。

論より証拠と言いますし、まずは実感してみてください。


[ 必要なもの ]
WCF 及び PowerShell を利用するためには、以下の 3 つが必要です。

.NET Framework ( バージョン 3.0 以降であれば OK です。 )
Windows SDK for Windows Vista
Windows PowerShell 1.0 ( 適切なのを選んでください。 )


[ WCF サービス ]
本来は、C# で WCF サービスを実装して IIS ホスティングなりセルフホスティングなりするんですが、この記事ではどうせなので PowerShell 上で作成 & ホスティングします。通信プロトコルには net.tcp プロトコルを使用することとします。また、WSDL を公開するために Http プロトコルも使用します。

まず、PowerShell を管理者権限で起動してください。管理者権限で起動する理由は、ホスティングを行う際に Http プロトコルを利用するためです。管理者権限が無いとエラーになってしまいます。
PowerShell を起動したら、なんも考えず、以下のコードを全てコピーして PowerShell にペーストしてください。

CounterService のホスティング
function Compile-CSCode
{
    param([String[]]$sources, [string[]]$referenceAssemblyNames, [String]$assemblyName)
    trap { break; }
   
    $tempDirectory = "$Env:Temp\PSCompile";
    if (!(Test-Path $tempDirectory))
    {
        New-Item $tempDirectory -ItemType "Directory" | Out-Null;
    }
    Remove-Item "$tempDirectory\*" -Recurse -Force;
   
    $codeProvider = New-Object "Microsoft.CSharp.CSharpCodeProvider";
    $parameters = New-Object "System.CodeDom.Compiler.CompilerParameters";
    $parameters.GenerateExecutable = $False;
    $parameters.GenerateInMemory = $True;
    $parameters.TreatWarningsAsErrors = $False;
    if (($() -ne $assemblyName) -and (0 -lt $assemblyName.Length))
    {
        $parameters.OutputAssembly = "$tempDirectory\$assemblyName";
    }
    if ($() -ne $referenceAssemblyNames)
    {
        $parameters.ReferencedAssemblies.AddRange($referenceAssemblyNames);
    }
   
    $result = $codeProvider.CompileAssemblyFromSource($parameters, $sources);
    if ($result.Errors.Count -ne 0)
    {
        $errorMessage = "コンパイルエラーが発生しました。`n";
        foreach ($err in $result.Errors)
        {
            $errorMessage += "{0}:{1}`n  Row:{2} Column:{3}`n" -f @($err.ErrorNumber, $err.ErrorText, $err.Line, $err.Column);
        }
        throw New-Object "ArgumentException" @($errorMessage);
    }
    return $result.CompiledAssembly;
}

function Compile-CounterService
{
    $iCounterServiceCode =
@"
    using System;
    using System.ServiceModel;
   
    namespace Sample.WCFServices
    {
        [ServiceContract(Namespace = "http://schemas.sample.net/WCFServices", SessionMode = SessionMode.Allowed)]
        public interface ICounterService
        {
            [OperationContract]
            void Access();
   
            [OperationContract]
            void Release();
   
            [OperationContract]
            int GetCount();
        }
    }
"@;
    $counterServiceCode =
@"
    using System;
    using System.ServiceModel;
   
    namespace Sample.WCFServices
    {
        [ServiceBehavior(Namespace = "http://schemas.sample.net/WCFServices", InstanceContextMode = InstanceContextMode.PerSession)]
        public class CounterService : ICounterService
        {
            private int _count;
           
            public CounterService()
            {
                Console.WriteLine("インスタンスが生成されました。");
            }
           
            public void Access()
            {
                Console.WriteLine("Access メソッドが呼び出されました。");
                if (10 < this._count)
                {
                    FaultReasonText overflowReasonText = new FaultReasonText("もう数えられない。。。");
                    FaultReason overflowReason = new FaultReason(overflowReasonText);
                    FaultCode overflowCode = FaultCode.CreateReceiverFaultCode("overflow", "http://schemas.sample.net/WCFServices/Fault");
                    throw new FaultException(overflowReason, overflowCode);
                }
                this._count++;
            }
   
            [OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.AfterCall)]
            public void Release()
            {
                Console.WriteLine("Release メソッドが呼び出されました。インスタンスは破棄されます。");
            }
   
            public int GetCount()
            {
                Console.WriteLine("GetCount メソッドが呼び出されました。");
                return this._count;
            }
        }
    }
"@;
    $referenceAssemblyNames =
        @(
            [System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel").Location,
            [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime.Serialization").Location
        );
    Compile-CSCode @($iCounterServiceCode, $counterServiceCode) $referenceAssemblyNames | Out-Null;
}

function Execute-CounterServiceHosting
{
    trap
    {
        if ($() -ne $counterServiceHost)
        {
            $counterServiceHost.Abort();
            $counterServiceHost.Close();
        }
        break;
    }
   
    if (("Sample.WCFServices.CounterService" -as [Type]) -eq $())
    {
        Compile-CounterService;
    }
    $counterServiceType = [Sample.WCFServices.CounterService];
    $counterServiceBinding = New-Object "System.ServiceModel.NetTcpBinding" @([System.ServiceModel.SecurityMode]::None);
    $counterServiceBinding.Namespace = "http://schemas.sample.net/WCFServices";
    $counterServiceContractType = [Sample.WCFServices.ICounterService];
    $metadataBehavior = New-Object "System.ServiceModel.Description.ServiceMetadataBehavior";
    $metadataBehavior.HttpGetEnabled = $True;
    $metadataBehavior.HttpGetUrl = "http://localhost/Sample/WCFServices/CounterService.wsdl";
    $counterServiceAddress = [Uri]"net.tcp://localhost:10000/Sample/WCFServices/CounterService.svc";
    $counterServiceHost = New-Object "System.ServiceModel.ServiceHost" @($counterServiceType, $counterServiceAddress);
    $counterServiceHost.AddServiceEndpoint($counterServiceContractType, $counterServiceBinding, [System.String]::Empty) | Out-Null;
    $counterServiceHost.Description.Behaviors.Add($metadataBehavior);
   
    $counterServiceHost.Open();
    "WCF サービス の ホスティングを開始しました。";
    foreach($dispatcher in $counterServiceHost.ChannelDispatchers)
    {
        "Listening uri: $($dispatcher.Listener.Uri)";
    }
    "Enter キーを押すと終了します。";
    Read-Host | Out-Null;
    $counterServiceHost.Close();
}

Execute-CounterServiceHosting;


以下のメッセージが出力されれば成功です。

WCF サービス の ホスティングを開始しました。
Listening uri: net.tcp://localhost:10000/Sample/WCFServices/CounterService.svc
Listening uri: http://localhost/Sample/WCFServices/CounterService.wsdl
Enter キーを押すと終了します。

これで、「CounterService」 という WCF サービスがホスティングされました。
「CounterService」 は、以下の 3 つのオペレーションコントラクトを提供します。

void Access() ・・・ 呼び出すたびにカウントが +1 されます。
int GetCount() ・・・ 現在のカウントを取得します。
void Release() ・・・ サービスインスタンスを解放します。 ( カウントが 0 にリセットされます。 )

Enter キーを押してしまうとホスティングが終了してしまうので、そのまま何もせずに PowerShell 立ち上げておいてください。


[ WCF クライアント ]

ここからが本題の WCF クライアントです。
まず、準備として PowerShell 上でWCF クライアントを扱うための関数 ( 後述 ) を定義します。
既に立ち上がっている PowerShell とは別に、もう一つ PowerShell を起動してください。こちらは管理者権限で起動する必要はありません。
以下のコードを全てコピーし、今起動した方の PowerShell にペーストしてください。

WCF クライアントを扱うための 4 つの関数
function global:New-WCFClientContents
{
    param ([String[]]$metadataPaths, [String]$outputDirectory, [String]$assemblyName = "Client", [string[]]$referenceAssemblyNames)
  
    trap { break; }
    [System.Reflection.Assembly]::LoadWithPartialName("System.Configuration") | Out-Null;
    [System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel") | Out-Null;
  
    if ("$metadataPaths".Trim().Length -eq 0) { throw "引数 metadataPaths が不正です。"; }
    if ("$outputDirectory".Trim().Length -eq 0) { throw "引数 outputDirectory が不正です。"; }
    if ("$assemblyName".Trim().Length -eq 0) { throw "引数 assemblyName が不正です。"; }
  
    # SvcUtil の実行
    $svcUtilExe = "$Env:ProgramFiles\Microsoft SDKs\Windows\v6.0\Bin\SvcUtil.exe";
    $workDirectory = "$outputDirectory\$([Guid]::NewGuid())"
    $configFileName = "$assemblyName.dll.config";
    &$svcUtilExe /target:"Code" "$metadataPaths" /directory:"$workDirectory" /config:"$configFileName" /noLogo | Out-Null;
    if (!(Test-Path $workDirectory)) { throw "WCF クライアントの作成に失敗しました。;" }
  
    # クライアントコードのコンパイル
    $sourceFiles = Get-ChildItem $workDirectory -Include "*.cs" -recurse;
    $sourcePaths = ($sourceFiles | % { $_.FullName; });
    $codeProvider = New-Object "Microsoft.CSharp.CSharpCodeProvider";
    $parameters = New-Object "System.CodeDom.Compiler.CompilerParameters";
    $parameters.GenerateExecutable = $False;
    $parameters.GenerateInMemory = $False;
    $parameters.OutputAssembly = "$outputDirectory\$assemblyName.dll";
    $referenceAssemblyNames +=
        @(
            [System.Reflection.Assembly]::LoadWithPartialName("System").Location,
            [System.Reflection.Assembly]::LoadWithPartialName("System.Data").Location,
            [System.Reflection.Assembly]::LoadWithPartialName("System.Xml").Location,
            [System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel").Location,
            [System.Reflection.Assembly]::LoadWithPartialName("System.IdentityModel").Location,
            [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime.Serialization").Location
        );
    $parameters.ReferencedAssemblies.AddRange($referenceAssemblyNames);
    $compilerResult = $codeProvider.CompileAssemblyFromFile($parameters, $sourcePaths);
    if ($compilerResult.Errors.Count -ne 0)
    {
        $errorMessage = "コンパイルエラーが発生しました。`n";
        foreach ($err in $compilerResult.Errors)
        {
            $errorMessage += "{0}:{1}`n  Row:{2} Column:{3}`n" -f @($err.ErrorNumber, $err.ErrorText, $err.Line, $err.Column);
        }
        throw New-Object "ArgumentException" @($errorMessage);
    }
  
    # コンテンツの配置
    Copy-Item -path "$workDirectory\$configFileName" -Destination "$outputDirectory\$configFileName";
    Remove-Item $workDirectory -Recurse -Force;
  
    # 戻り値の設定
    $assembly = $compilerResult.CompiledAssembly;
    $types = $assembly.GetTypes();
    [Type[]]$clientClasses = $types | ? { ($_.BaseType.IsGenericType) -and ($_.BaseType.GetGenericTypeDefinition() -eq [System.ServiceModel.ClientBase``1]); };
    [Type[]]$serviceContractInterfaces = $types | ? { $_.IsDefined([System.ServiceModel.ServiceContractAttribute], $False); };
    $assemblyPath = "$outputDirectory\$assemblyName.dll";
    $configurationPath = "$outputDirectory\$configFileName";
    $configuration = [System.Configuration.ConfigurationManager]::OpenExeConfiguration($assemblyPath);
    $serviceModelSectionGroup = [System.ServiceModel.Configuration.ServiceModelSectionGroup]::GetSectionGroup($configuration);
    [String[]]$endpointConfigurationNames = ($serviceModelSectionGroup.Client.Endpoints | % { $_.Name });
    $result =
        @{
            Assembly = $assembly;
            ClientClasses = $clientClasses;
            ServiceContractInterfaces = $serviceContractInterfaces;
            AssemblyPath = $assemblyPath;
            ConfigurationPath = $configurationPath;
            EndpointConfigurationNames = $endpointConfigurationNames;
        };
    return $result;
}

function global:New-WCFClient
{
    param ([Type]$clientClass, [String] $configurationPath, [String]$endpointConfigurationName)
  
    trap { break; }
    [System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel") | Out-Null;
  
    if (($() -eq $clientClass.BaseType) -or (!$clientClass.BaseType.IsGenericType) -or ($clientClass.BaseType.GetGenericTypeDefinition() -ne [System.ServiceModel.ClientBase``1]))
    {
        throw "$clientClass は、System.ServiceModel.ClientBase ジェネリッククラスの派生クラスではありません。";
    }
    if ([String]::IsNullOrEmpty($configurationPath))
    {
        $configurationPath = $clientClass.Assembly.Location + ".config";
    }
    return New-WCFClientOrFactory $clientClass $configurationPath $endpointConfigurationName;
}

function global:New-WCFChannelFactory
{
    param ([Type]$serviceContractInterface, [String] $configurationPath, [String]$endpointConfigurationName)
  
    trap { break; }
    [System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel") | Out-Null;
   
    if ((!$serviceContractInterface.IsInterface) -or (!$serviceContractInterface.IsDefined([System.ServiceModel.ServiceContractAttribute], $False)))
    {
        throw "$serviceContractInterface は、サービスコントラクトではありません。";
    }
    if ([String]::IsNullOrEmpty($configurationPath))
    {
        $configurationPath = $serviceContractInterface.Assembly.Location + ".config";
    }
    $factoryType = [System.ServiceModel.ChannelFactory``1].MakeGenericType($serviceContractInterface);
    return New-WCFClientOrFactory $factoryType $configurationPath $endpointConfigurationName;
}

function global:New-WCFClientOrFactory
{
    param ([Type]$clientOrFactoryType, [String] $configurationPath, [String]$endpointConfigurationName)
  
    trap { break; }
    [System.Reflection.Assembly]::LoadWithPartialName("System.Configuration") | Out-Null;
    $serviceModelAssembly = [System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel");
  
    # コンフィギュレーションの取得
    $clientConfigurationMap = New-Object "System.Configuration.ExeConfigurationFileMap";
    $clientConfigurationMap.ExeConfigFilename = $configurationPath;
    $clientConfiguration = [System.Configuration.ConfigurationManager]::OpenMappedExeConfiguration($clientConfigurationMap, "None");
    $clientServiceModelSectionGroup = [System.ServiceModel.Configuration.ServiceModelSectionGroup]::GetSectionGroup($clientConfiguration);
  
    # エンドポイント要素の取得
    if ($clientServiceModelSectionGroup.Client.Endpoints.Count -eq 0) { throw "コンフィギュレーションにエンドポイント要素が存在しません。"; }
    if ([String]::IsNullOrEmpty($endpointConfigurationName))
    {
        if (1 -lt $clientServiceModelSectionGroup.Client.Endpoints.Count) { throw "コンフィギュレーションにエンドポイント要素が複数存在します。"; }
        $endpointElement = $clientServiceModelSectionGroup.Client.Endpoints[0];
    }
    else
    {
        $endpointElement = $clientServiceModelSectionGroup.Client.Endpoints | ? { $_.Name -eq $endpointConfigurationName; };
        if ($() -eq $endpointElement) { throw "コンフィギュレーションに名前が `"$endpointConfigurationName`" のエンドポイント要素が存在しません。。" }
    }
  
    # エンドポイントアドレスの取得
    $configLoaderType = $serviceModelAssembly.GetType("System.ServiceModel.Description.ConfigLoader", $False, $True);
    $flagsByLoadIdentityMethod = [System.Reflection.BindingFlags]([System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Static);
    $loadIdentityMethod = $configLoaderType.GetMethod("LoadIdentity", $flagsByLoadIdentityMethod);
    &{
        trap [System.Reflection.TargetInvocationException] { throw "コンフィギュレーションのエンドポイント ID の指定が不正です。`n$($_.Exception.InnerException)"; }
        $identity = $loadIdentityMethod.Invoke($(), @($endpointElement.Identity));
    };
    $endpointAddress = New-Object "System.ServiceModel.EndpointAddress" @($endpointElement.Address, $identity, $endpointElement.Headers.Headers);
  
    # バインディング設定の取得
    if ([String]::IsNullOrEmpty($endpointElement.Binding)) { throw "コンフィギュレーションのエンドポイント要素の binding 属性が不正です。"; }
    $bindingSetElement = $clientServiceModelSectionGroup.Bindings[$endpointElement.Binding];
    $bindingType = $bindingSetElement.BindingType;
    $binding = New-Object $bindingType;
    if (![String]::IsNullOrEmpty($endpointElement.BindingConfiguration))
    {
        $bindingConfigurationElement = $bindingSetElement.ConfiguredBindings | ? { $_.Name -eq $endpointElement.BindingConfiguration; };
        if ($() -eq $bindingConfigurationElement) { throw "コンフィギュレーションのエンドポイント要素の bindingConfiguration 属性が不正です。"; }
        $bindingConfigurationElement.ApplyConfiguration($binding)
    }
  
    # クライアントオブジェクトまたはファクトリオブジェクトの生成
    $clientOrFactory = [Activator]::CreateInstance($clientOrFactoryType, @($binding.PSObject.BaseObject, $endpointAddress.PSObject.BaseObject));
    if ($clientOrFactory.Endpoint.Contract.Name -ne $endpointElement.Contract)
    {
        throw "`"$endpointConfigurationName`" のエンドポイント要素はコントラクトが `"$($client.Endpoint.Contract.Name)`" ではありません。";
    }
  
    # エンドポイントビヘイビアの適用
    if (![String]::IsNullOrEmpty($endpointElement.BehaviorConfiguration))
    {
        $endpointBehaviorElement = $clientServiceModelSectionGroup.Behaviors.EndpointBehaviors[$endpointElement.BehaviorConfiguration];
        $flagsByCreateBehaviorMethod = [System.Reflection.BindingFlags]([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic);
        $createBehaviorMethod = [System.ServiceModel.Configuration.BehaviorExtensionElement].GetMethod("CreateBehavior", $flagsByCreateBehaviorMethod);
        $endpointBehaviors = $endpointBehaviorElement | % { $createBehaviorMethod.Invoke($_, $()); };
        $endpointBehaviors | % { $clientOrFactory.Endpoint.Behaviors.Add($_); };
    }
  
    return $clientOrFactory;
}

function global:Get-OperationContract
{
    param ($target)
  
    trap { break; }
    [System.Reflection.Assembly]::LoadWithPartialName("System.ServiceModel") | Out-Null;
  
    if ($target -isnot [Type])
    {
        if ($() -ne $target.GetType)
        {
            $targetType = $target.GetType();
        }
        else
        {
            $targetType = [Type]"$target";
        }
    }
    else
    {
        $targetType = $target;
    }
  
    $serviceContractAttributeClass = [System.ServiceModel.ServiceContractAttribute];
    if (($targetType.IsInterface) -and ($targetType.IsDefined($serviceContractAttributeClass, $False)))
    {
        $contractInterface = $targetType;
    }
    else
    {
        $contractInterface = $targetType.GetInterfaces() | ? {$_.IsDefined($serviceContractAttributeClass, $False);}
        if ($() -eq $contractInterface) { return; }
    }
  
    $operationContractAttributeClass = [System.ServiceModel.OperationContractAttribute];
    $operationContractMethods = $contractInterface.GetMethods() | ? { $_.IsDefined($operationContractAttributeClass, $False); };
    if ($() -ne $operationContractMethods)
    {
        $operationContractTexts = $operationContractMethods | % { $_.ToString() };
    }
    return $operationContractTexts;
}



これでクライアント側の準備は完了です。

ここからは、最初に起動した方の PowerShell でホスティングされている 「CounterService」 を利用するサンプルコードになります。
まず、以下のコマンドを実行すると、New-WCFClientContents 関数によって、( 「CounterService」 が公開している WSDL を元に ) クライアントクラスがコンパイルされて "C:\work\wcf" というフォルダ内に作成されます。また、コンフィギュレーションも一緒に作成されます。また、この関数の戻り値から、作成されたコンテンツの情報も取得できます。

CounterService のクライアントコンテンツを作成
$clientContents = New-WCFClientContents "http://localhost/Sample/WCFServices/CounterService.wsdl" "C:\work\wcf" "CounterServiceClient";



以下のコマンドを実行すると、今コンパイルされた クライアントクラスのインスタンスが、 New-WCFClient 関数によって、 PowerShell 上に生成されます。

CounterService のクライアントのインスタンス生成
$client = New-WCFClient $clientContents.ClientClasses[0] $clientContents.ConfigurationPath $clientContents.EndpointConfigurationNames[0];


これで、$client 変数を使用して WCF サービスを利用することができるようになりました。
以下のコマンドを実行すれば、「CounterService」 を利用できていることがわかります。

CounterService にアクセス
$client.Open();

$client.GetCount();
$client.Access();
$client.GetCount();
$client.Release();
$client.GetCount();

$client.Close();




New-WCFChannelFactory 関数を使用すれば、サービスコントラクトインターフェイスから ChannelFactory<T> クラスを生成することもできます。

CounterService 用のチャネルファクトリの生成
$channelFactory = New-WCFChannelFactory ([ICounterService]) "C:\work\wcf\CounterServiceClient.dll.config";


チャネルファクトリにてオープン・クローズ及びクライアントの生成を行えば、先ほどのクライアントオブジェクトと同様に WCF サービスが利用できます。

チャネルファクトリの使用
$channelFactory.Open();
$client = $channelFactory.CreateChannel();

$client.GetCount();
$client.Access();
$client.GetCount();
$client.Release();
$client.GetCount();

$channelFactory.Close();




[ 使用した関数について ]
先ほど、クライアント側の「準備」として、PowerShell 上でWCF クライアントを扱うための関数を定義しました。そして、その関数を使用して WCF クライアントの作成等を行ってきました。
これらの関数は、私が自作した関数で、こちらの記事で紹介しています。詳細な説明を載せていますので、ご覧ください。
また、私が自作した全ての関数 ( WCF 関連以外のものも含む ) は、こちらの記事からダウンロードすることができます。

今回は、説明を簡単にするために、「準備」にコピペという手段を取りましたが、PowerShell の「プロファイル」という機能 ( ここここを参照 ) を利用すれば、一々コピペする必要もなくなります。つまり、最初から New-WCFClientContents 関数や New-Client 関数を使用できるわけです。

PowerShell を使えば、Visual Studio を立ち上げる必要もビルドする必要もありません。PowerShell にやりたいことを記述して Enter を押すだけで即実行されます。WCF 開発には是非 PowerShell を活用してください。
タグ: .NET C# PowerShell WCF











トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/185-292e33ef