Các cân nhắc khi thiết kế SignalR API
Bài viết này cung cấp hướng dẫn xây dựng các API dựa trên SignalR.
Sử dụng tham số đối tượng tùy chỉnh để đảm bảo tương thích ngược (backwards-compatibility)
Thêm tham số vào phương thức hub (hub method) của SignalR (ở phía client hoặc server) là một thay đổi phá vỡ tương thích (breaking change). Điều này có nghĩa là các client/server cũ sẽ gặp lỗi khi cố gọi phương thức mà không có đủ số lượng tham số. Tuy nhiên, thêm thuộc tính vào một tham số đối tượng tùy chỉnh thì không phải là breaking change. Điều này có thể được sử dụng để thiết kế các API tương thích, có khả năng phục hồi trước các thay đổi ở client hoặc server.
Ví dụ, hãy xem xét một API phía server như sau:
public int GetTotalLength(string param1)
{
return param1.Length;
}Client JavaScript gọi phương thức này bằng invoke như sau:
connection.invoke("GetTotalLength", "value1");Nếu bạn sau đó thêm tham số thứ hai vào phương thức server, các client cũ sẽ không cung cấp giá trị tham số này. Ví dụ:
public int GetTotalLength(string param1, string param2)
{
return param1.Length + param2.Length;
}Khi client cũ cố gọi phương thức này, nó sẽ nhận được lỗi như sau:
Microsoft.AspNetCore.SignalR.HubException: Failed to invoke 'GetTotalLength' due to an error on the server.
Trên server, bạn sẽ thấy thông báo log như sau:
System.IO.InvalidDataException: Invocation provides 1 argument(s) but target expects 2.
Client cũ chỉ gửi một tham số, nhưng API server mới hơn yêu cầu hai tham số. Sử dụng đối tượng tùy chỉnh làm tham số cho bạn sự linh hoạt hơn. Hãy thiết kế lại API ban đầu để sử dụng đối tượng tùy chỉnh:
public class TotalLengthRequest
{
public string Param1 { get; set; }
}
public int GetTotalLength(TotalLengthRequest req)
{
return req.Param1.Length;
}Bây giờ, client sử dụng một đối tượng để gọi phương thức:
connection.invoke("GetTotalLength", { param1: "value1" });Thay vì thêm tham số, hãy thêm thuộc tính vào đối tượng TotalLengthRequest:
public class TotalLengthRequest
{
public string Param1 { get; set; }
public string Param2 { get; set; }
}
public int GetTotalLength(TotalLengthRequest req)
{
var length = req.Param1.Length;
if (req.Param2 != null)
{
length += req.Param2.Length;
}
return length;
}Khi client cũ gửi một tham số duy nhất, thuộc tính Param2 thêm vào sẽ có giá trị null. Bạn có thể phát hiện thông điệp được gửi từ client cũ bằng cách kiểm tra Param2 có giá trị null không và áp dụng giá trị mặc định. Client mới có thể gửi cả hai tham số.
connection.invoke("GetTotalLength", { param1: "value1", param2: "value2" });Kỹ thuật tương tự cũng hoạt động cho các phương thức được định nghĩa ở phía client. Bạn có thể gửi một đối tượng tùy chỉnh từ phía server:
public async Task Broadcast(string message)
{
await Clients.All.SendAsync("ReceiveMessage", new
{
Message = message
});
}Ở phía client, bạn truy cập thuộc tính Message thay vì sử dụng tham số:
connection.on("ReceiveMessage", (req) => {
appendMessageToChatWindow(req.message);
});Nếu sau đó bạn quyết định thêm người gửi thông điệp vào payload (dữ liệu), hãy thêm một thuộc tính vào đối tượng:
public async Task Broadcast(string message)
{
await Clients.All.SendAsync("ReceiveMessage", new
{
Sender = Context.User.Identity.Name,
Message = message
});
}Các client cũ sẽ không mong đợi giá trị Sender, vì vậy chúng sẽ bỏ qua nó. Client mới có thể chấp nhận nó bằng cách cập nhật để đọc thuộc tính mới:
connection.on("ReceiveMessage", (req) => {
let message = req.message;
if (req.sender) {
message = req.sender + ": " + message;
}
appendMessageToChatWindow(message);
});Trong trường hợp này, client mới cũng có khả năng chịu lỗi với server cũ không cung cấp giá trị Sender. Vì server cũ sẽ không cung cấp giá trị Sender, client kiểm tra xem nó có tồn tại hay không trước khi truy cập nó.