简体   繁体   中英

With Microsoft.Extensions.DependencyInjection, can I resolve the type and construct an instance while providing extra constructor parameters?

With Microsoft.Extensions.DependencyInjection , can I resolve the type and construct an instance while providing extra constructor parameters, in one go?

What I'd like to do is easy to illustrate by example. Below, in CreateSomethingWithContext I need to use ActivatorUtilities.CreateInstance to call a parameterized constructor, but I don't know the concrete type for ISomething in advance.

I could use serviceProvider.GetRequiredService to resolve the type and create a default instance, but then I couldn't pass context parameter.

I could use a factory version of AddTransient , but that would break the code which already uses GetRequiredService to create default instances.

Below, a possible kludge (which I dislike) is to create a redundant default instance of Something only to figure out its type and pass it to ActivatorUtilities.CreateInstance , along with the context parameter.

Is there a better way of doing this? To clarify, I have no control over the actual library that implements ISomething / Something .

using System;
using Microsoft.Extensions.DependencyInjection;

namespace App
{
  public interface ISomething
  {
    void DoSomething() => Console.WriteLine(nameof(DoSomething));    
  }  
  
  public class Something: ISomething
  {
    public Something() =>
      Console.WriteLine($"{this.GetType().Name} created");

    public Something(object context) =>
      Console.WriteLine($"{this.GetType().Name} created with {context}");
  }

  static class Program
  {
    private static IServiceProvider BuildServices() => new ServiceCollection()
      .AddTransient<ISomething, Something>()
      .BuildServiceProvider();

    static ISomething CreateSomething(IServiceProvider serviceProvider) =>
      serviceProvider.GetRequiredService<ISomething>();

    static ISomething CreateSomethingWithContext(IServiceProvider serviceProvider, object context) 
    {
      // first I need an instance of ISomething, only to learn its concrete type for
      var something = serviceProvider.GetRequiredService<ISomething>();
      var type = something.GetType();  
      // now that I have the type, I can use ActivatorUtilities.CreateInstance
      var something2 = (ISomething)ActivatorUtilities.CreateInstance(
        serviceProvider, type, context);
      return something2;
    }

    static void Main()
    {
      var serviceProvider = BuildServices();
      CreateSomething(serviceProvider);
      CreateSomethingWithContext(serviceProvider, Guid.NewGuid());
    }
  }
}

Personally, I agree with you about not wanting to do this with ActivatorUtilities.CreateInstance - although it will work, if ever the constructor's signature changes you'd end up getting a runtime error, and I'm sure a compile time error would be preferable.

Why not register a completely separate factory for constructing your object with the context - Leave the AddTransient() registration untouched so that all calling code that relies on the default constructor would still work, and use newly created factory where you need to insert your context ?

The thing you are asking for is something that goes fundamentally against the idea of the dependency inversion principle, which is the basis for dependency injection. So it is unlikely that you will find support for something like this in common dependency injection containers.

To understand this, let's think about why you are actually registering your concrete implementation Something as its interface type ISomething . By doing that, you are allowing components to depend on the abstraction instead of a concrete type. This reduces coupling as it allows you to easily swap the implementation with something else that satisfies the interface specification.

The idea is that you don't actually want to know what implementation you get when you depend on ISomething as long as it is compatible to the interface. That also means that you do not want to know what dependencies that concrete implementation might have itself. Instead, the dependent component gives up the control to an outside system, usually the DI container, and just expects the system to give you the thing you need.

Now, when comparing this idea with ActivatorUtilities.CreateInstance , one might observe that this utility method is actually just syntactic sugar around the service locator pattern, which is usually in direct contrast to dependency injection: Instead of relying on the system to give you whatever you depend on, you can now actively request your dependencies from the container itself. ActivatorUtiltilies.CreateInstance just does that while also allowing you to pass values that should not be resolved from the container.

Let's assume that a method existed that allowed you to do CreateInstance<ISomething>(additionalValues) . You are now asking the container to provide you with an implementation for ISomething but apparently you know so much about the concrete implementation of that, that you are able to tell what parameters you would want to pass in order to resolve it. And that contradicts what I explained above, that you usually (deliberately) don't want to know what the concrete implementation is.


Instead, I would suggest you to create a proper factory for this, something that is made to create that Something for you, and offers a properly typed way to add that “context” data:

public class SomethingFactory : ISomethingFactory
{
    private readonly SomeDependency _someDependency;
    public SomethingFactory(SomeDependency someDependency)
    {
        _someDependency = someDependency;
    }

    public ISomething Create()
        => new Something(_someDependency);

    public ISomething CreateContext(object context)
        => new Something(_someDependency, context);
}

That factory is then made for the Something implementation, so it is perfectly fine that it knows what kind of parameters it would need to pass to the Something constructor, and since it has an abstraction ISomethingFactory itself, you are still able to do this properly using dependency injection instead of using the service locator pattern.

I've settled on this implementaiton ( gist , fiddle ), which uses the factory pattern based on ActivatorUtilities.CreateInstance and a custom extension IServiceProvider extension method Inject :

#nullable enable
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;

namespace App
{
    static class Program
    {
        private static IServiceProvider BuildServices() => new ServiceCollection()
            .AddFactory<ISomething, Something>()
            .BuildServiceProvider();

        static void Main()
        {
            var serviceProvider = BuildServices();

            var something0 = serviceProvider.GetRequiredService<ISomething>();

            var something1 = serviceProvider.Inject<ISomething>();

            var something2 = serviceProvider.Inject<ISomething>(something1);

            var something3 = serviceProvider.Inject<ISomething>(Guid.NewGuid());
        }
    }

    /// <summary>
    /// A sample interface for DI
    /// </summary>
    public interface ISomething
    {
        void DoSomething() => Console.WriteLine(nameof(DoSomething));
    }

    public class Something : ISomething
    {
        public Something() =>
          Console.WriteLine($"{this.GetType()} created");

        public Something(Guid context) =>
          Console.WriteLine($"{this.GetType()} created with {context.GetType()}");

        public Something(ISomething source) =>
            Console.WriteLine($"{this.GetType()} created with {source.GetType()}");

        public Something(IServiceProvider sp) =>
            Console.WriteLine($"{this.GetType()} created with {sp.GetType()}");

        public Something(IServiceProvider sp, Guid context) =>
            Console.WriteLine($"{this.GetType()} created with {sp.GetType()}, {context.GetType()}");

        public Something(IServiceProvider sp, ISomething context) =>
            Console.WriteLine($"{this.GetType()} created with {sp.GetType()}, {context.GetType()}");
    }

    /// <summary>
    /// DependencyInjectionExtensions
    /// </summary>
    public static class DependencyInjectionExtensions
    {
        public delegate TService Factory<out TService>(IServiceProvider? serviceProvider, params object[] parameters);

        /// <summary>
        /// Add a factory for parametrized DI injections
        /// </summary>
        public static IServiceCollection AddFactory<TService, TInstance>(this IServiceCollection @this)
            where TService : class
            where TInstance : class, TService
        {
            return @this
                .AddSingleton<Factory<TService>>(factoryServiceProvider =>
                    (callerServiceProvider, parameters) =>
                        ActivatorUtilities.CreateInstance<TInstance>(
                            callerServiceProvider ?? factoryServiceProvider, parameters))

                .AddTransient<TService>(serviceProvider =>
                    ActivatorUtilities.CreateInstance<TInstance>(serviceProvider, serviceProvider));
        }

        /// <summary>
        /// Inject with parameters
        /// </summary>
        public static TService Inject<TService>(this IServiceProvider @this, params object[] parameters)
            where TService : class
        {
            var factory = @this.GetService<Factory<TService>>();
            if (factory != null)
            {
                if (!parameters.Any(p => p is IServiceProvider))
                {
                    // add the current IServiceProvider to the param list
                    return factory(@this, parameters.Prepend(@this).ToArray());
                }
                return factory(@this, parameters);
            }
            return @this.GetRequiredService<TService>();
        }
    }
}

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