Wstrzykiwanie zależności z klasami innymi niż klasa kontrolera

84

W tym momencie z łatwością wstrzykuję rzeczy do moich kontrolerów, w niektórych przypadkach budując własną klasę ResolverServices. Życie jest dobre .

To, czego nie potrafię zrobić, to sprawić, by framework automatycznie wstawiał do klas niebędących kontrolerami. To, co działa, to automatyczne wstrzykiwanie frameworka do mojego kontrolera IOptions, który jest w rzeczywistości konfiguracją dla mojego projektu:

public class MessageCenterController : Controller
{
    private readonly MyOptions _options;

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

Zastanawiam się, czy mogę zrobić to samo dla moich własnych zajęć. Zakładam, że jestem blisko, kiedy naśladuję kontroler, w ten sposób:

public class MyHelper
{
    private readonly ProfileOptions _options;

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

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

Myślę, że zawodzę, gdy nazywam to tak:

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

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

Problem, który mam ze śledzeniem tego, to praktycznie wszystko, co mówi o DI, mówi o tym na poziomie kontrolera. Próbowałem znaleźć miejsce, w którym to się dzieje w Controllerkodzie źródłowym obiektu, ale robi się tam trochę szalenie.

Wiem, że mogę ręcznie utworzyć wystąpienie IOptions i przekazać je MyHelperkonstruktorowi, ale wydaje się, że powinienem być w stanie to zrobić, ponieważ działa Controllers.

Robert Paulsen
źródło
8
Kiedy używasz iniekcji zależności, nie wywołujesz new. Nigdy dla obiektów, które powinny zostać rozwiązane
Tseng
1
Kiedy próbuję utworzyć wystąpienie MyHelper, nie nazywam nowego? (1) To brzmi zbyt łatwo, (2) To błąd składni. :-)
Robert Paulsen
2
Tak, o to właśnie chodzi we wstrzykiwaniu zależności (zwłaszcza jeśli używasz inwersji kontenerów sterujących, które zarządzają i wykonują tę instancję). Aby wypchnąć instancję poza usługi / klasy do momentu, w którym kontener ioc robi to wewnętrznie. W przypadkach, gdy nie możesz wstrzyknąć go za pomocą konstruktora, tworzysz fabrykę i przekazujesz jej interfejs do swojej usługi. jego implementacja używa kontenera, aby go rozwiązać, w ASP.NET Core wstrzyknięcie sprawy IServiceProviderdo fabryki i wywołanieIMyHelper helper = services.RequestService<IMyHelper>()
Tseng

Odpowiedzi:

42

Poniżej znajduje się działający przykład użycia DI bez niczego, co dotyczy kontrolerów MVC. To właśnie musiałem zrobić, aby zrozumieć ten proces, więc może pomoże to komuś innemu.

Obiekt ShoppingCart pobiera przez DI instancję INotifier (która powiadamia klienta o jego zamówieniu).

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
źródło
17
Co się stanie, jeśli chcę utworzyć wystąpienie ShoppingCartw innej klasie lub metodzie, do której nie mamy dostępu do serviceProviderobiektu?
Bagherani
1
ten sam problem tutaj
Casey
4
Dziękuję, musiałem przeszukać zbyt szeroko, aby dostać się do ActivatorUtilities.CreateInstance.
H. Tugkan Kibar
1
co jeśli nie mam serviceProvider ?? !!
HelloWorld
1
Dziękuję Ci! Używam go z Microsoft.AspNetCore.TestHost, tworzę TestServer i wzywam ActivatorUtilities.CreateInstance <MyCustomerController> (_server.Host.Services);
Tolga,
35

Powiedzmy, że MyHelperjest używany przez MyServicektóry z kolei jest używany przez kontroler.

Sposób rozwiązania tej sytuacji to:

  • Zarejestruj zarówno MyServicei MyHelperw Startup.ConfigureServices.

    services.AddTransient<MyService>();
    services.AddTransient<MyHelper>();
    
  • Kontroler otrzymuje wystąpienie MyServicew swoim konstruktorze.

    public HomeController(MyService service) { ... }
    
  • MyServicekonstruktor z kolei otrzyma wystąpienie MyHelper.

    public MyService(MyHelper helper) { ... }
    

Framework DI będzie w stanie bez problemu rozwiązać cały graf obiektu. Jeśli martwisz się, że nowe instancje będą tworzone za każdym razem, gdy obiekt zostanie rozwiązany, możesz przeczytać o różnych okresach istnienia i opcjach rejestracji, takich jak okresy istnienia pojedynczych lub żądań.

Powinieneś być naprawdę podejrzliwy, gdy myślisz, że musisz ręcznie utworzyć wystąpienie jakiejś usługi, ponieważ możesz skończyć w anty-wzorca lokalizatora usług . Lepiej pozostaw tworzenie obiektów w DI Container. Jeśli naprawdę znajdziesz się w takiej sytuacji (powiedzmy, że tworzysz fabrykę abstrakcyjną), możesz użyć IServiceProviderbezpośrednio (albo zażądaj IServiceProviderw konstruktorze, albo użyj tego, który jest ujawniony w httpContext ).

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

Zalecałbym przeczytanie szczegółowej dokumentacji dotyczącej frameworka ASP.Net 5 DI i ogólnie o wstrzykiwaniu zależności.

Daniel JG
źródło
Problem, który mam z tym polega na tym, że używam usług w tle, które współdziałają z bazą danych, więc zakres życia z dbcontext nie działa, prawda? Jak prawidłowo używać DI z podstawowymi usługami EF i usługami w tle?
6

Niestety nie ma bezpośredniego sposobu. Jedynym sposobem, w jaki udało mi się to zrobić, jest utworzenie statycznej klasy i użycie jej wszędzie indziej, jak poniżej:

public static class SiteUtils
{

 public static string AppName { get; set; }

    public static string strConnection { get; set; }

}

Następnie w swojej klasie startowej wypełnij to jak poniżej:

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

Chociaż jest to zły wzorzec, ponieważ pozostanie przez cały cykl życia aplikacji i nie mogłem znaleźć lepszego sposobu na użycie go poza kontrolerem.

Ali
źródło
4

Oto bardziej kompletny przykład, aby bezpośrednio odpowiedzieć na pytanie OP, w oparciu o aktualną dokumentację .NET Core 2.2 DI tutaj . Dodanie tej odpowiedzi może pomóc komuś, kto jest nowy w .NET Core DI i ponieważ to pytanie jest najlepszym wynikiem wyszukiwania Google.

Najpierw dodaj interfejs dla MyHelper:

public interface IMyHelper
{
    bool CheckIt();
}

Po drugie, zaktualizuj klasę MyHelper, aby zaimplementować interfejs (w programie Visual Studio naciśnij ctrl-., Aby zaimplementować interfejs):

public class MyHelper : IMyHelper
{
    private readonly ProfileOptions _options;

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

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

Po trzecie, zarejestruj interfejs jako usługę udostępnioną przez platformę w kontenerze usługi DI. Zrób to, rejestrując usługę IMyHelper z konkretnym typem MyHelper w metodzie ConfigureServices w Startup.cs.

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

Po czwarte, utwórz zmienną prywatną, aby odwołać się do wystąpienia usługi. Przekaż usługę jako argument w konstruktorze (za pomocą iniekcji konstruktora), a następnie zainicjuj zmienną za pomocą wystąpienia usługi. Odwołaj się do wszelkich właściwości lub wywołaj metody w tym wystąpieniu klasy niestandardowej za pośrednictwem zmiennej prywatnej.

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 mówi, że przywróć Monikę
źródło
0

Możesz użyć Activator.CreateInstance (). Oto funkcja opakowująca. Sposób korzystania z tego jest następujący.

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

Teraz masz wystąpienie obj, którego wystąpienie jest tworzone na podstawie typu określonego programowo. Ten obiekt można wstawić do klas niebędących kontrolerami.

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

}

PS: Możesz iterować przez System.AppDomain.CurrentDomain.GetAssemblies (), aby określić nazwę zestawu, w którym znajduje się twoja klasa. Ta nazwa jest używana w trzecim parametrze funkcji opakowania.

Denny Jacob
źródło