简体   繁体   中英

Pattern for injecting a type as multiple discrete singleton instances

I am using asp.net core 2.2, I have a type which needs to be a singleton for the intended use case , however I require multiple discrete singleton instances of this same type such that they may be uniquely identifiable and injected where applicable.

In other words, for use case A , one singleton must be used when ever functionality associated with use case A is required. For use case n , one singleton must be used when ever functionality associated with use case n is required.

The singleton is not semantically a singleton in the app domain, it is a singleton within all individual use cases.

A naive approach would be to refactor the interfaces so the following pattern could be used:

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

Then to use a specific instance of the singleton type:

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

However this is neither scalable or reasonable. What pattern exists that would allow me to perform the above without continuously refactoring the singleton to derive from new narrower interfaces ( ITypeA , ITypeB ) which all implement the actual functionality I need ( IMySingleton )?

What pattern exists that would allow me to perform the above without continuously refactoring the singleton to derive from new narrower interfaces

The Factory Pattern would.

Instead of injecting your target service, inject a Factory that returns the one the instances of your service. EG

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

This required creating a few extra classes and a unit test. The test resolves services from the container and confirms that they were resolved and injected according to the specifications in your question. If we can configure the container so that the test passes, we've succeeded.

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

The idea is that ServiceA and ServiceB both depend on ISharedService . We need to resolve each of them multiple times to test:

  • When we resolve IServiceA , do we always get the same instance of SharedService ?
  • When we resolve IServiceB , do we always get the same instance of SharedService ?
  • When we resolve IServiceA and IServiceB , do we get different instances of SharedService ?

Here's the outline of the unit test:

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

We can't do this with just IServiceCollection/IServiceProvider unless we do some really ugly stuff that I just don't want to do. Instead we can use different IoC containers, as recommended by this documentation :

The built-in service container is meant to serve the needs of the framework and most consumer apps. We recommend using the built-in container unless you need a specific feature that it doesn't support.

In other words, if we want all the bells and whistles we have to use another container. Here are some examples of how to do that:


Autofac

This solution uses Autofac.Extensions.DependencyInjection . You can alter it according to the example there which uses the Startup class.

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

We're registering ISharedService twice with different names, each as a singleton. Then, when registering IServiceA and ServiceB we're specifying the name of the registered component to use.

IServiceA and IServiceB are transient (not specified, but it's the default).


Castle Windsor

This solution uses 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);
}

We're registering ISharedService twice with different names, each as a singleton. (It's not specified, but that's the default.) Then, when registering IServiceA and ServiceB we're specifying the name of the registered component to use.


In both cases I'm creating a ServiceCollection and not doing anything with it. The point is that you can still register types directly with the IServiceCollection rather than through Autofac or Windsor. So if you registered this:

serviceCollection.AddTransient<Whatever>();

...you can resolve Whatever . Adding another container doesn't mean that you now have to register everything with that container.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM