Injeção de dependência no SignalR 1.x
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.
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 teste (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);
}
No 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 FileLogger
por 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 dela.
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 a 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 diretamente hubs; 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 a DI. Registre a função chamando GlobalHost.DependencyResolver.Register.
protected void Application_Start()
{
GlobalHost.DependencyResolver.Register(
typeof(ChatHub),
() => new ChatHub(new ChatMessageRepository()));
RouteTable.Routes.MapHubs();
// ...
}
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 é adequado 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 pelo gerenciamento de dependências. Você registra tipos com o contêiner e, em seguida, usa 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 contém os preços das ações e os atualiza periodicamente.
StockTickerHub
manté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 um pouco essas dependências. 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 StockTicker
. 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 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 o StockTickerHub
StockTicker
da classe .
O Visual Studio facilita esse tipo de 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.
Na caixa de diálogo Extrair Interface , clique em Selecionar Tudo. Deixe os outros padrões. Clique em OK.
O Visual Studio cria uma nova interface chamada IStockTicker
e 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, selecione Console do Gerenciador dePacotes>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 de Ninject
Agora, usaremos o Ninject para declarar associações de tipo.
Abra o arquivo RegisterHubs.cs. RegisterHubs.Start
No método , crie o contêiner Ninject, que Ninject chama o 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 deverá criar uma instância do StockTicker
. Em segundo lugar, a StockTicker
classe deve ser criada como um objeto singleton. O 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<IHubConnectionContext>().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 MapHubs :
RouteTable.Routes.MapHubs(config);
Agora, o SignalR usará o resolvedor especificado em MapHubs, em vez do resolvedor padrão.
Aqui está a listagem de código completa para 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);
}
}
Para executar o aplicativo StockTicker no Visual Studio, pressione F5. Na janela do navegador, navegue até http://localhost:*port*/SignalR.Sample/StockTicker.html
.
O aplicativo tem exatamente a mesma funcionalidade de 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.