Considerações sobre o design de API do SignalR

Por Andrew Stanton-Nurse

Este artigo fornece diretrizes para a criação de APIs baseadas no SignalR.

Use parâmetros de objeto personalizados para garantir a compatibilidade com versões anteriores

Adicionar parâmetros a um método de hub SignalR (no cliente ou no servidor) é uma alteração interruptiva. Isso significa que clientes ou servidores mais antigos receberão erros quando tentarem invocar o método sem o número apropriado de parâmetros. No entanto, adicionar propriedades a um parâmetro de objeto personalizado não é uma alteração interruptiva. Isso pode ser usado para criar APIs compatíveis que são resilientes a alterações no cliente ou no servidor.

Por exemplo, considere uma API do lado do servidor como a seguinte:

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

O cliente JavaScript chama esse método por meio deinvoke da seguinte maneira:

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

Se posteriormente você adicionar um segundo parâmetro ao método de servidor, os clientes mais antigos não fornecerão esse valor de parâmetro. Por exemplo:

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

Quando o cliente antigo tentar invocar esse método, ele receberá um erro como este:

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

No servidor, você verá uma mensagem de log como essa:

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

O cliente antigo enviou apenas um parâmetro, mas a API do servidor mais recente exigia dois parâmetros. Usar objetos personalizados como parâmetros oferece mais flexibilidade. Vamos reprojetar a API original para usar um objeto personalizado:

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

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

Agora, o cliente usa um objeto para chamar o método :

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

Em vez de adicionar um parâmetro, adicione uma propriedade ao objeto 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;
}

Quando o cliente antigo enviar um único parâmetro, a propriedade extra Param2 será deixada null. Você pode detectar uma mensagem enviada por um cliente mais antigo verificando o Param2 para ver se o null está implantando e aplicando um valor padrão. Um novo cliente pode enviar ambos os parâmetros.

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

A mesma técnica funciona para métodos definidos no cliente. Você pode enviar um objeto personalizado do lado do servidor:

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

No lado do cliente, você acessa a propriedade Message em vez de usar um parâmetro:

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

Se posteriormente você decidir adicionar o remetente da mensagem ao conteúdo, adicione uma propriedade ao objeto :

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

Os clientes mais antigos não estarão esperando o valor Sender, portanto, eles o ignorarão. Um novo cliente pode aceitá-lo atualizando para ler a nova propriedade:

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

Nesse caso, o novo cliente também é tolerante a um servidor antigo que não fornece o valor Sender. Como o servidor antigo não fornecerá o valor Sender, o cliente verifica se ele existe antes de acessá-lo.

Recursos adicionais