57

I've created an ASP.NET Core MVC/WebApi site that has a RabbitMQ subscriber based off James Still's blog article Real-World PubSub Messaging with RabbitMQ.

In his article he uses a static class to start the queue subscriber and define the event handler for queued events. This static method then instantiates the event handler classes via a static factory class.

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

    }
}

It works great up to the point where I now need to resolve a service in my message processor factory rather than just write to the 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");
            }
        }
    }
}

Is there any way to access the ASP.NET Core IoC container to resolve the dependencies? I don't really want to have to spin up the whole stack of dependencies by hand :(

Or, is there a better way to subscribe to RabbitMQ from an ASP.NET Core application? I found RestBus but it's not been updated for Core 1.x

Wahid Bitar
  • 13,776
  • 13
  • 78
  • 106
Nick
  • 4,115
  • 10
  • 45
  • 57

7 Answers7

47

You can avoid the static classes and use Dependency Injection all the way through combined with:

  • The use of IApplicationLifetime to start/stop the listener whenever the application starts/stops.
  • The use of IServiceProvider to create instances of the message processors.

First thing, let's move the configuration to its own class that can be populated from the 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
  }
}

Next, convert MessageHandlerFactory into a non-static class that receives an IServiceProvider as a dependency. It will use the service provider to resolve the message processor instances:

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

This way your message processor classes can receive in the constructor any dependencies they need (as long as you configure them in Startup.ConfigureServices). For example, I am injecting an ILogger into one of my sample processors:

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

Now convert MessageListener into a non-static class that depends on IOptions<RabbitOptions> and MessageHandlerFactory.It's very similar to your original one, I just replaced the parameters of the Start methods with the options dependency and the handler factory is now a dependency instead of a static class:

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

Almost there, you will need to update the Startup.ConfigureServices method so it knows about your services and options (You can create interfaces for the listener and handler factory if you want):

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

Finally, update the Startup.Configure method to take an extra IApplicationLifetime parameter and start/stop the message listener in the ApplicationStarted/ApplicationStopped events (Although I noticed a while ago some issues with the ApplicationStopping event using IISExpress, as in 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();
    });

    // ...
}
Community
  • 1
  • 1
Daniel J.G.
  • 34,266
  • 9
  • 112
  • 112
  • 1
    I wonder how dependencies registered as transient AND implement IDisposable behave in this case. In asp.net core, if you resolve transient dependency - it will be disposed after request is finished. But here there is no request. – Evk Dec 01 '16 at 16:32
  • The built-in DI is relatively simple in some aspects like lifetime management. It might be worth considering hooking a 3rd party container like Autofact, StructureMap, Unity, etc in a case like this and create a scope per message for example – Daniel J.G. Dec 01 '16 at 17:00
  • 1
    Yes but if you wont do this and use default one, I hope it at least won't get disposed by container? – Evk Dec 01 '16 at 17:02
  • 1
    You could do `using (var scope = services.CreateScope())` and then resolve services from `scope.ServiceProvider` which will get disposed of when the scope is disposed. – Daniel J.G. Dec 01 '16 at 17:08
  • Thanks for this. I've not had time to try it, but I'd love to get away from the static implementation and this looks like it makes a lot of sense. – Nick Dec 07 '16 at 10:18
  • No worries! The ugliest part using the built-in DI container is that you cannot register multiple named `IMessageProcessor` and resolve them by name, so the abstract factory end a bit ugly. I wouldn't discard 3rd party containers (Autofact, StructureMap, Unity, etc). There are a few questions around that topic like [this](http://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core) and [this](http://stackoverflow.com/questions/39072001/dependency-injection-resolving-by-name) – Daniel J.G. Dec 07 '16 at 14:55
40

Even though using Dependency Injection is a better solution, but in some cases you have to use static methods (like in Extension Methods).

For those cases you can add a static property to your static class and initialize it in your ConfigureServices method.

For example:

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

and in your ConfigureServices:

EnumExtentions.StringLocalizerFactory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
HamedH
  • 2,814
  • 1
  • 26
  • 37
  • Thanks @HamedH. I was looking for this `services.BuildServiceProvider().GetService();` – Seevali H Rathnayake May 25 '19 at 13:08
  • 1
    I get a warning: `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'.` – kofifus Nov 18 '19 at 00:21
  • 2
    This was a practical solution for using extension methods with a singleton from the container. Note: set the static object in `Configure()`, not `ConfigureServices()` (for .Net Core 3+ anyway). e.g `public void Configure(IApplicationBuilder app, IStringLocalizerFactory factory)` – Jevon Kendon Mar 05 '20 at 23:29
24

I know my answer is late, but I wanted to share how I did it.

First of all: It's Antipattern to use ServiceLocator so try not to use it as you can. In my case I needed it to call MediatR inside of my DomainModel to implement the DomainEvents logic.

However, I had to find a way to call a static class in my DomainModel to get an instance of some registered service from DI.

So I've decided to use the HttpContext to access the IServiceProvider but I needed to access it from a static method without mention it in my domain model.

Let's do it:

1- I've created an interface to wrap the IServiceProvider

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

2- Then I've created a static class to be my ServiceLocator access point

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- I've created an implementation for the IServiceProviderProxy which use internally the 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- I should register the IServiceProviderProxy in the DI like this

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

5- Final step is to initialize the ServiceLocator with an instance of IServiceProviderProxy at the Application startup

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

As a result now you can call the ServiceLocator in your DomainModel classes "Or and needed place" and resolve the dependencies that you need.

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));
    }
}
Wahid Bitar
  • 13,776
  • 13
  • 78
  • 106
  • 2
    Thank you! This is EXACTLY what I am attempting to do, as I want to raise the events with Mediatr from within my domain. – Meester Over Feb 23 '20 at 15:23
  • Thank you! Your answer helped me a lot. I have one question that has came to my mind seeing this solution. How would it be possible to resolve your dependencies for a service that can handle HTTP requests, and messages from a message bus (or a scheduled job)? Because for handling messages that come from a message bus event handler, the IHttpContextAccessor is not going to have any HttpContext Initialised. Thanks in advance :) – pablocom Feb 25 '22 at 08:28
  • 1
    @pablocom96 in that case you need to define your scope and create IoC Scope before calling the background message. For example, if you're receiving a message from a service bus, your scope most probably will be on the message received. ```await using var scope = serviceProvider.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService();``` – Wahid Bitar Mar 01 '22 at 05:09
  • Okay I see, and how we would access to the same Scope we created for a message handle in our ServiceLocator? I'm trying to see if either Masstransit or NServiceBus provides a way to do it similar to IHttpContextAccessor – pablocom Mar 06 '22 at 20:45
  • 1
    @pablocom96 I guess you can use the Middleware idea in Masstransit https://masstransit-project.com/advanced/middleware/ I'm not experienced in NServiceBus but for sure they have the same feature. You can create a middleware to create and dispose your scopes – Wahid Bitar Mar 07 '22 at 09:35
  • 1
    Thanks a lot! It worked by using a middleware in Masstransit that intercepts the IServiceScope in the ConsumeContext, and then setting it into an AsyncLocal property in BusEventHandlerContextAccessor (as singleton), that mimics how HttpContextAccessor behaves. So then I'm able to resolve scoped services inside my static DomainEvents class. Again thank you very much! :) :) – pablocom Mar 07 '22 at 20:00
  • Why would a domain object need to talk to MediatR in the first place, the domain models should raise domain events and the propogation to infrastructure concerns such as MediatR would be handled in the outer layer of the onion. – craigw Feb 22 '23 at 21:57
  • @craigw in my case, I was using the Mediatr to raise the events – Wahid Bitar Feb 23 '23 at 06:34
  • @WahidBitar it's worth reading up onion architecture and domain modelling to avoid this. If you can correctly follow the principles of DDD then you wouldn't need to cater to domain objects implementing technical concerns. – craigw Feb 23 '23 at 07:13
  • 1
    Great solution for allowing access of scoped services from a singleton. – tsiorn Apr 21 '23 at 13:16
5

Here is my opinion about your case:

If possible i would send resolved service as a parameter

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

Otherwise service lifetime would be important.

If service is singleton i would just set dependency on configure method:

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

If service lifetime is scoped i would use 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
}
adem caglin
  • 22,700
  • 10
  • 58
  • 78
  • 2
    upvoted. but why not just use httpcontextaccessor regardless if it is scoped or singleton? is there a danger on using it in singleton? – Alvin Oct 24 '18 at 17:49
2

Here is a good explanation and some code for ServiceLocator it is using Scope also. So will work for even IHttpContextAccessor!

Just copy this class into your code. And then register ServiceLocator

 ServiceActivator.Configure(app.ApplicationServices);

IMPORTANT NOTE: ServiceLocator is consider as ANTI-PATTERN so do not use it if you have ANY other choice!!!!

Major
  • 5,948
  • 2
  • 45
  • 60
  • Could you please update your answer. The first link doesn't work. Also, please provide the class (in case the link breaks). I know it will be a long answer but community needs it. – Post Impatica Mar 10 '23 at 16:34
0

About answer of Wahid Bitar

Amazing! For .Net Core 6 I do on Program.cs:

builder.Services.AddSingleton<IServiceProviderProxy, HttpContextServiceProviderProxy>();

ServiceLocator.Initialize(app.Services.GetService<IServiceProviderProxy>());
  • 3
    This gives a strong impression of saying thanks for an existing answer. Is this meant to be an answer according to [answer]? If so please [edit] to make that more obvious. Otherwise please delete this. – Yunnosch Feb 01 '23 at 22:57
  • 1
    The only thing thast keeps me from flagging this as "Not An Answer; thanks for most upvoted existing answer" is that I cannot pinpoint which part of the existing answer this is thanking for. That in turn makes me suspect that there might be an answer hidden in here. Can somebody please either confirm that this is an answer or help me dissamble it into "thanks" + quote from existing answers? After a day of no feedback I think we need to turn this into an answer without the help of the author - or get it deleted. – Yunnosch Feb 03 '23 at 14:56
-2

You can get the service reference in Configure:

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

and then ie pass it to an init function or set a static member on a class

of course dependency injection would be a better solution as explained in the other answers ...

kofifus
  • 17,260
  • 17
  • 99
  • 173