【C#】外部サーバーへ接続するためのサンプルコード

# 概要
C#でデスクトップアプリから外部サーバーへアクセスするためのサンプルコードを記載しました。NET USEを使用して接続する方法と、WindowsAPIを使用して接続する方法を記載しています。

# コード
業務で行った対応を元にサンプルコードを記載しています。
業務ではNET USEコマンドでは現在ログイン中のユーザーIDを用いて接続、WindowsAPIではあらかじめ決められたユーザーIDとパスワードを用いて接続、接続できなければユーザーIDとパスワードを入力する画面を表示、という形で実装しましたので、サンプルコードもその形で記載しています。

### 1. NET USEコマンドを使用して外部サーバーに接続する

#### NET USEコマンドとは
Windowsコマンドの一つで、ネットワーク上の外部サーバーの共有フォルダに接続したり、切断することができるコマンドです。netコマンドというネットワーク関連を管理できるコマンドの一つです。

```C#:ServerConnectorByNetUse.cs
    public class ServerConnectorByNetUse
    {
        /// <summary>UNCパス</summary>
        private string UncPath { get; set; }

        /// <summary>ユーザーID</summary>
        private string UserId { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="uncPath">UNCパス</param>
        /// <param name="userId">ユーザーID</param>
        public ServerConnectorByNetUse(string uncPath, string userId)
        {
            UncPath = uncPath;
            UserId = userId;
        }

        /// <summary>
        /// サーバーに接続する
        /// </summary>
        /// <returns>結果 (true : 成功、false : 失敗)</returns>
        public bool TryConnect()
        {
            try
            {
                using (var open = new Process())
                {
                    // 既に接続済みの場合がある為、一旦接続を解除する
                    Disconnect();

                    open.StartInfo.FileName = "cmd.exe";                                // コマンド名
                    open.StartInfo.Arguments = "/c";                                    // 引数①
                    open.StartInfo.Arguments += $@"net use {UncPath} /user:{UserId}";   // 引数② (接続コマンド)
                    open.StartInfo.CreateNoWindow = true;                               // DOSプロンプトの黒い画面を非表示
                    open.StartInfo.UseShellExecute = false;                             // プロセスを新しいウィンドウで起動するか否か
                    open.StartInfo.RedirectStandardOutput = true;                       // 標準出力をリダイレクトで取得する
                    open.StartInfo.RedirectStandardError = true;                        // エラー出力をリダイレクトで取得する
                    open.Start();

                    if (!open.WaitForExit(1000))
                    {
                        // タイムアウトした場合
                        return false;
                    }

                    string standerdOutput = open.StandardOutput.ReadToEnd(); // 標準出力のメッセージを取得 (ここではこの変数は使用していません)
                    string standardError = open.StandardError.ReadToEnd();   // エラー出力のメッセージを取得
                    int exitCode = open.ExitCode;

                    if (!string.IsNullOrEmpty(standardError))
                    {
                        // エラー出力がある場合

                        string strErrorCode = GetErrorCode(standardError);
                        if (int.TryParse(strErrorCode, out int errorCode))
                        {
                            // エラーコードをログ出力 (省略)

                            // 他ユーザーアカウントで既に接続が確立されている場合は接続OKとする
                            if (errorCode == 1219) return true;
                        }
                    }

                    return exitCode == 0;

                }
            }
            catch (Exception)
            {
                // エラーハンドル処理 (省略)
            }
        }

        /// <summary>
        /// サーバーへの接続を解除する
        /// </summary>
        public void Disconnect()
        {
            try
            {
                using (var open = new Process())
                {

                    open.StartInfo.FileName = "cmd.exe";                                // コマンド名
                    open.StartInfo.CreateNoWindow = true;                               // DOSプロンプトの黒い画面を非表示
                    open.StartInfo.UseShellExecute = false;                             // プロセスを新しいウィンドウで起動するか否か
                    open.StartInfo.RedirectStandardOutput = true;                       // 標準出力をリダイレクトで取得したい
                    open.StartInfo.RedirectStandardError = true;                        // エラー出力をリダイレクトで取得したい
                    open.StartInfo.Arguments = "/c";                                    // 引数①
                    open.StartInfo.Arguments += $@"net use {UncPath} /delete /no";      // 引数② (切断コマンド)
                    open.Start();
                    if (!open.WaitForExit(1000))
                    {
                        // タイムアウトした場合
                        return;
                    }

                    string standerdOutput = open.StandardOutput.ReadToEnd(); // 標準出力のメッセージを取得 (ここではこの変数は使用していません)
                    string standardError = open.StandardError.ReadToEnd();   // エラー出力のメッセージを取得
                    int exitCode = open.ExitCode;

                    if (!string.IsNullOrEmpty(standardError))
                    {
                        // エラー出力がある場合
                        string strErrorCode = GetErrorCode(standardError);
                        if (int.TryParse(strErrorCode, out int errorCode))
                        {
                            // エラーコードをログ出力 (省略)
                        }
                    }
                }
            }
            catch (Exception)
            {
                // エラーハンドル処理 (省略)
            }
        }

        /// <summary>
        /// エラーメッセージからエラーコードを返却する
        /// </summary>
        /// <param name="errorMessage">エラーメッセージ</param>
        /// <returns>エラーコード</returns>
        private string GetErrorCode(string errorMessage)
        {
            var errorMessageList = errorMessage.Split(' ', '\n').ToList();
            foreach (string targetMessage in errorMessageList)
            {
                if (int.TryParse(targetMessage, out _)) return targetMessage;
            }

            return string.Empty;
        }
    }
```

#### 補足
- 正常に接続が成功すると、`int exitCode = open.ExitCode;`で`0`が返却されます。

- 以下のように
    ```C#
    open.StartInfo.RedirectStandardOutput = true;
    open.StartInfo.RedirectStandardError = true;
    ```
    を行っていないと
    ```C#
    string standerdOutput = open.StandardOutput.ReadToEnd();
    string standardError = open.StandardError.ReadToEnd();
    ```    
    の部分でエラーとなります。


### NET USE接続時のエラーコードについて
エラー出力 (コード上の`open.StandardError.ReadToEnd();`で得られる文字列) がある場合、エラー出力の文字列からエラーコードを抽出しています。(`GetErrorCode()`メソッドの部分)
エラーコードは沢山ありますが、よくある？のは下記かと思います。
|エラーコード|説明|
|---|---|
|-99|サーバー接続時にエラーが発生しました。|
|-1|サーバー接続時にタイムアウトしました。|
|53|ネットワークパスが見つかりません。|
|67|ネットワーク名が見つかりません。|
|87|パラメーターが間違っています。|
|1219|同じユーザーによる、サーバーまたは共有リソースへの複数のユーザー名での複数の接続は許可されません。サーバーまたは共有リソースへの以前の接続をすべて切断してから、再試行してください。|
|1223|この操作はユーザーによって取り消されました。|
|1312|指定されたログオン セッションは存在しません。そのセッションは既に終了している可能性があります。|
|1326|ユーザー名またはパスワードが正しくありません。|
|2250|このネットワーク接続はありません。|

参考：[システム エラー コード](https://learn.microsoft.com/ja-jp/windows/win32/debug/system-error-codes)

### 2. WindowsAPIを使用して外部サーバーに接続する

#### WNetAddConnection2について
WindowsAPIに「WNetAddConnection2」というメソッドが用意されており、このメソッドを使用することでネットワーク接続が可能です。

```C#:ServerConnectorByAPI.cs
    public class ServerConnectorByAPI
    {
        private NETRESOURCE netResource;

        [DllImport("mpr.dll", EntryPoint = "WNetAddConnection2", CharSet = CharSet.Unicode)]
        private static extern int WNetAddConnection2(ref NETRESOURCE lpNetResource, string lpPassword, string lpUsername, Int32 dwFlags);

        [DllImport("mpr.dll", EntryPoint = "WNetCancelConnection2", CharSet = CharSet.Unicode)]
        private static extern int WNetCancelConnection2(string lpName, Int32 dwFlags, bool fForce);


        /// <summary>UNCパス</summary>
        private string UncPath { get; set; }
        /// <summary>ユーザーID</summary>
        private string UserID { get; set; }
        /// <summary>パスワード</summary>
        private string Password { get; set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="uncPath">UNCパス</param>
        /// <param name="userId">ユーザーID</param>
        /// <param name="password">パスワード</param>
        public ServerConnectorByAPI(string uncPath, string userId, string password)
        {
            UncPath = uncPath;
            UserID = userId;
            Password = password;
        }

        /// <summary>
        /// ネットワーク情報
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct NETRESOURCE
        {
            public int dwScope;
            public int dwType;
            public int dwDisplayType;
            public int dwUsage;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpLocalName;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpRemoteName;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpComment;
            [MarshalAs(UnmanagedType.LPWStr)]
            public string lpProvider;
        }

        /// <summary>
        /// サーバーに接続する
        /// </summary>
        /// <returns>結果 (true : 成功、false : 失敗)</returns>
        public bool TryConnect()
        {
            try
            {
                netResource.dwScope = 0;                    // 列挙の範囲
                netResource.dwType = 1;                     // リソースタイプ
                netResource.dwDisplayType = 0;              // 表示オブジェクト
                netResource.dwUsage = 0;                    // リソースの使用方法
                netResource.lpLocalName = null;             // ローカルデバイス名
                netResource.lpRemoteName = UncPath;         // UNCパス
                netResource.lpProvider = null;              // プロバイダ名

                // 既に接続済みの場合がある為、一旦接続を解除する
                WNetCancelConnection2(UncPath, 0, true);

                // 接続
                int ret = WNetAddConnection2(ref netResource, Password, UserID, 8);

                // 他ユーザーアカウントで既に接続が確立されている場合 (1219) は接続OKとする
                return ret == 0 || ret == 1219;
            }
            catch (Exception)
            {
                // エラーハンドル処理 (省略)
            }
        }

        /// <summary>
        /// サーバーへの接続を解除する
        /// </summary>
        /// <returns>結果 (true : 成功、false : 失敗)</returns>
        public void TryDisConnect()
        {
            try
            {
                WNetCancelConnection2(UncPath, 0, true);
            }
            catch (Exception)
            {
                // エラーハンドル処理 (省略)
            }
        }
    }
```
#### 補足
- このメソッドでは引数で指定したログインIDとパスワードを使用していますが、接続できなかった場合は資格情報を入力する画面が表示され、ログインIDやパスワードを手入力できます。
- もし資格情報を入力する画面でキャンセルした場合は、`WNetCancelConnection2()`の戻り値としてエラーコード"1223"が返却されます。

### 使用方法
- NET USEを使用する方法

```C#
    // 現在ログインしているユーザーIDを使用しNET USEで接続
    var connectorByNeteUse = new ServerConnectorByNetUse(serverPath, Environment.UserName);
    bool ret = connectorByNeteUse.TryConnect();
```

- WindowsAPIを使用する方法
```C#
    // 既定のID/PWを使用しWindowsAPIで接続
    // 既定のID/PWでログインできなければ入力画面を表示
    var ConnectorByAPI = new ServerConnectorByAPI(toServerPath, userID, password);
    bool ret = ConnectorByAPI.TryConnect();
```

# 終わりに
既に接続済みの場合は、NET USEやWindowsAPIともに、戻り値が`0`ではなく、`1219`で返却されます。戻り値が`0`以外でも挙動上は問題無い場合があるので、戻り値の知識も最低限は必要になってくるかなと感じました。サーバーへアクセスした後は、サーバー内のファイルの情報を読み取ったり、サーバー内のファイルをコピーしてきたりができると思います。