Considerações sobre o design de API do SignalR
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.