ローカル環境でSOAP通信やってみる


## 目次

- [SOAP通信チュートリアル](#soap通信チュートリアル)
  - [目次](#目次)
  - [ゴール](#ゴール)
  - [開発環境等](#開発環境等)
  - [サーバーサイド](#サーバーサイド)
    - [プロジェクトの作成(Server)](#プロジェクトの作成server)
    - [Webサービスの作成](#webサービスの作成)
    - [サーバーの例](#サーバーの例)
    - [EX wsdlがある場合](#ex-wsdlがある場合)
  - [クライアントサイド](#クライアントサイド)
    - [プロジェクトの作成(Client)](#プロジェクトの作成client)
    - [ProxyCodeの作成](#proxycodeの作成)
    - [リクエストを送ってみる](#リクエストを送ってみる)
  - [例外処理](#例外処理)
    - [SoapException](#soapexception)
  - [EX](#ex)
    - [サーバーが立ち上がらない](#サーバーが立ち上がらない)
    - [Responseのステータスコード、Bodyを取得したい](#responseのステータスコードbodyを取得したい)
      - [方針1 ProxyCodeを使わずに実装する](#方針1-proxycodeを使わずに実装する)
      - [方針2 TraceSourceを利用する](#方針2-tracesourceを利用する)

## ゴール

- SOAP通信をローカルで実行できるようにする
- サーバー・クライアントどちらもデバッグできるようにする
- ログ出力する

## 開発環境等

- VisualStudio2022
- C#
- .netflamework
- wsdl

<div style="page-break-before:always"></div>

## サーバーサイド

### プロジェクトの作成(Server)

1. プロジェクトテンプレートの選択 ASP.NETWeb アプリケーション ![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355343/0d3435d1-3099-b09f-07f9-1f3fe0e343aa.png) 
※出てこない場合はインストーラーでAsp.NetとWeb開発 .netflamework プロジェクトと項目テンプレートをインストール

2. ASP.NETのテンプレート選択 空
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355343/7dbb8d0a-1d1d-5b44-c3f4-424fe90ed83a.png)

### Webサービスの作成

1. asmxの追加 
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355343/4c6fe70c-8863-10eb-2e71-8924e99f48f6.png)
 以下のようなコードが生成される [WebMethod]属性をつけると外部に公開されクライアントから呼び出すことが可能になる。

     ```csharp
    public class WebService1 : System.Web.Services.WebService
    {
        [WebMethod]
        public string HelloWorld()
        {
            return "Hello World";
        }
    }
    ```

2. 実行してみると以下のようにブラウザが立ち上がる ![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355343/57b53229-5936-f04e-be78-08889965c58a.png)
3. HelloWorldクリック → 起動ボタンクリック とするとレスポンスを確認できるメソッドの定義通りに「HelloWorld」が返却されている。

    ```xml
    <string xmlns="http://tempuri.org/">Hello World</string>
    ```

4. ログ出力 
以下のメソッドをasmx.csに追加しHelloWorldメソッド内で呼び出すとログがテキスト形式で出力される。WebServiceを継承しているためContextプロパティからRequestを取得することができる。

    ```csharp
    void LogRequest()
    {
        var req = Context.Request;
        string basePath = Context.Request.PhysicalApplicationPath;
        string filename = basePath + "HelloWorld.txt";

        using (StreamWriter sw = new StreamWriter(filename, true))
        {
            foreach (var key in req.Headers.AllKeys)
            {
                sw.WriteLine(key + " : " + req.Headers[key]);
            }

            var bodyStream = new StreamReader(req.InputStream);
            bodyStream.BaseStream.Seek(0, SeekOrigin.Begin);
            sw.WriteLine(bodyStream.ReadToEnd());

        }
    }
    ```

### サーバーの例

この後クライアントサイドの実装に移っていくがその際に以下のサーバーを使用する。引数なしだと寂しいので引数があるメソッドを定義した。

```csharp
using System.IO;
using System.Web.Services;

namespace WebApplication1
{
    /// <summary>
    /// WebService1 の概要の説明です
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    // この Web サービスを、スクリプトから ASP.NET AJAX を使用して呼び出せるようにするには、次の行のコメントを解除します。
    // [System.Web.Script.Services.ScriptService]
    public class WebService1 : System.Web.Services.WebService
    {

        [WebMethod]
        public string GetKSyntax(int ageNow, int yearsLater)
        {
            LogRequest();
            return $"{yearsLater}年後の自分は何歳かなと考えると{ageNow + yearsLater}歳なんですね";
        }

        void LogRequest()
        {
            var req = Context.Request;
            string basePath = Context.Request.PhysicalApplicationPath;
            string filename = basePath + "GetKSyntax.txt";

            using (StreamWriter sw = new StreamWriter(filename, true))
            {
                foreach (var key in req.Headers.AllKeys)
                {
                    sw.WriteLine(key + " : " + req.Headers[key]);
                }

                var bodyStream = new StreamReader(req.InputStream);
                bodyStream.BaseStream.Seek(0, SeekOrigin.Begin);
                sw.WriteLine(bodyStream.ReadToEnd());

            }
        }
    }
}

```

## クライアントサイド

### プロジェクトの作成(Client)

- 何でもよい。今回はコンソールアプリケーションを使用する。

### ProxyCodeの作成

1. サーバーのwsdlを取得する。Webサービスの作成の2の手順の際にブラウザのURLの末尾に「?wsdl」と入力する。以下のようにwsdlを取得できる。「ctrl + s」でファイル名.wsdlの形式で保存する。![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355343/35f1b38e-3e89-eaae-7d4d-a8f279a2d756.png)


2. 1で保存したファイルを作成したプロジェクトフォルダ配下へ移動。Developer PowerShellもしくは開発者コマンドプロンプトで以下のコマンドを実行

    ```powershell
    ----                 -------------         ------ ----
    d-----        2022/07/26     15:47                bin
    d-----        2022/07/26     15:47                obj
    d-----        2022/07/26     15:47                Properties
    -a----        2022/07/26     15:47            187 App.config
    -a----        2022/07/26     15:47            260 Program.cs
    -a----        2022/07/26     15:04           3098 WebService1.wsdl


    PS C:\Users\hogehoge\Desktop\SOAPTestingDocWork\ConsoleApp1\consoleapp1> wsdl.exe Webservice1.wsdl
    ```

    以下のようなコードが生成されるのでプロジェクトに追加する(一部抜粋)

    ```csharp
    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "4.8.3928.0")]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    [System.Web.Services.WebServiceBindingAttribute(Name="WebService1Soap", Namespace="http://tempuri.org/")]
    public partial class WebService1 : System.Web.Services.Protocols.SoapHttpClientProtocol {
    
    private System.Threading.SendOrPostCallback GetKSyntaxOperationCompleted;
    
    /// <remarks/>
    public WebService1() {
        this.Url = "https://localhost:44345/WebService1.asmx";
    }
    
    /// <remarks/>
    public event GetKSyntaxCompletedEventHandler GetKSyntaxCompleted;
    
    /// <remarks/>
    [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tempuri.org/GetKSyntax", RequestNamespace="http://tempuri.org/", ResponseNamespace="http://tempuri.org/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
    public string GetKSyntax(int ageNow, int yearsLater) {
        object[] results = this.Invoke("GetKSyntax", new object[] {
                    ageNow,
                    yearsLater});
        return ((string)(results[0]));
    }

    ```

### リクエストを送ってみる

ProxyCodeの作成で生成されたクラスをインスタンス化しメソッドを呼ぶだけでよい。

```csharp
var service = new WebService1();
Console.WriteLine(service.GetKSyntax(20,30));
// 30年後の自分は何歳かなと考えると50歳なんですねと表示される
```

サーバーサイドで取得したログ

```xml
Connection : Keep-Alive
Content-Length : 345
Content-Type : text/xml; charset=utf-8
Expect : 100-continue
Host : localhost:44345
User-Agent : Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protocol 4.0.30319.42000)
SOAPAction : "http://tempuri.org/GetKSyntax"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
    <GetKSyntax xmlns="http://tempuri.org/">
    <ageNow>20</ageNow>
    <yearsLater>30</yearsLater>
    </GetKSyntax>
</soap:Body>
</soap:Envelope>
```

これでクライアントとサーバーが実装できたのでいくらでもSOAP通信を試すことができるようになった。

## 例外処理

ProxyCodeを用いたSOAP通信では以下の３つの例外が生じます。 [SoapHttpClientProtocol.Invoke(String, Object[]) メソッド](https://docs.microsoft.com/ja-jp/dotnet/api/system.web.services.protocols.soaphttpclientprotocol.invoke?view=netframework-4.8)

- SoapException
- InvalidOperationException
- WebException

ここではSoapExceptionを主に取り扱います。

### SoapException

SOAPにおいて何らかのエラーが発生した際にSoapFalutが返却されることがあります。SoapFaultはの４つの要素を持つxmlです。[(詳細)](https://docs.microsoft.com/ja-jp/previous-versions/msdn/architecture-center/cc465562(v=msdn.10))

ProxyCodeを用いるとライブラリ内でxmlが解析され、そこからSoapExceptionが生成されまる。サーバーでSoapExceptionを発生させ、クライアントでその内容を取得してみる。

- サーバーサイドに引数をチェックし例外を投げるコードを追加、GetKSyntaxメソッドで呼び出されるようにする。

    ```csharp
    private void CheckParameter(int ageNow, int yearsLater)
    {
        if (ageNow >= 0 && yearsLater >= 0)
        {
            return;
        }

        //soapfaultを投げる
        XmlDocument doc = new XmlDocument();
        XmlNode detailNode = doc.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace);
        XmlNode errtxt = doc.CreateNode(XmlNodeType.Text, String.Empty, null);
        errtxt.Value = "パラメータはどちらも正の整数である必要があります";
        detailNode.AppendChild(errtxt);
        throw new SoapException("引数が不正です", SoapException.ClientFaultCode, Context.Request.Url.AbsoluteUri, detailNode);
    }
    ```

- クライアントサイドでSOAPFaultの内容を取得できるようにする。

    ```csharp
    static void Main(string[] args)
    {
        try
        {
            var service = new WebService1();
            //  負の値をパラメータにセット
            Console.WriteLine(service.GetKSyntax(-1, 30));
        }         
        catch (SoapException ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.Code.Name);
            Console.WriteLine(ex.Actor);
            Console.WriteLine(ex.Detail.OuterXml);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
    ```

- 実行結果 
SOAPFaultの4要素を取得できた。SOAPExceptionの仕様上detailはxml形式となるのでサーバー側の実装に合わせてパースするとなおよい。（ここでは割愛）

    ```txt
    System.Web.Services.Protocols.SoapException: メソッド呼び出しが不正です
    Client
    https://localhost:44345/WebService1.asmx
    <detail>パラメータはどちらも正の整数である必要があります</detail>
    ```

## EX 悩んだことなど

ここではSOAPの環境を作成する中でつまずいた点およびその解決法を記載する。

### サーバーが立ち上がらない

- HTTP Error 403.14 - Forbidden ![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2355343/1ba32080-6a5c-7ec6-d1bb-ce0e83b6b961.png)

  
  - 原因
    サーバー側のプロジェクトにクラスファイルを追加し、それを参照するコードを書いた場合に生じる。
  - 解決法(どちらか)
    - Asp.netフォルダの追加から「App_Code」フォルダを追加。その中にクラスファイル を移動する。namespaceもフォルダ構成に合わせる。
    - Web.configに「directoryBrowse enabled="true"」を追加する

        ```xml
        <configuration>
            <system.webServer>
                <directoryBrowse enabled="true" />
            </system.webServer>
        </configuration>
        ```

### Responseのステータスコード、Bodyを取得したい

上で記載した方法ではhttp通信のステータスコードを取得できない。そもそも生のレスポンスを取得することができない。サーバーの仕様を完全に把握できるのならば問題ないが、そうでない場合はどのようなレスポンスが帰ってきているのかを確認したい場合がある。

#### 方針1 ProxyCodeを使わずに実装する

[SoapHttpClientProtocol](https://referencesource.microsoft.com/#System.Web.Services/System/Web/Services/Protocols/SoapClientProtocol.cs,2f408436dbe7d204)のInvokeメソッドの実装を見るとhttp通信を行っていることが分かる。ProxyCodeを用いて送信しているリクエストと同様のものを送ればサーバーは認識してくれる。

- ProxyCodeなしSOAP通信クライアント
  
    <details><summary>コード</summary>

    ```csharp
    static void Main(string[] args)
    {
        string soapBody = @"<?xml version=""1.0"" encoding=""utf-8""?>
                            <soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
                                <soap:Body>
                                <GetKSyntax xmlns=""http://tempuri.org/"">
                                    <ageNow>45</ageNow>
                                    <yearsLater>30</yearsLater>
                                </GetKSyntax>
                                </soap:Body>
                            </soap:Envelope>";
        try
        {

            //バイト型配列に変換
            byte[] postDataBytes = Encoding.UTF8.GetBytes(soapBody);

            //WebRequestの作成
            WebRequest req = WebRequest.Create("https://localhost:44345/WebService1.asmx");
            req.Method = "POST";
            req.ContentType = "text/xml; charset=utf-8";
            req.ContentLength = postDataBytes.Length;

            // JX手順で実行するメソッドをここで指定
            req.Headers.Add("SOAPAction", @"http://tempuri.org/GetKSyntax");

            // 送信データを書き込み
            Stream reqStream = req.GetRequestStream();

            reqStream.Write(postDataBytes, 0, postDataBytes.Length);
            reqStream.Close();

            //サーバーからの応答を受信するためのWebResponseを取得
            WebResponse res = req.GetResponse();

            //応答データを受信するためのStreamを取得
            Stream resStream = res.GetResponseStream();

            // ステータスコード
            Console.WriteLine(((HttpWebResponse)res).StatusDescription);
            Console.WriteLine(((HttpWebResponse)res).StatusCode);


            // 受信して表示
            using (StreamReader sr = new System.IO.StreamReader(resStream, Encoding.UTF8))
            {
                Console.WriteLine(sr.ReadToEnd());
            }
        }

        catch (WebException ex)
        {
            // ProtocolErrorの場合原因を表示
            if (ex.Status == System.Net.WebExceptionStatus.ProtocolError)
            {
                WebResponse errResp = ex.Response;
                using (Stream respStream = errResp.GetResponseStream())
                {
                    StreamReader reader = new StreamReader(respStream);
                    string text = reader.ReadToEnd();
                    Console.WriteLine(text);

                }
            }
            else
                Console.WriteLine(ex);
        }

        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

    }
    ```

    </details>

- 実行結果

    ```xml
        statuscode : OK
        <?xml version="1.0" encoding="utf-8"?>
        <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <soap:Body>
            <GetKSyntaxResponse xmlns="http://tempuri.org/">
            <GetKSyntaxResult>30年後の自分は何歳かなと考えると75歳なんですね</GetKSyntaxResult>
            </GetKSyntaxResponse>
        </soap:Body>
        </soap:Envelope>
    ```

- 問題点
    - xmlパーサーを自分で書く必要がある。
    - SOAPExceptionがでないのでxmlそれ用のxmlパーサーを書く必要がある。
    - とにかくクライアント側で書くコードの量が増える

#### 方針2 TraceSourceを利用する

System.netにはあらかじめロギングが用意されているためそれを利用する。[ネットワークのトレースを行う](https://dobon.net/vb/dotnet/programing/networktrace.html)

- App.config に以下を追加

 ```xml
 <system.diagnostics>
    <sources>
        <source name="System.Net.Sockets" switchName="NetSwitch"/>
        <source name="System.Net" switchName="NetSwitch"/>
        <source name="System.Net.HttpListener" switchName="NetSwitch"/>
        <source name="System.Net.Cache" switchName="NetSwitch"/>
    </sources>
    <switches>
        <add name="NetSwitch" value="Verbose"/>
    </switches>
    </system.diagnostics>
 ```

- 取得できるログ(必要な個所のみ抜粋)

<details><summary>長いので折り畳み</summary>

```txt

System.Net Information: 0 : [21940] Connection#26239245 - 受信された状態行: Version=1.1、StatusCode=200、StatusDescription=OK

System.Net Verbose: 0 : [31664] 00000000 : 3C 3F 78 6D 6C 20 76 65-72 73 69 6F 6E 3D 22 31 : <?xml version="1
System.Net Verbose: 0 : [31664] 00000010 : 2E 30 22 20 65 6E 63 6F-64 69 6E 67 3D 22 75 74 : .0" encoding="ut
System.Net Verbose: 0 : [31664] 00000020 : 66 2D 38 22 3F 3E 3C 73-6F 61 70 3A 45 6E 76 65 : f-8"?><soap:Enve
System.Net Verbose: 0 : [31664] 00000030 : 6C 6F 70 65 20 78 6D 6C-6E 73 3A 73 6F 61 70 3D : lope xmlns:soap=
System.Net Verbose: 0 : [31664] 00000040 : 22 68 74 74 70 3A 2F 2F-73 63 68 65 6D 61 73 2E : "http://schemas.
System.Net Verbose: 0 : [31664] 00000050 : 78 6D 6C 73 6F 61 70 2E-6F 72 67 2F 73 6F 61 70 : xmlsoap.org/soap
System.Net Verbose: 0 : [31664] 00000060 : 2F 65 6E 76 65 6C 6F 70-65 2F 22 20 78 6D 6C 6E : /envelope/" xmln
System.Net Verbose: 0 : [31664] 00000070 : 73 3A 78 73 69 3D 22 68-74 74 70 3A 2F 2F 77 77 : s:xsi="http://ww
System.Net Verbose: 0 : [31664] 00000080 : 77 2E 77 33 2E 6F 72 67-2F 32 30 30 31 2F 58 4D : w.w3.org/2001/XM
System.Net Verbose: 0 : [31664] 00000090 : 4C 53 63 68 65 6D 61 2D-69 6E 73 74 61 6E 63 65 : LSchema-instance
System.Net Verbose: 0 : [31664] 000000A0 : 22 20 78 6D 6C 6E 73 3A-78 73 64 3D 22 68 74 74 : " xmlns:xsd="htt
System.Net Verbose: 0 : [31664] 000000B0 : 70 3A 2F 2F 77 77 77 2E-77 33 2E 6F 72 67 2F 32 : p://www.w3.org/2
System.Net Verbose: 0 : [31664] 000000C0 : 30 30 31 2F 58 4D 4C 53-63 68 65 6D 61 22 3E 3C : 001/XMLSchema"><
System.Net Verbose: 0 : [31664] 000000D0 : 73 6F 61 70 3A 42 6F 64-79 3E 3C 47 65 74 4B 53 : soap:Body><GetKS
System.Net Verbose: 0 : [31664] 000000E0 : 79 6E 74 61 78 52 65 73-70 6F 6E 73 65 20 78 6D : yntaxResponse xm
System.Net Verbose: 0 : [31664] 000000F0 : 6C 6E 73 3D 22 68 74 74-70 3A 2F 2F 74 65 6D 70 : lns="http://temp
System.Net Verbose: 0 : [31664] 00000100 : 75 72 69 2E 6F 72 67 2F-22 3E 3C 47 65 74 4B 53 : uri.org/"><GetKS
System.Net Verbose: 0 : [31664] 00000110 : 79 6E 74 61 78 52 65 73-75 6C 74 3E 33 30 E5 B9 : yntaxResult>30..
System.Net Verbose: 0 : [31664] 00000120 : B4 E5 BE 8C E3 81 AE E8-87 AA E5 88 86 E3 81 AF : ................
System.Net Verbose: 0 : [31664] 00000130 : E4 BD 95 E6 AD B3 E3 81-8B E3 81 AA E3 81 A8 E8 : ................
System.Net Verbose: 0 : [31664] 00000140 : 80 83 E3 81 88 E3 82 8B-E3 81 A8 37 35 E6 AD B3 : ...........75...
System.Net Verbose: 0 : [31664] 00000150 : E3 81 AA E3 82 93 E3 81-A7 E3 81 99 E3 81 AD 3C : ...............<
System.Net Verbose: 0 : [31664] 00000160 : 2F 47 65 74 4B 53 79 6E-74 61 78 52 65 73 75 6C : /GetKSyntaxResul
System.Net Verbose: 0 : [31664] 00000170 : 74 3E 3C 2F 47 65 74 4B-53 79 6E 74 61 78 52 65 : t></GetKSyntaxRe
System.Net Verbose: 0 : [31664] 00000180 : 73 70 6F 6E 73 65 3E 3C-2F 73 6F 61 70 3A 42 6F : sponse></soap:Bo
System.Net Verbose: 0 : [31664] 00000190 : 64 79 3E 3C 2F 73 6F 61-70 3A 45 6E 76 65 6C 6F : dy></soap:Envelo
System.Net Verbose: 0 : [31664] 000001A0 : 70 65 3E                                        : pe>

```

</details>

マルチバイト文字が表示されていないがデコードをすればよい。ProxyCodeを用いる以上基本的には例外情報から問題を特定することになるが、それでも特定できなかった場合の保険としてなので多少解読に手間がかかってしまうログであってもそこまで問題にならない。

## 参考サイト
- [dobon.net](https://dobon.net/vb/dotnet/programing/networktrace.html)
- [未確認飛行C](https://ufcpp.net/)
- [ホワイトペーパー](https://docs.microsoft.com/ja-jp/dotnet/csharp/)
- [.NET Framework source code](https://referencesource.microsoft.com/)