Compartilhar via


Injeção de dependência no SignalR

por Patrick Fletcher

Aviso

Esta documentação não é para a versão mais recente do SignalR. Dê uma olhada em ASP.NET Core SignalR.

Versões de software usadas neste tópico

Versões anteriores deste tópico

Para obter informações sobre versões anteriores do SignalR, consulte Versões mais antigas do SignalR.

Perguntas e comentários

Deixe comentários sobre como você gostou deste tutorial e o que poderíamos melhorar nos comentários na parte inferior da página. Se você tiver perguntas que não estão diretamente relacionadas ao tutorial, poderá postá-las no fórum do ASP.NET SignalR ou StackOverflow.com.

A injeção de dependência é uma maneira de remover dependências embutidas em código entre objetos, facilitando a substituição das dependências de um objeto, seja para testar (usando objetos fictícios) ou para alterar o comportamento em tempo de execução. Este tutorial mostra como executar a injeção de dependência em hubs do SignalR. Ele também mostra como usar contêineres de IoC com o SignalR. Um contêiner de IoC é uma estrutura geral para injeção de dependência.

O que é injeção de dependência?

Ignore esta seção se você já estiver familiarizado com a injeção de dependência.

A DI (injeção de dependência) é um padrão em que os objetos não são responsáveis por criar suas próprias dependências. Aqui está um exemplo simples para motivar a DI. Suponha que você tenha um objeto que precisa registrar mensagens em log. Você pode definir uma interface de registro em log:

interface ILogger 
{
    void LogMessage(string message);
}

Em seu objeto, você pode criar um ILogger para registrar mensagens em log:

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

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

Isso funciona, mas não é o melhor design. Se você quiser substituir por FileLogger outra ILogger implementação, precisará modificar SomeComponent. Supondo que muitos outros objetos usem FileLogger, você precisará alterar todos eles. Ou se você decidir fazer FileLogger um singleton, também precisará fazer alterações em todo o aplicativo.

Uma abordagem melhor é "injetar" um ILogger no objeto , por exemplo, usando um argumento de construtor:

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

Agora, o objeto não é responsável por selecionar qual ILogger usar. Você pode alternar implementações ILogger sem alterar os objetos que dependem dele.

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

Esse padrão é chamado de injeção de construtor. Outro padrão é a injeção de setter, em que você define a dependência por meio de um método ou propriedade setter.

Injeção de dependência simples no SignalR

Considere o aplicativo chat do tutorial Introdução com o SignalR. Aqui está a classe de hub desse aplicativo:

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

Suponha que você queira armazenar mensagens de chat no servidor antes de enviá-las. Você pode definir uma interface que abstrai essa funcionalidade e usar DI para injetar a interface na 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);
    }

O único problema é que um aplicativo SignalR não cria hubs diretamente; O SignalR os cria para você. Por padrão, o SignalR espera que uma classe de hub tenha um construtor sem parâmetros. No entanto, você pode registrar facilmente uma função para criar instâncias de hub e usar essa função para executar DI. Registre a função chamando GlobalHost.DependencyResolver.Register.

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

    App.MapSignalR();

    // ...
}

Agora, o SignalR invocará essa função anônima sempre que precisar criar uma ChatHub instância.

Contêineres de IoC

O código anterior é bom para casos simples. Mas você ainda tinha que escrever isso:

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

Em um aplicativo complexo com muitas dependências, talvez seja necessário escrever muito desse código de "fiação". Esse código pode ser difícil de manter, especialmente se as dependências estiverem aninhadas. Também é difícil fazer o teste de unidade.

Uma solução é usar um contêiner de IoC. Um contêiner de IoC é um componente de software responsável por gerenciar dependências. Registre tipos com o contêiner e use o contêiner para criar objetos. O contêiner descobre automaticamente as relações de dependência. Muitos contêineres de IoC também permitem controlar itens como tempo de vida e escopo do objeto.

Observação

"IoC" significa "inversão de controle", que é um padrão geral em que uma estrutura chama o código do aplicativo. Um contêiner de IoC constrói seus objetos para você, o que "inverte" o fluxo usual de controle.

Usando contêineres de IoC no SignalR

O aplicativo chat provavelmente é muito simples para se beneficiar de um contêiner de IoC. Em vez disso, vamos examinar o exemplo do StockTicker .

O exemplo do StockTicker define duas classes main:

  • StockTickerHub: a classe hub, que gerencia conexões de cliente.
  • StockTicker: um singleton que mantém os preços das ações e os atualiza periodicamente.

StockTickerHub contém uma referência ao StockTicker singleton, enquanto StockTicker mantém uma referência ao IHubConnectionContext para o StockTickerHub. Ele usa essa interface para se comunicar com StockTickerHub instâncias. (Para obter mais informações, consulte Transmissão de servidor com ASP.NET SignalR.)

Podemos usar um contêiner de IoC para desembaraçar essas dependências um pouco. Primeiro, vamos simplificar as StockTickerHub classes e StockTicker . No código a seguir, comentei as partes que não precisamos.

Remova o construtor sem parâmetros de StockTickerHub. Em vez disso, sempre usaremos a DI para criar o 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;
    }

    // ...

Para StockTicker, remova a instância singleton. Posteriormente, usaremos o contêiner de IoC para controlar o tempo de vida do StockTicker. Além disso, torne o construtor público.

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

Em seguida, podemos refatorar o código criando uma interface para StockTicker. Usaremos essa interface para desacoplar a StockTickerHubStockTicker da classe .

O Visual Studio facilita essa refatoração. Abra o arquivo StockTicker.cs, clique com o botão direito do mouse na declaração de StockTicker classe e selecione Refatorar ... Extrair Interface.

Captura de tela do menu suspenso do clique com o botão direito do mouse no código do Visual Studio com as opções Refractor e Extract Interface realçadas.

Na caixa de diálogo Extrair Interface , clique em Selecionar Tudo. Deixe os outros padrões. Clique em OK.

Captura de tela da caixa de diálogo Extrair Interface com a opção Selecionar Tudo realçada, com todas as opções disponíveis sendo selecionadas.

O Visual Studio cria uma nova interface chamada IStockTickere também muda StockTicker para derivar de IStockTicker.

Abra o arquivo IStockTicker.cs e altere a interface para pública.

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

StockTickerHub Na classe , altere as duas instâncias de StockTicker para IStockTicker:

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

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

A criação de uma IStockTicker interface não é estritamente necessária, mas eu queria mostrar como a DI pode ajudar a reduzir o acoplamento entre componentes em seu aplicativo.

Adicionar a Biblioteca Ninject

Há muitos contêineres de IoC de software livre para .NET. Para este tutorial, usarei o Ninject. (Outras bibliotecas populares incluem Castle Windsor, Spring.Net, Autofac, Unity e StructureMap.)

Use o Gerenciador de Pacotes NuGet para instalar a biblioteca Ninject. No Visual Studio, no menu Ferramentas, selecioneConsole do Gerenciador de Pacotes do Gerenciador de Pacotes> do NuGet. Na janela Console do Gerenciador de Pacotes, digite o seguinte comando:

Install-Package Ninject -Version 3.0.1.10

Substituir o Resolvedor de Dependência do SignalR

Para usar o Ninject no SignalR, crie uma classe derivada 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));
    }
}

Essa classe substitui os métodos GetService e GetServices de DefaultDependencyResolver. O SignalR chama esses métodos para criar vários objetos em runtime, incluindo instâncias de hub, bem como vários serviços usados internamente pelo SignalR.

  • O método GetService cria uma única instância de um tipo. Substitua esse método para chamar o método TryGet do kernel Ninject. Se esse método retornar nulo, volte para o resolvedor padrão.
  • O método GetServices cria uma coleção de objetos de um tipo especificado. Substitua esse método para concatenar os resultados do Ninject com os resultados do resolvedor padrão.

Configurar associações Ninject

Agora, usaremos o Ninject para declarar associações de tipo.

Abra a classe Startup.cs do aplicativo (que você criou manualmente de acordo com as instruções do pacote em readme.txtou que foi criada adicionando autenticação ao seu projeto). Startup.Configuration No método , crie o contêiner Ninject, que Ninject chama de kernel.

var kernel = new StandardKernel();

Crie uma instância do nosso resolvedor de dependência personalizado:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Crie uma associação para da IStockTicker seguinte maneira:

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

Este código está dizendo duas coisas. Primeiro, sempre que o aplicativo precisar de um IStockTicker, o kernel deve criar uma instância do StockTicker. Em segundo lugar, a StockTicker classe deve ser um criado como um objeto singleton. Ninject criará uma instância do objeto e retornará a mesma instância para cada solicitação.

Crie uma associação para IHubConnectionContext da seguinte maneira:

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

Esse código cria uma função anônima que retorna um IHubConnection. O método WhenInjectedInto informa ao Ninject para usar essa função somente ao criar IStockTicker instâncias. O motivo é que o SignalR cria instâncias IHubConnectionContext internamente e não queremos substituir como o SignalR as cria. Essa função só se aplica à nossa StockTicker classe.

Passe o resolvedor de dependência para o método MapSignalR adicionando uma configuração de hub:

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

Atualize o método Startup.ConfigureSignalR na classe Startup da amostra com o novo parâmetro:

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

Agora, o SignalR usará o resolvedor especificado em MapSignalR, em vez do resolvedor padrão.

Aqui está a listagem de código completa para 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);
    }
}

Para executar o aplicativo StockTicker no Visual Studio, pressione F5. Na janela do navegador, navegue até http://localhost:*port*/SignalR.Sample/StockTicker.html.

Captura de tela de uma janela do navegador Explorer internet, exibindo a página da Web Exemplo do A SP dot NET Signal R Stock Ticker.

O aplicativo tem exatamente a mesma funcionalidade que antes. (Para obter uma descrição, consulte Transmissão de servidor com ASP.NET SignalR.) Não alteramos o comportamento; apenas facilitou o teste, a manutenção e a evolução do código.