ASP.NET Core SignalRを使用してサーバからメッセージをプッシュする

# 目的

・Webアプリケーションとしてローカルネットワーク内で使用する
・WebSocket等を使用してサーバからメッセージをプッシュする
・.NET Coreを使用する

# SignalR の概要（公式ドキュメントからの引用）
https://docs.microsoft.com/ja-jp/aspnet/core/signalr/introduction?view=aspnetcore-3.1
ASP.NET Core SignalR は、リアルタイムの web 機能をアプリに簡単に追加できるオープンソースライブラリです。 リアルタイム web 機能を使用すると、サーバー側のコードでクライアントにコンテンツを瞬時にプッシュできます。

SignalR は、リアルタイム通信を処理するための次の手法をサポートしています (正常なフォールバックの順序)。
WebSocket

# チュートリアル
https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/signalr?view=aspnetcore-3.1&tabs=visual-studio

公式ドキュメントのチュートリアルとしてチャットアプリを作成する。
機能としては、クライアントからメッセージを送信、サーバはメッセージを受け取ると、全クライアントに対してそのメッセージを送信する。
省かれている工程もなく、Visual Studio、Visual Studio Code、Visual Studio for Macそれぞれの手順が記載されており、公式ドキュメント通りに実装すれば問題なくできた。
以下で手順を追いながらコメントを記載。

## 必須コンポーネント
- Visual Studio Code
- C# for Visual Studio Code (最新バージョン)
- .NET Core 3.0 SDK 以降

※C# for Visual Studio CodeはVisual Studio Codeの拡張機能

## Web アプリ プロジェクトを作成する
ターミナルで以下コマンドを実行

```
dotnet new webapp -o SignalRChat
code -r SignalRChat
```
SignalRChatフォルダにwebアプリケーションのプロジェクトを作成
SignalRChatフォルダをVisual Studio Codeで開く

※newコマンドで作成できるプロジェクトの種類については以下参照
https://docs.microsoft.com/ja-jp/dotnet/core/tools/dotnet-new

## SignalR クライアント ライブラリを追加する

SignalRChatフォルダ内で以下コマンドを実行

```
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js
```

## SignalR ハブを作成する

SignalRChat以下にHubsフォルダを作成、Hubsフォルダ内にChatHub.csを作成する
サーバ側の処理を実装する

```c#
// ChatHub.cs

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
```
SendMessageというメッセージをクライアントから受信した場合に、すべてのクライアントに対して"ReceiveMessage"というメッセージをパラメータ（user, message）をつけて送信する
メッセージ受信処理は非同期処理としてタスクにしている

## SignalR を構成する
Startup.csに以下の"// +++ 追加 +++"部分を追加する

```c#
// Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SignalRChat.Hubs; // +++ 追加 +++
 
namespace SignalRChat
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddSignalR(); // +++ 追加 +++
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapHub<ChatHub>("/chathub"); // +++ 追加 +++
            });
        }
    }
}
```

## SignalR クライアント コードを追加する
### クライアント側の画面を実装する
userを入力するテキスト、messageを入れるテキスト、送信するためのボタン、受信したメッセージを表示するリストを配置する
Pages\Index.cshtmlを以下の内容に書き換える

```html
@page
    <div class="container">
        <div class="row">&nbsp;</div>
        <div class="row">
            <div class="col-2">User</div>
            <div class="col-4"><input type="text" id="userInput" /></div>
        </div>
        <div class="row">
            <div class="col-2">Message</div>
            <div class="col-4"><input type="text" id="messageInput" /></div>
        </div>
        <div class="row">&nbsp;</div>
        <div class="row">
            <div class="col-6">
                <input type="button" id="sendButton" value="Send Message" />
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <hr />
        </div>
    </div>
    <div class="row">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
```

### クライアント側の処理を実装する
wwwroot/js フォルダー内にchat.js 作成する

```javascript
// chat.js

"use strict";

// コネクション作成（URLはStartup.csで追加済）
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

//Disable send button until connection is established
document.getElementById("sendButton").disabled = true;

// "ReceiveMessage"受信時にメッセージをリストに追加
connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    var encodedMsg = user + " says " + msg;
    var li = document.createElement("li");
    li.textContent = encodedMsg;
    document.getElementById("messagesList").appendChild(li);
});

// コネクション確立時に送信ボタンの有効化
connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

// 送信ボタンクリック時に"SendMessage"を入力されたパラメータ（user, message）をつけて送信
document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
```

## アプリを実行する

以下コマンドを実行する

```
dotnet watch run -p SignalRChat.csproj
```

watchをつけることで、コード変更をリアルタイムに画面に反映する
アプリケーションを配布する場合は`dotnet publish`する