Inyección de dependencia con clases distintas a la clase de controlador

84

En este punto, estoy inyectando cosas en mis Controllers con facilidad, en algunos casos construyendo mi propia clase ResolverServices. La vida es buena .

Lo que no puedo averiguar cómo hacer es hacer que el marco se inyecte automáticamente en las clases que no son controladores. Lo que funciona es que el marco se inyecte automáticamente en mi controlador IOptions, que es efectivamente la configuración para mi proyecto:

public class MessageCenterController : Controller
{
    private readonly MyOptions _options;

    public MessageCenterController(IOptions<MyOptions> options)
    {
        _options = options.Value;
    }
}

Estoy pensando si puedo hacer lo mismo con mis propias clases. Supongo que estoy cerca cuando imito el controlador, así:

public class MyHelper
{
    private readonly ProfileOptions _options;

    public MyHelper(IOptions<ProfileOptions> options)
    {
        _options = options.Value;
    }

    public bool CheckIt()
    {
        return _options.SomeBoolValue;
    }
}

Creo que estoy fallando cuando lo llamo así:

public void DoSomething()
{
    var helper = new MyHelper(??????);

    if (helper.CheckIt())
    {
        // Do Something
    }
}

El problema que tengo para rastrear esto es que prácticamente todo lo que habla de DI lo está hablando a nivel del controlador. Intenté buscar dónde sucede en el Controllercódigo fuente del objeto, pero se vuelve un poco loco allí.

Sé que puedo crear manualmente una instancia de IOptions y pasarla al MyHelperconstructor, pero parece que debería poder hacer que el marco lo haga, ya que funciona Controllers.

Robert Paulsen
fuente
8
Cuando usa la inyección de dependencia, no llama new. Nunca para objetos que deberían resolverse
Tseng
1
Cuando intento crear una instancia de MyHelper, no llamo a new? (1) Eso suena demasiado fácil, (2) Es un error de sintaxis. :-)
Robert Paulsen
2
Sí, ese es el objetivo de la inyección de dependencia (especialmente si se usa la inversión de contenedores de control que administran y hacen esta instanciación). Para empujar la instancia fuera de sus servicios / clases hasta el punto en que el contenedor ioc lo haga internamente. En los casos en los que no puede inyectarlo a través del constructor, crea una fábrica y pasa la interfaz de la fábrica a su servicio. la implementación usa el contenedor para resolverlo, en el caso de ASP.NET Core inyectando IServiceProvideren su fábrica y llamandoIMyHelper helper = services.RequestService<IMyHelper>()
Tseng

Respuestas:

42

A continuación se muestra un ejemplo práctico del uso de DI sin nada que involucre controladores MVC. Esto es lo que necesitaba hacer para comprender el proceso, así que quizás ayude a alguien más.

El objeto ShoppingCart obtiene, a través de DI, una instancia de INotifier (que notifica al cliente de su pedido).

using Microsoft.Extensions.DependencyInjection;
using System;

namespace DiSample
{
    // STEP 1: Define an interface.
    /// <summary>
    /// Defines how a user is notified. 
    /// </summary>
    public interface INotifier
    {
        void Send(string from, string to, string subject, string body);
    }

    // STEP 2: Implement the interface
    /// <summary>
    /// Implementation of INotifier that notifies users by email.
    /// </summary>
    public class EmailNotifier : INotifier
    {
        public void Send(string from, string to, string subject, string body)
        {
            // TODO: Connect to something that will send an email.
        }
    }

    // STEP 3: Create a class that requires an implementation of the interface.
    public class ShoppingCart
    {
        INotifier _notifier;

        public ShoppingCart(INotifier notifier)
        {
            _notifier = notifier;
        }

        public void PlaceOrder(string customerEmail, string orderInfo)
        {
            _notifier.Send("[email protected]", customerEmail, $"Order Placed", $"Thank you for your order of {orderInfo}");
        }

    }

    public class Program
    {
        // STEP 4: Create console app to setup DI
        static void Main(string[] args)
        {
            // create service collection
            var serviceCollection = new ServiceCollection();

            // ConfigureServices(serviceCollection)
            serviceCollection.AddTransient<INotifier, EmailNotifier>();

            // create service provider
            var serviceProvider = serviceCollection.BuildServiceProvider();

            // This is where DI magic happens:
            var myCart = ActivatorUtilities.CreateInstance<ShoppingCart>(serviceProvider);

            myCart.PlaceOrder("[email protected]", "2 Widgets");

            System.Console.Write("Press any key to end.");
            System.Console.ReadLine();
        }
    }
}
Robert Paulsen
fuente
17
¿Qué pasa si quiero crear una instancia ShoppingCarten otra clase o método al que no accedemos al serviceProviderobjeto?
Bagherani
1
teniendo el mismo problema aquí
Casey
4
Gracias, tuve que buscar demasiado para llegar a ActivatorUtilities.CreateInstance.
H. Tugkan Kibar
1
¿Qué pasa si no tengo serviceProvider ?? !!
HelloWorld
1
¡Gracias! Lo estoy usando con Microsoft.AspNetCore.TestHost, creando un TestServer y llamando a ActivatorUtilities.CreateInstance <MyCustomerController> (_server.Host.Services);
Tolga
35

Digamos que MyHelperes usado por lo MyServiceque a su vez es usado por su controlador.

La forma de resolver esta situación es:

  • Regístrese tanto MyServicecomo MyHelperen Startup.ConfigureServices.

    services.AddTransient<MyService>();
    services.AddTransient<MyHelper>();
    
  • El controlador recibe una instancia de MyServiceen su constructor.

    public HomeController(MyService service) { ... }
    
  • MyServicea su vez, el constructor recibirá una instancia de MyHelper.

    public MyService(MyHelper helper) { ... }
    

El marco DI podrá resolver todo el gráfico de objetos sin problemas. Si le preocupa que se creen nuevas instancias cada vez que se resuelve un objeto, puede leer acerca de las diferentes opciones de vida y registro, como la vida útil única o de solicitud.

Debería sospechar mucho cuando crea que tiene que crear manualmente una instancia de algún servicio, ya que podría terminar en el antipatrón del localizador de servicios . Mejor dejar la creación de los objetos en el contenedor DI. Si realmente te encuentras en esa situación (digamos que creas una fábrica abstracta), entonces podrías usar el IServiceProviderdirectamente (O solicita un IServiceProvideren tu constructor o usa el expuesto en el httpContext ).

var foo = serviceProvider.GetRequiredService<MyHelper>();

Recomendaría leer la documentación específica sobre el marco ASP.Net 5 DI y sobre la inyección de dependencias en general.

Daniel JG
fuente
El problema que tengo con esto es que utilizo servicios en segundo plano que interactúan con la base de datos, por lo que la vida útil del alcance con dbcontext no funciona, ¿verdad? ¿Cómo se usa DI correctamente con los servicios básicos y en segundo plano de EF?
6

Desafortunadamente, no existe una forma directa. La única forma en que logré hacerlo funcionar es creando una clase estática y usándola en cualquier otro lugar como se muestra a continuación:

public static class SiteUtils
{

 public static string AppName { get; set; }

    public static string strConnection { get; set; }

}

Luego, en su clase de inicio, complételo de la siguiente manera:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    //normal as detauls , removed for space 
    // set my variables all over the site

    SiteUtils.strConnection = Configuration.GetConnectionString("DefaultConnection");
    SiteUtils.AppName = Configuration.GetValue<string>("AppName");
}

Aunque este es un patrón incorrecto, ya que se mantendrá durante todo el ciclo de vida de la aplicación y no pude encontrar una mejor manera de usarlo fuera del controlador.

Ali
fuente
4

Aquí hay un ejemplo más completo para responder directamente a la pregunta del OP, basado en la documentación actual de .NET Core 2.2 DI aquí . Agregar esta respuesta ya que puede ayudar a alguien que es nuevo en .NET Core DI, y porque esta pregunta es el resultado de búsqueda principal de Google.

Primero, agregue una interfaz para MyHelper:

public interface IMyHelper
{
    bool CheckIt();
}

En segundo lugar, actualice la clase MyHelper para implementar la interfaz (en Visual Studio, presione ctrl-. Para implementar la interfaz):

public class MyHelper : IMyHelper
{
    private readonly ProfileOptions _options;

    public MyHelper(IOptions<ProfileOptions> options)
    {
        _options = options.Value;
    {

    public bool CheckIt()
    {
        return _options.SomeBoolValue;
    }
}

En tercer lugar, registre la interfaz como un servicio proporcionado por el marco en el contenedor de servicios DI. Haga esto registrando el servicio IMyHelper con el tipo concreto MyHelper en el método ConfigureServices en Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddScoped<IMyHelper, MyHelper>();
    ...
}

Cuarto, cree una variable privada para hacer referencia a una instancia del servicio. Pase el servicio como argumento en el constructor (a través de la inyección del constructor) y luego inicialice la variable con la instancia del servicio. Haga referencia a cualquier propiedad o método de llamada en esta instancia de la clase personalizada a través de la variable privada.

public class MessageCenterController : Controller
{
    private readonly MyOptions _options;
    private readonly IMyHelper _myHelper;

    public MessageCenterController(
        IOptions<MyOptions> options,
        IMyHelper myHelper
    )
    {
        _options = options.value;
        _myHelper = myHelper;
    }

    public void DoSomething()
    {
        if (_myHelper.CheckIt())
        {
            // Do Something
        }
    }
}
sfors dice reinstalar a Monica
fuente
0

Puede utilizar Activator.CreateInstance (). Aquí hay una función contenedora para ello. La forma en que usa esto es la siguiente.

var determinedProgrammatically = "My.NameSpace.DemoClass1"; // implements IDemo interface
var obj = CreateInstance<My.NameSpace.IDemo, string>(determinedProgrammatically, "This goes into the parameter of the constructor.", "Omit this parameter if your class lives in the current assembly");

Ahora tiene una instancia de obj que se crea a partir del tipo determinado mediante programación. Este obj se puede inyectar en clases no controladoras.

public TInterface CreateInstance<TInterface, TParameter>(string typeName, TParameter constructorParam, string dllName = null)
{
    var type = dllName == null ? System.Type.GetType(typeName) :
            System.AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName.StartsWith(dllName, System.StringComparison.OrdinalIgnoreCase)).GetType(typeName);
    return (TInterface)System.Activator.CreateInstance(type, constructorParam);

}

PD: puede iterar a través de System.AppDomain.CurrentDomain.GetAssemblies () para determinar el nombre del ensamblado que alberga su clase. Este nombre se utiliza en el tercer parámetro de la función contenedora.

Denny Jacob
fuente