C#にNetSuiteのWebServiceを追加する

業務でNetSuiteのWebServiceをC#に追加することになったけど、英語のドキュメントばかりで大変だったので記録。
コードはコピペと手打ちなので、ミスがあればご指摘いただければと思います。

# 前提

* 認証方法はTBA(Token-Based Authentication)方式
* 認証に必要なAccount, Token id, Token secret, Consumer key, Consumer secretの発行などは正しくされているとする
* C#は.NET Framework 4.7.2

# 1. Postmanを使って確認

Postmanを使ってAPI叩けるかを確認します。
認証用のtokenとかが正しいかを確認できます。

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

### 1-1. サンプルアプリをダウンロード

以下のURLのAccountIDを入力し、サンプルアプリをダウンロードします。

https://\<accountID\>.app.netsuite.com/app/external/integration/integrationDownloadPage.nl

### 1-2. Postmanにサンプルアプリをインポート

Postmanはインストールしなくても使えるので、ブラウザで進めます。
左上の「import」からインポートできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/9da5ccb0-8802-94a3-2ca3-a4b197a98502.png)
インポートできると「NetSuite REST API Tutorial」のコレクションが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/4bd5da02-48d9-d797-3b9f-2a67ca19ed96.png)


### 1-3. 認証情報を追加

追加された「NetSuite REST API Tutorial」をクリックし、Authorizationを編集します。
画像と同じものを選択しつつ、Account, Token id, Token secret, Consumer key, Consumer secretを設定していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/2234c616-7f7b-0087-b8b3-0eacee932a56.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/4035ea21-8aa1-693f-fa6f-f471aa0f9cdf.png)

オレンジ色の「{{xxx}}」は変数です。
「Variables」タブに切り替えれば、変数名の宣言と値の設定が可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/8b9eed22-af65-bade-5ade-cd767316f4fb.png)

### 1-4. TBA認証で通過できるかを確認する

「0 Test」の「Example 0.1: Test Request」を開きます。
「REST_SERVISE」変数に「https://<AccountID>.suitetalk.api.netsuite.com/services/rest」を入れ、Sendを押します。
200番台のレスポンスが返ってくれば認証は通過できています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/fa85d9cb-858c-bb35-0195-2ec794e725d3.png)

# 2. .NETのサンプルアプリを実行

公式が用意している.NET用のサンプルアプリをダウンロードして、実行してみます。

### 2-1. .NETのサンプルアプリをダウンロードする

以下のサイトからサンプルアプリをダウンロードします。
リンクをクリックしてもダウンロードできないので、リンクをコピーしてから、ブラウザのアドレスバーに直接貼り付けてください。

https://www.netsuite.com/portal/developers/resources/suitetalk-sample-applications.shtml

### 2-2. サンプルアプリを修正する

サンプルアプリを実行するため、2つのファイルを修正します。

```config:App.config
// 省略
  <appSettings>
    <!-- If set to false, login parameter values will be used from below. If set
         to true, the user will be prompted for values.  -->
    <add key="promptForLogin" value="false"/>
    <!-- Login field values -->
    <add key="login.email" value="EMAIL"/>
    <add key="login.password" value="PASSWORD"/>
    <add key="login.roleNSkey" value="3"/>
    <add key="login.acct" value="ACCOUNT"/>
    <add key="login.appId" value="8CA80836-4422-4A14-B91D-B386AE9815FD"/>
    <add key="login.useTba" value="false"/>
    <add key="login.tbaConsumerKey" value="CONSUMERKEY"/>
    <add key="login.tbaConsumerSecret" value="CONSUMERSECRET"/>
    <add key="login.tbaTokenId" value="TOKENID"/>
    <add key="login.tbaTokenSecret" value="TOKENSECRET"/>
  </appSettings>
// 省略
```
login.useTbaをtrueにし、ACCOUNT, CONSUMERKEY, CONSUMERSECRET, TOKENID, TOKENSECRETを指定します。
TBA認証を使うので、その他の項目は変更しなくて大丈夫です。

```cs:NSClient.cs
namespace NSClient
{
    public class NSClient
    {
        // 省略

		private TokenPassportSignature ComputeSignature(string compId, string consumerKey, string consumerSecret,  
										string tokenId, string tokenSecret, string nonce, long timestamp)
		{
			string baseString = compId + "&" + consumerKey + "&" + tokenId + "&" + nonce + "&" + timestamp;
			string key = consumerSecret + "&" + tokenSecret;
			string signature = "";
			var encoding = new System.Text.ASCIIEncoding();
			byte[] keyBytes = encoding.GetBytes(key);
			byte[] baseStringBytes = encoding.GetBytes(baseString);
            // HMAC-SHA1 -> HMAC-SHA256 に変更
			using (var hmacSha256 = new HMACSHA256(keyBytes))
			{
				byte[] hashBaseString = hmacSha1.ComputeHash(baseStringBytes);
				signature = Convert.ToBase64String(hashBaseString);
			}
			TokenPassportSignature sign = new TokenPassportSignature();
            // ここも HMAC-SHA1 -> HMAC-SHA256 に変更
			sign.algorithm = "HMAC-SHA256";
			sign.Value = signature;
			return sign;
		}

        // 省略
    }
}
```

### 2-3. サンプルアプリを実行する

VS 2022を管理者として実行し、NSClient.slnを開き、実行してみます。
カスタマ取得をしてみます。
Internal IDが一致すると、画像のように値を取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/457ee514-7b13-a213-1289-0d9d8f441c49.png)

# 3. C#アプリにNetSuiteのWebServiceを追加

サンプルアプリを参考に、C#アプリに必要な処理を移植していきます。

### 3-1. サービス参照からWebServiceを追加する

ソリューションを右クリックし、「追加 > サービス参照」をクリックします。
次に左下の「詳細設計」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/444364e0-1d8e-8f45-9e2d-9b40f883164a.png)
「Web参照の追加」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/7e18e90d-bb5e-b6a2-9e09-4a799fc5ee48.png)
URLに「https://webservices.netsuite.com/wsdl/v2021_2_0/netsuite.wsdl 」を入力し、「参照の追加」をクリックして追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/6ff578cb-e9b4-beb0-b20c-802d005ca6ab.png)


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

次にTBA方式の認証をするにあたって必要なコードをサンプルアプリから移植。
とりあえずNSClient.csファイルを作成します。

```c#:NSClient.cs
using xxx.com.netsuite.WebService;
// 省略

namespace xxx.yyy
{
    public class NSClient
    {
        private NetSuiteService _service;
        
        public NetSuiteService Service {
            get
            {
                _service.tokenPassport = CreateTokenPassport();
                return _service;
            }
        }
        
        public bool IsAuthenticated { get; private set; }
        
        /// <summary>
        /// Constructor
        /// </summary>
        public NSClient()
        {
            IsAuthenticated = false;
            _service = new DataCenterAwareNetSuiteService();
            _service.Timeout = 1000 * 60 * 60 * 2;
        }
    
        public TokenPassport CreateTokenPassport()
        {
            string account = "Your Account";
            string consumerKey = "Your ConsumerKey";
            string consumerSecret = "Your ConsumerSecret";
            string tokenId = "Your TokenId";
            string tokenSecret = "Your TokenSecret";
                        
            string nonce = ComputeNonce();
            long timestamp = ComputeTimestamp();
            TokenPassportSignature signature = ComputeSignature(account, consumerKey, consumerSecret, tokenId, tokenSecret, nonce, timestamp);
    
            TokenPassport tokenPassport = new TokenPassport();
            tokenPassport.account = account;
            tokenPassport.consumerKey = consumerKey;
            tokenPassport.token = tokenId;
            tokenPassport.nonce = nonce;
            tokenPassport.timestamp = timestamp;
            tokenPassport.signature = signature;
            return tokenPassport;
        }
    
        private string ComputeNonce()
        {
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            byte[] data = new byte[20];
            rng.GetBytes(data);
            int value = Math.Abs(BitConverter.ToInt32(data, 0));
            return value.ToString();
        }
    
        private long ComputeTimestamp()
        {
            return ((long) (DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds);
        }

        private TokenPassportSignature ComputeSignature(string compId, string consumerKey, string consumerSecret,  
										string tokenId, string tokenSecret, string nonce, long timestamp)
		{
			string baseString = compId + "&" + consumerKey + "&" + tokenId + "&" + nonce + "&" + timestamp;
			string key = consumerSecret + "&" + tokenSecret;
			string signature = "";
			var encoding = new System.Text.ASCIIEncoding();
			byte[] keyBytes = encoding.GetBytes(key);
			byte[] baseStringBytes = encoding.GetBytes(baseString);
			using (var hmacSha256 = new HMACSHA256(keyBytes))
			{
				byte[] hashBaseString = hmacSha1.ComputeHash(baseStringBytes);
				signature = Convert.ToBase64String(hashBaseString);
			}
			TokenPassportSignature sign = new TokenPassportSignature();
			sign.algorithm = "HMAC-SHA256";
			sign.Value = signature;
			return sign;
		}

        public String GetStatusDetails(Status status)
		{
			System.Text.StringBuilder sb = new System.Text.StringBuilder();
			for (int i=0; i < status.statusDetail.Length; i++)
			{
				sb.Append( "[Code=" + status.statusDetail[i].code + "] " + status.statusDetail[i].message + "\n" );
			}
			return sb.ToString();
		}
    }

    class DataCenterAwareNetSuiteService : NetSuiteService
    {
        private System.Uri OriginalUri;

        public DataCenterAwareNetSuiteService()
            : base()
        {
            OriginalUri = new System.Uri(this.Url);
            string account = "Your Account";
            DataCenterUrls urls = getDataCenterUrls(account).dataCenterUrls;
            Uri dataCenterUri = new Uri(urls.webservicesDomain + OriginalUri.PathAndQuery);
            this.Url = dataCenterUri.ToString();
        }
    }

    class NSBase
    {
        protected static NSClient _client;

        public static NSClient Client
        {
            get
            {
                if (_client == null)
                {
                    _client = new NSClient();
                }
                return _client;
            }
            set
            {
                _client = value;
            }
        }
    }

    // NSEntity.csから移植
    class NSEntity : NSBase
    {
        // internalIdが一致するCustomerを一件取得
        public static string GetCustomer(string internalId)
        {
            RecordRef recordRef = new RecordRef();
            recordRef.internalId = internalId;
            // recordRef.type：テーブルの指定
            recordRef.type = RecordType.customer;
            recordRef.typeSpecified = true;

            ReadResponse response = Client.Service.get(recordRef);

            Customer customer = (Customer)response.record;
            ProcessCustomerReadResponse(response, customer);
        }

        // internalIdが一致するCustomerを全権取得
        public static List<string> getCustomerList(string[] internalIds)
        {
            RecordRef[] recordRef = new RecordRef[internalIds.Length];
            for (int i = 0; i < internalIds.Length; i++)
            {
                RecordRef recordRef = new RecordRef();
                recordRef.internalId = internalIds[i];
                recordRef.type = RecordType.customer;
                recordRef.typeSpecified = true;
            }
            ReadResponse[] getResponse = Client.Service.getList(recordRefs).readResponse;

            var returnStringList = new List<string>();
            for (int i = 0; i < getResponse.Length; i++ )
            {
                returnStringList.Add(ProcessCustomerReadResponse(getResponse[i], internalIds[i]));
            }
            return returnStringList;
        }

        private static bool ProcessCustomerReadResponse(ReadResponse response, Customer customer)
        {
            // Process the response
            if (response.status.isSuccess)
            {
                return (
                    "    internalId=" + customer.internalId +
                    "\n    entityId=" + customer.entityId +
                    (customer.companyName == null ? "" : ("\n    companyName=" + customer.companyName)) +
                    (customer.entityStatus == null ? "" : ("\n    status=" + customer.entityStatus.name)) +
                    (customer.email == null ? "" : ("\n    email=" + customer.email)) +
                    (customer.phone == null ? "" : ("\n    phone=" + customer.phone)) +
                    "\n    isInactive=" + customer.isInactive +
                    (!customer.dateCreatedSpecified ? "" : ("\n    dateCreated=" + customer.dateCreated.ToShortDateString())));
            }
            else
            {
                return Client.GetStatusDetails(response.status);
            }
        }
    }
}
```

あとは「NSEntity.GetCustomer(internalId)」のように呼べばOK

# 4. その他

### 4-1. WebServiceのリリース状況について

WebServiceのリリース状況に関しては、documentationタグの値を確認してください。
![2021のWebService.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/277fdf2e-7b79-e239-3f2f-5acedb3b8fe9.png)
![2022のWebService.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495159/b744cc9d-f037-9e80-443a-16c10a0392f7.png)

# 参考

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

https://www.netsuite.com/portal/developers/resources/suitetalk-sample-applications.shtml

https://followingnetsuite.com/2018/09/21/suitetalk-tba-example-in-c/

https://followingnetsuite.com/2019/05/02/suitetalk-when-to-switch-endpoints/

https://followingnetsuite.com/2021/06/17/suitetalk-2021-2-deprecating-hmac-sha1/