简体   繁体   中英

ASP.NET Core DI based on requesting type

How do I configure dependency injection in ASP.NET Core to return a certain instance depending on the type it's being injected into?

Let's say I have a simple interface,

public interface IHello
{
    string SayHello();
}

And two different implementations:

public class Hello : IHello
{
    public string SayHello() => "Hello...";
}

public class Hey : IHello
{
    public string SayHello() => "HEY!";
}

And finally I have a few classes that all depend on an instance of IHello :

public class Class1
{
    public Class1(IHello hello)
    {
    }
}

public class Class2
{
    public Class2(IHello hello)
    {
    }
}

Now, in ConfigureServices I would do something like this:

services.AddSingleton<IHello, Hello>();

to configure any class depending on IHello to always get the same instance of Hello .

BUT: What I really want is for Class1 to always get the same singleton instance of Hey and all other classes should just get an instance of Hello . It could look like this in ConfigureServices (doesn't work, obviously):

services.AddSingleton<IHello, Hello>();
services.AddSingleton<IHello, Hey, Class1>(); // Doesn't work, but would be neat if it did...

Here's a simple approach. It lacks a certain elegance, but it will do what you need:

public static void Register(IServiceCollection serviceCollection)
{
    serviceCollection.AddSingleton<Hello>();
    serviceCollection.AddSingleton<Hey>();

    serviceCollection.AddSingleton<ClassThatDependsOnIHello1>(serviceProvider =>
        new ClassThatDependsOnIHello1(serviceProvider.GetService<Hello>()));

    serviceCollection.AddSingleton<ClassThatDependsOnIHello2>(serviceProvider =>
        new ClassThatDependsOnIHello2(serviceProvider.GetService<Hey>()));
}

There are two classes that depend on IHello . The registration for each of them includes a function. That function resolves either Hello or Hey from the service provider and passes it to the constructor of each respective class. That way you get control over which implementation gets passed to which class.

(It's beside the point that the service provider hasn't been built yet. The function you're providing will be executed later, and the service provider passed to it will be the one that has been built from the service collection.)

A downside to this is that now your DI registration explicitly calls your constructors. That can be a nuisance because if the constructors change (maybe you inject other dependencies) then you'll have to edit this code. That's not great, but it's not uncommon.


Plan B would be to do as Microsoft suggests and use another container.

Autofac

First, add the Autofac.Extensions.DependencyInjection NuGet package. This references Autofac and also provides the extensions needed to add an Autofac container to a service collection.

I've arranged this to focus on the way dependencies get registered with Autofac. It's similar to IServiceCollection and IServiceProvider . You create a ContainerBuilder , register dependencies, and then build a Container from it:

static void RegisterDependencies(this ContainerBuilder containerBuilder)
{
    containerBuilder.RegisterType<Hello>().Named<IHello>("Hello");
    containerBuilder.RegisterType<Hey>().Named<IHello>("Hey");

    containerBuilder.RegisterType<ClassThatDependsOnIHello1>().WithParameter(
        new ResolvedParameter((parameter, context) => parameter.ParameterType == typeof(IHello),
            (parameter, context) => context.ResolveNamed<IHello>("Hello")
        ));

    containerBuilder.RegisterType<ClassThatDependsOnIHello2>().WithParameter(
        new ResolvedParameter((parameter, context) => parameter.ParameterType == typeof(IHello),
            (parameter, context) => context.ResolveNamed<IHello>("Hey")
        ));
}

That's not really pretty either, but it sidesteps the problem of calling the constructors.

First it registers two implementations of IHello and gives them names.

Then it registers the two classes that depend on IHello . WithParameter(new ResolvedParameter()) uses two functions:

  • The first function determines whether a given parameter is the one we want to resolve. So in each case we're saying, "If the parameter to resolve is IHello , then resolve it using the next function."
  • It then resolves IHello by specifying which named registration to use.

I'm not excited by how complicated that is, but it does mean that if those classes have other dependencies injected, they'll be resolved normally. You can resolve ClassThatDependsOnIHello1 without actually calling its constructor.

You can also do it without the names:

static void RegisterDependencies(this ContainerBuilder containerBuilder)
{
    containerBuilder.RegisterType<Hello>();
    containerBuilder.RegisterType<Hey>();

    containerBuilder.RegisterType<ClassThatDependsOnIHello1>().WithParameter(
        new ResolvedParameter((parameter, context) => parameter.ParameterType == typeof(IHello),
            (parameter, context) => context.Resolve<Hello>()
        ));

    containerBuilder.RegisterType<ClassThatDependsOnIHello2>().WithParameter(
        new ResolvedParameter((parameter, context) => parameter.ParameterType == typeof(IHello),
            (parameter, context) => context.Resolve<Hey>()
        ));

    containerBuilder.RegisterType<SomethingElse>().As<ISomethingElse>();
}

We can clean that up some with an method that simplifies creating that ResolvedParameter because that's so hideous.

public static ResolvedParameter CreateResolvedParameter<TDependency, TImplementation>()
    where TDependency : class
    where TImplementation : TDependency
{
    return new ResolvedParameter((parameter, context) => parameter.ParameterType == typeof(TDependency),
        (parameter, context) => context.Resolve<TImplementation>());
}

Now the previous registration becomes:

containerBuilder.RegisterType<ClassThatDependsOnIHello1>().WithParameter(
    CreateResolvedParameter<IHello, Hello>());

containerBuilder.RegisterType<ClassThatDependsOnIHello2>().WithParameter(
    CreateResolvedParameter<IHello, Hey>());

Better!

That leaves the details of how you integrate it with your application, and that varies with your application. Here's Autofac's documentation which provides more detail.

For testing purposes you can do this:

public static IServiceProvider CreateServiceProvider()
{
    var containerBuilder = new ContainerBuilder();
    containerBuilder.RegisterDependencies();
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
}

I like to write unit tests for this sort of thing. This method will create an IServiceProvider from the Autofac container, and then you can test resolving things from the container to make sure they get resolved as expected.

If you prefer another container, see if it has similar integrations to use it with Microsoft's container. You might find one you like better.


Windsor

Here's a similar example using Castle.Windsor.MsDependencyInjection .

public static class WindsorRegistrations
{
    public static IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection)
    {
        var container = new WindsorContainer();
        container.RegisterDependencies();
        return WindsorRegistrationHelper.CreateServiceProvider(container, serviceCollection);
    }

    public static void RegisterDependencies(this IWindsorContainer container)
    {
        container.Register(
            Component.For<Hello>(),
            Component.For<Hey>(),

            Component.For<ClassThatDependsOnIHello1>()
                .DependsOn(Dependency.OnComponent<IHello, Hello>()),
            Component.For<ClassThatDependsOnIHello2>()
                .DependsOn(Dependency.OnComponent<IHello, Hey>())
        );
    }
}

There are two things I like about this:

  • It's so much easier to read! When resolving ClassThatDependsOnIHello2 , fulfill the dependency on IHello with Hey . Simple.
  • This - WindsorRegistrationHelper.CreateServiceProvider(container, serviceCollection) - allows you to register dependencies with the IServiceCollection and include them in the service provider. So if you have existing code that registers lots of dependencies with IServiceCollection you can still use it. You can register other dependencies with Windsor. Then CreateServiceProvider mixes them all together. (Maybe Autofac has a way to do that too. I don't know.)

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