从静态工厂类访问 ASP.NET Core DI 容器

Posted

技术标签:

【中文标题】从静态工厂类访问 ASP.NET Core DI 容器【英文标题】:Accessing ASP.NET Core DI Container From Static Factory Class 【发布时间】:2016-11-15 13:58:07 【问题描述】:

我根据 James Still 的博客文章 Real-World PubSub Messaging with RabbitMQ 创建了一个具有 RabbitMQ 订阅者的 ASP.NET Core MVC/WebApi 站点。

在他的文章中,他使用一个静态类来启动队列订阅者并定义队列事件的事件处理程序。然后,此静态方法通过静态工厂类实例化事件处理程序类。

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

    

在我现在需要解析消息处理器工厂中的服务而不是仅仅写入控制台时,它的效果很好。

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

有没有办法访问 ASP.NET Core IoC 容器来解决依赖关系?我真的不想手动启动整个依赖堆栈:(

或者,有没有更好的方法从 ASP.NET Core 应用程序订阅 RabbitMQ?我找到了RestBus,但它没有针对 Core 1.x 进行更新

【问题讨论】:

能否将 MessageListener 转换为依赖项,并在需要的地方注入它自己的注入依赖项? 我很好奇,下面的答案有帮助吗? 【参考方案1】:

您可以避免使用静态类并一直使用依赖注入:

在应用程序启动/停止时使用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&lt;RabbitOptions&gt;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,如this question):

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

    // ...

【讨论】:

我想知道注册为瞬态和实现 IDisposable 的依赖项在这种情况下如何表现。在 asp.net 核心中,如果您解决了瞬态依赖关系 - 它将在请求完成后被释放。但是这里没有要求。 内置DI在生命周期管理等某些方面相对简单。在这种情况下,可能值得考虑挂钩 Autofact、StructureMap、Unity 等第 3 方容器,并为每条消息创建一个范围,例如 是的,但如果你不这样做并使用默认的,我希望它至少不会被容器处理掉? 您可以执行using (var scope = services.CreateScope()),然后解析来自scope.ServiceProvider 的服务,这些服务将在处置范围时被处置。 谢谢。我没有时间尝试它,但我很想摆脱静态实现,这看起来很有意义。【参考方案2】:

尽管使用依赖注入是一个更好的解决方案,但在某些情况下你必须使用静态方法(比如在扩展方法中)。

对于这些情况,您可以向静态类添加静态属性并在 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>();

【讨论】:

谢谢@HamedH。我在找这个services.BuildServiceProvider().GetService&lt;IStringLocalizerFactory&gt;(); 我收到警告:Warnin ASP0000 Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'. 这是一个实用的解决方案,用于将扩展方法与容器中的单例一起使用。注意:在Configure() 中设置静态对象,而不是ConfigureServices()(无论如何都适用于.Net Core 3+)。例如public void Configure(IApplicationBuilder app, IStringLocalizerFactory factory)【参考方案3】:

我知道我的回答迟了,但我想分享一下我是如何做到的。

首先:使用ServiceLocator是Antipattern所以尽量不要使用它。 在我的情况下,我需要它在我的 DomainModel 中调用 MediatR 来实现 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- 我应该像这样在 DI 中注册 IServiceProviderProxy

public void ConfigureServices(IServiceCollection services)

    services.AddHttpContextAccessor();
    services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
    .......

5- 最后一步是在应用程序启动时使用IServiceProviderProxy 的实例初始化ServiceLocator

public void Configure(IApplicationBuilder app, IHostingEnvironment env,IServiceProvider sp)

    ServiceLocator.Initialize(sp.GetService<IServiceProviderProxy>());

因此,您现在可以在 DomainModel 类中调用 ServiceLocator“或者和需要的地方”并解决您需要的依赖关系。

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

【讨论】:

谢谢!这正是我想要做的,因为我想在我的域内使用 Mediatr 引发事件。 谢谢!你的回答对我帮助很大。看到这个解决方案,我想到了一个问题。如何解决您对可以处理 HTTP 请求和来自消息总线(或计划作业)的消息的服务的依赖关系?因为为了处理来自消息总线事件处理程序的消息,IHttpContextAccessor 不会初始化任何 HttpContext。在此先感谢:) @pablocom96 在这种情况下,您需要在调用后台消息之前定义您的范围并创建 IoC 范围。例如,如果您从服务总线接收消息,则您的范围很可能会在收到的消息上。 await using var scope = serviceProvider.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService&lt;UnifiedDatabaseDbContext&gt;(); @pablocom96 我猜你可以在 Masstransit 中使用中间件理念 masstransit-project.com/advanced/middleware 我在 NServiceBus 方面没有经验,但可以肯定它们具有相同的功能。您可以创建一个中间件来创建和处置您的范围 非常感谢!它通过在 Masstransit 中使用中间件来拦截 ConsumeContext 中的 IServiceScope,然后将其设置为 BusEventHandlerContextAccessor 中的 AsyncLocal 属性(作为单例),模仿 HttpContextAccessor 的行为方式。因此,我可以在我的静态 DomainEvents 类中解析范围服务。再次非常感谢您! :) :)【参考方案4】:

以下是我对您的案例的看法:

如果可能,我会将已解析的服务作为参数发送

public static IMessageProcessor Create(string messageType, IIpSetService ipService)

    //

否则服务寿命很重要。

如果服务是单例的,我只需设置对配置方法的依赖:

 // 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

【讨论】:

赞成。但是为什么不直接使用 httpcontextaccessor 而不管它是作用域的还是单例的呢?在单例中使用它有危险吗?【参考方案5】:

Here 是一个很好的 ServiceLocator 实现,它也使用 Scope。因此,即使是 IHttpContextAccessor 也适用!

只需将this class 复制到您的代码中即可。然后注册ServiceLocator

 ServiceActivator.Configure(app.ApplicationServices);

重要提示:ServiceLocator 被视为 ANTI-PATTERN,因此如果您有任何其他选择,请不要使用它!!!!

【讨论】:

【参考方案6】:

您可以在Configure获取服务参考:

app.UseMvc();
var myServiceRef = app.ApplicationServices.GetService<MyService>();

然后将其传递给初始化函数或在类上设置静态成员

当然,依赖注入将是一个更好的解决方案,如其他答案中所述...

【讨论】:

以上是关于从静态工厂类访问 ASP.NET Core DI 容器的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core中的依赖注入:依赖注入(DI)

来自静态类的 ASP.NET Core Web API 日志记录

ASP.NET Core 依赖注入(DI)

asp.net core 2.0 DI将多个IInterface类注入控制器

从 ASP.NET Core Web API 中的控制器访问用户身份

ASP.NET Core - 从 WebApi 提供静态内容 [重复]