3

I have a .NET Core 3.1 API that may either be using RabbitMq or Azure Service Bus. The choice will be determined via a configuration parameter. Since the configuration to use is a runtime decision, I wish to use a factory pattern along with .NET Core's dependency injection. I found an article at https://medium.com/@mailbox.viksharma/factory-pattern-using-built-in-dependency-injection-of-asp-net-core-f91bd3b58665, but cannot get the factory to work. Any help will be greatly appreciated.

The issue is occurring within the Factory class due to IServiceProvider. I am receiving the error System.NullReferenceException: Object reference not set to an instance of an object. from the attempt to GetService.

Factory class

public class MessageServiceFactory
{
    readonly IServiceProvider serviceProvider;

    public MessageServiceFactory(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public IMessagingService GetMessagingService()
    {
        var messageProvider = ConfigProvider.GetConfig("MessageService", "Messaging_Service");

        switch(messageProvider)
        {
            case "AzureServiceBus": return (IMessagingService)serviceProvider.GetService(typeof(MassTransitAzureServiceBusMessagingService));
            case "RabbitMq": return (IMessagingService)serviceProvider.GetService(typeof(MassTransitRabbitMqMessagingService));
                default: throw new ArgumentException("Invalid message service");
        };
    }
}

Service Interface

public interface IMessagingService
{
    Task Publish(object payload);
}

RabbitMq Concrete Implementation

public class MassTransitRabbitMqMessagingService : IMessagingService
{
    readonly IMassTransitRabbitMqTransport massTransitTransport;

    public MassTransitRabbitMqMessagingService(IMassTransitRabbitMqTransport massTransitTransport)
    {
        //transport bus config already happens in massTransitTransport constructor
        this.massTransitTransport = massTransitTransport;
    }

    public async Task Publish(object payload)
    {
        ....
    }
}

ConfigureServices in Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(Configuration);

        services.AddScoped<IMassTransitRabbitMqTransport, MassTransitRabbitMqTransport>();
        services.AddScoped<IMassTransitAzureServiceBusTransport, MassTransitAzureServiceBusTransport>();

        services.AddScoped<MessageServiceFactory>();

        services.AddScoped<IMessagingService, MassTransitAzureServiceBusMessagingService>(s => s.GetService<MassTransitAzureServiceBusMessagingService>());
        services.AddScoped<IMessagingService, MassTransitRabbitMqMessagingService>(s => s.GetService<MassTransitRabbitMqMessagingService>());

        services.AddControllers();
    }

Controller

[ApiController]
[Route("api/[controller]")]
public class ListenerController : ControllerBase
{
    readonly ILogger<ListenerController> logger;
    readonly MessageServiceFactory messageFactory;

    public ListenerController(
        ILogger<ListenerController> logger,
        MessageServiceFactory messageFactory)
    {
        this.logger = logger;
        this.messageFactory = messageFactory;
    }

    [HttpPost]
    public async Task<IActionResult> Post()
    {
        var payload = new
        {
            ...
        };

        await messageFactory.GetMessagingService().Publish(payload);

        return Ok(
            new GDMSResponse()
            {
                ProcessedDate = DateTime.Now,
                SuccessFlag = true
            }
        );
    }
}
6
  • Couldn't you just decide in ConfigureServices what implementation to use? Or should you be able to change the configuration file without restarting the application? Commented May 27, 2020 at 19:44
  • @Michael, there was a sample of code I came across that does the instantiation of each type of service in the DI container but that feels like a bad code smell since instead of doing DI, it is instantiating each type but without the ability to determine which will be used at runtime. Would you share the example you have in mind? If I don't need a factory that would be great.At this point, I have removed the DI container code and am just creating the type needed in the factory. Commented May 27, 2020 at 19:53
  • It depends on whether you need to be able to change the configuration without restarting the application. Commented May 27, 2020 at 19:55
  • @Michael I am a bit confused - if I wanted to change the configuration on the fly, I would just use services.AddScoped(Configuration). How does that help with a factory pattern? Commented May 27, 2020 at 20:00
  • I ment if you need to be able to switch between RabbitMQ and AzureServiceBus without restarting the application. See if my suggestion helps. At least that is how I would do it. Commented May 27, 2020 at 20:05

1 Answer 1

4

This requires restart after changing the configuration, but I see no problem in doing it like this.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(Configuration);

    services.AddScoped<IMassTransitRabbitMqTransport, MassTransitRabbitMqTransport>();
    services.AddScoped<IMassTransitAzureServiceBusTransport, MassTransitAzureServiceBusTransport>();

    var messageProvider = Configuration.GetConfig("MessageService", "Messaging_Service");
    switch(messageProvider)
    {
        case "AzureServiceBus": 
            services.AddScoped<IMessagingService, MassTransitAzureServiceBusMessagingService>();
            break;
        case "RabbitMq": 
            services.AddScoped<IMessagingService, MassTransitRabbitMqMessagingService>();
            break;
        default: 
            throw new ArgumentException("Invalid message service");
    };

    services.AddControllers();
}

Other note

I noticed that you supplied both the concrete type and a factory:

services.AddScoped<IMessagingService, MassTransitAzureServiceBusMessagingService>(s => s.GetService<MassTransitAzureServiceBusMessagingService>());

I think it should be:

services.AddScoped<IMessagingService>(s => s.GetService<MassTransitAzureServiceBusMessagingService>());

Not sure it it makes a difference.

UPDATE Jan 2021 Recently I had to do this myself and came up with this solution:

public static IServiceCollection ConfigureEventBus(this IServiceCollection services, IConfiguration configuration)
{
    var queueSettings = new QueueSettings();
    configuration.GetSection("QueueSettings").Bind(queueSettings);

    if (configuration.GetValue<bool>("AzureServiceBusEnabled"))
    {
        services.AddMassTransit(x =>
        {
            x.UsingAzureServiceBus((context, cfg) =>
            {
                cfg.Host(queueSettings.HostName);
            });
        });
    }
    else
    {
        services.AddMassTransit(x =>
        {
            x.UsingRabbitMq((context, cfg) =>
            {
                cfg.ConfigureEndpoints(context);
                cfg.Host(queueSettings.HostName, queueSettings.VirtualHost, h =>
                {
                    h.Username(queueSettings.UserName);
                    h.Password(queueSettings.Password);
                });
            });
        });
    }

    services.AddMassTransitHostedService();
    services.AddSingleton<IEventBus, MassTransitEventBus>();

    return services;
}
Sign up to request clarification or add additional context in comments.

5 Comments

That works and is more pure since the controller does not need the factory. Also, the services.AddScoped<MessageServiceFactory>(); can also be removed. Thanks for your help. In regards to your note, it does not make a difference now since I got rid of the factory but I don't think it would have mattered anyway since the issue was caused by IServiceProvider not working.
@JuanDelaCruz not sure what you mean. This evaluates on runtime based on configuration in the appsettings.json.
@Michael I don't know if ConfigureServices runs each request, because if not it means the configuration is evaluated only the first time and cannot be changed later on if it changes, which I thought was the requirement.
That's correct. But based on that the OP has accepted the answer I assumed that it should only evaluate at startup. I also write that this solution requires a restart after changing config ;)
As I was learning about the factory pattern, I was thinking how I an apply the factory pattern in MVC and was searching for the answer since due to the configuation hardcoded approach for injecting the dependency is occured in the Startup configruaiton function, so if i have multiple concrete classes out of an interface, how would I change at runtime. So I think the Factory method which returns the actual class to the client or in my case to the controller is done in the ConfigurationServics() method. I am not sure your first solution works but I got the idea.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.