使用.NET Core 3.1依赖注入和工厂模式

3
我有一个 .NET Core 3.1 API,可能使用 RabbitMq 或 Azure Service Bus。选择将通过配置参数确定。由于使用的配置是运行时决策,我希望使用工厂模式以及 .NET Core 的依赖注入。我在这篇文章中找到了一些信息,但无法使工厂运作。非常感谢您的任何帮助。
因为 IServiceProvider 的问题,工厂类出现错误。我收到了“System.NullReferenceException:对象引用未设置为对象实例”的错误提示,在尝试 GetService 时发生该错误。 工厂类
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");
        };
    }
}

服务接口

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

RabbitMQ 具体实现

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)
    {
        ....
    }
}

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

控制器

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

你不能在 ConfigureServices 中决定使用哪个实现吗?或者说,你应该能够在不重启应用程序的情况下更改配置文件吗? - Michael
@Michael,我遇到了一段代码示例,它在DI容器中实例化了每种类型的服务,但这感觉像是一种糟糕的代码味道,因为它不是进行DI,而是实例化每种类型,但没有能力确定在运行时将使用哪种类型。你能分享一下你心目中的例子吗?如果我不需要工厂,那就太好了。此时,我已经删除了DI容器代码,只是在工厂中创建所需的类型。 - Lee Z
这取决于您是否需要在不重启应用程序的情况下更改配置。 - Michael
@Michael 我有点困惑 - 如果我想在运行时更改配置,我只需使用services.AddScoped(Configuration)。这对工厂模式有什么帮助吗? - Lee Z
我是说,如果您需要在不重启应用程序的情况下在RabbitMQ和AzureServiceBus之间切换,请看看我的建议是否有帮助。至少这是我会这样做的。 - Michael
与您链接的参考文章相比,您未能将MassTransitAzureServiceBusMessagingService/MassTransitRabbitMqMessagingService本身添加为AddScoped,而不是通过IMessagingService接口。这可能是罪魁祸首吗? - orhtej2
1个回答

4

更改配置后,这需要重新启动,但我认为这样做没有问题。

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

其他注意事项

我注意到你提供了具体类型和工厂:

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

我认为应该是:

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

不确定这是否有区别。

2021年1月更新 最近我自己也需要这样做,并想出了这个解决方案:

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

那个方法可行且更加纯粹,因为控制器不需要工厂。此外,services.AddScoped<MessageServiceFactory>(); 也可以被移除。感谢您的帮助。关于您的注释,现在已经没有影响了,因为我已经摆脱了工厂,但我认为这并不重要,因为问题是由 IServiceProvider 没有工作引起的。 - Lee Z
@JuanDelaCruz 不确定你的意思。这个在运行时根据appsettings.json中的配置进行评估。 - Michael
@Michael 我不知道ConfigureServices是否在每个请求中运行,因为如果不是这样的话,那么配置只会在第一次评估,并且如果更改后就无法再更改,而我认为这是要求。 - Juan De la Cruz
没错。但是基于OP已经接受了答案,我假设它只应该在启动时进行评估。我还写道,此解决方案需要在更改配置后重新启动 ;) - Michael
当我学习工厂模式时,我在思考如何将工厂模式应用到MVC中,并一直在寻找答案。由于在启动配置函数中发生依赖项注入的硬编码方法,如果我有多个实现接口的具体类,如何在运行时更改。因此,我认为返回实际类到客户端的工厂方法或在我的情况下到控制器是在ConfigurationServices()方法中完成的。我不确定你的第一个解决方案是否可行,但我明白了其中的思路。 - Faisal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接