ODCからSOAP APIを呼ぶ方法

ODCでは、Consume/Expose SOAP Web Serviceの作成ができない（OutSystems 11ではできた）。
Exposeの方は無理だが、ConsumeならExternal Logicを使った代替案がある。
[Supporting SOAP in ODC](https://success.outsystems.com/documentation/outsystems_developer_cloud/building_apps/extend_your_apps_with_external_logic_using_custom_code/supporting_soap_in_odc/)に手順が載っているので、ほぼそのままになるが以下で実地に確認してみる。

# 手順サマリ
1. SOAPのAPIからWSDLファイルを取得する
1. dotnet-svcutilでWSDLファイルからSOAPサービスのクライアントコードを生成
1. External Logicプロジェクトを用意
1. 用意したクライアントコードをプロジェクト下に移動し、利用コードを書く
1. External Logicをアップロードして利用する

# 環境情報
## ODC
ODC Studio(Version 1.4.13)
Visual Studio Code (Version 1.88.1)
## OutSystems 11 (SOAPサービス作成用）
Personal Environment(Version 11.27.0 (Build 42818))
Service Studio(Version 11.54.64)

# SOAPのサービスを用意する
## OutSystems 11のExpose SOAP Web Serviceを使う
Service StudioでOutSystems 11環境に接続し、「Expose SOAP Web Service」を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/4b4e4a4d-85e7-c170-c09d-962cd78e67e7.png)

作成されたSOAP Web Serviceを右クリックし、「Add Web Service Method」を選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/6b27866d-9892-21da-d550-773b65bc48dc.png)

作成されたWeb Service Methodは普通のActionと同じように実装すればよい。

## 作成例
整数値を2つ受取り、それを足し合わせた結果を返す単純なCalculateSum Serviceを以下の通り実装する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/da9851c8-7f95-e744-613a-d2a4c8f97096.png)

内部にはAssignを1つだけ起き、以下のように設定した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/5aad147a-b652-e382-2f3d-a49ef093df11.png)

## WSDLファイルを取得する
SOAPのサービスには通常、WSDLというサービスを定義するファイルがある。
このファイルがあると、.NETのツールでSOAPを利用するためのクライアントコードを自動生成できる。

OutSystems 11の場合、作成した「SOAP Web Service」を右クリックし、「Open Documentation」でドキュメントページを開き、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/0faa3319-87ed-fd25-940f-8b2c50769da3.png)

「Service Description」のリンク先から取得できる。リンク先をファイルに保存しておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/3fe63236-2f6f-c2e6-93bb-cfc568a2729f.png)


# SOAPのサービスをODCから呼ぶ
## External Logicのプロジェクトを用意
[External LogicをVisual Studio Codeで作成する > コマンドサマリ](https://qiita.com/jyunji_watanabe/items/04ffd0bc25ed4e845661#%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%B5%E3%83%9E%E3%83%AA)に書いたコマンドで、SoapConsumeTestというプロジェクトをQiitaSamplesソリューションの下に用意する。

``` :コマンド例（ソリューションを配置するフォルダで実行）
dotnet new sln -o QiitaSamples
cd .\QiitaSamples\
dotnet new classlib -o SoapConsumeSample --framework net6.0
dotnet sln add .\SoapConsumeSample\SoapConsumeSample.csproj
cd .\SoapConsumeSample\
dotnet add package OutSystems.ExternalLibraries.SDK
```

## SOAPを利用するClassを用意する
### dotnet-svcutilツールをインストール
ドキュメントは[.NET Core 用 WCF dotnet-svcutil ツール](https://learn.microsoft.com/ja-jp/dotnet/core/additional-tools/dotnet-svcutil-guide?tabs=dotnetsvcutil2x)

SOAPのメタデータを提供するWSDLファイルから、C#のクライアントコードを生成するツール。

``` :コマンド例
dotnet tool install --global dotnet-svcutil
次のコマンドを使用してツールを呼び出せます。dotnet-svcutil
ツール 'dotnet-svcutil' (バージョン '2.1.0') が正常にインストールされました。
```

### dotnet-svcutilコマンドでSOAPサービスクライアントコードを生成する
保存しておいたWSDLファイルをパラメータにdotnet-svcutilコマンドを実行すると、実行したパスにServiceReferenceフォルダが作成され、その下にSOAPサービスのクライアントコードがReference.csという名前で作成される。
``` :コマンド例
dotnet-svcutil .\WebService1.xml
Microsoft (R) WCF Service Model Proxy Generation Tool for .Net Core platform
[Microsoft.Tools.ServiceModel.Svcutil、バージョン 2.1.0]
Copyright (c) Microsoft Corporation.  All rights reserved.

このツールによって、ツールの改善を目的として使用法に関する情報が収集されます。この機能は、環境変数 "DOTNET_SVCUTIL_TELEMETRY_OPTOUT" を 1 に設定して無効にできます。

'C:\work\OutSystems\ODC\ExternalLogicTest\WebService1.xml' からメタデータを読み込もうとしています。
ファイルを生成しています...
C:\work\OutSystems\ODC\ExternalLogicTest\ServiceReference\Reference.cs
```

SOAPサービスクライアントはC#のファイルなので、プロジェクトの下に配置したい。
そこで、生成されたServiceReferenceフォルダをプロジェクトフォルダの下に移動しておく。

:::note info
WSDLファイルは、URLで指定することもできる
``` :コマンド例 (OutSystems 11のExpose SOAP Web Serviceが対象の場合）
dotnet-svcutil https://<ホスト>/<モジュール>/<SOAP Web Service>.asmx?WSDL
```
:::

## External Logicの実装
### Interface定義例
属性の意味や書き方については参考リンクを参照。
``` cs:ISoapConsumeSample.cs
using OutSystems.ExternalLibraries.SDK;

namespace SoapConsumeSample;
[OSInterface(Description = "ODCでSOAPのサービスを利用するサンプル")]
public interface ISoapConsumeSample
{
    [OSAction(Description = "受け取った２つの値の合計を返す")]
    public int CalculateSum(int Num1, int Num2);
}

```

### クライアントを利用するのに必要なNuGetパッケージを追加する
VSCode上でTerminalを開き、以下のコマンドを入力する。
--Versionの指定をしないと、External Logicで使う.NET 6.0と互換性のないバージョンがインストールされてしまう。

``` :コマンド例
dotnet add package System.ServiceModel.Primitives --version 6.2.0
dotnet add package System.ServiceModel.Http --version 6.2.0
```

### Class定義例
↑で定義したinterfaceを実装するClassを用意する。

``` cs:SoapConsumeSample.cs
using OutSystems.ExternalLibraries.SDK;
using System.ServiceModel;
using System.Xml;

namespace SoapConsumeSample;

public class SoapConsumeSample : ISoapConsumeSample
{
    // 1
    #region ドキュメントの "Call the SOAP web service" 1と2で指定されていたコード（一部改変）
    private static readonly string ENDPOINTADDRESS = "<SOAP Web ServiceのURL (.asmxで終わる) ";
    private static readonly int TIMEOUT = 30;
    private readonly BasicHttpBinding _binding;
    private readonly EndpointAddress _address;
    public SoapConsumeSample()
    {
        _binding = new BasicHttpBinding
        {
            SendTimeout = TimeSpan.FromSeconds(TIMEOUT),
            MaxBufferSize = int.MaxValue,
            MaxReceivedMessageSize = int.MaxValue,
            AllowCookies = true,
            ReaderQuotas = XmlDictionaryReaderQuotas.Max,
            Security = new BasicHttpSecurity
            {
                Mode = BasicHttpSecurityMode.Transport
            }
        };
        _address = new EndpointAddress(ENDPOINTADDRESS);
    }
    #endregion

    // 3
    public int CalculateSum(int Num1, int Num2)
    {
        return CalculateSumAsync(Num1, Num2).Result.Sum;
    }

    // 2
    private async Task<CalculateSumResponse> CalculateSumAsync(int Num1, int Num2)
    {
        // dotnet-svcutil で作成したClass <SOAP Web Service名>SoapClientを使って非同期にSOAPサービスを呼び出し
        using (var proxy = new WebService1SoapClient(_binding, _address))
        {
            // awaitをつけることで、CalculateSumAsyncの非同期呼び出しが終わるまでこのメソッドを止めている
            var result = await proxy.CalculateSumAsync(Num1, Num2);
            return result;
        }
    }
}
```

1. ドキュメントで指定されているメンバ変数とコンストラクタ
この部分は基本的にはそのままコピーでよい。ENDPOINTADDRESS変数の値だけは実際のURLに変更する必要がある。

2. SOAPサービスを直接呼ぶAsyncメソッド
dotnet-svcutilが生成するコードは非同期なので、接続先のWeb Service Method1つに対して1つ非同期のメソッド（asyncキーワードを付与し、Taskを返すメソッド）を作成する。
なお戻り値のTask<>の「<>」内は、呼び出すメソッドの戻り値として使われているものを指定する。
Reference.csのメソッドが以下のシグネチャであれば、Task<CalculateSumResponse>を使う。
「public System.Threading.Tasks.Task<CalculateSumResponse> CalculateSumAsync(int Num1, int Num2)」

3. Action提供用メソッド
External LogicのActionに対する実装。
2.のメソッドを内部で呼び、その結果を返す。
非同期のメソッドを同期的に返すため、2.の戻り値に対し.Resultを適用している。

## 動作確認
### PublishしてODC Portalに登録する
この部分は参考リンクの手順を参照。

### ODCのAppから呼んでみた結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/1794daf5-bf1b-c668-6533-c93d4cc853df.png)

### OutSystems 11側のIntegration Logsで確認
Service CenterでMonitoring > IntegrationsでREST/SOAP実行ログを見られる。
確かに呼び出しされている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/4e3f631f-66e1-77dd-beb2-5aa36d187f5a.png)

# 参考リンク
External Logicの作成方法一般については以下を参照：

https://qiita.com/jyunji_watanabe/items/2698fb5a80b857875a48

Visual Studio Codeを使ってExternal Logic向けのC#プロジェクトを作る手順については以下を参照：

https://qiita.com/jyunji_watanabe/items/04ffd0bc25ed4e845661#%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%B5%E3%83%9E%E3%83%AA

OutSystemsの公式手順ドキュメント：

https://success.outsystems.com/documentation/outsystems_developer_cloud/building_apps/extend_your_apps_with_external_logic_using_custom_code/supporting_soap_in_odc/