Partager via


Consommer un service réparti

Ce document décrit la totalité du code, des modèles et des avertissements pertinents pour l’acquisition, l’utilisation générale et l’élimination d’un service réparti. Pour apprendre à utiliser un service réparti particulier une fois acquis, recherchez la documentation particulière pour ce service réparti.

Pour tout le code de ce document, il est fortement recommandé d’activer la fonctionnalité des types de référence Nullable de C#.

Récupération d’un IServiceBroker

Pour acquérir un service réparti, vous devez d’abord avoir une instance de IServiceBroker. Lorsque votre code est en cours d’exécution dans le contexte de MEF (Managed Extensibility Framework) ou d’un VSPackage, vous voulez généralement le répartiteur de services global.

Les services répartis eux-mêmes doivent utiliser le IServiceBroker auquel ils sont affectés lorsque leur fabrique de service est appelée.

Répartiteur de services global

Visual Studio propose deux façons d’acquérir le répartiteur de services global.

Utilisez GlobalProvider.GetServiceAsync pour demander le SVsBrokeredServiceContainer :

IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
IServiceBroker serviceBroker = container.GetFullAccessServiceBroker();

À compter de Visual Studio 2022, le code exécuté dans une extension activée par le MEF peut importer le répartiteur de services global :

[Import(typeof(SVsFullAccessServiceBroker))]
IServiceBroker ServiceBroker { get; set; }

Remarquez l’argument typeof de l’attribut d’importation, qui est requis.

Chaque requête pour le IServiceBroker global produit une nouvelle instance d’un objet qui sert de fenêtre sur le conteneur de service réparti global. Cette instance unique du répartiteur de services permet à votre client de recevoir des événements AvailabilityChanged spécifiques à l’utilisation qu’il en fait. Nous recommandons que chaque client/classe de votre extension acquière son propre courtier de services en utilisant l’une des approches ci-dessus plutôt que d’acquérir une instance et de la partager avec l’ensemble de votre extension. Ce modèle favorise également les modèles de codage sécurisés, dans lesquels un service réparti ne doit pas utiliser le répartiteur de services global.

Important

En général, les implémentations de IServiceBroker n’implémentent pas IDisposable, mais ces objets ne peuvent pas être collectés tant qu’il existe des gestionnaires AvailabilityChanged. Veillez à équilibrer l’ajout et la suppression des gestionnaires d’événements, en particulier lorsque le code peut ignorer le répartiteur de services pendant la durée de vie du processus.

Répartiteurs de services spécifiques au contexte

L’utilisation du répartiteur de services approprié constitue une exigence importante du modèle de sécurité des services répartis, en particulier dans le contexte des sessions Live Share.

Les services répartis sont activés avec leur propre IServiceBroker et doivent utiliser cette dernière pour tous leurs propres besoins de services répartis, y compris les services fournis avec Proffer. Ce code fournit un BrokeredServiceFactory qui reçoit un répartiteur de services à utiliser par le service réparti instancié.

Récupération d’un proxy de service réparti

La récupération d’un service réparti est généralement effectuée avec la méthode GetProxyAsync.

La méthode GetProxyAsync nécessite une ServiceRpcDescriptor et une interface de service en tant qu’argument de type générique. La documentation sur le service réparti que vous demandez doit indiquer où obtenir le descripteur et l’interface à utiliser. Pour les services répartis inclus dans Visual Studio, l’interface à utiliser doit apparaître dans la documentation IntelliSense sur le descripteur. Découvrez comment rechercher des descripteurs pour les services répartis Visual Studio dans Découverte des services répartis disponibles.

IServiceBroker broker; // Acquired as described earlier in this topic
IMyService? myService = await broker.GetProxyAsync<IMyService>(serviceDescriptor, cancellationToken);
using (myService as IDisposable)
{
    Assumes.Present(myService); // Throw if service was not available
    await myService.SayHelloAsync();
}

Comme pour toutes les demandes de service réparti, le code précédent active une nouvelle instance d’un service réparti. Après avoir utilisé le service, le code précédent supprime le proxy lorsque l’exécution quitte le bloc using.

Important

Chaque proxy récupéré doit être supprimé, même si l’interface de service ne dérive pas de IDisposable. La suppression est importante, car le proxy a souvent des ressources d’E/S qui l’empêchent d’être récupérés par le récupérateur de mémoire. L’élimination met fin aux E/S, ce qui permet au proxy d’être récupéré par le récupérateur de mémoire. Utilisez une conversion conditionnelle en IDisposable pour la suppression et préparez-vous à l’échec de la conversion pour éviter une exception pour les proxies null ou les proxies qui n’implémentent pas réellement IDisposable.

Veillez à installer le dernier package NuGet Microsoft.ServiceHub.Analyzers et à maintenir les règles d’analyseur ISBxxxx activées pour empêcher ces fuites.

La suppression du proxy entraîne celle du service réparti qui a été dédié à ce client.

Si votre code nécessite un service réparti et ne peut pas terminer son travail lorsque le service n’est pas disponible, vous pouvez afficher une boîte de dialogue d’erreur destinées à l’utilisateur si le code possède l’expérience utilisateur plutôt que de lever une exception.

Cibles RPC du client

Certains services répartis acceptent ou nécessitent une cible RPC client (appel de procédure distante) pour les « rappels ». Une telle option ou exigence doit se trouver dans la documentation de ce service réparti particulier. Pour les services répartis Visual Studio, ces informations doivent être incluses dans la documentation IntelliSense sur le descripteur.

Dans ce cas, un client peut en fournir un en utilisant ServiceActivationOptions.ClientRpcTarget comme suit :

IMyService? myService = await broker.GetProxyAsync<IMyService>(
    serviceDescriptor,
    new ServiceActivationOptions
    {
        ClientRpcTarget = new MyCallbackObject(),
    },
    cancellationToken);

Invocation du proxy client

Le résultat de la demande d’un service réparti est une instance de l’interface de service implémentée par un proxy. Ce proxy transmet les appels et les événements dans chaque direction, avec quelques différences de comportement importantes par rapport à ce que l’on pourrait attendre en appelant le service directement.

Modèle Observateur

Si le contrat de service prend des paramètres de type IObserver<T>, vous pouvez en savoir plus sur la façon de construire un tel type en consultant Comment implémenter un observateur.

Une ActionBlock<TInput> peut être adaptée pour implémenter IObserver<T> avec la méthode d’extension AsObserver. La classe System.Reactive.Observer du framework Réactive est une autre solution pour implémenter soi-même l’interface.

Exceptions levées à partir du proxy

  • Attendez-vous à ce que RemoteInvocationException soit levé pour toute exception levée à partir du service réparti. L’exception d’origine se trouve dans le InnerException. Il s’agit d’un comportement naturel pour un service hébergé à distance, car il s’agit du comportement de JsonRpc. Lorsque le service est local, le proxy local encapsule toutes les exceptions de la même manière, de sorte que le code client ne dispose que d’un seul chemin d’exception qui fonctionne pour les services locaux et distants.
    • Vérifiez la propriété ErrorCode si la documentation du service suggère que des codes spécifiques sont définis en fonction de conditions spécifiques sur lesquelles vous pouvez vous baser.
    • Un ensemble plus large d’erreurs est communiqué en interceptant RemoteRpcException, qui est le type de base pour le RemoteInvocationException.
  • Attendez-vous à ce que ConnectionLostException soit levé à partir d’un appel lorsque la connexion à un service distant s’interrompt ou que le processus hébergeant le service plante. Ce problème se pose surtout lorsque le service peut être acquis à distance.

Mise en cache du proxy

L’activation d’un service réparti et du proxy associé entraîne certains frais, en particulier lorsque le service provient d’un processus distant. Lorsque l’utilisation fréquente d’un service réparti justifie la mise en cache du proxy à travers de nombreux appels à une classe, le proxy peut être stocké dans un champ de cette classe. La classe conteneur doit être éliminable et supprimer le proxy à l’intérieur de sa méthode Dispose. Prenons cet exemple :

class MyExtension : IDisposable
{
    readonly IServiceBroker serviceBroker;
    IMyService? serviceProxy;

    internal MyExtension(IServiceBroker serviceBroker)
    {
        this.serviceBroker = serviceBroker;
    }

    async Task SayHiAsync(CancellationToken cancellationToken)
    {
        if (this.serviceProxy is null)
        {
            this.serviceProxy = await this.serviceBroker.GetProxyAsync<IMyService>(serviceDescriptor, cancellationToken);
            Assumes.Present(this.serviceProxy);
        }

        await this.serviceProxy.SayHelloAsync();
    }

    public void Dispose()
    {
        (this.serviceProxy as IDisposable)?.Dispose();
    }
}

Le code précédent est à peu près correct, mais il ne tient pas compte des conditions de concurrence entre Dispose et SayHiAsync. Le code ne tient pas non plus compte des événements AvailabilityChanged qui devraient conduire à l’élimination du proxy précédemment acquis et à sa réacquisition la prochaine fois qu’il sera nécessaire.

La classe ServiceBrokerClient est conçue pour gérer ces conditions de concurrence et d’invalidation pour vous aider à simplifier votre propre code. Prenons l’exemple suivant, qui met en cache le proxy à l’aide de cette classe d’assistance :

class MyExtension : IDisposable
{
    readonly ServiceBrokerClient serviceBrokerClient;

    internal MyExtension(IServiceBroker serviceBroker)
    {
        this.serviceBrokerClient = new ServiceBrokerClient(serviceBroker);
    }

    async Task SayHiAsync(CancellationToken cancellationToken)
    {
        using var rental = await this.serviceBrokerClient.GetProxyAsync<IMyService>(descriptor, cancellationToken);
        Assumes.Present(rental.Proxy); // Throw if service is not available
        IMyService myService = rental.Proxy;
        await myService.SayHelloAsync();
    }

    public void Dispose()
    {
        // Disposing the ServiceBrokerClient will dispose of all proxies
        // when their rentals are released.
        this.serviceBrokerClient.Dispose();
    }
}

Le code précédent est toujours responsable de la suppression du ServiceBrokerClient et de chaque location d’un proxy. Les conditions de concurrence entre l’élimination et l’utilisation du proxy sont gérées par l’objet ServiceBrokerClient, qui supprimera chaque proxy mis en cache au moment de sa propre suppression ou lorsque la dernière location de ce proxy aura été libérée, selon ce qui survient en dernier.

Avertissements importants concernant le ServiceBrokerClient

  • ServiceBrokerClient indexe les proxies mis en cache en se basant uniquement sur le ServiceMoniker. Si vous transférez ServiceActivationOptions et qu’un proxy mis en cache est déjà disponible, ce dernier est renvoyé sans utiliser le ServiceActivationOptions, ce qui entraîne un comportement inattendu du service. Dans un tel cas, envisagez d’utiliser IServiceBroker directement.

  • Ne stockez pas le ServiceBrokerClient.Rental<T> obtenue à partir de ServiceBrokerClient.GetProxyAsync dans un champ. Le proxy est déjà mis en cache au-delà de l’étendue d’une méthode par le ServiceBrokerClient. Si vous avez besoin d’un contrôle accru sur la durée de vie du proxy, en particulier en cas de réacquisition due à un événement AvailabilityChanged, utilisez plutôt IServiceBroker directement et stockez le proxy de service dans un champ.

  • Créez et stockez ServiceBrokerClient dans un champ plutôt qu’une variable locale. Si vous le créez et l’utilisez comme variable locale dans une méthode, il n’apporte aucune valeur ajoutée par rapport à l’utilisation directe de IServiceBroker, mais vous devez désormais supprimer deux objets (le client et la location) au lieu d’un seul (le service).

Choisir entre IServiceBroker et ServiceBrokerClient

Les deux sont conviviaux, et la valeur par défaut doit probablement être IServiceBroker.

Catégorie IServiceBroker ServiceBrokerClient
Convivial Oui Oui
Nécessite la suppression Non Oui
Gère la durée de vie du proxy Non. Le propriétaire doit supprimer le proxy lorsqu’il a terminé de l’utiliser. Oui, ils sont conservés en vie et réutilisés tant qu’ils sont valides.
Applicable aux services sans état Oui Oui
Applicable aux services avec état Oui Non
Approprié lorsque des gestionnaires d’événements sont ajoutés au proxy Oui Non
Événement à notifier lorsque l’ancien proxy est invalidé AvailabilityChanged Invalidated

ServiceBrokerClient fournit un moyen pratique pour obtenir une réutilisation rapide et fréquente d’un proxy, sans se soucier du fait que le service sous-jacent soit modifié entre les opérations de haut niveau. Mais si vous vous souciez de ces questions et que vous souhaitez gérer vous-même la durée de vie de vos proxys, ou si vous avez besoin de gestionnaires d’événements (ce qui implique que vous devez gérer la durée de vie du proxy), vous devez utiliser IServiceBroker.

Résilience aux interruptions de service

Il existe quelques types d’interruptions de service possibles avec les services répartis :

Échecs d’activation d’un service réparti

Lorsqu’une demande de service réparti peut être satisfaite par un service disponible, mais que la fabrique de service lève une exception non gérée, une ServiceActivationFailedException est renvoyée au client afin qu’il puisse comprendre et signaler l’échec à l’utilisateur.

Lorsqu’une demande de service réparti ne peut pas être mise en correspondance avec un service disponible, null est renvoyée au client. Dans ce cas, AvailabilityChanged sera lancée au moment et si ce service est disponible ultérieurement.

La demande de service peut être refusée, non pas parce que le service n’est pas disponible, mais parce que la version proposée est inférieure à la version demandée. Votre plan de secours peut inclure une nouvelle tentative de demande de service avec des versions inférieures dont votre client connaît l’existence et avec lesquelles il est capable d’interagir.

Si/quand la latence due à toutes les vérifications de version échouées devient perceptible, le client peut demander le VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest pour avoir une idée complète des services et des versions disponibles à partir d’une source distante.

Gestion des connexions interrompues

Un proxy de service réparti correctement acquis peut échouer en raison d’une connexion interrompue ou d’un incident dans le processus qui l’héberge. Après une telle interruption, tout appel effectué sur ce proxy entraîne la levée de ConnectionLostException.

Un client de service réparti peut détecter et réagir de manière proactive à ces interruption de connexion en gérant l’événement Disconnected. Pour atteindre cet événement, un proxy doit être converti en IJsonRpcClientProxy pour obtenir l’objet JsonRpc. Cette distribution doit être effectuée de manière conditionnelle afin d’échouer de façon appropriée lorsque le service est local.

if (this.myService is IJsonRpcClientProxy clientProxy)
{
    clientProxy.JsonRpc.Disconnected += JsonRpc_Disconnected;
}

void JsonRpc_Disconnected(object? sender, JsonRpcDisconnectedEventArgs args)
{
    if (args.Reason == DisconnectedReason.RemotePartyTerminated)
    {
        // consider reacquisition of the service.
    }
}

Gestion des changement de disponibilité du service

Les clients de services répartis peuvent recevoir des notifications leur indiquant quand ils doivent demander un service réparti qu’ils ont précédemment demandé en gérant l’événement AvailabilityChanged. Les gestionnaires de cet événement doivent être ajoutés avant de demander un service réparti, pour s’assurer qu’un événement déclenché peu après l’envoi d’une demande de service n’est pas perdu en raison d’une condition de concurrence.

Lorsqu’un service réparti n’est demandé que pour la durée d’exécution d’une méthode asynchrone, il n’est pas recommandé de gérer cet événement. Ce dernier concerne surtout les clients qui conservent leur proxy pendant de longues périodes, de sorte qu’ils doivent compenser les changements de service et qu’ils sont en mesure de rafraîchir leur proxy.

Cet événement peut être déclenché par n’importe quel thread, éventuellement en même temps qu’un code qui utilise un service décrit par l’événement.

Plusieurs changements d’état peuvent entraîner le déclenchement de cet événement, notamment :

  • Ouverture ou fermeture d’une solution ou d’un dossier.
  • Lancement d’une session Live Share.
  • Un service réparti inscrit dynamiquement qui vient d’être découvert.

Un service réparti impacté entraîne uniquement l’apparition de cet événement pour les clients qui ont précédemment demandé ce service, que cette demande ait été satisfaite ou non.

L’événement est déclenché au maximum une fois par service après chaque demande de ce service. Par exemple, si le client demande le service A et le service B connaît un changement de disponibilité, aucun événement ne sera signalé à ce client. Ultérieurement, lorsque le service A connaît un changement de disponibilité, le client reçoit l’événement. Si le client ne redemande pas le service A, les changements de disponibilité ultérieurs pour A ne donneront pas lieu à d’autres notifications pour ce client. Lorsque le client demande A de nouveau, il devient éligible pour recevoir la notification suivante concernant ce service.

L’événement est déclenché lorsqu’un service devient disponible, n’est plus disponible ou connaît un changement d’implémentation qui exige que tous les clients du service antérieurs puissent le demander à nouveau.

Le ServiceBrokerClient gère automatiquement les événements de changement de disponibilité concernant les proxies mis en cache en supprimant les anciens proxies lorsque les locations ont été renvoyées et en demandant une nouvelle instance du service lorsque et si son propriétaire en fait la demande. Cette classe peut simplifier considérablement votre code lorsque le service est sans état et ne nécessite pas que votre code attache des gestionnaires d’événements au proxy.

Récupération d’un canal de service réparti

Bien que l’accès à un service réparti à travers un proxy soit la technique la plus courante et la plus pratique, dans des scénarios avancés, il peut s’avérer préférable ou nécessaire de demander un canal vers ce service, afin que le client puisse contrôler directement le RPC ou communiquer directement tout autre type de données.

Un canal vers le service réparti peut être obtenu via la méthode GetPipeAsync. Cette méthode prend un ServiceMoniker au lieu d’un ServiceRpcDescriptor, car les comportements du RPC fournis par un descripteur ne sont pas obligatoires. Lorsque vous avez un descripteur, vous pouvez obtenir le moniker à partir de celui-ci via la propriété ServiceRpcDescriptor.Moniker.

Bien que les canaux soient liés aux E/S, ils ne sont pas éligibles pour le nettoyage de la mémoire. Évitez les fuites de mémoire en mettant toujours fin à ces canaux lorsqu’ils ne sont plus utilisés.

Dans l’extrait de code suivant, un service réparti est activé et le client dispose d’un canal direct vers celui-ci. Le client envoie ensuite le contenu d’un fichier au service et se déconnecte.

async Task SendMovieAsync(string movieFilePath, CancellationToken cancellationToken)
{
    IServiceBroker serviceBroker;
    IDuplexPipe? pipe = await serviceBroker.GetPipeAsync(serviceMoniker, cancellationToken);
    if (pipe is null)
    {
        throw new InvalidOperationException($"The brokered service '{serviceMoniker}' is not available.");
    }

    try
    {
        // Open the file optimized for async I/O
        using FileStream fs = new FileStream(movieFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
        await fs.CopyToAsync(pipe.Output.AsStream(), cancellationToken);
    }
    catch (Exception ex)
    {
        // Complete the pipe, passing through the exception so the remote side understands what went wrong.
        await pipe.Input.CompleteAsync(ex);
        await pipe.Output.CompleteAsync(ex);
        throw;
    }
    finally
    {
        // Always complete the pipe after successfully using the service.
        await pipe.Input.CompleteAsync();
        await pipe.Output.CompleteAsync();
    }
}

Test des clients des services répartis

Les services répartis constituent une dépendance raisonnable à simuler lorsque vous testez votre extension. Lors de la simulation d’un service réparti, nous vous recommandons d’utiliser un framework de simulation qui implémente l’interface en votre nom et injecte du code dont vous avez besoin pour les membres spécifiques que votre client appellera. Ainsi, vos tests peuvent continuer à se compiler et à s’exécuter sans interruption lorsque des membres sont ajoutés à l’interface de service réparti.

Lorsque vous utilisez Microsoft.VisualStudio.Sdk.TestFramework pour tester votre extension, votre test peut inclure du code standard pour proposer un service fictif que votre code client peut interroger et utiliser. Par exemple, supposons que vous vouliez simuler le service réparti VisualStudioServices.VS2022.FileSystem dans vos tests. Vous pouvez proposer la simulation avec ce code :

IBrokeredServiceContainer sbc = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
Mock<IFileSystem> mockFileSystem = new Mock<IFileSystem>();
sbc.Proffer(VisualStudioServices.VS2022.FileSystem, (ServiceMoniker moniker, ServiceActivationOptions options, IServiceBroker serviceBroker, CancellationToken cancellationToken) => new ValueTask<object?>(mockFileSystem.Object));

Le conteneur de service réparti fictif n’exige pas qu’un service proposé soit d’abord enregistré, comme c’est le cas de Visual Studio.

Votre code en cours de test peut acquérir le service réparti comme d’habitude, sauf que dans le cadre du test, il obtiendra votre simulacre au lieu du véritable service qu’il obtiendrait s’il s’exécutait sous Visual Studio :

IBrokeredServiceContainer sbc = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
IServiceBroker serviceBroker = sbc.GetFullAccessServiceBroker();
IFileSystem? proxy = await serviceBroker.GetProxyAsync<IFileSystem>(VisualStudioServices.VS2022.FileSystem);
using (proxy as IDisposable)
{
    Assumes.Present(proxy);
    await proxy.DeleteAsync(new Uri("file://some/file"), recursive: false, null, this.TimeoutToken);
}