ガルーンAPIのAdmin系のAPIを使う

#始めに
まず、先人にお礼を。
サイボウズガルーンを利用しており、管理等いろいろ簡易化できないかと考えている際に、次の記事を参考にさせて頂きました。ありがとうございました。
<a href="https://qiita.com/HAGITAKO/items/b684f4e31552e5a5db69" target="_blank">VisualStudio2017でガルーンAPIを使ってメッセージを送ってみる</a>
<a href="https://qiita.com/GreatFailure/items/2379098476f847a0319c" target="_blank">VisualStudio2017でガルーンAPIを使ってファイル添付メッセージを送ってみる</a>

Qiitaにも初投稿で、過去にクライアントサーバー間の通信プログラムを作ったこともないので、おかしな所があれば教えて下さい。

#やりたいこと
上記記事と同様にAdminBindigクラスからAdmin系のAPIを利用しようとしてもエラーが出てしまうことに直面。調べても次の記事のとおりSOAP通信のレスポンスをうまく取れないと…。なんとかしてAdmin系のAPIを利用したい。
<a href="https://developer.cybozu.io/hc/ja/community/posts/255632966" target="_blank">C# ガルーンAPI WSDL参照追加後のコードについて</a>


#開発環境
・Visual Studio 2010(古くてごめんなさい)
・サイボウス ガルーン(パッケージ版 Ver 4.2.4)

#実装内容に関する説明
WSDL参照を追加し、Reference.csを修正するまでは、参考にさせて頂いた下記記事と同様です。
<a href="https://qiita.com/HAGITAKO/items/b684f4e31552e5a5db69" target="_blank">VisualStudio2017でガルーンAPIを使ってメッセージを送ってみる</a>

本記事では、代表例として、AdminGetUserDetailByIds関数を利用する場合の実装内容を記載します。

主に記載するクラスは以下の2つです。
・「SoapAdmin」
・「MyAdminGetUserDetailByIds」
SoapAdminクラスはスーパークラスで、それを継承するMyAdminGetUserDetailByIdsクラスというクラス関係にしています。
これは、各関数ごとにその関数に対応するサブクラスを追加していけば、各関数を利用できるようにしたかったのでこういうクラス関係にしました。

細かく書きましたが、一言で言うと、「AdminBindingクラス使えねーから、自作してやんよ。」というお話です。

#実装内容
まずはスーパークラスであるSoapAdminクラスを実装。

```C#:SoapAdmin.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Net;
using System.Net.Sockets;
using System.Xml;
using System.IO;
using System.Xml.Serialization;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Windows.Forms;
using test.cybz; //WSDL参照

/// <summary>
/// AdminAPISoap通信クラス
/// WSDLで自動生成したソース(Reference.cs等)の一部が正常に動作しないため自作
/// My○○クラスはこのクラスを継承して利用(○○はAPI関数名)
/// </summary>
abstract class SoapAdmin
{
    // Admin系のsoap通信リクエストデータ
    // 継承先でこのメンバにリクエストを代入
    private Object adminRequest;
    protected Object AdminRequest
    {
        get { return adminRequest; }
        set { adminRequest = value; }
    }

    // Admin系のsoap通信レスポンスデータ
    // 継承先でこのメンバにレスポンスを代入
    private Object adminResponse;
    protected Object AdminResponse
    {
        get { return adminResponse; }
        set { adminResponse = value; }
    }

    private ActionElement actionElement;
    public ActionElement ActionElement
    {
        get { return actionElement; }
        set { actionElement = value; }
    }

    private SecurityElement securityElement;
    public SecurityElement SecurityElement
    {
        get { return securityElement; }
        set { securityElement = value; }
    }

    private UsernameTokenElement userNameTokenElement;
    public UsernameTokenElement UserNameTokenElement
    {
        get { return userNameTokenElement; }
        set { userNameTokenElement = value; }
    }

    private TimestampElement timeStampElement;
    public TimestampElement TimeStampElement
    {
        get { return timeStampElement; }
        set { timeStampElement = value; }
    }

    private string hostName;
    public string HostName
    {
        get { return hostName; }
        set { hostName = value; }
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public SoapAdmin()
    {
        this.actionElement = new ActionElement();
        this.securityElement = new SecurityElement();
        this.userNameTokenElement = new UsernameTokenElement();
        this.timeStampElement = new TimestampElement();
    }

    /// <summary>
    /// Soap通信のメイン関数
    /// </summary>
    public virtual void soapSendReceive()
    {
        //TimeStampElementクラスの生成
        this.TimeStampElement.Created = DateTime.UtcNow;
        this.TimeStampElement.Expires = TimeStampElement.Created.AddDays(8);

        //送信するXmlデータ
        XmlDocument soapEnvelopeXml = new XmlDocument();

        //送信するXmlのベース生成(正しい方法が分からないので直書き)
        soapEnvelopeXml.LoadXml(@"<?xml version=""1.0"" encoding=""utf-8""?>
                <soap:Envelope xmlns:soap=""http://www.w3.org/2003/05/soap-envelope"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
                    <soap:Header>
                    </soap:Header>
                    <soap:Body>
                    </soap:Body>
                </soap:Envelope>");

        //Header要素にActionタグ追加
        XmlDocument headerActionXmlDocument = new XmlDocument();
        headerActionXmlDocument = createSerializedXmlDocument(ActionElement, "Action");
        XmlNode actionNode = soapEnvelopeXml.ImportNode(headerActionXmlDocument.DocumentElement, true);
        soapEnvelopeXml["soap:Envelope"]["soap:Header"].AppendChild(actionNode);

        //Header要素にSecurityタグ追加
        XmlDocument headerSecurityXmlDocument = new XmlDocument();
        headerSecurityXmlDocument = createSerializedXmlDocument(SecurityElement, "Security");
        XmlNode securityNode = soapEnvelopeXml.ImportNode(headerSecurityXmlDocument.DocumentElement, true);
        soapEnvelopeXml["soap:Envelope"]["soap:Header"].AppendChild(securityNode);

        //Header要素にTimestampタグ追加
        XmlDocument headerTimestampXmlDocument = new XmlDocument();
        headerTimestampXmlDocument = createSerializedXmlDocument(TimeStampElement, "Timestamp");
        XmlNode timestampNode = soapEnvelopeXml.ImportNode(headerTimestampXmlDocument.DocumentElement, true);
        soapEnvelopeXml["soap:Envelope"]["soap:Header"].AppendChild(timestampNode);

        //Body要素内のタグの生成
        XmlDocument bodyNameXmlDocument = new XmlDocument();
        bodyNameXmlDocument.LoadXml("<" + ActionElement.actionValue + "></" + ActionElement.actionValue + ">");
        XmlDocument paramXmlDocument = new XmlDocument();
        paramXmlDocument = createSerializedXmlDocument(AdminRequest, "parameters");
        XmlNode bodyNode = bodyNameXmlDocument.ImportNode(paramXmlDocument.DocumentElement, true);
        bodyNameXmlDocument[ActionElement.actionValue].AppendChild(bodyNode);
        XmlNode actionNameNode = soapEnvelopeXml.ImportNode(bodyNameXmlDocument.DocumentElement, true);
        soapEnvelopeXml["soap:Envelope"]["soap:Body"].AppendChild(actionNameNode);

        //Httpリクエストクラスの作成
        HttpWebRequest request = CreateWebRequest();

        try
        {
            //HTTPリクエスト
            using (Stream stream = request.GetRequestStream())
            {
                soapEnvelopeXml.Save(stream);
            }

            //HTTPレスポンス
            using (WebResponse response = request.GetResponse())
            {
                using (StreamReader rd = new StreamReader(response.GetResponseStream()))
                {
                    //xmlDocumentに格納
                    XmlDocument xmldoc = new XmlDocument();
                    xmldoc.LoadXml(rd.ReadToEnd());
                    //タグ<admin:○○Response>内の要素を抽出
                    string tmp = xmldoc.GetElementsByTagName(ActionElement.actionValue.Replace("Admin", "admin:") + "Response")[0].InnerXml;
                    //"2回以上続く空白"と"改行"を削除して整形
                    tmp = System.Text.RegularExpressions.Regex.Replace(tmp, @"\n\s{2,}", "").Replace("\r", "").Replace("\n", "");
                    StringReader reader = new StringReader(tmp);
                    //ルートタグを"returns"としデシリアライズ
                    System.Xml.Serialization.XmlSerializer serializer = new XmlSerializer(this.adminResponse.GetType(), new System.Xml.Serialization.XmlRootAttribute("returns"));
                    this.adminResponse = serializer.Deserialize(reader);
                }
            }
        }
        catch (Exception e)
        {
            //Console.WriteLine(e.Message);
            System.Windows.Forms.MessageBox.Show(e.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    /// <summary>
    /// Create a soap webrequest to [Url]
    /// </summary>
    /// <returns></returns>
    private HttpWebRequest CreateWebRequest()
    {
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(@"https://" + this.hostName + @"/cgi-bin/cbgrn/grn.cgi/sysapi/admin/api?");
        webRequest.ContentType = "text/xml; charset=\"utf-8\"";
        webRequest.Accept = "text/xml";
        webRequest.Host = this.hostName;
        webRequest.Method = "POST";
        return webRequest;
    }

    /// <summary>
    /// シリアライズされたデータの生成
    /// </summary>
    /// <param name="target">シリアライズするクラス</param>
    /// <param name="rootTagName">ルートタグ名</param>
    /// <returns></returns>
    private XmlDocument createSerializedXmlDocument(Object target, string rootTagName)
    {
        XmlSerializer serializer = new XmlSerializer(target.GetType(), new System.Xml.Serialization.XmlRootAttribute(rootTagName));
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
        ns.Add(string.Empty, string.Empty);
        StringWriter st = new StringWriter();
        serializer.Serialize(st, target, ns);
        XmlDocument xmlDocument = new XmlDocument();
        xmlDocument.LoadXml(st.ToString());
        return xmlDocument;
    }

}
```

　
次にサブクラスであるMyAdminGetUserDetailByIdsを実装。

```C#:MyAdminGetUserDetailByIds.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using test.cybz; //WSDL参照

/// <summary>
/// 自作AdminGetUserDetailByIdsクラス
/// </summary>
class MyAdminGetUserDetailByIds : SoapAdmin
{
    private AdminGetUserDetailByIdsResponseType adminGetUserDetailByIdsResponse;
    public AdminGetUserDetailByIdsResponseType AdminGetUserDetailByIdsResponse
    {
        get { return adminGetUserDetailByIdsResponse; }
        set { adminGetUserDetailByIdsResponse = value; }
    }

    private AdminGetUserDetailByIdsRequestType adminGetUserDetailByIdsRequest;
    public AdminGetUserDetailByIdsRequestType AdminGetUserDetailByIdsRequest
    {
        get { return adminGetUserDetailByIdsRequest; }
        set { adminGetUserDetailByIdsRequest = value; }
    }

    public MyAdminGetUserDetailByIds()
        : base()
    {
        this.ActionElement.actionValue = "AdminGetUserDetailByIds";
        this.adminGetUserDetailByIdsRequest = new AdminGetUserDetailByIdsRequestType();
        this.adminGetUserDetailByIdsResponse = new AdminGetUserDetailByIdsResponseType();
    }

    public override void soapSendReceive()
    {
        this.AdminRequest = this.adminGetUserDetailByIdsRequest;
        this.AdminResponse = this.adminGetUserDetailByIdsResponse;
        base.soapSendReceive();
        this.adminGetUserDetailByIdsResponse = (AdminGetUserDetailByIdsResponseType)this.AdminResponse;
    }
}

```

詳しくは分かりませんが、デシリアライズ時に失敗するので、Reference.csを修正。
XmlTypeAttributeのnamespaceを変更する。
修正箇所は、「//修正start」から「//修正end」まで。

他のAPIを利用する際に、レスポンスをデシリアライズした結果が空になってしまう場合は、この修正が必要な場合が多いです。
（自分のデシリアライズの仕方が悪いというのもあると思いますが。）

```C#:Reference.cs
(中略)
    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1055.0")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    //修正start
    //[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://wsdl.cybozu.co.jp/admin/2008/types")]
    [System.Xml.Serialization.XmlTypeAttribute(Namespace = "")]
    //修正end
    public partial class AdminGetUserDetailByIdsResponseType
    {

        private string number_usersField;

        private UserDetail[] userDetailField;

        /// <remarks/>
        [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified, DataType = "integer")]
        public string number_users
        {
            get
            {
                return this.number_usersField;
            }
            set
            {
                this.number_usersField = value;
            }
        }

        /// <remarks/>
        [System.Xml.Serialization.XmlElementAttribute("userDetail", Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
        public UserDetail[] userDetail
        {
            get
            {
                return this.userDetailField;
            }
            set
            {
                this.userDetailField = value;
            }
        }
    }

(中略)

    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.6.1055.0")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    //修正start
    //[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://schemas.cybozu.co.jp/admin/2008")]
    [System.Xml.Serialization.XmlTypeAttribute(Namespace = "")]
    //修正end
    public partial class UserDetail
    {

        private string userIdField;

        private string login_nameField;

        private string display_nameField;

        /// <remarks/>
        [System.Xml.Serialization.XmlElementAttribute(DataType = "integer")]
        public string userId
        {
            get
            {
                return this.userIdField;
            }
            set
            {
                this.userIdField = value;
            }
        }

        /// <remarks/>
        public string login_name
        {
            get
            {
                return this.login_nameField;
            }
            set
            {
                this.login_nameField = value;
            }
        }

        /// <remarks/>
        public string display_name
        {
            get
            {
                return this.display_nameField;
            }
            set
            {
                this.display_nameField = value;
            }
        }
    }


```

上記が完了すれば準備は終わりです。
利用する方法を次のソースで示します。


```C#:Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using test.cybz; //WSDL参照

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    /// <summary>
    /// ボタン押下時の処理
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void button1_Click(object sender, EventArgs e)
    {
        string[] userIds = {"1","2","3","4"};
        List<string> UserLoginNameList = getUserLoginNameFromUserIds(userIds.ToList());
    }

    /// <summary>
    /// ユーザーIDからログイン名を取得する
    /// </summary>
    /// <param name="userIdsList">ユーザーIDのリスト</param>
    /// <returns>ユーザーのログイン名のリスト</returns>
    private List<string> getUserLoginNameFromUserIds(List<string> userIdsList)
        {
        List<string> userLoginNameList = new List<string>();

        //SecurityElementを生成
        SecurityElement securityElement = new SecurityElement();
        UsernameTokenElement userNameTokenElement = new UsernameTokenElement();
        userNameTokenElement.Username = "admin";
        userNameTokenElement.Password = "adminpassword";
        securityElement.usernameToken = userNameTokenElement;
        //自作AdminAPIクラスを生成
        MyAdminGetUserDetailByIds myAdminGetUserDetailByIds = new MyAdminGetUserDetailByIds();
        myAdminGetUserDetailByIds.SecurityElement = securityElement;
        myAdminGetUserDetailByIds.HostName = "cybz.com"; //インストールしたサーバーのホスト名
        //リクエストデータの生成
        AdminGetUserDetailByIdsRequestType adminGetUserDetailByIdsRequest = new AdminGetUserDetailByIdsRequestType();
        adminGetUserDetailByIdsRequest.userId = userIdsList.ToArray();
        //自作AdminAPIクラスにリクエストデータを設定
        myAdminGetUserDetailByIds.AdminGetUserDetailByIdsRequest = adminGetUserDetailByIdsRequest; 
        //AdminAPIの実行
        myAdminGetUserDetailByIds.soapSendReceive();
        //レスポンスデータの取得
        List<UserDetail> userDetailList = myAdminGetUserDetailByIds.AdminGetUserDetailByIdsResponse.userDetail.ToList();

        //ユーザーのログイン名のみを抽出
        foreach (string userId in userIdsList)
        {
            userLoginNameList.Add(userDetailList.FindAll(x => x.userId == userId)[0].login_name);
        }
        return userLoginNameList;
    }
}
```

以上になります。

色々試してみたり調べてみたりしましたが、結局AdminBindingを利用した場合にレスポンスが空になる原因は分からず、諦めました。