Considérations sur la conception d’API SignalR

Par Andrew Stanton-Nurse

Cet article fournit des conseils pour la création d’API basées sur SignalR.

Utiliser des paramètres d’objet personnalisés pour garantir la compatibilité descendante

L’ajout de paramètres à une méthode hub SignalR (sur le client ou le serveur) est un changement cassant. Cela signifie que les anciens clients/serveurs obtiennent des erreurs lorsqu’ils essaient d’appeler la méthode sans le nombre approprié de paramètres. Toutefois, l’ajout de propriétés à un paramètre d’objet personnalisé n’est pas une modification cassant. Cela peut être utilisé pour concevoir des API compatibles qui sont résilientes aux modifications sur le client ou le serveur.

Par exemple, considérez une API côté serveur comme suit :

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

Le client JavaScript appelle cette méthode en utilisant invoke comme suit :

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

Si vous ajoutez ultérieurement un deuxième paramètre à la méthode serveur, les clients plus anciens ne fournissent pas cette valeur de paramètre. Par exemple :

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

Lorsque l’ancien client tente d’appeler cette méthode, il obtient une erreur semblable à celle-ci :

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

Sur le serveur, vous verrez un message de journal comme suit :

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

L’ancien client n’a envoyé qu’un seul paramètre, mais l’API de serveur plus récente en nécessitait deux. L’utilisation d’objets personnalisés en tant que paramètres vous offre plus de flexibilité. Reconcevoir l’API d’origine pour utiliser un objet personnalisé :

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

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

À présent, le client utilise un objet pour appeler la méthode :

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

Au lieu d’ajouter un paramètre, ajoutez une propriété à l’objet 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;
}

Lorsque l’ancien client envoie un seul paramètre, la propriété supplémentaire Param2 est laissée null. Vous pouvez détecter un message envoyé par un client plus ancien en vérifiant Param2 pour null et en appliquant une valeur par défaut. Un nouveau client peut envoyer les deux paramètres.

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

La même technique fonctionne pour les méthodes définies sur le client. Vous pouvez envoyer un objet personnalisé côté serveur :

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

Côté client, vous accédez à la propriété Message plutôt qu’à l’aide d’un paramètre :

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

Si vous décidez par la suite d’ajouter l’expéditeur du message à la charge utile, ajoutez une propriété à l’objet :

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

Les clients plus anciens ne s’attendent pas à la valeur Sender, donc ils l’ignorent. Un nouveau client peut l’accepter en le mettant à jour pour lire la nouvelle propriété :

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

Dans ce cas, le nouveau client est également tolérant à l’égard d’un ancien serveur qui ne fournit pas la valeurSender. Étant donné que l’ancien serveur ne fournit pas la valeur Sender, le client vérifie si elle existe avant d’y accéder.

Ressources supplémentaires