UiPath から SAP S/4HANA の OData を呼び出す - その３ 問題解決編

## はじめに
[前回の記事](https://qiita.com/MasahiroHarada1105/items/31e7d017082cf7ef2c73)では、``HTTP 要求`` アクティビティを使って S/4HANA の OData API の呼び出し方法について説明しました。本記事では、前回の実装の問題点および改善方法について掘り下げていこうと思います。

## 何が問題なのか？
``HTTP 要求`` アクティビティの Cookie プロパティに **セッション名をハードコーディングしています。** セッション名は ``SAP_SESSIONID_<sid>_<client>`` の形式で、 SID と クライアント ID が変わるとセッション 名も変わります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/b200397e-bce2-14be-333f-91b6e0dddf97.png)

本来であれば、セッション ID 名を変数化したいところですが、``HTTP 要求`` アクティビティ v1.18.0 時点では、 Cookie プロパティの  **Cookie 名に変数を指定することができません。**

そのため、開発環境で動作確認したワークフローを本番環境で動かすには、ハードコーディングされているセッション名の変更のために、**ワークフローの修正が必要になります。**

:::note warn
HTTP ヘッダーに``Cookie : {セッション名}={セッションID}`` として直接 Cookie を設定したいところですが、残念ながら ``HTTP 要求`` アクティビティではこの形式をサポートしていないようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/de260874-dc3f-dc1f-f1bf-642446d94652.png)
:::

## 改善してみる
今回は上記問題の改善方法を 2 つご紹介します。

### 1. 条件分岐を使う
``条件分岐 (switch)`` アクティビティにより、クライアント ID 毎に ``HTTP 要求`` アクティビティを用意する方法です。OData API の呼び出し箇所が少ない場合に有効です。

* クライアント ID = 804 の場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/fa158a66-fcad-71cb-7134-9dd541818423.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/94d504cc-81d8-0aca-49d6-9c80a0d32f9b.png)

* クライアント ID = 100 の場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/5c1f75ca-4863-38a7-99d8-0906b6c98ee7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/7045060e-1477-1d78-119c-2eb4e1325135.png)

実装が容易である一方で、OData API 呼び出し箇所が多いと、**クライアント ID 数分 ``HTTP 要求`` アクティビティを複製していくことになるため修正漏れのリスクあるかと思います。**

### 2. HTTP 要求アクティビティを使わない
``HTTP 要求`` アクティビティを使う代わりに、``コードを呼び出し`` アクティビティやコード化されたワークフローを使って、HTTP 通信処理を VB.NET または C# のコードで記述する方法です。

今回は、コード化されたワークフローを使っての実現方法について説明します。コード化されたワークフローについては別記事で紹介しています。よろしければご確認ください。

https://qiita.com/MasahiroHarada1105/items/fa4bf9e96339d0054a5c

#### コード化されたワークフローの作成
UiPath Studio でプロジェクトを作成したら、デザインリボンの ``新規`` から ``コード化されたワークフロー`` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/f4056875-e07b-0677-7445-5332156534b3.png)

作成したコード化されたワークフロー「HTTP呼び出し.cs」のコードはこちらになります。プロジェクト名を 「SAP OData 呼び出しサンプル」としているので、ネームスペースが ``SAPOData呼び出しサンプル`` となっています。コピーする場合はプロジェクト名に合わせてネームスペースを変更してください。

````cs:HTTP呼び出し.cs
using SAPOData呼び出しサンプル.ObjectRepository;
using System;
using System.Collections.Generic;
using System.Data;
using UiPath.CodedWorkflows;
using UiPath.Core;
using UiPath.Core.Activities.Storage;
using UiPath.Orchestrator.Client.Models;
using UiPath.Testing;
using UiPath.Testing.Activities.TestData;
using UiPath.Testing.Activities.TestDataQueues.Enums;
using UiPath.Testing.Enums;
using UiPath.UIAutomationNext.API.Contracts;
using UiPath.UIAutomationNext.API.Models;
using UiPath.UIAutomationNext.Enums;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Newtonsoft.Json;

namespace SAPOData呼び出しサンプル
{
    public class HTTP呼び出し : CodedWorkflow
    {
        private static readonly HttpClient httpClient;
        
        static HTTP呼び出し()
        {
            httpClient = new HttpClient(new HttpClientHandler() {UseCookies = true});
        }

        [Workflow]
        public (string tokenValue, int statusCode, string responseData)
            Execute(
                string uri,
                string httpMethod,
                string clientId,
                string tokenValue,
                Dictionary<string, Object> payload,
                string username,
                string password)
        {
            var request = new HttpRequestMessage(new HttpMethod(httpMethod), uri);

            // HTTP リクエストヘッダーの設定
            var headers = request.Headers;
            headers.Add("sap-client", clientId);
            headers.Add("accept", "application/json");

            // 引数 tokenValue に値が設定されていない場合は、x-csrf-token ヘッダの値に Fetch を付与する
            headers.Add("x-csrf-token", string.IsNullOrEmpty(tokenValue) ? "Fetch" : tokenValue);

            // 引数 username に値が設定されている場合は、リクエストヘッダーに BASIC 認証の設定をする
            if (!string.IsNullOrEmpty(username))
            {
            	headers.Authorization = new AuthenticationHeaderValue(
                    "Basic",
                    Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", username, password))));
            }

            // 引数 payload に値が設定されている場合は、リクエストに JSON 文字列として付与する
            if (payload != null && payload.Count != 0)
            {
            	request.Content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json");
            }

            // リクエスト送信
            var response = httpClient.SendAsync(request).Result;

            var statusCode = (int) response.StatusCode;
            var responseData = response.Content.ReadAsStringAsync().Result;

            // レスポンスヘッダーに x-csrf-token が含まれている場合は、その値を取得する
            if (response.Headers.Contains("x-csrf-token"))
            {
                // "response.Headers.GetValues("x-csrf-token").First()" としたかったけどコンパイルエラーとなったので ↓ で代用
                var values = response.Headers.GetValues("x-csrf-token").GetEnumerator();
                while (values.MoveNext())
                {
                    tokenValue = values.Current;
                }
            }

            return (tokenValue, statusCode, responseData);
        }
    }
}

````

:::note info
**引数について**
コード化されたワークフローでは、引数のタイプに応じて定義方法が下記のようになります。

**入力引数**：Execute メソッドの引数として定義
→ uri, httpMethod, clientId, payload, username, password が該当

**出力引数**：Execute メソッドの前に戻り値として定義 <sup>(※)</sup>
→ statusCode, responseData が該当

**入力・出力引数**：上記両方を定義
→ tokenValue が該当

注）出力引数が 1つの場合は、型のみを記述して変数名は ``Output`` で固定となります。
:::

``HttpClient`` のインスタンスを static として定義しており、プロセス実行中は使いまわすようにしています。それにより、Cookie の取り扱いを ``HttpClient`` 任せにすることで、 **SAP_SESSIONID を意識しない実装となっています。**

````cs
private static readonly HttpClient httpClient;

static HTTP呼び出し()
{
    httpClient = new HttpClient(new HttpClientHandler() {UseCookies = true});
}
````

#### 呼び出し側のワークフローを修正する
修正後のワークフローはこちらになります。呼び出し側のワークフローで CSRF トークンやセッションに関する記述がなくなったことで、業務処理の流れに集中することが出来るようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/1032e5c5-6869-62d6-29d7-6f1b4c1feedd.png)

処理内容変更により以下の変数は使用しなくなりましたので削除ください。
<table>
  <thead>
    <tr>
      <th>変数</th><th>型</th>
    </tr>
  </thead>
  <tr>
    <td>disResHeader</td>
    <td>Dictionary<Of String, String></td>
  </tr>
  <tr>
    <td>strSapSessionId</td>
    <td>String</td>
  </tr>
  <tr>
    <td>strCsrfToken</td>
    <td>String</td>
  </tr>
  <tr>
    <td>regexResult</td>
    <td>IEnumerable&lt;Match&gt;</td>
  </tr>
  <tr>
    <td>jsonResContent</td>
    <td>JObject</td>
  </tr>
</table>

大きな変更点としては、``HTTP 呼び出し`` アクティビティの代わりに、``ワークフロー ファイルを呼び出し`` アクティビティで「HTTP呼び出し.cs」を呼び出しているところです。

:::note warn
2024/3/26 時点では、Windows プロジェクトで作成されたコード化されたワークフローの引数を「引数をインポート」で自動的にインポートすることが出来ません。引数は手動で定義する必要があります。

[入力引数と出力引数を使用する](https://docs.uipath.com/ja/studio/standalone/2024.4/user-guide/working-with-input-and-output-parameters)
:::

こちらは、セッション ID と CSRF トークン取得用の呼び出し (GET /A_PurchaseHistory?$metadata) 部分です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/e20d7866-d901-857a-200d-6000f76976cc.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/114eaf39-3175-2618-637f-d150f9d07d6f.png)

続いて、購買発注の呼び出し (POST /A_PurchaseHistory) 部分です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/d8328d61-cd93-7f47-6a56-b53a8e29e356.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3322296/f3738055-e9f2-527c-fc22-ac923c5f3872.png)

## おわりに
OData API のご利用規模に応じてどちらの改善策を適用するかご検討いただければと思います。個人的には、コード化されたワークフローで HTTP 呼び出し処理を実装してしまったほうが Cookie の取り扱いから解放され結果的に全体がシンプルになるのではと考えています。

UiPath から SAP S/4HANA の OData を呼び出すシリーズ

https://qiita.com/MasahiroHarada1105/items/f3d92f7d54de40b67f47

https://qiita.com/MasahiroHarada1105/items/31e7d017082cf7ef2c73