用于将类型作为多个离散单例实例注入的模式

时间:2019-07-06 13:18:40

标签: c# asp.net-core dependency-injection

我正在使用asp.net core 2.2,对于预期的用例,我的类型必须为 ,但是我需要多个离散的singleton实例相同的类型,以便可以对其进行唯一标识和注入(如果适用)。

换句话说,对于用例 A ,当需要与用例 A 相关联的功能时,必须使用一个单例。对于用例 n ,当需要与用例 n 相关的功能时,必须使用一个单例。

从语义上讲,单例不是应用程序域中的单例,而是在所有单个用例中的单例。

幼稚的方法是重构接口,以便可以使用以下模式:

using Microsoft.Extensions.DependencyInjection;

class Program
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ITypeA, MySingleton>();
        services.AddSingleton<ITypeB, MySingleton>();
    }
}

public class MySingleton : ITypeA, ITypeB
{
}

public interface ITypeA : IMySingleton
{
}

public interface ITypeB : IMySingleton
{
}

public interface IMySingleton
{
}

然后使用单例类型的特定实例:

class Foo
{
    private readonly IMySingleton instance;
    public Foo(ITypeA instance) => this.instance = instance;
}
class Bar
{
    private readonly IMySingleton instance;
    public Bar(ITypeB instance) => this.instance = instance;
}

但是,这既不可扩展,也不合理。现有的模式可以使我执行上述操作,而无需不断重构单例以从新的更窄的接口(ITypeAITypeB)中派生,这些接口都实现了我所需的实际功能(IMySingleton) ?

2 个答案:

答案 0 :(得分:1)

  

存在什么模式可以使我执行上述操作,而无需不断重构单例以从新的较窄的接口派生

工厂模式会。

注入一个Factory而不是注入目标服务,而该工厂将返回您的服务实例。 EG

interface IMyService
{
   . . .
}
interface IMyServiceFactory
{
    IMyService GetInstance(string Name);
}

答案 1 :(得分:0)

这需要创建一些额外的类和一个单元测试。该测试从容器中解析服务,并根据您的问题的规格确认已解决并注入了服务。如果我们可以配置容器以使测试通过,那么我们就成功了。

public interface IServiceA
{
    ISharedService SharedService { get; }
}

public interface IServiceB
{
    ISharedService SharedService { get; }
}

public class ServiceA : IServiceA
{
    public ServiceA(ISharedService sharedService)
    {
        SharedService = sharedService;
    }

    public ISharedService SharedService { get; }
}

public class ServiceB : IServiceB
{
    public ServiceB(ISharedService sharedService)
    {
        SharedService = sharedService;
    }

    public ISharedService SharedService { get; }
}

public interface ISharedService { }

public class SharedService : ISharedService { }

这个想法是ServiceAServiceB都依赖于ISharedService。我们需要多次解决每个问题以进行测试:

  • 我们解析IServiceA时,是否总是得到相同的SharedService实例?
  • 我们解析IServiceB时,是否总是得到相同的SharedService实例?
  • 当我们解析IServiceAIServiceB时,会得到SharedService的不同实例吗?

这是单元测试的概要:

public class DiscreteSingletonTests
{
    [TestMethod]
    public void ResolvesDiscreteSingletons()
    {
        var serviceProvider = GetServiceProvider();
        var resolvedA1 = serviceProvider.GetService<IServiceA>();
        var resolvedA2 = serviceProvider.GetService<IServiceA>();
        var resolvedB1 = serviceProvider.GetService<IServiceB>();
        var resolvedB2 = serviceProvider.GetService<IServiceB>();

        // Make sure we're resolving multiple instances of each. 
        // That way we'll know that the "discrete" singleton is really working.
        Assert.AreNotSame(resolvedA1, resolvedA2);
        Assert.AreNotSame(resolvedB1, resolvedB2);

        // Make sure that all instances of ServiceA or ServiceB receive
        // the same instance of SharedService.
        Assert.AreSame(resolvedA1.SharedService, resolvedA2.SharedService);
        Assert.AreSame(resolvedB1.SharedService, resolvedB2.SharedService);

        // ServiceA and ServiceB are not getting the same instance of SharedService.
        Assert.AreNotSame(resolvedA1.SharedService, resolvedB1.SharedService);
    }

    private IServiceProvider GetServiceProvider()
    {
        // This is the important part.
        // What goes here?
        // How can we register our services in such a way
        // that the unit test will pass?
    }
}

除非我们做一些我不想做的非常丑陋的事情,否则我们不能仅靠IServiceCollection/IServiceProvider来做到这一点。相反,我们可以按照this documentation的建议使用不同的IoC容器:

  

内置服务容器旨在满足框架和大多数消费者应用程序的需求。我们建议使用内置容器,除非您需要它不支持的特定功能。

换句话说,如果我们想要所有的钟声,我们就必须使用另一个容器。以下是一些有关此操作的示例:


Autofac

此解决方案使用Autofac.Extensions.DependencyInjection。您可以根据其中使用Startup类的示例进行更改。

private IServiceProvider GetServiceProvider()
{
    var serviceCollection = new ServiceCollection();
    var builder = new ContainerBuilder();
    builder.Populate(serviceCollection);

    builder.RegisterType<SharedService>().As<ISharedService>()
        .Named<ISharedService>("ForServiceA")
        .SingleInstance();
    builder.RegisterType<SharedService>().As<ISharedService>()
        .Named<ISharedService>("ForServiceB")
        .SingleInstance();
    builder.Register(ctx => new ServiceA(ctx.ResolveNamed<ISharedService>("ForServiceA")))
        .As<IServiceA>();
    builder.Register(ctx => new ServiceB(ctx.ResolveNamed<ISharedService>("ForServiceB")))
        .As<IServiceB>();

    var container = builder.Build();
    return new AutofacServiceProvider(container);
}

我们正在用不同的名称两次注册ISharedService,每个名称都是一个单例。然后,在注册IServiceAServiceB时,我们指定要使用的已注册组件的名称。

IServiceAIServiceB是瞬态的(未指定,但这是默认值)。


温莎城堡

此解决方案使用Castle.Windsor.MsDependencyInjection

private IServiceProvider GetServiceProvider()
{
    var container = new WindsorContainer();
    var serviceCollection = new ServiceCollection();

    container.Register(
        Component.For<ISharedService, SharedService>().Named("ForServiceA"),
        Component.For<ISharedService, SharedService>().Named("ForServiceB"),
        Component.For<IServiceA, ServiceA>()
            .DependsOn(Dependency.OnComponent(typeof(ISharedService), "ForServiceA"))
            .LifestyleTransient(),
        Component.For<IServiceB, ServiceB>()
            .DependsOn(Dependency.OnComponent(typeof(ISharedService), "ForServiceB"))
            .LifestyleTransient()
    );
    return WindsorRegistrationHelper.CreateServiceProvider(container, serviceCollection);
}

我们正在用不同的名称两次注册ISharedService,每个名称都是一个单例。 (未指定,但这是默认设置。)然后,在注册IServiceAServiceB时,我们指定要使用的已注册组件的名称。


在这两种情况下,我都在创建ServiceCollection而不对其进行任何操作。关键是您仍然可以直接通过IServiceCollection注册类型,而不是通过Autofac或Windsor注册类型。因此,如果您注册了此地址:

serviceCollection.AddTransient<Whatever>();

...您可以解析Whatever。添加另一个容器并不意味着您现在必须在该容器中注册所有内容。