Partager via


Injection de dépendances dans SignalR

par Patrick Fletcher

Avertissement

Cette documentation ne concerne pas la dernière version de SignalR. Consultez ASP.NET Core SignalR.

Versions logicielles utilisées dans cette rubrique

Versions précédentes de cette rubrique

Pour plus d’informations sur les versions antérieures de SignalR, consultez Anciennes versions de SignalR.

Questions et commentaires

Laissez vos commentaires sur la façon dont vous avez aimé ce tutoriel et sur ce que nous pourrions améliorer dans les commentaires en bas de la page. Si vous avez des questions qui ne sont pas directement liées au tutoriel, vous pouvez les publier sur le forum ASP.NET SignalR ou StackOverflow.com.

L’injection de dépendances permet de supprimer les dépendances codées en dur entre des objets, ce qui facilite le remplacement des dépendances d’un objet, soit pour le test (à l’aide d’objets fictifs) soit pour modifier le comportement d’exécution. Ce tutoriel montre comment effectuer l’injection de dépendances sur des hubs SignalR. Il montre également comment utiliser des conteneurs IoC avec SignalR. Un conteneur IoC est une infrastructure générale pour l’injection de dépendances.

Qu’est-ce que l’injection de dépendances ?

Ignorez cette section si vous êtes déjà familiarisé avec l’injection de dépendances.

L’injection de dépendances (DI) est un modèle dans lequel les objets ne sont pas responsables de la création de leurs propres dépendances. Voici un exemple simple pour motiver l’AI. Supposons que vous ayez un objet qui doit journaliser les messages. Vous pouvez définir une interface de journalisation :

interface ILogger 
{
    void LogMessage(string message);
}

Dans votre objet, vous pouvez créer un ILogger pour journaliser les messages :

// Without dependency injection.
class SomeComponent
{
    ILogger _logger = new FileLogger(@"C:\logs\log.txt");

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

Cela fonctionne, mais ce n’est pas la meilleure conception. Si vous souhaitez remplacer par FileLogger une autre ILogger implémentation, vous devez modifier SomeComponent. En supposant que de nombreux autres objets utilisent FileLogger, vous devrez tous les modifier. Ou si vous décidez d’effectuer FileLogger un singleton, vous devez également apporter des modifications dans l’application.

Une meilleure approche consiste à « injecter » un ILogger dans l’objet, par exemple à l’aide d’un argument de constructeur :

// With dependency injection.
class SomeComponent
{
    ILogger _logger;

    // Inject ILogger into the object.
    public SomeComponent(ILogger logger)
    {
        if (logger == null)
        {
            throw new NullReferenceException("logger");
        }
        _logger = logger;
    }

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

À présent, l’objet n’est pas responsable de la sélection de l’objet ILogger à utiliser. Vous pouvez changer ILogger d’implémentations sans modifier les objets qui en dépendent.

var logger = new TraceLogger(@"C:\logs\log.etl");
var someComponent = new SomeComponent(logger);

Ce modèle est appelé injection de constructeur. Un autre modèle est l’injection de setter, où vous définissez la dépendance par le biais d’une méthode ou d’une propriété setter.

Injection de dépendances simple dans SignalR

Considérez l’application Chat du didacticiel Prise en main avec SignalR. Voici la classe hub de cette application :

public class ChatHub : Hub
{
    public void Send(string name, string message)
    {
        Clients.All.addMessage(name, message);
    }
}

Supposons que vous souhaitiez stocker des messages de conversation sur le serveur avant de les envoyer. Vous pouvez définir une interface qui extrait cette fonctionnalité et utiliser l’idY pour injecter l’interface dans la ChatHub classe.

public interface IChatRepository
{
    void Add(string name, string message);
    // Other methods not shown.
}

public class ChatHub : Hub
{
    private IChatRepository _repository;

    public ChatHub(IChatRepository repository)
    {
        _repository = repository;
    }

    public void Send(string name, string message)
    {
        _repository.Add(name, message);
        Clients.All.addMessage(name, message);
    }

Le seul problème est qu’une application SignalR ne crée pas directement des hubs ; SignalR les crée pour vous. Par défaut, SignalR s’attend à ce qu’une classe hub ait un constructeur sans paramètre. Toutefois, vous pouvez facilement inscrire une fonction pour créer des instances hub et utiliser cette fonction pour effectuer une di. Inscrivez la fonction en appelant GlobalHost.DependencyResolver.Register.

public void Configuration(IAppBuilder app)
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    App.MapSignalR();

    // ...
}

SignalR appelle désormais cette fonction anonyme chaque fois qu’il a besoin de créer un ChatHub instance.

Conteneurs IoC

Le code précédent est correct pour les cas simples. Mais vous deviez quand même écrire ceci :

... new ChatHub(new ChatMessageRepository()) ...

Dans une application complexe avec de nombreuses dépendances, vous devrez peut-être écrire une grande partie de ce code de « câblage ». Ce code peut être difficile à gérer, en particulier si les dépendances sont imbriquées. Il est également difficile d’effectuer un test unitaire.

Une solution consiste à utiliser un conteneur IoC. Un conteneur IoC est un composant logiciel qui est responsable de la gestion des dépendances. Vous inscrivez des types avec le conteneur, puis utilisez le conteneur pour créer des objets. Le conteneur détermine automatiquement les relations de dépendance. De nombreux conteneurs IoC vous permettent également de contrôler des éléments tels que la durée de vie et l’étendue des objets.

Notes

« IoC » signifie « inversion du contrôle », qui est un modèle général où une infrastructure appelle dans le code d’application. Un conteneur IoC construit vos objets pour vous, ce qui « inverse » le flux de contrôle habituel.

Utilisation de conteneurs IoC dans SignalR

L’application Chat est probablement trop simple pour tirer parti d’un conteneur IoC. Examinons plutôt l’exemple StockTicker .

L’exemple StockTicker définit deux classes main :

  • StockTickerHub: classe hub, qui gère les connexions clientes.
  • StockTicker: singleton qui contient les cours des actions et les met à jour régulièrement.

StockTickerHub contient une référence au StockTicker singleton, tandis que StockTicker contient une référence au IHubConnectionContext pour le StockTickerHub. Il utilise cette interface pour communiquer avec StockTickerHub les instances. (Pour plus d’informations, consultez Diffusion de serveur avec ASP.NET SignalR.)

Nous pouvons utiliser un conteneur IoC pour démêler ces dépendances un peu. Tout d’abord, nous allons simplifier les StockTickerHub classes et StockTicker . Dans le code suivant, j’ai commenté les parties dont nous n’avons pas besoin.

Supprimez le constructeur sans paramètre de StockTickerHub. Au lieu de cela, nous utiliserons toujours di pour créer le hub.

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly StockTicker _stockTicker;

    //public StockTickerHub() : this(StockTicker.Instance) { }

    public StockTickerHub(StockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

    // ...

Pour StockTicker, supprimez le singleton instance. Plus tard, nous utiliserons le conteneur IoC pour contrôler la durée de vie de StockTicker. En outre, rendez le constructeur public.

public class StockTicker
{
    //private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(
    //    () => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

    // Important! Make this constructor public.
    public StockTicker(IHubConnectionContext<dynamic> clients)
    {
        if (clients == null)
        {
            throw new ArgumentNullException("clients");
        }

        Clients = clients;
        LoadDefaultStocks();
    }

    //public static StockTicker Instance
    //{
    //    get
    //    {
    //        return _instance.Value;
    //    }
    //}

Ensuite, nous pouvons refactoriser le code en créant une interface pour StockTicker. Nous allons utiliser cette interface pour dissocier le StockTickerHub de la StockTicker classe.

Visual Studio facilite ce type de refactorisation. Ouvrez le fichier StockTicker.cs, cliquez avec le bouton droit sur la StockTicker déclaration de classe, puis sélectionnez Refactoriser ... Extraire l’interface.

Capture d’écran du menu déroulant clic droit sur le code Visual Studio avec les options Refractor et Extraire l’interface mises en évidence.

Dans la boîte de dialogue Extraire l’interface , cliquez sur Sélectionner tout. Conservez les autres valeurs par défaut. Cliquez sur OK.

Capture d’écran de la boîte de dialogue Extraire l’interface avec l’option Sélectionner tout en surbrillance, toutes les options disponibles étant sélectionnées.

Visual Studio crée une interface nommée IStockTicker, et modifie StockTicker également pour dériver de IStockTicker.

Ouvrez le fichier IStockTicker.cs et remplacez l’interface par public.

public interface IStockTicker
{
    void CloseMarket();
    IEnumerable<Stock> GetAllStocks();
    MarketState MarketState { get; }
    void OpenMarket();
    void Reset();
}

Dans la StockTickerHub classe , remplacez les deux instances de par StockTickerIStockTicker:

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly IStockTicker _stockTicker;

    public StockTickerHub(IStockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

La création d’une IStockTicker interface n’est pas strictement nécessaire, mais je voulais montrer comment l’ID peut aider à réduire le couplage entre les composants de votre application.

Ajouter la bibliothèque Ninject

Il existe de nombreux conteneurs IoC open source pour .NET. Pour ce tutoriel, j’utiliserai Ninject. (D’autres bibliothèques populaires incluent Castle Windsor, Spring.Net, Autofac, Unity et StructureMap.)

Utilisez le Gestionnaire de package NuGet pour installer la bibliothèque Ninject. Dans Visual Studio, dans le menu Outils, sélectionnez NuGet Package ManagerPackage Manager> Console. Dans la fenêtre Console du Gestionnaire de package, entrez la commande suivante :

Install-Package Ninject -Version 3.0.1.10

Remplacez le résolveur de dépendances SignalR

Pour utiliser Ninject dans SignalR, créez une classe qui dérive de DefaultDependencyResolver.

internal class NinjectSignalRDependencyResolver : DefaultDependencyResolver
{
    private readonly IKernel _kernel;
    public NinjectSignalRDependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }

    public override object GetService(Type serviceType)
    {
        return _kernel.TryGet(serviceType) ?? base.GetService(serviceType);
    }

    public override IEnumerable<object> GetServices(Type serviceType)
    {
        return _kernel.GetAll(serviceType).Concat(base.GetServices(serviceType));
    }
}

Cette classe remplace les méthodes GetService et GetServices de DefaultDependencyResolver. SignalR appelle ces méthodes pour créer différents objets au moment de l’exécution, notamment des instances hub, ainsi que divers services utilisés en interne par SignalR.

  • La méthode GetService crée une instance unique d’un type. Remplacez cette méthode pour appeler la méthode TryGet du noyau Ninject. Si cette méthode retourne null, revenez au programme de résolution par défaut.
  • La méthode GetServices crée une collection d’objets d’un type spécifié. Remplacez cette méthode pour concaténer les résultats de Ninject avec les résultats du programme de résolution par défaut.

Configurer des liaisons Ninject

Nous allons maintenant utiliser Ninject pour déclarer des liaisons de type.

Ouvrez la classe Startup.cs de votre application (que vous avez créée manuellement conformément aux instructions du package dans , ou qui a été créée en readme.txtajoutant l’authentification à votre projet). Dans la Startup.Configuration méthode, créez le conteneur Ninject, que Ninject appelle le noyau.

var kernel = new StandardKernel();

Créez un instance de notre programme de résolution de dépendances personnalisé :

var resolver = new NinjectSignalRDependencyResolver(kernel);

Créez une liaison pour IStockTicker comme suit :

kernel.Bind<IStockTicker>()
    .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
    .InSingletonScope();  // Make it a singleton object.

Ce code dit deux choses. Tout d’abord, chaque fois que l’application a besoin d’un IStockTicker, le noyau doit créer un instance de StockTicker. Deuxièmement, la StockTicker classe doit être créée en tant qu’objet singleton. Ninject crée une instance de l’objet et retourne le même instance pour chaque requête.

Créez une liaison pour IHubConnectionContext comme suit :

kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                     ).WhenInjectedInto<IStockTicker>();

Ce code crée une fonction anonyme qui retourne un IHubConnection. La méthode WhenIn éject indique à Ninject d’utiliser cette fonction uniquement lors de la création d’instances IStockTicker . La raison en est que SignalR crée des instances IHubConnectionContext en interne, et nous ne voulons pas remplacer la façon dont SignalR les crée. Cette fonction s’applique uniquement à notre StockTicker classe.

Transmettez le programme de résolution de dépendances à la méthode MapSignalR en ajoutant une configuration hub :

var config = new HubConfiguration();
config.Resolver = resolver;
Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config);

Mettez à jour la méthode Startup.ConfigureSignalR dans la classe Startup de l’exemple avec le nouveau paramètre :

public static void ConfigureSignalR(IAppBuilder app, HubConfiguration config)
{
    app.MapSignalR(config);
}

À présent, SignalR utilise le programme de résolution spécifié dans MapSignalR, au lieu du programme de résolution par défaut.

Voici la liste complète du code pour Startup.Configuration.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888

        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
            .InSingletonScope();  // Make it a singleton object.

        kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                    ).WhenInjectedInto<IStockTicker>();

        var config = new HubConfiguration();
        config.Resolver = resolver;
        Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config);
    }
}

Pour exécuter l’application StockTicker dans Visual Studio, appuyez sur F5. Dans la fenêtre du navigateur, accédez à http://localhost:*port*/SignalR.Sample/StockTicker.html.

Capture d’écran d’une fenêtre de navigateur Internet Explorer affichant la page web A S P dot NET Signal R Stock Ticker Sample.

L’application a exactement les mêmes fonctionnalités qu’auparavant. (Pour obtenir une description, consultez Diffusion de serveur avec ASP.NET SignalR.) Nous n’avons pas modifié le comportement; a simplement facilité le test, la maintenance et l’évolution du code.