c# - iservicecollection - Acceso a ASP.NET Core DI Container desde Static Factory Class
inyeccion de dependencias.net core (4)
He creado un sitio ASP.NET Core MVC / WebApi que tiene un suscriptor de RabbitMQ basado en el artículo de blog de James Still Real-World PubSub Messaging con RabbitMQ .
En su artículo, utiliza una clase estática para iniciar el suscriptor de la cola y definir el controlador de eventos para los eventos en cola. Este método estático crea una instancia de las clases del controlador de eventos a través de una clase de fábrica estática.
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
namespace NST.Web.MessageProcessing
{
public static class MessageListener
{
private static IConnection _connection;
private static IModel _channel;
public static void Start(string hostName, string userName, string password, int port)
{
var factory = new ConnectionFactory
{
HostName = hostName,
Port = port,
UserName = userName,
Password = password,
VirtualHost = "/",
AutomaticRecoveryEnabled = true,
NetworkRecoveryInterval = TimeSpan.FromSeconds(15)
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.ExchangeDeclare(exchange: "myExchange", type: "direct", durable: true);
var queueName = "myQueue";
QueueDeclareOk ok = _channel.QueueDeclare(queueName, true, false, false, null);
_channel.QueueBind(queue: queueName, exchange: "myExchange", routingKey: "myRoutingKey");
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += ConsumerOnReceived;
_channel.BasicConsume(queue: queueName, noAck: false, consumer: consumer);
}
public static void Stop()
{
_channel.Close(200, "Goodbye");
_connection.Close();
}
private static void ConsumerOnReceived(object sender, BasicDeliverEventArgs ea)
{
// get the details from the event
var body = ea.Body;
var message = Encoding.UTF8.GetString(body);
var messageType = "endpoint"; // hardcoding the message type while we dev...
// instantiate the appropriate handler based on the message type
IMessageProcessor processor = MessageHandlerFactory.Create(messageType);
processor.Process(message);
// Ack the event on the queue
IBasicConsumer consumer = (IBasicConsumer)sender;
consumer.Model.BasicAck(ea.DeliveryTag, false);
}
}
}
Funciona muy bien hasta el punto en que ahora necesito resolver un servicio en mi fábrica de procesadores de mensajes en lugar de simplemente escribir en la consola.
using NST.Web.Services;
using System;
namespace NST.Web.MessageProcessing
{
public static class MessageHandlerFactory
{
public static IMessageProcessor Create(string messageType)
{
switch (messageType.ToLower())
{
case "ipset":
// need to resolve IIpSetService here...
IIpSetService ipService = ???????
return new IpSetMessageProcessor(ipService);
case "endpoint":
// need to resolve IEndpointService here...
IEndpointService epService = ???????
// create new message processor
return new EndpointMessageProcessor(epService);
default:
throw new Exception("Unknown message type");
}
}
}
}
¿Hay alguna forma de acceder al contenedor IoC Core de ASP.NET para resolver las dependencias? Realmente no quiero tener que girar toda la pila de dependencias a mano :(
O, ¿hay una mejor manera de suscribirse a RabbitMQ desde una aplicación Core de ASP.NET? Encontré RestBus pero no se ha actualizado para Core 1.x
Aquí está mi opinión sobre su caso:
Si es posible enviaría un servicio resuelto como parámetro.
public static IMessageProcessor Create(string messageType, IIpSetService ipService)
{
//
}
De lo contrario , la vida útil del servicio sería importante.
Si el servicio es singleton, solo establecería la dependencia en el método de configuración:
// configure method
public IApplicationBuilder Configure(IApplicationBuilder app)
{
var ipService = app.ApplicationServices.GetService<IIpSetService>();
MessageHandlerFactory.IIpSetService = ipService;
}
// static class
public static IIpSetService IpSetService;
public static IMessageProcessor Create(string messageType)
{
// use IpSetService
}
Si el tiempo de vida del servicio es de alcance, usaría HttpContextAccessor:
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
public IApplicationBuilder Configure(IApplicationBuilder app)
{
var httpContextAccessor= app.ApplicationServices.GetService<IHttpContextAccessor>();
MessageHandlerFactory.HttpContextAccessor = httpContextAccessor;
}
// static class
public static IHttpContextAccessor HttpContextAccessor;
public static IMessageProcessor Create(string messageType)
{
var ipSetService = HttpContextAccessor.HttpContext.RequestServices.GetService<IIpSetService>();
// use it
}
Aunque el uso de la inyección de dependencia es una mejor solución, en algunos casos debe usar métodos estáticos (como en los métodos de extensión).
Para esos casos, puede agregar una propiedad estática a su clase estática e inicializarla en su método ConfigureServices.
Por ejemplo:
public static class EnumExtentions
{
static public IStringLocalizerFactory StringLocalizerFactory { set; get; }
public static string GetDisplayName(this Enum e)
{
var resourceManager = StringLocalizerFactory.Create(e.GetType());
var key = e.ToString();
var resourceDisplayName = resourceManager.GetString(key);
return resourceDisplayName;
}
}
y en tus servicios de configuración:
EnumExtentions.StringLocalizerFactory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
Puede evitar las clases estáticas y usar la inyección de dependencia en forma combinada con:
- El uso de
IApplicationLifetime
para iniciar / detener la escucha cada vez que la aplicación se inicia / detiene. - El uso de
IServiceProvider
para crear instancias de los procesadores de mensajes.
Primero, movamos la configuración a su propia clase que se puede completar desde appsettings.json:
public class RabbitOptions
{
public string HostName { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public int Port { get; set; }
}
// In appsettings.json:
{
"Rabbit": {
"hostName": "192.168.99.100",
"username": "guest",
"password": "guest",
"port": 5672
}
}
A continuación, convierta MessageHandlerFactory
en una clase no estática que reciba un IServiceProvider
como una dependencia. Utilizará el proveedor de servicios para resolver las instancias del procesador de mensajes:
public class MessageHandlerFactory
{
private readonly IServiceProvider services;
public MessageHandlerFactory(IServiceProvider services)
{
this.services = services;
}
public IMessageProcessor Create(string messageType)
{
switch (messageType.ToLower())
{
case "ipset":
return services.GetService<IpSetMessageProcessor>();
case "endpoint":
return services.GetService<EndpointMessageProcessor>();
default:
throw new Exception("Unknown message type");
}
}
}
De esta manera, sus clases de procesadores de mensajes pueden recibir en el constructor las dependencias que necesiten (siempre que las configure en Startup.ConfigureServices
). Por ejemplo, estoy inyectando un ILogger en uno de mis procesadores de muestra:
public class IpSetMessageProcessor : IMessageProcessor
{
private ILogger<IpSetMessageProcessor> logger;
public IpSetMessageProcessor(ILogger<IpSetMessageProcessor> logger)
{
this.logger = logger;
}
public void Process(string message)
{
logger.LogInformation("Received message: {0}", message);
}
}
Ahora convierta MessageListener
en una clase no estática que depende de IOptions<RabbitOptions>
y MessageHandlerFactory
. Es muy similar a la original, acabo de reemplazar los parámetros de los métodos Start con la dependencia de opciones y la fábrica de controladores es ahora una dependencia en lugar de una clase estática:
public class MessageListener
{
private readonly RabbitOptions opts;
private readonly MessageHandlerFactory handlerFactory;
private IConnection _connection;
private IModel _channel;
public MessageListener(IOptions<RabbitOptions> opts, MessageHandlerFactory handlerFactory)
{
this.opts = opts.Value;
this.handlerFactory = handlerFactory;
}
public void Start()
{
var factory = new ConnectionFactory
{
HostName = opts.HostName,
Port = opts.Port,
UserName = opts.UserName,
Password = opts.Password,
VirtualHost = "/",
AutomaticRecoveryEnabled = true,
NetworkRecoveryInterval = TimeSpan.FromSeconds(15)
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.ExchangeDeclare(exchange: "myExchange", type: "direct", durable: true);
var queueName = "myQueue";
QueueDeclareOk ok = _channel.QueueDeclare(queueName, true, false, false, null);
_channel.QueueBind(queue: queueName, exchange: "myExchange", routingKey: "myRoutingKey");
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += ConsumerOnReceived;
_channel.BasicConsume(queue: queueName, noAck: false, consumer: consumer);
}
public void Stop()
{
_channel.Close(200, "Goodbye");
_connection.Close();
}
private void ConsumerOnReceived(object sender, BasicDeliverEventArgs ea)
{
// get the details from the event
var body = ea.Body;
var message = Encoding.UTF8.GetString(body);
var messageType = "endpoint"; // hardcoding the message type while we dev...
//var messageType = Encoding.UTF8.GetString(ea.BasicProperties.Headers["message-type"] as byte[]);
// instantiate the appropriate handler based on the message type
IMessageProcessor processor = handlerFactory.Create(messageType);
processor.Process(message);
// Ack the event on the queue
IBasicConsumer consumer = (IBasicConsumer)sender;
consumer.Model.BasicAck(ea.DeliveryTag, false);
}
}
Casi allí, necesitará actualizar el método Startup.ConfigureServices
para que conozca sus servicios y opciones (puede crear interfaces para la fábrica de escuchas y controladores si lo desea):
public void ConfigureServices(IServiceCollection services)
{
// ...
// Add RabbitMQ services
services.Configure<RabbitOptions>(Configuration.GetSection("rabbit"));
services.AddTransient<MessageListener>();
services.AddTransient<MessageHandlerFactory>();
services.AddTransient<IpSetMessageProcessor>();
services.AddTransient<EndpointMessageProcessor>();
}
Finalmente, actualice el método Startup.Configure
para tomar un parámetro IApplicationLifetime
adicional e inicie / detenga el servicio de escucha de mensajes en los eventos ApplicationStarted
/ ApplicationStopped
(Aunque noté hace un tiempo algunos problemas con el evento ApplicationStopping usando IISExpress, como en esta pregunta ):
public MessageListener MessageListener { get; private set; }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime)
{
appLifetime.ApplicationStarted.Register(() =>
{
MessageListener = app.ApplicationServices.GetService<MessageListener>();
MessageListener.Start();
});
appLifetime.ApplicationStopping.Register(() =>
{
MessageListener.Stop();
});
// ...
}
Sé que mi respuesta es tarde, pero quería compartir cómo lo hice.
En primer lugar : es Antipattern para usar ServiceLocator, así que trata de no usarlo como puedas. En mi caso, lo necesitaba para llamar a MediatR dentro de mi DomainModel para implementar la lógica DomainEvents .
Sin embargo , tuve que encontrar una manera de llamar a una clase estática en mi DomainModel para obtener una instancia de algún servicio registrado de DI.
Así que decidí usar HttpContext
para acceder a IServiceProvider
pero necesitaba acceder a él desde un método estático sin mencionarlo en mi modelo de dominio.
Vamos a hacerlo:
1- He creado una interfaz para envolver el IServiceProvider
public interface IServiceProviderProxy
{
T GetService<T>();
IEnumerable<T> GetServices<T>();
object GetService(Type type);
IEnumerable<object> GetServices(Type type);
}
2- Luego he creado una clase estática para que sea mi punto de acceso a ServiceLocator
public static class ServiceLocator
{
private static IServiceProviderProxy diProxy;
public static IServiceProviderProxy ServiceProvider => diProxy ?? throw new Exception("You should Initialize the ServiceProvider before using it.");
public static void Initialize(IServiceProviderProxy proxy)
{
diProxy = proxy;
}
}
3- He creado una implementación para IServiceProviderProxy
que usa internamente el IHttpContextAccessor
public class HttpContextServiceProviderProxy : IServiceProviderProxy
{
private readonly IHttpContextAccessor contextAccessor;
public HttpContextServiceProviderProxy(IHttpContextAccessor contextAccessor)
{
this.contextAccessor = contextAccessor;
}
public T GetService<T>()
{
return contextAccessor.HttpContext.RequestServices.GetService<T>();
}
public IEnumerable<T> GetServices<T>()
{
return contextAccessor.HttpContext.RequestServices.GetServices<T>();
}
public object GetService(Type type)
{
return contextAccessor.HttpContext.RequestServices.GetService(type);
}
public IEnumerable<object> GetServices(Type type)
{
return contextAccessor.HttpContext.RequestServices.GetServices(type);
}
}
4- Debería registrar el IServiceProviderProxy
en el DI como este
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
.......
}
5- El paso final es inicializar el ServiceLocator
con una instancia de IServiceProviderProxy
en el inicio de la aplicación
public void Configure(IApplicationBuilder app, IHostingEnvironment env,IServiceProvider sp)
{
ServiceLocator.Initialize(sp.GetService<IServiceProviderProxy>());
}
Como resultado, ahora puede llamar al ServiceLocator en sus clases de DomainModel "O y el lugar que necesita" y resolver las dependencias que necesita.
public class FakeModel
{
public FakeModel(Guid id, string value)
{
Id = id;
Value = value;
}
public Guid Id { get; }
public string Value { get; private set; }
public async Task UpdateAsync(string value)
{
Value = value;
var mediator = ServiceLocator.ServiceProvider.GetService<IMediator>();
await mediator.Send(new FakeModelUpdated(this));
}
}