web-dev-qa-db-fra.com

Accès au conteneur ASP.NET Core DI à partir de la classe d'usine statique

J'ai créé un site ASP.NET Core MVC/WebApi avec un abonné RabbitMQ basé sur l'article de blog de James Still Messagerie PubSub en monde réel avec RabbitMQ .

Dans son article, il utilise une classe statique pour démarrer l'abonné de la file d'attente et définir le gestionnaire d'événements pour les événements en file d'attente. Cette méthode statique instancie ensuite les classes du gestionnaire d'événements via une classe fabrique statique.

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

    }
}

Cela fonctionne très bien jusqu'au moment où je dois maintenant résoudre un service dans mon usine de traitement de messages plutôt que d'écrire sur la console.

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

Est-il possible d'accéder au conteneur ASP.NET Core IoC pour résoudre les dépendances? Je ne veux pas vraiment avoir à faire tourner toute la pile de dépendances à la main :(

Ou, existe-t-il un meilleur moyen de s'abonner à RabbitMQ à partir d'une application ASP.NET Core? J'ai trouvé RestBus mais cela n'a pas été mis à jour pour Core 1.x

27
Nick

Vous pouvez éviter les classes statiques et utiliser l’injection de dépendance en combinaison avec:

  • L'utilisation de IApplicationLifetime pour démarrer/arrêter l'écouteur chaque fois que l'application démarre/s'arrête.
  • Utilisation de IServiceProvider pour créer des instances des processeurs de message.

Tout d’abord, déplaçons la configuration dans sa propre classe, qui peut être renseignée à partir du fichier 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
  }
}

Ensuite, convertissez MessageHandlerFactory en une classe non statique qui reçoit un IServiceProvider en tant que dépendance. Il utilisera le fournisseur de services pour résoudre les instances du processeur de messages:

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 cette façon, vos classes de traitement de messages peuvent recevoir dans le constructeur les dépendances dont elles ont besoin (à condition de les configurer dans Startup.ConfigureServices). Par exemple, j'injecte un ILogger dans l'un de mes processeurs d'échantillons:

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

Maintenant convertissez MessageListener en une classe non statique qui dépend de IOptions<RabbitOptions> and MessageHandlerFactory. Il est très similaire à l'original, je viens de remplacer les paramètres des méthodes Start par les options dependency et la fabrique de gestionnaires est maintenant une dépendance au lieu d'une classe statique:

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

Presque là, vous devrez mettre à jour le Startup.ConfigureServices méthode pour qu’il connaisse vos services et vos options (vous pouvez créer des interfaces pour la fabrique de programmes d'écoute et de gestionnaires si vous le souhaitez):

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

Enfin, mettez à jour le Startup.Configure méthode pour prendre un paramètre supplémentaire IApplicationLifetime et démarrer/arrêter l'écouteur de message dans les événements ApplicationStarted/ApplicationStopped (bien que j'aie remarqué quelques problèmes avec l'événement ApplicationStopping en utilisant IISExpress, comme dans cette 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();
    });

    // ...
}
27
Daniel J.G.

Même si utiliser Dependency Injection est une meilleure solution, vous devez dans certains cas utiliser des méthodes statiques (comme dans les méthodes d'extension).

Dans ces cas, vous pouvez ajouter une propriété statique à votre classe statique et l'initialiser dans votre méthode ConfigureServices.

Par exemple:

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

et dans vos ConfigureServices:

EnumExtentions.StringLocalizerFactory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
14
HamedH

Voici mon opinion sur votre cas:

Si possible, j'enverrais un service résolu en tant que paramètre

public static IMessageProcessor Create(string messageType, IIpSetService ipService)
{
    //
}

Sinon la durée de vie serait importante.

Si service est singleton, je voudrais juste définir la dépendance sur la méthode 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
}

Si la durée de vie du service est limitée, j'utiliserais 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
adem caglin

Je sais que ma réponse est en retard, mais je voulais partager comment je l'ai fait.

Tout d’abord : C’est Antipattern à utiliser ServiceLocator alors essayez de ne pas l'utiliser comme vous le pouvez. Dans mon cas, j'avais besoin d'appeler MediatR à l'intérieur de mon DomainModel pour implémenter la logique DomainEvents .

Cependant , je devais trouver un moyen d'appeler une classe statique dans DomainModel pour obtenir une instance d'un service enregistré de DI.

J'ai donc décidé d'utiliser HttpContext pour accéder à IServiceProvider mais je devais y accéder à partir d'une méthode statique sans le mentionner dans mon modèle de domaine.

Faisons le:

1- J'ai créé une interface pour envelopper IServiceProvider

public interface IServiceProviderProxy
{
    T GetService<T>();
    IEnumerable<T> GetServices<T>();
    object GetService(Type type);
    IEnumerable<object> GetServices(Type type);
}

2- Puis j'ai créé une classe statique pour être mon point d'accès 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- J'ai créé une implémentation pour le IServiceProviderProxy qui utilise en interne le 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- Je devrais enregistrer le IServiceProviderProxy dans le DI comme ceci

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpContextAccessor();
    services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();
    .......
}

5- La dernière étape consiste à initialiser le ServiceLocator avec une instance de IServiceProviderProxy au démarrage de l'application

public void Configure(IApplicationBuilder app, IHostingEnvironment env,IServiceProvider sp)
{
    ServiceLocator.Initialize(sp.GetService<IServiceProviderProxy>());
}

En conséquence, vous pouvez maintenant appeler ServiceLocator dans vos classes DomainModel "Ou et emplacement requis" et résoudre les dépendances dont vous avez besoin.

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));
    }
}
1
Wahid Bitar