SignalR Рекомендации по проектированию API

Эндрю Стэнтон-Нёрс

В этой статье приводятся рекомендации по созданию API на основе SignalR.

Использование параметров пользовательского объекта для обеспечения обратной совместимости

Добавление параметров в метод концентратора SignalR (на клиенте или сервере) является критическим изменением. Это означает, что старые клиенты и серверы получат ошибки при попытке вызвать метод без соответствующего количества параметров. Однако добавление свойств в параметр пользовательского объекта не является критическим изменением. Это можно использовать для разработки совместимых API, устойчивых к изменениям на клиенте или сервере.

Например, рассмотрим серверный API, как показано ниже:

public int GetTotalLength(string param1)
{
    return param1.Length;
}

Клиент JavaScript вызывает этот метод invoke следующим образом:

connection.invoke("GetTotalLength", "value1");

Если позже добавить второй параметр в метод сервера, старые клиенты не будут предоставлять это значение параметра. Рассмотрим пример.

public int GetTotalLength(string param1, string param2)
{
    return param1.Length + param2.Length;
}

Когда старый клиент пытается вызвать этот метод, он получит ошибку следующим образом:

Microsoft.AspNetCore.SignalR.HubException: Failed to invoke 'GetTotalLength' due to an error on the server.

На сервере появится следующее сообщение журнала:

System.IO.InvalidDataException: Invocation provides 1 argument(s) but target expects 2.

Старый клиент отправил только один параметр, но более новый API сервера требует двух параметров. Использование пользовательских объектов в качестве параметров обеспечивает большую гибкость. Давайте перепроектируем исходный API для использования пользовательского объекта:

public class TotalLengthRequest
{
    public string Param1 { get; set; }
}

public int GetTotalLength(TotalLengthRequest req)
{
    return req.Param1.Length;
}

Теперь клиент использует объект для вызова метода:

connection.invoke("GetTotalLength", { param1: "value1" });

Вместо добавления параметра добавьте свойство в 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;
}

Когда старый клиент отправляет один параметр, дополнительное Param2 свойство останется null. Вы можете обнаружить сообщение, отправленное старым клиентом, проверив Param2 и null применив значение по умолчанию. Новый клиент может отправлять оба параметра.

connection.invoke("GetTotalLength", { param1: "value1", param2: "value2" });

Тот же метод работает для методов, определенных на клиенте. Вы можете отправить пользовательский объект на стороне сервера:

public async Task Broadcast(string message)
{
    await Clients.All.SendAsync("ReceiveMessage", new
    {
        Message = message
    });
}

На стороне клиента вы обращаетесь к свойству Message , а не используете параметр:

connection.on("ReceiveMessage", (req) => {
    appendMessageToChatWindow(req.message);
});

Если позже вы решите добавить отправителя сообщения в полезную нагрузку, добавьте свойство в объект:

public async Task Broadcast(string message)
{
    await Clients.All.SendAsync("ReceiveMessage", new
    {
        Sender = Context.User.Identity.Name,
        Message = message
    });
}

Старые клиенты не ожидают значения Sender и проигнорируют его. Новый клиент может принять это, обновив настройку для использования нового свойства.

connection.on("ReceiveMessage", (req) => {
    let message = req.message;
    if (req.sender) {
        message = req.sender + ": " + message;
    }
    appendMessageToChatWindow(message);
});

В этом случае новый клиент также допускает работу со старым сервером, который не предоставляет значение Sender. Поскольку старый сервер не предоставит значение Sender, клиент проверяет его наличие, прежде чем получить к нему доступ.

Дополнительные ресурсы