Доступ к контейнеру Core Core ASP.NET из статического Factory класса
Я создал сайт ASP.NET Core MVC/WebApi, на котором есть подписчик RabbitMQ, основанный на статье блога Джеймса Стилла Реальные сообщения PubSub в реальном времени с RabbitMQ.
В своей статье он использует статический класс для запуска подписчика очереди и определения обработчика событий для событий в очереди. Этот статический метод затем создает экземпляры классов обработчика событий через статический класс factory.
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);
}
}
}
Он отлично работает до такой степени, что теперь мне нужно разрешить службу в моем процессоре сообщений factory, а не просто писать на консоль.
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");
}
}
}
}
Есть ли способ получить доступ к контейнеру Core IoC ASP.NET для разрешения зависимостей? Я действительно не хочу, чтобы вручную развернуть весь набор зависимостей: (
Или, есть ли лучший способ подписаться на RabbitMQ из приложения ASP.NET Core? Я нашел RestBus, но он не был обновлен для Core 1.x
Ответы
Ответ 1
Вы можете избежать статических классов и использовать Dependency Injection полностью в сочетании с:
- Использование
IApplicationLifetime
для запуска/остановки прослушивателя при каждом запуске/остановке приложения. - Использование
IServiceProvider
для создания экземпляров обработчиков сообщений.
Прежде всего, давайте переместим конфигурацию в свой собственный класс, который можно заполнить из 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
}
}
Затем преобразуйте MessageHandlerFactory
в нестатический класс, который получает IServiceProvider
в качестве зависимости. Он будет использовать поставщика услуг для разрешения экземпляров обработчика сообщений:
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");
}
}
}
Таким образом, классы вашего обработчика сообщений могут получать в конструкторе любые необходимые им зависимости (если вы настраиваете их в Startup.ConfigureServices
). Например, я внедряю ILogger в один из моих примеров процессоров:
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);
}
}
Теперь преобразуйте MessageListener
в нестатический класс, который зависит от IOptions<RabbitOptions>
и MessageHandlerFactory
Он очень похож на ваш исходный, я просто заменил параметры методов Start на зависимость параметров, и теперь фабрика обработчиков является зависимостью вместо статический класс:
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);
}
}
Почти там вам нужно будет обновить метод Startup.ConfigureServices
чтобы он знал о ваших сервисах и опциях (вы можете создать интерфейсы для фабрики слушателей и обработчиков, если хотите):
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>();
}
И, наконец, обновить Startup.Configure
метод принять дополнительную IApplicationLifetime
параметр и запустить/остановить сообщение слушателя в ApplicationStarted
/ApplicationStopped
событий (хотя я заметил некоторое время назад некоторые проблемы с событием ApplicationStopping использования IISExpress, так как в этом вопросе):
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();
});
// ...
}
Ответ 2
Несмотря на то, что использование Dependency Injection является лучшим решением, но в некоторых случаях вам нужно использовать статические методы (например, в методах расширения).
В этих случаях вы можете добавить статическое свойство в свой статический класс и инициализировать его в вашем методе ConfigureServices.
Например:
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;
}
}
и в вашем ConfigureServices:
EnumExtentions.StringLocalizerFactory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
Ответ 3
Вот мое мнение о вашем случае:
Если возможно, я отправил разрешенную услугу в качестве параметра
public static IMessageProcessor Create(string messageType, IIpSetService ipService)
{
//
}
Иначе срок службы службы будет важен.
Если служба singleton, я бы просто установил зависимость от метода configure:
// 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
}
Если срок службы службы ограничен, я бы использовал 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
}
Ответ 4
Я знаю, что мой ответ опоздал, но я хотел бы поделиться, как я это сделал.
Прежде всего: это Antipattern, чтобы использовать ServiceLocator, поэтому старайтесь не использовать его, как вы можете. В моем случае мне нужно было вызвать MediatR внутри моей DomainModel для реализации логики DomainEvents. В любом случае мне нужно было найти способ вызвать статический класс в моей DomainModel, чтобы получить экземпляр некоторой зарегистрированной службы от DI.
Поэтому я решил использовать HttpContext
для доступа к IServiceProvider
но мне нужно было получить к нему доступ статическим методом, не упоминая его в моей доменной модели.
Давай сделаем это:
1- Я создал интерфейс для упаковки IServiceProvider
public interface IServiceProviderProxy
{
T GetService<T>();
IEnumerable<T> GetServices<T>();
object GetService(Type type);
IEnumerable<object> GetServices(Type type);
}
2- Затем я создал статический класс для моей точки доступа 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- Я создал реализацию для IServiceProviderProxy
которая внутренне использует 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- Я должен зарегистрировать IServiceProviderProxy
в DI, как это
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
.......
}
5- Последний шаг - инициализация ServiceLocator
с экземпляром IServiceProviderProxy
при запуске приложения.
public void Configure(IApplicationBuilder app, IHostingEnvironment env,IServiceProvider sp)
{
ServiceLocator.Initialize(sp.GetService<IServiceProviderProxy>());
}
В результате теперь вы можете вызывать ServiceLocator в ваших классах DomainModel "Или и нужное место" и разрешать зависимости, которые вам нужны.
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));
}
}