Injektáž závislostí v centrech SignalR 1.x

Patrick Fletcher

Upozornění

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

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 účely testování (pomocí napodobených objektů), nebo pro změnu chování za běhu. V tomto kurzu se dozvíte, 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í?

Tuto část přeskočte, pokud už injektáž závislostí zná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, který motivuje DI. Předpokládejme, že máte objekt, který potřebuje protokolovat zprávy. Můžete definovat protokolovací rozhraní:

interface ILogger 
{
    void LogMessage(string message);
}

V objektu můžete vytvořit k ILogger 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. Za předpokladu, že mnoho dalších objektů používá FileLogger, budete muset změnit všechny z nich. Nebo pokud se rozhodnete provést FileLogger singleton, budete také muset provést změny v celé aplikaci.

Lepší přístup je "vložit" ILogger 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. Můžete přepínat ILogger implementace beze změny objektů, které jsou na něm závislé.

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

Tento model se nazývá injektáž konstruktoru. Dalším vzorem je injektáž setter, kdy se závislost nastavuje prostřednictvím metody nebo vlastnosti setter.

Jednoduchá injektáž závislostí v SignalR

Zvažte použití aplikace Chat z kurzu Začínáme se službou 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 zprávy chatu před odesláním uložit na server. 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. Služba SignalR ve výchozím nastavení očekává, že třída centra bude mít konstruktor bez parametrů. Můžete ale snadno zaregistrovat funkci pro vytvoření instancí rozbočovače a použít ji k provádění závislostí. Zaregistrujte funkci voláním funkce GlobalHost.DependencyResolver.Register.

protected void Application_Start()
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    RouteTable.Routes.MapHubs();

    // ...
}

SignalR teď vyvolá tuto anonymní funkci, kdykoli bude potřebovat vytvořit ChatHub instanci.

Kontejnery IoC

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

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

Ve složité aplikaci s mnoha závislostmi možná budete muset napsat hodně tohoto kódu pro "zapojení". Údržba tohoto kódu může být obtíž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 kontejner použijete k vytváření objektů. Kontejner automaticky zjistí vztahy závislostí. Mnoho kontejnerů IoC také umožňuje řídit věci, jako je životnost objektu a rozsah.

Poznámka

"IoC" znamená "inverze řízení", což je obecný vzor, kdy architektura volá kód aplikace. Kontejner IoC vytvoří objekty za vás, což "invertuje" obvyklý tok řízení.

Použití kontejnerů IoC v SignalR

Aplikace Chat je pravděpodobně příliš jednoduchá na to, aby mohla těžit z kontejneru IoC. Podívejme se místo toho na ukázku StockTicker .

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

  • StockTickerHub: Třída centra, která spravuje připojení klientů.
  • StockTicker: Jednoúčel, 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.)

K tomu, abychom tyto závislosti trochu rozmotáli, můžeme použít kontejner IoC. 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 .StockTicker Místo toho k vytvoření centra vždy použijeme DI.

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

    // ...

Pro StockTicker odeberte instanci singleton. Později použijeme kontejner IoC k řízení životnosti StockTicker. Konstruktor také zpřístupněte jako veřejný.

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 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 od StockTicker třídy .

Visual Studio tento druh refaktoringu usnadňuje. 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 zobrazenou po kliknutí pravým tlačítkem myši v editoru Visual Studio Code se zvýrazněnými možnostmi Refaktorovat 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 a zobrazenou možností O K

Visual Studio vytvoří nové rozhraní s názvem IStockTickera také se změní StockTicker tak, aby bylo odvozeno 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 pomoct 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

Pokud chcete použít Ninject v rámci 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řepisuje 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.

  • GetService Metoda 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.
  • GetServices Metoda vytvoří kolekci objektů zadaného typu. Přepsat tuto metodu zřetězení výsledků z Ninject s výsledky z výchozí resolver.

Konfigurace vazeb Ninject

Teď použijeme Ninject k deklaraci vazeb typu.

Otevřete soubor RegisterHubs.cs. RegisterHubs.Start V metodě 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řená jako jednoúčelový 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<IHubConnectionContext>().ToMethod(context =>
    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
).WhenInjectedInto<IStockTicker>();

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

Předejte překladač závislostí do metody MapHubs :

RouteTable.Routes.MapHubs(config);

Teď SignalR místo výchozího překladače použije překladač zadaný v MapHubs.

Tady je kompletní výpis kódu pro RegisterHubs.Start.

public static class RegisterHubs
{
    public static void Start()
    {
        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()
            .InSingletonScope();

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

        var config = new HubConfiguration()
        {
            Resolver = resolver
        };

        // Register the default hubs route: ~/signalr/hubs
        RouteTable.Routes.MapHubs(config);
    }
}

Aplikaci StockTicker spustíte v sadě Visual Studio stisknutím klávesy F5. V okně prohlížeče přejděte na http://localhost:*port*/SignalR.Sample/StockTicker.html.

Snímek obrazovky ukázky burzovního tickeru A S P dot NET Signal R Stock Ticker zobrazující se v okně prohlížeče Internet Explorer

Aplikace má přesně 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 usnadnili testování, údržbu a vývoj kódu.