HttpClientを使ってNetSuiteのREST APIをコールする

以前紹介した、NetSuiteのREST APIをPostmanから投げたのと同じように、C#からリクエストを投げたいと考えましたが、実装できるまで苦労したので記録用に。

## 前提

* `HttpClient`を使ってリクエストを投げる
* .NET Framework 4.7.2を使用
* 認証に使うtokenなどはきちんと発行されている
* TBA認証

## 0. 必要な値の準備

今回はサンプルとして、以下の値を使います。
値は以下のサイトから。

https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534941088.html

|名前|値|
|--|--|
|URL|https://123456.suitetalk.api.netsuite.com/services/rest/record/v1/employee/40|
|HttpMethod |Get|
|TokenKey|2b0ce516420110bcbd36b69e99196d1b7f6de3c6234c5afb799b73d87569f5cc|
|TokenSecret|c29a677df7d5439a458c063654187e3d678d73aca8e3c9d8bea1478a3eb0d295|
|ConsumerKey|ef40afdd8abaac111b13825dd5e5e2ddddb44f86d5a0dd6dcf38c20aae6b67e4|
|ConsumerSecret|d26ad321a4b2f23b0741c8d38392ce01c3e23e109df6c96eac6d099e9ab9e8b5|
|SignatureMethod |HMAC-SHA256|
|Nonce (固定値ではない)|fjaLirsIcCGVZWzBX0pg|
|Timestamp (固定値ではない)|1508242306|
|Version|1.0|
|Realm (AccountId)|123456|

## 1. Signatureを作る

以下の資料を参考に、Signatureを作る処理をメソッドを実装していきます。

https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534941088.html

https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1534939551.html

### 1-1. メソッドを作る

引数で`HttpMethod`, `url`, `nonce`, `timestamp`を受け取ります。
必要に応じて引数は追加してください。

```csharp
public static string ComputeSignature(
  HttpMethod httpMethod, string url, string tokenKey, string tokenSecret,
  string consumerKey, string consumerSecret, string signatureMethod,
  string nonce, string timestamp, string version
)
{
  return "";
}
```

### 1-2. パラメータ無しのURLを取得

後で使うので、パラメータは取っておきます。
スキーマ (http、https) とホスト名は小文字である必要がありますが、今回は渡すときに小文字にするので処理は書いていません。

```csharp
var baseUrl = url;
var getParams = "";
if (url.Contains("?"))
{
  var index = url.IndexOf("?");
  baseUrl = url.Substring(0, index);
  getParams = url.Substring(index + 1);
}
```

### 1-3. パラメータを用意

まずは必須の値をリストに入れます。

```csharp
var paramList = new List<string>()
{
  "oauth_token=" + tokenKey,
  "oauth_consumer_key=" + consumerKey,
  "oauth_signature_method=" + signatureMethod,
  "oauth_nonce=" + nonce,
  "oauth_timestamp=" + timestamp,
  "oauth_version=" + version,
};
```

次に、URLのパラメータを`paramList`に追加していきます。

```csharp
if (!string.IsNullOrEmpty(getParams))
{
  foreach (var param in getParams.Split('&'))
  {
    var parsed = param.Split('=');
    if (!string.IsNullOrEmpty(parsed[0]))
    {
      var name = Uri.UnescapeDataString(parsed[0]);
      var value = parsed.Length >= 2 ? Uri.UnescapeDataString(parsed[1]) : "";
      if (paramList.Any(x => x.StartsWith(value + "=")))
      {
        var removeParam = paramList.Where(x => x.StartsWith(value + "=")).FirstOrDefault();
        paramList.Remove(removeParam);
      }
      paramList.Add(name + "=" + value);
    }
  }
}
```

パラメータを昇順に並び替え、`"&"`で`join`していきます。

```csharp
paramList.Sort();
var baseParams = string.Join("&", paramList);
```

### 1-4. Base Stringを作る

`HttpMethod`は大文字に、URLとParamはパーセントエンコーディングします。

```csharp
var baseString = httpMethod.ToString().ToUpper()
                 + "&" + Uri.UnescapeDataString(baseUrl)
                 + "&" + Uri.UnescapeDataString(baseParams);
```

baseStringには、以下の値が入っているはずです。
```
GET&https%3A%2F%2F123456.suitetalk.api.netsuite.com%2Fservices%2Frest%2Frecord%2Fv1%2Femployee%2F40&oauth_consumer_key%3Def40afdd8abaac111b13825dd5e5e2ddddb44f86d5a0dd6dcf38c20aae6b67e4%26oauth_nonce%3DfjaLirsIcCGVZWzBX0pg%26oauth_signature_method%3DHMAC-SHA256%26oauth_timestamp%3D1508242306%26oauth_token%3D2b0ce516420110bcbd36b69e99196d1b7f6de3c6234c5afb799b73d87569f5cc%26oauth_version%3D1.0
```

### 1-5. Signatureを作る

HMACで署名を作るためのキーを作成します。

```csharp
var key = Uri.UnescapeDataString(consumerSecret) + "&" + Uri.UnescapeDataString(tokenSecret);
```

続いて、HMAC-SHA256で署名を作成していきます。

```csharp
var signature = "";
var encording = new System.Text.ASCIIEncording();
using (var hmacsha256 = new HMACSHA256(encording.GetBytes(key)))
{
  byte[] hashBaseString = hmacsha256.ComputeHash(encording.GetBytes(baseString));
  signature = Convert.ToBase64String(hashBaseString);
}
```

Signatureができたので、最後に`return`してメソッドは完成です。

### 1-6. 完成したメソッド

```csharp
public static string ComputeSignature(
  HttpMethod httpMethod, string url, string tokenKey, string tokenSecret,
  string consumerKey, string consumerSecret, string signatureMethod,
  string nonce, string timestamp, string version
)
{
  // パラメータ無しのURLを取得
  var baseUrl = url;
  var getParams = "";
  if (url.Contains("?"))
  {
    var index = url.IndexOf("?");
    baseUrl = url.Substring(0, index);
    getParams = url.Substring(index + 1);
  }

  // 必須のパラメータを用意
  var paramList = new List<string>()
  {
    "oauth_token=" + tokenKey,
    "oauth_consumer_key=" + consumerKey,
    "oauth_signature_method=" + signatureMethod,
    "oauth_nonce=" + nonce,
    "oauth_timestamp=" + timestamp,
    "oauth_version=" + version,
  };

  // URLのパラメータをparamListに追加
  if (!string.IsNullOrEmpty(getParams))
  {
    foreach (var param in getParams.Split('&'))
    {
      var parsed = param.Split('=');
      if (!string.IsNullOrEmpty(parsed[0]))
      {
        var name = Uri.UnescapeDataString(parsed[0]);
        var value = parsed.Length >= 2 ? Uri.UnescapeDataString(parsed[1]) : "";
        if (paramList.Any(x => x.StartsWith(value + "=")))
        {
          var removeParam = paramList.Where(x => x.StartsWith(value + "=")).FirstOrDefault();
          paramList.Remove(removeParam);
        }
        paramList.Add(name + "=" + value);
      }
    }
  }

  // パラメータを昇順に並び替え、&でjoin
  paramList.Sort();
  var baseParams = string.Join("&", paramList);
  var baseString = httpMethod.ToString().ToUpper()
                   + "&" + Uri.EscapeDataString(baseUrl)
                   + "&" + Uri.EscapeDataString(baseParams);

  // HMAC-SHA256で署名を作成
  var key = Uri.UnescapeDataString(consumerSecret) + "&" + Uri.UnescapeDataString(tokenSecret);
  var signature = "";
  var encording = new System.Text.ASCIIEncording();
  using (var hmacsha256 = new HMACSHA256(encording.GetBytes(key)))
  {
    byte[] hashBaseString = hmacsha256.ComputeHash(encording.GetBytes(baseString));
    signature = Convert.ToBase64String(hashBaseString);
  }

  return signature;
}
```

## 2. リクエストのHeaderに詰めるAuthenticationを作る

Authenticationを作るためのメソッドを下記の通り作ります。

```csharp
public static AuthenticationHeaderValue GenerateAuthenticationHeader(
  HttpMethod httpMethod, string url
)
{
  var tokenKey = "2b0ce516420110bcbd36b69e99196d1b7f6de3c6234c5afb799b73d87569f5cc";
  var tokenSecret = "c29a677df7d5439a458c063654187e3d678d73aca8e3c9d8bea1478a3eb0d295";
  var consumerKey = "ef40afdd8abaac111b13825dd5e5e2ddddb44f86d5a0dd6dcf38c20aae6b67e4";
  var consumerSecret = "d26ad321a4b2f23b0741c8d38392ce01c3e23e109df6c96eac6d099e9ab9e8b5";
  var signatureMethod = "HMAC-SHA256";
  var nonce = "fjaLirsIcCGVZWzBX0pg"; // 本来は毎回違う値を用意
  var timestamp = "1508242306"; // 本来は毎回違う値を用意
  var version = "1.0";
  var realm = "123456";
  var signature = ComputeSignature(httpMethod, url, tokenKey, tokenSecret,
                                   consumerKey, consumerSecret, signatureMethod,
                                   nonce, timestamp, version);

  var authString = @"realm=""" + Uri.UnescapeDataString(realm) + @""", " +
                   @"oauth_token=""" + Uri.UnescapeDataString(tokenKey) + @""", " +
                   @"oauth_consumer_key=""" + Uri.UnescapeDataString(consumerKey) + @""", " +
                   @"oauth_nonce=""" + Uri.UnescapeDataString(nonce) + @""", " +
                   @"oauth_timestamp=""" + Uri.UnescapeDataString(timestamp) + @""", " +
                   @"oauth_signature_method=""" + Uri.UnescapeDataString(signatureMethod) + @""", " +
                   @"oauth_version=""" + Uri.UnescapeDataString(version) + @""", " +
                   @"oauth_signature=""" + Uri.UnescapeDataString(signature) + @"""";

  return new AuthenticationHeaderValue("OAuth", authString);
}
```

## 3. リクエストを投げる

※このままだと1ページ目しか取れないので、実際は`do-while`文などを使ってください。

```csharp
public static async Task<string> GetEmployeeAsync(string internalId)
{
  var url = "https://123456.suitetalk.api.netsuite.com/services/rest/record/v1/employee/" + internalId;
  var httpMethod = HttpMethod.Get;

  var content = "";
  using (var client = new HttpClient())
  {
    var request = neww HttpRequestMessage(httpMethod, url);
    request.Header.Authorization = GenerateAuthenticationHeader(httpMethod, url);
    var response = await client.SendAsync(request);
  
    content = await response.Content.ReadAsStringAsync();
  }

  return content;
} 
```

`content`にPostmanで見たような値が入っていれば成功です。