quarta-feira, 8 de agosto de 2018

SignalR c/ SimpleInjector DependencyResolver

SignalR é sem dúvida uma ótima biblioteca, mas eu me deparei com muitas dificuldades ao tentar integrar o SimpleInjector com a versão 2.3.0 (última versão até o momento da liberação deste post). Pretendo dizer o porquê e como resolvi.

No MSDN é possível encontrar a documentação oficial para implementação de um custom Dependency Resolver (https://docs.microsoft.com/en-us/aspnet/signalr/overview/advanced/dependency-injection), porém eu acredito que o código que está lá não funciona. Segue o código proposto pela Microsoft:

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

Baseado neste código, o passo inicial foi tentar criar um parecido, só que usando o SimpleInjector.

internal class SimpleInjectorAsSignalrDependencyResolver : DefaultDependencyResolver
{
    private readonly Container_container;
    public SimpleInjectorAsSignalrDependencyResolver(Container container)
    {
        _container = container;
    }

    public override object GetService(Type serviceType)
    {
        return _container.GetInstance(serviceType) ?? base.GetService(serviceType);
    }

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

Mas...
  1. Nesta classe foi implementado apenas os métodos GetService. Analisando o código do DefaultDependencyResovler (https://github.com/SignalR/SignalR/blob/2.3.0/src/Microsoft.AspNet.SignalR.Core/DefaultDependencyResolver.cs) é possível perceber que default services são registrados através do método Register, e estes caras não são salvos no container. Ou seja, se você ou o próprio SignalR precisar requisitar os default services, esquece, pois o SimpleInjector gera exceptions quando os serviços não estão registrados! Foi o meu caso ao tentar implementar um custom HubDispatcher.
    Uma possível solução seria colocar um try-catch mitigando quaisquer ActivationExceptions para sempre retornar nulo, mas isso abafa erros legítimos (ou seja, erros que não são frutos da indisponibilidade dos serviços no container).
    Uma outra possível solução seria eu fazer o meu sistema registrar os default services manualmente, mas isso aumentaria fortemente o acoplamento entre o custom resolver a versão do DefaultDepedencyResolver, além de quebrar o Open-Closed Principle. Sendo assim, precisei também implementar os métodos de Register para que os default services fossem devidamente registrados.
  2. Uma implementação simplória dos métodos Register não funciona também, pois o construtor da classe base começa um processo de registro de serviços e, antes mesmo que o construtor do custom resolver fosse chamado e o container fosse salvo localmente, os métodos de Register são invocados, gerando NullReferenceException. Que mancada, hein Microsoft!
  3. Além do problema acima, um simples esquema para postegar o registro dos serviços para um momento após o container ficar disponível também não funciona, pois no próprio processo de registro os métodos GetServices são invocados, requerindo a disponibilidade do container. Que mancada, hein Microsoft! 
Depois desses problemas, cheguei à seguinte implementação:

public class SimpleInjectorContainerAsSignalRDependencyResolver
 : DefaultDependencyResolver
{
 #region ' ctor '

 public SimpleInjectorContainerAsSignalRDependencyResolver(
  Container container)
  : base()
 {
  _container = container;
  //
  Initialize();
 }

 #endregion

 #region ' Members '

 private readonly Container _container;
 private readonly DeferredRegistrationsManager deferredRegistrations = new DeferredRegistrationsManager();
 private bool IsInitialized { get; set; } = false;

 #endregion

 private void Initialize()
 {
  deferredRegistrations.Complete();
  //
  IsInitialized = true;
 }

 #region ' IDependencyResolver '

 public override object GetService(Type serviceType)
 {
  if (!IsInitialized) return base.GetService(serviceType);

  try
  {
   return _container.GetInstance(serviceType);
  }
  catch (ActivationException)
  {
   // Despite the SimpleInjector container that throws ActivationException when 
   // service is not registered, the SignalR internal services expects dependency
   // resolver returns null in such cases.
   return null;
  }
 }

 public override IEnumerable<object> GetServices(Type serviceType)
 {
  if (!IsInitialized) return base.GetServices(serviceType);

  try
  {
   return _container.GetAllInstances(serviceType);
  }
  catch (ActivationException)
  {
   // Despite the SimpleInjector container that throws ActivationException when 
   // service is not registered, the SignalR internal services expects dependency
   // resolver returns null in such cases.
   return null;
  }
 }

 public override void Register(Type serviceType, Func<object> activator)
 {
  if (!IsInitialized)  base.Register(serviceType, activator);

  // deffers the execution of the registration to the time when this is initialized.
  deferredRegistrations.Defer(serviceType, () =>
  {
   _container.Register(serviceType, activator, Lifestyle.Singleton);

   // Marcus Miris @ 06/ago/2018:
   // As classes internas do SignalR ocasionalmente requisitam Single Intances Services atrav�s
   // do m�todo `GetServices` que retorna cole��es de inst�ncias. Tal opera��o, quando realizada
   // no container do Simple Injector, gera uma Exception, requerindo do caller a utiliza��o do m�todo
   // `GetAllInstance` ao inv�s do m�todo `GetInstance`. 
   // Para mitigar tal erro, o mesmo servi�o � registrado no container como uma cole��o.
   if (!typeof(IEnumerable).IsAssignableFrom(serviceType))
    Register(serviceType, new[] { activator });
  });

 }

 public override void Register(Type serviceType, IEnumerable<Func<object>> activators)
 {
  if (!IsInitialized) base.Register(serviceType, activators);
  
  // deffers the execution of the registration to the time when this is initialized.
  deferredRegistrations.Defer(
   serviceType, 
   () => _container.RegisterCollection(serviceType, activators, Lifestyle.Singleton));
 }

 #endregion

}



Classes auxiliares:



private class DeferredRegistrationsManager
{
    private readonly Queue<(Type, Action)> _queue = new Queue<(Type, Action)>();

    public bool Completed { get; private set; } = false;
    private static readonly object completionLockObject = new object();

    /// <summary>
    ///     Submete a action de registro para o buffer.
    ///     Caso o mesmo esteja liberado, a action � executada imediatamente.
    ///     Caso contrario, ser� liberada no invoke do m�todo `Complete`.
    /// </summary>
    public void Defer(Type serviceType, Action registration)
    {
        if (Completed)
            DoRegistration(serviceType, registration);
        else
            lock (completionLockObject) _queue.Enqueue((serviceType, registration));
    }

    public void Complete()
    {
        lock (completionLockObject)
        {
            while (_queue.Count > 0)
            {
                (Type serviceType, Action registration) = _queue.Dequeue();
                DoRegistration(serviceType, registration);
            }

            Completed = true;
        }
    }

    private void DoRegistration(Type serviceType, Action registration)
    {
        try
        {
            registration.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
                $"Erro ao processar registro de ${serviceType?.Name}: {e.GetBaseException().Message}.",
                e);
        }
    }
}



public static class SimpleInjectorExtensions
{

    public static void RegisterCollection(
        this Container container,
        Type serviceType,
        IEnumerable<Func<object>> activators,
        Lifestyle lifestyle)
    {
        if (container == null) throw new ArgumentNullException(nameof(container));

        container.RegisterCollection(
            serviceType,
            activators.Select(activator => new TransientLifestyleRegistration(
                lifestyle,
                container,
                serviceType,
                activator)));
    }


    #region ' Inner Classes '

    /// <summary>
    ///     Replicação da inner classe homônima declarada em `SimpleInjector.Lifestyles.TransientLifestyle`,
    ///     como é possível conferir em
    ///     https://github.com/simpleinjector/SimpleInjector/blob/master/src/SimpleInjector/Lifestyles/TransientLifestyle.cs
    /// </summary>
    public class TransientLifestyleRegistration
        : Registration
    {
        private readonly Func<object> _instanceCreator;

        public TransientLifestyleRegistration(
            Lifestyle lifestyle,
            Container container,
            Type implementationType,
            Func<object> instanceCreator = null)
            : base(lifestyle, container)
        {
            ImplementationType = implementationType;
            _instanceCreator = instanceCreator;
        }

        #region ' Registration implementation '

        public override Type ImplementationType { get; }

        public override Expression BuildExpression() =>
            this._instanceCreator == null
                ? this.BuildTransientExpression()
                : this.BuildTransientExpression(this._instanceCreator);

        #endregion
    }

    #endregion

}



public class SimpleInjectorHubDispatcher
    : HubDispatcher
{
    private readonly Container _container;

    #region ' ctor '

    public SimpleInjectorHubDispatcher(
        Container container,
        HubConfiguration configuration)
        : base(configuration)
    {
        _container = container;
    }

    #endregion

    private Task WithNewAsyncScope(Func<Task> action)
    {
        var scope = AsyncScopedLifestyle.BeginScope(_container);
        return action().ContinueWith(t => scope.Dispose());
    }

    #region ' HubDispatcher '

    protected override Task OnConnected(IRequest request, string connectionId)
        => WithNewAsyncScope(() => base.OnConnected(request, connectionId));

    protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
        => WithNewAsyncScope(() => base.OnDisconnected(request, connectionId, stopCalled));

    protected override Task OnReceived(IRequest request, string connectionId, string data)
        => WithNewAsyncScope(() => base.OnReceived(request, connectionId, data));

    protected override Task OnReconnected(IRequest request, string connectionId)
        => WithNewAsyncScope(() => base.OnReconnected(request, connectionId));

    #endregion
}

Como configurar no Owin:




public static void UseSocket(this IAppBuilder app, string mapName, Container container)
{
    GlobalHost.DependencyResolver = new SimpleInjectorContainerAsSignalRDependencyResolver(container);
    app.MapSignalR<SimpleInjectorHubDispatcher>(mapName, with(new HubConfiguration(), _ =>
        {
            _.EnableJSONP = true;
            _.EnableDetailedErrors = true;
            _.EnableJavaScriptProxies = true;
        }));
}


Nenhum comentário: