VisualStudio2017 で SalesForce の SOAP API を叩く(Enterprise版)

# やりたいこと

C# で SOAPAPI を利用して SalesForce 上のオブジェクトを操作
公式ドキュメントは[こちら](https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_quickstart_intro.htm)

## Enterprise 版

- 強い型付け
- typo に強い
- カスタムオブジェクトや項目が追加・更新された場合は再生成が必要

Partner 版の記事は[こちら](https://qiita.com/haserror/items/498f54d4cf5bae2e8c55)

# 事前準備

## WSDL 追加

### ブラウザでやること

・SalesForce 設定画面の左メニュー「ビルド - 開発 - API」で `Enterprise WSDL の作成`
・XMLが表示されたURLをコピー
※VSのweb参照追加画面からやるとJSエラーで表示できないため

### VisualStudio でやること

#### 参照追加
・ソリューションエクスプローラーで[追加]→[サービス参照]→[詳細設定]→[Web参照の追加]
・コピーしたURLを貼り付けるとSalseForceログイン画面が表示される
・ログインするとWSDLが表示されるので、適当なweb参照名(今回はsf_test)を付けて追加

#### Reference.cs修正

なんかそのままだとエラーが出る

``` :エラー
System.InvalidOperationException
  HResult=0x80131509
  Message=一時クラスを生成できません (result=1)。
error CS0030: 型 'SFAPI.sf_test.ListViewRecordColumn[]' を型 'SFAPI.sf_test.ListViewRecordColumn' に変換できません。
error CS0030: 型 'SFAPI.sf_test.ListViewRecordColumn[]' を型 'SFAPI.sf_test.ListViewRecordColumn' に変換できません。
error CS0029: 型 'SFAPI.sf_test.ListViewRecordColumn' を型 'SFAPI.sf_test.ListViewRecordColumn[]' に暗黙的に変換できません。
error CS0029: 型 'SFAPI.sf_test.ListViewRecordColumn' を型 'SFAPI.sf_test.ListViewRecordColumn[]' に暗黙的に変換できません。
```


`Reference.cs`内の`ListViewRecordColumn`に余計な`[]`が付いてるっぽいので削除(2箇所)


``` C#:Reference.cs
//private ListViewRecordColumn[][] recordsField;    // 削除
private ListViewRecordColumn[] recordsField;        // 追加
```
``` C#:Reference.cs
/// <remarks/>
[System.Xml.Serialization.XmlArrayItemAttribute("columns", typeof(ListViewRecordColumn), IsNullable=false)]
//public ListViewRecordColumn[][] records { // 削除
public ListViewRecordColumn[] records {     // 追加
    get {
        return this.recordsField;
    }
    set {
        this.recordsField = value;
    }
}
```

# 実装

## ログイン/ログアウト

何はともあれログイン処理
テスト用に適当なWindowsフォームで実装
本来はログイン→なんらかのAPI処理→ログアウトまでをセットにした方がよさげ

``` C#:Form1.cs
using SFAPI.sf_test;
using System;
using System.Web.Services.Protocols;
using System.Windows.Forms;

namespace SFAPI
{
    public partial class Form1 : Form
    {
        // bindingオブジェクト
        private SforceService binding;

        public Form1()
        {
            InitializeComponent();

            // 初期化
            binding = new SforceService();
            binding.Timeout = 30000;
        }

        /// <summary>
        /// ログインボタン
        /// </summary>
        private void LoginBtn_Click(object sender, EventArgs e)
        {
            Login();
        }

        /// <summary>
        /// ログイン処理
        /// </summary>
        /// <returns></returns>
        private bool Login()
        {
            string username = "SalesForceのログインID";
            string password = "SalesForceのパスワード";

            LoginResult loginResult;
            try
            {
                loginResult = binding.login(username, password);
            }
            catch (SoapException e)
            {
                // 失敗
                MessageBox.Show(e.Code + ":" + e.Message);
                return false;
            }

            if (loginResult.passwordExpired)
            {
                // パスワード失効
                // binding.setPassword(userId, newPassword) で更新が必要
                return false;
            }

            // エンドポイント更新
            string authEndPoint = binding.Url;      // 初期化時のログイン認証URL
            binding.Url = loginResult.serverUrl;    // 認証後のサービス用URL

            // セッションヘッダ追加
            binding.SessionHeaderValue = new SessionHeader();
            binding.SessionHeaderValue.sessionId = loginResult.sessionId;


            // 結果表示
            GetUserInfoResult userInfo = loginResult.userInfo;

            string msg = "";
            msg += "UserID: " + userInfo.userId + "\r\n";
            msg += "User Full Name: " + userInfo.userFullName + "\r\n";
            msg += "User Email: " + userInfo.userEmail + "\r\n";
            msg += "\r\n";
            msg += "SessionID: " + loginResult.sessionId + "\r\n";
            msg += "Auth End Point: " + authEndPoint + "\r\n";
            msg += "Service End Point: " + loginResult.serverUrl + "\r\n";

            MessageBox.Show(msg);

            return true;
        }

        /// <summary>
        /// ログアウトボタン
        /// </summary>
        private void LogoutBtn_Click(object sender, EventArgs e)
        {
            Logout();
        }

        /// <summary>
        /// ログアウト
        /// </summary>
        private bool Logout()
        {
            try
            {
                binding.logout();
            }
            catch (SoapException e)
            {
                MessageBox.Show(e.Message);
                return false;
            }
            return true;
        }
    }
}
```

## オブジェクト操作

例として Contact の操作

``` C#:Read.cs
/// <summary>
/// Contact取得
/// </summary>
private void getContacts()
{
    // SOQL発行
    // リレーション項目も取得可能(取引先担当者が所属する取引先名Account.Name)
    String soqlQuery = "SELECT Id, FirstName, LastName, Account.Name FROM Contact";
    try
    {
        // デフォルトだと500件までしか取れない
        QueryResult qr = binding.query(soqlQuery);

        string msg = "";
        while (true)
        {
            sObject[] records = qr.records;
            for (int i = 0; i < records.Length; i++)
            {
                Contact con = (Contact)records[i];
                msg += con.Id + ":" + con.LastName + " " + con.FirstName + " : " + con.Account.Name + "\r\n";
            }

            if (qr.done)
            {
                // おわり
                break;
            }
            else
            {
                // 500件以上存在する場合、次の500件を取得
                qr = binding.queryMore(qr.queryLocator);
            }
        }

        MessageBox.Show(msg);
    }
    catch (Exception ex)
    {
        // エラー
        MessageBox.Show(ex.Message);
    }
}
```

``` C#:Upsert.cs
/// <summary>
/// Contact更新
/// </summary>
public void upsertContacts()
{
    // 更新対象配列作成(テストなので適当)
    sObject[] upserts = new Contact[1];

    Contact c0 = new Contact();
    c0.Email = "test@example.com";
    c0.LastName = "てすと";
    c0.FirstName = "たろう";
    c0.AccountId = "001000000000000";

    // カスタム項目を扱う場合、指定フラグを立てる必要がある
    c0.CustomDate__cSpecified = true;
    c0.CustomDate__c = System.DateTime.Now;
    
    upserts[0] = c0;

    try
    {
        // Email一致の場合UPDATE、それ以外の場合INSERT
        UpsertResult[] upsertResults = binding.upsert("Email", upserts);

        string msg = "";

        foreach (UpsertResult result in upsertResults)
        {
            if (result.success)
            {
                msg += result.id + " : " + (result.created ? "Insert" : "Update") + "\r\n";
            }
            else
            {
                MessageBox.Show("Error!: " + result.errors[0].message);
            }
        }

        MessageBox.Show(msg);
    }
    catch (SoapException e)
    {
        MessageBox.Show(e.Message);
    }
}
```


# おわりに

Reference.cs 修正しないと動かないのが謎。
別サービスでも似たようなことがあったし流行ってるんですかね…

カスタムオブジェクトや項目の追加、変更があった場合、
WSDL 読み直しになるから面倒。