# [實作] .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`

---
### 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) 進行加入聊天房間測試

#### (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>
```
---