[C#] REST-APIサーバーを構築する

# はじめに

C# の `HttpListener` クラスを使用して簡易的なREST APIサーバーを構築することができます。

# 環境設定

今回実行した環境は下記の通りです。

```Markdown:実行環境
Microsoft .NET Framework 3.5
```

外部からのアクセスを有効にするために、APIサーバーがデプロイされているWindowsで下記のアクセス設定を行います。

## netshの設定
   - コマンドプロンプトで以下のコマンドを実行します。

```bash:コマンドプロンプト
netsh http add urlacl url=http://+:8888/ user=everyone
```        

## ファイアウォールの設定
- コントロールパネルでファイアウォールを開いて「詳細設定」をクリックします。
- 受信の規則で目的のルールをクリックするか新規に作成します。
    - 次のプログラム: `system`
    - プロトコルの種類: `TCP`
    - ローカルポート: `特定のポート`, `8888`

# 設定ファイル

configでAPIサーバーの設定を行います。

```xml:app.config
<setting name="API_PATH" serializeAs="String">
    <value>api</value>
</setting>
<setting name="API_PORT" serializeAs="String">
    <value>8888</value>
</setting>
```

- 「API_PATH」は、URLを指定します。
- 「API_PORT」は、ポート番号を指定します。


# REST APIサーバーの実装

APIサーバーの起動の実装は下記の通りです。

```C#:ApiService.cs
class ApiService
{
    private static Logger log = Logger.GetInstance();
    private HttpListener listener;
    private ControllerMapper mapper = new ControllerMapper();

    /// <summary>
    /// APIサービスを起動する
    /// </summary>
    public void Start()
    {
        try
        {
            // HTTPサーバーを起動する
            this.listener = new HttpListener();
            this.listener.Prefixes.Add(String.Format("http://+:{0}/{1}/", Settings.Default.API_PORT, Settings.Default.API_PATH));
            this.listener.Start();

            while (this.listener.IsListening)
            {
                IAsyncResult result = this.listener.BeginGetContext(OnRequested, this.listener);
                result.AsyncWaitHandle.WaitOne();
            }
        }
        catch (Exception ex)
        {
            /* ～エラー処理～ */
        }
    }

    /// <summary>
    /// リクエスト時の処理を実行する
    /// </summary>
    /// <param name="result">結果</param>
    private void OnRequested(IAsyncResult result)
    {
        HttpListener clsListener = (HttpListener)result.AsyncState;
        if (!clsListener.IsListening)
        {
            return;
        }

        HttpListenerContext context = clsListener.EndGetContext(result);
        HttpListenerRequest req = context.Request;
        HttpListenerResponse res = context.Response;

        try
        {
            mapper.Execute(req, res);
        }
        catch (Exception ex)
        {
            log.Error(ex.ToString());
        }
        finally
        {
            try
            {
                if (null != res)
                {
                    res.Close();
                }
            }
            catch (Exception clsEx)
            {
                log.Error(clsEx.ToString());
            }
        }
    }
```

`result.AsyncWaitHandle.WaitOne();` でサーバーにアクセスがあるまで処理を待ちます。アクセスがあると `OnRequested` メソッドが呼び出されます。このメソッドは異なるスレッドで呼び出されます。

APIサーバーのパス毎の振り分け処理の実装は下記の通りです。

```C#:ControllerMapper.cs
class ControllerMapper
{
    private const string CONTENT_TYPE_JSON = "application/json";
    private static Logger log = Logger.GetInstance();

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public ControllerMapper()
    {
    }

    /// <summary>
    /// 実行する
    /// </summary>
    /// <param name="req">リクエスト情報</param>
    /// <param name="res">レスポンス情報</param>
    public void Execute(HttpListenerRequest req, HttpListenerResponse res)
    {
        StreamReader reader = null;
        StreamWriter writer = null;
        string reqBody = null;
        string resBoby = null;

        try
        {
            res.StatusCode = (int)HttpStatusCode.OK;
            res.ContentType = CONTENT_TYPE_JSON;
            res.ContentEncoding = Encoding.UTF8;

            reader = new StreamReader(req.InputStream);
            writer = new StreamWriter(res.OutputStream);
            reqBody = reader.ReadToEnd();

            LogStart(req, reqBody);
            resBoby = ExecuteController(req, res, reqBody);
        }
        catch (Exception ex)
        {
            /* ～エラー処理～ */
        }
        finally
        {
            try
            {
                writer.Write(resBoby);
                writer.Flush();

                if (null != reader)
                {
                    reader.Close();
                }
                if (null != writer)
                {
                    writer.Close();
                }
                LogEnd(res, resBoby);
            }
            catch (Exception clsEx)
            {
                log.Error(clsEx.ToString());
            }
        }
    }

    /// <summary>
    /// リクエストログを出力する
    /// </summary>
    /// <param name="req">リクエスト情報</param>
    /// <param name="body">リクエストボディ文字列</param>
    private void LogStart(HttpListenerRequest req, string body)
    {
        log.Info("########## Request [start] ##########");
        log.Info(String.Format(">> {0} {1}", req.HttpMethod, GetApiPath(req.RawUrl)));
        log.Info(">> IP: " + req.UserHostAddress);
        log.Info(">> UserAgent: " + req.UserAgent);
        log.Info(">> Header: " + ToNameValueString(req.Headers));
        if ("GET".Equals(req.HttpMethod))
        {
            if (0 < req.QueryString.Count)
            {
                log.Info(">> Query: " + ToNameValueString(req.QueryString));

            }
        }
        else
        {
            if (!string.IsNullOrEmpty(body))
            {
                log.Info(">> Body: " + body);
            }
        }
        log.Info("########## Request [end] ##########");
    }

    /// <summary>
    /// レスポンスログを出力する
    /// </summary>
    /// <param name="res">レスポンス情報</param>
    /// <param name="body">レスポンスボディ文字列</param>
    private void LogEnd(HttpListenerResponse res, string body)
    {
        log.Info("########## Response [start] ##########");
        log.Info(">> HTTP Status: " + res.StatusCode);
        log.Info(">> Header: " + ToNameValueString(res.Headers));
        if (!string.IsNullOrEmpty(body))
        {
            log.Info(">> Body: " + body);
        }
        log.Info("########## Response [end] ##########");
    }

    /// <summary>
    /// Name-Value文字列を取得する
    /// </summary>
    /// <param name="nvc">nvc</param>
    /// <returns>文字列</returns>
    private string ToNameValueString(NameValueCollection nvc)
    {
        return String.Join(", ", Array.ConvertAll(nvc.AllKeys, key => String.Format("{0}:{1}", key, nvc[key])));
    }

    /// <summary>
    /// APIパスを取得する
    /// </summary>
    /// <param name="srcPath">URLパス</param>
    /// <returns>APIパス</returns>
    private string GetApiPath(string srcPath)
    {
        string[] path = srcPath.Split('?');
        string condition = String.Format(@"^/{0}", Settings.Default.API_PATH);
        return Regex.Replace(path[0], condition, "");
    }

    /// <summary>
    /// APIコントローラを実行する
    /// </summary>
    /// <param name="req">リクエスト情報</param>
    /// <param name="res">レスポンス情報</param>
    /// <param name="reqBody">リクエストボディ</param>
    /// <returns>レスポンス文字列</returns>
    private string ExecuteController(HttpListenerRequest req, HttpListenerResponse res, string reqBody)
    {
        string path = GetApiPath(req.RawUrl);

        if ("/user/".Equals(path))
        {
            switch (req.HttpMethod)
            {
                case "GET":
                    return (new ReadUserController(req, res, reqBody)).Execute();
                case "POST":
                    return (new CreateUserController(req, res, reqBody)).Execute();
                case "PUT":
                    return (new UpdateUserController(req, res, reqBody)).Execute();
                case "DELETE":
                    return (new DeleteUserController(req, res, reqBody)).Execute();
            }
        }
        if ("/users/".Equals(path) && "GET".Equals("GET"))
        {
            return (new ReadUsersController(req, res, reqBody)).Execute();
        }
        return "";
    }
}
```

`ExecuteController` メソッドでAPIパス毎にコントローラーを振り分けています。例えば、`POST http:/localhost:8888/api/user/` でサーバーにアクセスすると `CreateUserController` コントローラーが呼び出されます。

# さいごに

ソースコードをGitHubに公開しています。

[ソースファイルはこちら](https://github.com/yunbow/CSharp-WebAPI)

以上です。