Injektáž závislostí v centrech SignalR

Patrick Fletcher

Upozornění

Tato dokumentace není určená pro nejnovější verzi SignalR. Podívejte se na ASP.NET Core SignalR.

Verze softwaru použité v tomto tématu

Předchozí verze tohoto tématu

Informace o starších verzích služby SignalR najdete v tématu Starší verze služby SignalR.

Dotazy a komentáře

Pošlete nám prosím zpětnou vazbu k tomu, jak se vám tento kurz líbil a co bychom mohli vylepšit v komentářích v dolní části stránky. Pokud máte dotazy, které přímo nesouvisejí s kurzem, můžete je publikovat na fóru ASP.NET SignalR nebo StackOverflow.com.

Injektáž závislostí je způsob, jak odebrat pevně zakódované závislosti mezi objekty, což usnadňuje nahrazení závislostí objektu, a to buď pro testování (pomocí napodobených objektů), nebo změnu chování za běhu. Tento kurz ukazuje, jak provádět injektáž závislostí v centrech SignalR. Ukazuje také, jak používat kontejnery IoC se službou SignalR. Kontejner IoC je obecná architektura pro injektáž závislostí.

Co je injektáž závislostí?

Pokud už injektáž závislostí znáte, tuto část přeskočte.

Injektáž závislostí (DI) je vzor, ve kterém objekty nejsou zodpovědné za vytváření vlastních závislostí. Tady je jednoduchý příklad, jak di motivovat. Předpokládejme, že máte objekt, který potřebuje protokolovat zprávy. Můžete definovat rozhraní protokolování:

interface ILogger 
{
    void LogMessage(string message);
}

V objektu můžete vytvořit ILogger protokol pro protokolování zpráv:

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

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

To funguje, ale není to nejlepší návrh. Pokud chcete nahradit FileLogger jinou ILogger implementací, budete muset upravit SomeComponent. Předpokládejme, že mnoho dalších objektů používá FileLogger, budete muset změnit všechny z nich. Nebo pokud se rozhodnete udělat FileLogger jednoúčelový, budete také muset provést změny v celé aplikaci.

Lepším přístupem je "vložit" ILogger objekt do objektu , například pomocí argumentu konstruktoru:

// 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");
    }
}

Objekt teď není zodpovědný za výběr, který ILogger se má použít. Implementace můžete přepínat ILogger beze změny objektů, které na ní závisejí.

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

Tento vzor se nazývá injektáž konstruktoru. Dalším vzorem je injektáž setteru, kde nastavíte závislost prostřednictvím metody nebo vlastnosti setter.

Jednoduchá injektáž závislostí v SignalR

Zvažte aplikaci Chat z kurzu Začínáme s SignalR. Tady je třída centra z této aplikace:

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

Předpokládejme, že chcete ukládat chatovací zprávy na server před jejich odesláním. Můžete definovat rozhraní, které abstrahuje tuto funkci, a použít DI k vložení rozhraní do ChatHub třídy.

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);
    }

Jediným problémem je, že aplikace SignalR nevytvoří přímo centra. SignalR je vytvoří za vás. SignalR ve výchozím nastavení očekává, že třída rozbočovače bude mít konstruktor bez parametrů. Můžete ale snadno zaregistrovat funkci pro vytvoření instancí centra a tuto funkci použít k provádění DI. Zaregistrujte funkci voláním GlobalHost.DependencyResolver.Register.

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

    App.MapSignalR();

    // ...
}

SignalR teď vyvolá tuto anonymní funkci vždy, když potřebuje vytvořit ChatHub instanci.

Kontejnery IoC

Předchozí kód je v jednoduchých případech v pořádku. Ale stále jste museli napsat toto:

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

Ve složité aplikaci s mnoha závislostmi může být potřeba napsat hodně tohoto "propojovacího" kódu. Údržba tohoto kódu může být náročná, zejména pokud jsou závislosti vnořené. Testování jednotek je také obtížné.

Jedním z řešení je použití kontejneru IoC. Kontejner IoC je softwarová komponenta, která zodpovídá za správu závislostí. V kontejneru zaregistrujete typy a pak pomocí kontejneru vytvoříte objekty. Kontejner automaticky zjistí vztahy závislostí. Mnoho kontejnerů IoC také umožňuje řídit věci, jako je životnost objektů a rozsah.

Poznámka

"IoC" je zkratka pro "inverzi řízení", což je obecný vzor, kdy architektura volá kód aplikace. Kontejner IoC vytvoří vaše objekty za vás, což "invertuje" obvyklý tok řízení.

Použití kontejnerů IoC v SignalR

Chatovací aplikace je pravděpodobně příliš jednoduchá na to, aby z kontejneru IoC profitovat. Místo toho se podívejme na ukázku StockTicker .

Ukázka StockTicker definuje dvě hlavní třídy:

  • StockTickerHub: Třída centra, která spravuje připojení klientů.
  • StockTicker: Jeden objekt, který uchovává ceny akcií a pravidelně je aktualizuje.

StockTickerHub obsahuje odkaz na StockTicker singleton, zatímco StockTicker obsahuje odkaz na IHubConnectionContext pro StockTickerHub. Toto rozhraní používá ke komunikaci s StockTickerHub instancemi. (Další informace najdete v tématu Všesměrové vysílání serveru s ASP.NET SignalR.)

Pomocí kontejneru IoC můžeme tyto závislosti trochu rozvést. Nejprve si zjednodušíme StockTickerHub třídy a StockTicker . V následujícím kódu jsem okomentoval části, které nepotřebujeme.

Odeberte konstruktor bez parametrů z StockTickerHub. Místo toho vždy použijeme di k vytvoření centra.

[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;
    }

    // ...

V případě StockTicker odeberte instanci typu singleton. Později použijeme kontejner IoC k řízení životnosti nástroje StockTicker. Konstruktor také zpřístupněte veřejnosti.

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;
    //    }
    //}

Dále můžeme refaktorovat kód vytvořením rozhraní pro StockTicker. Toto rozhraní použijeme k oddělení StockTickerHub třídy StockTicker .

Visual Studio usnadňuje tento druh refaktoringu. Otevřete soubor StockTicker.cs, klikněte pravým tlačítkem na StockTicker deklaraci třídy a vyberte Refaktorovat ... Extrahovat rozhraní.

Snímek obrazovky s rozevírací nabídkou po kliknutí pravým tlačítkem myši v editoru Visual Studio Code se zvýrazněnými možnostmi Refractor a Extrahovat rozhraní

V dialogovém okně Extrahovat rozhraní klikněte na Vybrat vše. Zbytek ponechte ve výchozím nastavení. Klikněte na OK.

Snímek obrazovky s dialogovým oknem Extrahovat rozhraní se zvýrazněnou možností Vybrat vše se všemi dostupnými možnostmi, které jsou vybrané

Visual Studio vytvoří nové rozhraní s názvem IStockTickera také změny StockTicker odvozené z IStockTicker.

Otevřete soubor IStockTicker.cs a změňte rozhraní na veřejné.

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

StockTickerHub Ve třídě změňte dvě instance na StockTickerIStockTicker:

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

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

Vytvoření IStockTicker rozhraní není nezbytně nutné, ale chtěl jsem ukázat, jak může di di pomoci omezit propojení mezi komponentami ve vaší aplikaci.

Přidání knihovny Ninject

Existuje mnoho opensourcových kontejnerů IoC pro .NET. Pro účely tohoto kurzu použiju Ninject. (Mezi další oblíbené knihovny patří Castle Windsor, Spring.Net, Autofac, Unity a StructureMap.)

Pomocí Správce balíčků NuGet nainstalujte knihovnu Ninject. V sadě Visual Studio v nabídce Nástroje vyberteKonzola Správce>balíčků NuGet. V okně konzoly Správce balíčků zadejte následující příkaz:

Install-Package Ninject -Version 3.0.1.10

Nahrazení překladače závislostí SignalR

Chcete-li použít Ninject v signalR, vytvořte třídu, která je odvozena z 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));
    }
}

Tato třída přepíše Metody GetService a GetServicesDefaultDependencyResolver. SignalR volá tyto metody k vytváření různých objektů za běhu, včetně instancí centra, a také různých služeb používaných interně službou SignalR.

  • Metoda GetService vytvoří jednu instanci typu. Přepište tuto metodu volání metody TryGet jádra Ninject. Pokud tato metoda vrátí hodnotu null, vraťte se k výchozímu překladače.
  • Metoda GetServices vytvoří kolekci objektů zadaného typu. Přepište tuto metodu a zřetězíte výsledky z Ninjectu s výsledky z výchozího překladače.

Konfigurace vazeb Ninject

Teď použijeme Ninject k deklaraci vazeb typu.

Otevřete třídu Startup.cs vaší aplikace (kterou jste buď vytvořili ručně podle pokynů k balíčku v readme.txt, nebo kterou jste vytvořili přidáním ověřování do projektu). V metodě Startup.Configuration vytvořte kontejner Ninject, který Ninject volá jádro.

var kernel = new StandardKernel();

Vytvořte instanci našeho vlastního překladače závislostí:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Vytvořte vazbu pro IStockTicker následujícím způsobem:

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

Tento kód říká dvě věci. Za prvé, kdykoli aplikace potřebuje IStockTicker, jádro by mělo vytvořit instanci .StockTicker Za druhé, StockTicker třída by měla být vytvořena jako jeden objekt. Ninject vytvoří jednu instanci objektu a pro každý požadavek vrátí stejnou instanci.

Vytvořte vazbu pro IHubConnectionContext následujícím způsobem:

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

Tento kód vytvoří anonymní funkci, která vrátí funkci IHubConnection. Metoda WhenInjectedInto říká Ninjectu, aby tuto funkci používal pouze při vytváření IStockTicker instancí. Důvodem je, že Služba SignalR interně vytváří instance IHubConnectionContext a nechceme přepsat způsob, jakým je SignalR vytváří. Tato funkce platí jenom pro naši StockTicker třídu.

Předejte překladač závislostí do metody MapSignalR přidáním konfigurace centra:

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

Aktualizujte metodu Startup.ConfigureSignalR ve třídě Startup ukázky pomocí nového parametru:

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

Teď bude SignalR místo výchozího překladače používat překladač zadaný v MapSignalR.

Tady je úplný výpis kódu pro 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);
    }
}

Pokud chcete spustit aplikaci StockTicker v sadě Visual Studio, stiskněte klávesu F5. V okně prohlížeče přejděte na http://localhost:*port*/SignalR.Sample/StockTicker.html.

Snímek obrazovky s oknem prohlížeče Internet Explorer zobrazující ukázkovou webovou stránku A S P dot NET Signal R Stock Ticker

Aplikace má úplně stejné funkce jako předtím. (Popis najdete v tématu Všesměrové vysílání serveru s ASP.NET SignalR.) Nezměnili jsme chování. jen usnadnil testování, údržbu a vývoj kódu.