¿Inyección de dependencia usando Azure WebJobs SDK?

El problema es que Azure WebJobs SDK solo admite métodos públicos estáticos como puntos de entrada de trabajos, lo que significa que no hay forma de implementar la inyección de constructores / propiedades.

No puedo encontrar nada sobre este tema en la documentación / recursos oficiales de WebJobs SDK. La única solución que encontré se basa en el patrón de localizador de servicios (anti) descrito en este artículo aquí .

¿Hay una buena manera de utilizar la dependency injection “adecuada” para los proyectos basados ​​en Azure WebJobs SDK?

Azure WebJobs SDK ahora admite métodos de instancia. Combinar esto con un IJobActivator personalizado le permite usar DI.

Primero, crea el IJobActivator personalizado que puede resolver un tipo de trabajo usando tu contenedor DI favorito:

public class MyActivator : IJobActivator { private readonly IUnityContainer _container; public MyActivator(IUnityContainer container) { _container = container; } public T CreateInstance() { return _container.Resolve(); } } 

Necesitas registrar esta clase usando una configuración personalizada de JobHost:

 var config = new JobHostConfiguration { JobActivator = new MyActivator(myContainer) }; var host = new JobHost(config); 

Entonces, puedes usar una clase simple con métodos de instancia para tus trabajos (aquí estoy usando la función de inyección de constructor de Unity):

 public class MyFunctions { private readonly ISomeDependency _dependency; public MyFunctions(ISomeDependency dependency) { _dependency = dependency; } public Task DoStuffAsync([QueueTrigger("queue")] string message) { Console.WriteLine("Injected dependency: {0}", _dependency); return Task.FromResult(true); } } 

Así es como manejé el scope usando el nuevo SDK. Usando el IJobactivator según lo descrito por Alexander Molenkamp.

 public class ScopedMessagingProvider : MessagingProvider { private readonly ServiceBusConfiguration _config; private readonly Container _container; public ScopedMessagingProvider(ServiceBusConfiguration config, Container container) : base(config) { _config = config; _container = container; } public override MessageProcessor CreateMessageProcessor(string entityPath) { return new CustomMessageProcessor(_config.MessageOptions, _container); } private class CustomMessageProcessor : MessageProcessor { private readonly Container _container; public CustomMessageProcessor(OnMessageOptions messageOptions, Container container) : base(messageOptions) { _container = container; } public override Task BeginProcessingMessageAsync(BrokeredMessage message, CancellationToken cancellationToken) { _container.BeginExecutionContextScope(); return base.BeginProcessingMessageAsync(message, cancellationToken); } public override Task CompleteProcessingMessageAsync(BrokeredMessage message, FunctionResult result, CancellationToken cancellationToken) { var scope = _container.GetCurrentExecutionContextScope(); if (scope != null) { scope.Dispose(); } return base.CompleteProcessingMessageAsync(message, result, cancellationToken); } } } 

Puede usar su proveedor de mensajería personalizado en su JobHostConfiguration como

 var serviceBusConfig = new ServiceBusConfiguration { ConnectionString = config.ServiceBusConnectionString }; serviceBusConfig.MessagingProvider = new ScopedMessagingProvider(serviceBusConfig, container); jobHostConfig.UseServiceBus(serviceBusConfig); 

Después de hacer mi propia pregunta sobre cómo manejar el scope … Acabo de llegar a esta solución: no creo que esto sea ideal, pero no pude encontrar ninguna otra solución por el momento.

En mi ejemplo, estoy tratando con ServiceBusTrigger.

Como estoy usando SimpleInjector , la implementación de la interfaz IJobActivator es así:

 public class SimpleInjectorJobActivator : IJobActivator { private readonly Container _container; public SimpleInjectorJobActivator(Container container) { _container = container; } public T CreateInstance() { return (T)_container.GetInstance(typeof(T)); } } 

Aquí, estoy lidiando con los trucos de webjobs.

Entonces tengo dos dependencias:

  • Un singleton:

     public interface ISingletonDependency { } public class SingletonDependency : ISingletonDependency { } 
  • Y otro que necesita vivir solo cuando se desencadena mi función:

     public class ScopedDependency : IScopedDependency, IDisposable { public void Dispose() { //Dispose what need to be disposed... } } 

Entonces, para tener un proceso que se ejecute independientemente del webjob. He encapsulado mi proceso en una clase:

 public interface IBrokeredMessageProcessor { Task ProcessAsync(BrokeredMessage incommingMessage, CancellationToken token); } public class BrokeredMessageProcessor : IBrokeredMessageProcessor { private readonly ISingletonDependency _singletonDependency; private readonly IScopedDependency _scopedDependency; public BrokeredMessageProcessor(ISingletonDependency singletonDependency, IScopedDependency scopedDependency) { _singletonDependency = singletonDependency; _scopedDependency = scopedDependency; } public async Task ProcessAsync(BrokeredMessage incommingMessage, CancellationToken token) { ... } } 

Entonces, cuando comience la webjob, necesito registrar mis dependencias según sus ámbitos:

 class Program { private static void Main() { var container = new Container(); container.Options.DefaultScopedLifestyle = new ExecutionContextScopeLifestyle(); container.RegisterSingleton(); container.Register(Lifestyle.Scoped); container.Register(Lifestyle.Scoped); container.Verify(); var config = new JobHostConfiguration { JobActivator = new SimpleInjectorJobActivator(container) }; var servicebusConfig = new ServiceBusConfiguration { ConnectionString = CloudConfigurationManager.GetSetting("MyServiceBusConnectionString") }; config.UseServiceBus(servicebusConfig); var host = new JobHost(config); host.RunAndBlock(); } } 

Y este es el trabajo desencadenado:

  • Solo tiene una dependencia: el contenedor IoC. Debido a que esta clase es parte de mi raíz de composición, debería estar bien.
  • Maneja el scope en la función disparada.

     public class TriggeredJob { private readonly Container _container; public TriggeredJob(Container container) { _container = container; } public async Task TriggeredFunction([ServiceBusTrigger("queueName")] BrokeredMessage message, CancellationToken token) { using (var scope = _container.BeginExecutionContextScope()) { var processor = _container.GetInstance(); await processor.ProcessAsync(message, token); } } } 

He usado un par de patrones que dependen del concepto de contenedores / scopes para niños (dependiendo de la terminología de su contenedor IoC de elección). No estoy seguro de cuáles lo admiten, pero puedo decir que StructureMap 2.6.x y AutoFac lo hacen.

La idea es desplegar un ámbito secundario para cada mensaje entrante, insertar cualquier contexto que sea exclusivo de esa solicitud, resolver el objeto de nivel superior desde el ámbito secundario y luego ejecutar su proceso.

Aquí hay un código generalizado que lo muestra con AutoFac. Hace una resolución directa desde el contenedor, similar al patrón de protección que intenta evitar, pero se ha aislado en un solo lugar.

En este caso, está utilizando un ServiceBusTrigger para iniciar el trabajo, pero podría ser cualquier cosa: un host de trabajo podría tener una lista de estos para las diferentes colas / procesos.

 public static void ServiceBusRequestHandler([ServiceBusTrigger("queuename")] ServiceBusRequest request) { ProcessMessage(request); } 

Este método es llamado por todas las instancias de los métodos anteriores. Envuelve la creación del scope del niño en un bloque de uso para asegurarse de que todo esté limpio. Luego, cualquier objeto que varíe según la solicitud y contenga el contexto utilizado por otras dependencias (información de usuario / cliente, etc.) se crearía y se inyectaría en el contenedor secundario (en este ejemplo, IRequestContext). Finalmente, el componente que hace el trabajo se resolverá desde el contenedor secundario.

 private static void ProcessMessage(T request) where T : IServiceBusRequest { try { using (var childScope = _container.BeginLifetimeScope()) { // create and inject things that hold the "context" of the message - user ids, etc var builder = new ContainerBuilder(); builder.Register(c => new ServiceRequestContext(request.UserId)).As().InstancePerLifetimeScope(); builder.Update(childScope.ComponentRegistry); // resolve the component doing the work from the child container explicitly, so all of its dependencies follow var thing = childScope.Resolve(); thing.Do(request); } } catch (Exception ex) { } }