---
# System prepended metadata

title: '[實作] .Net Framework WebAPI 實作 SignalR - 實作功能篇'
tags: [webSockets, SignalR, WebAPI]

---

# [實作] .Net Framework WebAPI 實作 SignalR - 實作功能篇

### :arrow_forward: **綱要**
1. **建構 SignalR 環境**
2. **資料庫 Schema 設定**
3. **SignalR 連線人數計算 - 程式碼實作**
4. **SignalR User Group - 程式碼實作**
5. **SignalR 後端 Method 呼叫 Web API - 程式碼實作**
---
### 1. 前情提要 - 如何建構 SignalR 環境
- 有關如何於 .Net Framework Web API 中建構 SignalR 環境，以及如何於前後端互相呼叫特定 SignalR 的方法，請參閱下方 HackMD 連結：
    - **[.Net Framework WebAPI 實作 SignalR - 建立環境篇](https://hackmd.io/@David0799/SykNjmcmA)**

- 本文將實作一個顯示全站人數和具有特定 群組(Group) 的 1對1 即時聊天功能 
---
### 2. 資料庫 Schema 設定
#### (1) User 會員表單
```csharp
public class User
{
    [Key]
    [Display(Name = "編號")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    
    ...
    
}
```
#### (2) ChatRoom 聊天室表單
```csharp
[Table("ChatRooms")]
public class ChatRooms
{
    [Key]
    [Display(Name = "編號")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Display(Name = "聊天室擁有者ID")]
    public int UserIdOwner { get; set; }

    [Display(Name = "聊天室對談者ID")]
    public int UserIdTalker { get; set; }

    [Display(Name = "訊息表單虛擬欄位")]
    public virtual ICollection<Record> Record { get; set; }
        
    ...
    
}
```
#### (3) Records 聊天訊息紀錄表單
```csharp
 [Table("Records")]
 public class Records
 {
     [Key]
     [Display(Name = "編號")]
     [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
     public int Id { get; set; }

     [Display(Name = "發訊者使用者Id")]
     public int UserIdSender { get; set; }

     [Required]
     [MaxLength(500)]
     public string Message { get; set; }

     [Display(Name = "聊天室外鍵")]
     public int ChatRoomId { get; set; }

     [JsonIgnore]
     [ForeignKey("ChatRoomId")]
     [Display(Name = "聊天室表單虛擬欄位")]
     public virtual ChatRoom ChatRoom { get; set; }
         
     ...
    
 }
```
---
### 3. SignalR 全域連線人數計算
#### (1) 於後端 SRHub.cs 檔中擴增以下程式
```csharp
public class SRHub : Hub
{
    // 加入以下
    private static int OnlineCount = 0;   // 宣告目前進站人數
    public override Task OnConnected()    // 若有人加入 Hub，SignalR 會自動執行此方法
    {
        OnlineCount++;
        return base.OnConnected();
    }
    public override Task OnDisconnected(bool stopCalled) // 若有人離開 Hub，SignalR 會自動執行此方法
    {
        OnlineCount--;
        return base.OnDisconnected(stopCalled);
    }
    public override Task OnReconnected()  // 若有人重新加入 Hub，SignalR 會自動執行此方法
    {
        OnlineCount++;
        return base.OnReconnected();
    }
    public int GetOnlineCount()          // 前端可以呼叫此方法，取得目前連上 Hub 的總人數
    {
        return OnlineCount;
    }
    // 加入以上
}
```
#### (2) 於前端 SRPage.cshtml 檔中的擴增以下程式
```javascript
    $.connection.hub.start().done(function () {
        // 省略程式碼
        // 加入以下
            // 呼叫後端的 getOnlineCount 方法，若成功，會印出目前線上人數
            // Note：前端呼叫後端方法要使用 小駝峰(第一個單字小寫)
        chat.server.getOnlineCount().done(function (result) {
            console.log('目前線上人數： ' + result);
        }).fail(function (error) {
            console.error('呼叫方法失敗，Error為： ' + error);
        });
        // 加入以上
    });
});
```
#### (3) 進行連線人數測試
運行專案，並使用多個無痕視窗開啟 `https://localhost:你自己的port號/Home/SRPage` 路由，打開瀏覽器開發者工具並選擇 Console，若不同的無痕視窗會顯示 `目前線上人數：X` 即代表前端頁面已經可以取得 Hub 線上總人數；可以再把其中的一個瀏覽器關閉(模擬使用者離開網站)，可以發現 `目前線上人數：X-1`
![image](https://hackmd.io/_uploads/ryGpa5TX0.png)

---
### 4. SignalR User Group
將使用者加入特定的 **群組(Group)** ，可以達到建立 `1對1` 或 `多對多` 的訊息房間；
在 **SignalR** 中，可以使用 `Groups.Add(Context.ConnectionId,房間名稱)`，其中 `房間名稱` 為可以自定義的字串型別，本文將使用 `Table("ChatRooms")` 中的 `Id` 作為房間名稱
#### (1) 於後端 SRHub.cs 檔中擴增以下程式
當前端呼叫 `JoinChatRoom()`，就會把使用者加入特定的 `chatroomId` 訊息房間
```csharp!
public async Task<string> JoinChatRoom(int chatroomId)
{
    // 將當前使用者加入特定的聊天室(Groups)，使用 Context.ConnectionId 和 自行指定的 chatroomId 參數
    await Groups.Add(Context.ConnectionId, chatroomId.ToString());
    return $"Return by Backend：Client join into chatroom - {chatroomId} successfully";
}
```
#### (2) 於前端 SRPage.cshtml 檔中的擴增以下程式
```javascript
    $.connection.hub.start().done(function () {
        // 省略程式碼
        // 加入以下
            // 呼叫後端的 joinChatRoom 方法，並傳入 100 作為 chatroomId，
            // 若成功，會印出後端 return 傳出的訊息
            // Note：前端呼叫後端方法要使用 小駝峰(第一個單字小寫)
        chat.server.joinChatRoom(100).done(function (result) {
            console.log('已經被成功加入聊天室： ' + result);
        }).fail(function (error) {
            console.error('呼叫方法失敗，Error為： ' + error);
        });
        // 加入以上
    });
});
```
#### (3) 進行加入聊天房間測試
![image](https://hackmd.io/_uploads/rJlFVrB40.png)

#### (4) 後續規劃、衍生問題與 API 規劃
- 後續規劃：
當前使用者被加入到特定的 `chatroomId` 訊息房間，接下來就要讓房間內的使用者可以**互相傳送和讀取彼此**的訊息
- 衍生問題：
1.應該要讓特定的使用者(`User.Id`)，         (=> 需透過 JWT 解析當前使用者 `User.Id`，並於資料庫
2.才能被加入特定的 `chatroomId` 訊息房間，   (=> 找到其所對應的`chatroomId` 訊息房間
3.以及，如何才能傳送訊息給這個房間內的人，     (=> 透過 API 捕捉相關訊息，存入資料庫
4.並於前端畫面進行顯示                     (=> 再透過 前端呼叫後端 SignalR Function
5.和，如何讀取過去的聊天室訊息?             (=> 前端透過 呼叫 API 即可解決)

以上的衍生問題，需要由後端 SRHub.cs 檔案中，和後端的 API 互動，才能解決上述的衍生問題，

- API 規劃：共有三支
A.取得當前使用者的所有對應的聊天房間`chatroomId`
B.取得當前使用者的特定聊天房間`chatroomId`內部的歷史訊息
C.當前使用者於特定聊天房間`chatroomId`傳送訊息

本文僅針對 C-API 進行說明，A-API 和 B-API 可透過簡單的 API 實作即可完成

---
### 5. SignalR 後端 Method 呼叫 Web API (C-API)
#### (1) 架構一支 建立和取得 房間號碼的 API
```csharp
public class ChatController : ApiController
{
    private DbModel db = new DbModel();

    [HttpPost]
    [Route("api/chat/SendMessageToChatroom")] // input 為 View Model，型別於下方 class
    public IHttpActionResult SendMessageToChatroom(ChatSendMessageCheck input)
    {
        // Note：有進行簡化，建議先確認 chatroomId 是否存在
        // 準備於 Records 資料表新增訊息留言資料
        var newRoomSet = new Records
        {
            UserIdSender = input.userIdSender,
            Message = input.message,
            ChatRoomId = input.chatroomId,
        };
        db.Records.Add(newRoomSet);
        db.SaveChanges();

        // 撈取聊天室資料
        var messageReturn = db.Records.Where(x => x.ChatRoomId == input.chatroomId).AsEnumerable();
        var result = new
        {
            statusCode = 200,
            status = "success",
            message = "訊息傳送成功",
            chatcontent = messageReturn?.Select(x => new
            {
                senderId = x.UserIdSender,
                message = x.Message,
            }).ToList()
        };
        return Content(HttpStatusCode.OK, result);
    }
}

public class ChatSendMessageCheck  // API 的 View Model
{
    [Display(Name = "chatroom")]
    public int chatroomId { get; set; }

    [Display(Name = "使用者Id=>發訊者")]
    public int userIdSender { get; set; }

    [Required]
    [MaxLength(500)]
    public string message { get; set; }
}
```
#### (2) 於後端 SRHub.cs 檔中擴增以下程式： SendMessageToApi()
於 SRHub 中建構一個 SendMessageToApi 的方法，其會回傳一個物件
```csharp   
public class SRHub : Hub
{
    // 建立變數，稍後會使用此變數去發送 Post 給自己的後端 API
    private readonly HttpClient _httpClient;

    // 建立建構子
    public SRHub()
    {
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri(@"http://4.224.41.94"); // 修改為後端domain路由
    }
    
    // 省略其他程式碼
    
    // 建構一個 SendMessageToApi 方法，傳入 (C-API) 所需要的相關參數
    public async Task<object> SendMessageToApi(int chatroomId, int userIdSender, string message)
    {
        var postData = new
        {
            chatroomId,
            userIdSender,
            message,
        };
        // 將資料序列化為 JSON 字串
        var jsonContent = JsonConvert.SerializeObject(postData);
        // 發送 POST 請求到 Web API(C-API)
        var response = await _httpClient.PostAsync("api/chat/SendMessageToChatroom", new StringContent(jsonContent, Encoding.UTF8, "application/json"));
        // 檢查是否成功
        if (response.IsSuccessStatusCode)
        {
            // 讀取成功的回應資料
            string responseBody = await response.Content.ReadAsStringAsync();
            // 將回應資料作為 JSON 物件格式返回給前端
            var data = JsonConvert.DeserializeObject(responseBody); 
            return data;
        }
        else
        {
            // 讀取失敗的回應資料
            string responseBody = await response.Content.ReadAsStringAsync();
            // 將回應資料作為 JSON 物件格式返回給前端
            var data = JsonConvert.DeserializeObject(responseBody);
            return data;
        }
    }
}
```
#### (3) 於後端 SRHub.cs 檔中擴增以下程式：SendMessageToRoom()
於 SRHub 中建構一個 SendMessageToRoom 的方法，其會呼叫 (2)中的 SendMessageToApi 方法
屆時，前端可以呼叫 SendMessageToRoom 方法，就會驅動整個 Function 的連動
```csharp
public class SRHub : Hub
{
    // 省略其他程式碼
    
    // 建構一個 SendMessageToRoom 方法，其內部會呼叫 SendMessageToApi 方法
    public async Task<string> SendMessageToRoom(int chatroomId, int userIdSender, string message)
    {
        // 透過 Clients.Group(chatroomId.ToString()) 指定特定聊天房間
        // 並呼叫前端所寫定的 receiveMessage()方法，將 SendMessageToApi 方法回傳物件傳送給他
        var SendMessageToGroup = await Clients.Group(chatroomId.ToString()).receiveMessage(await SendMessageToApi(chatroomId, userIdSender, message));
    }
}
```

#### (4) 於前端 SRPage.cshtml 檔中的擴增以下程式
```javascript
<script>
    $(function () {
        var chat = $.connection.sRHub;  

        // 建立前端方法 receiveMessage()，後端會把訊息傳送給這個方法，讓前端捕獲
        chat.client.receiveMessage = function (returnData) {
            // 以下需要前端修改，捕獲 returnData ，並於前端渲染
            console.log(returnData);
        };
    
        $.connection.hub.start().done(function () {
            // ... 省略相關程式碼
            // 建立一個呼叫後端的方法，傳入相關參數
            // 相關參數需要透過 JWT(User.Id)、API(chatroomId)和前端捕獲(message內容)
            // 以下假設 chatroomId=100 ; User.Id=1 ; message="我是David"
            chat.server.SendMessageToRoom(100,1,'我是David').done(function () {
                // 呼叫完成後端方法後，甚麼事情都不做
            }).fail(function (error) {
                console.error('呼叫方法失敗，Error為： ' + error);
            });
        });
    });
</script>
```
---
