Rozwiązywanie instancji za pomocą ASP.NET Core DI

302

Jak ręcznie rozwiązać typ za pomocą wbudowanej platformy wstrzykiwania zależności ASP.NET Core MVC?

Konfiguracja kontenera jest dość łatwa:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddTransient<ISomeService, SomeConcreteService>();
}

Ale jak mogę rozwiązać problem ISomeServicebez wykonywania zastrzyku? Na przykład chcę to zrobić:

ISomeService service = services.Resolve<ISomeService>();

Nie ma takich metod w IServiceCollection.

Dave New
źródło
3
Czy chcesz je rozwiązać w ConfigureServices()metodzie (z IServiceCollection) czy w dowolnym miejscu aplikacji?
Henk Mollema,
2
@HenkMollema: Właściwie w dowolnym miejscu w ramach Startup.
Dave New

Odpowiedzi:

484

IServiceCollectionInterfejs służy do budowy kontenera wtrysku zależność. Po pełnym skompilowaniu kompiluje się do IServiceProviderinstancji, której można użyć do rozwiązania usług. Możesz wstrzyknąć IServiceProviderdo dowolnej klasy. Te IApplicationBuilderi HttpContextklasy mogą zapewnić usługodawcy, jak również, za pośrednictwem swoich ApplicationServiceslub RequestServiceswłaściwości odpowiednio.

IServiceProviderdefiniuje GetService(Type type)metodę rozwiązania usługi:

var service = (IFooService)serviceProvider.GetService(typeof(IFooService));

Dostępnych jest również kilka metod rozszerzania wygody, takich jak serviceProvider.GetService<IFooService>()(dodaj usingza Microsoft.Extensions.DependencyInjection).

Rozwiązywanie usług w klasie startowej

Wstrzykiwanie zależności

Hosting usługodawca środowisko wykonawcze mogą wprowadzić pewne usługi do konstruktora Startupklasy, takie jak IConfiguration, IWebHostEnvironment( IHostingEnvironmentw wersji pre-3.0), ILoggerFactoryi IServiceProvider. Pamiętaj, że ten ostatni jest instancją zbudowaną przez warstwę hostingu i zawiera tylko niezbędne usługi do uruchomienia aplikacji .

Ta ConfigureServices()metoda nie zezwala na wstrzykiwanie usług, przyjmuje tylko IServiceCollectionargument. Ma to sens, ponieważ ConfigureServices()rejestrujesz usługi wymagane przez aplikację. Można tu jednak skorzystać z usług wprowadzonych do konstruktora startupu, na przykład:

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
    // Use Configuration here
}

Wszelkie zarejestrowane usługi ConfigureServices()można następnie wprowadzić do Configure()metody; możesz dodać dowolną liczbę usług po IApplicationBuilderparametrze:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IFooService>();
}

public void Configure(IApplicationBuilder app, IFooService fooService)
{
    fooService.Bar();
}

Ręczne rozwiązywanie zależności

Jeśli trzeba ręcznie usług postanowienie, należy najlepiej użyć ApplicationServicesświadczone przez IApplicationBuilderw Configure()metodzie:

public void Configure(IApplicationBuilder app)
{
    var serviceProvider = app.ApplicationServices;
    var hostingEnv = serviceProvider.GetService<IHostingEnvironment>();
}

Możliwe jest przekazanie i bezpośrednie użycie IServiceProviderkonstruktora Startupklasy, ale jak wyżej, będzie on zawierał ograniczony podzbiór usług , a zatem ma ograniczoną użyteczność:

public Startup(IServiceProvider serviceProvider)
{
    var hostingEnv = serviceProvider.GetService<IWebHostEnvironment>();
}

Jeśli musisz rozwiązać usługi w ConfigureServices()metodzie, wymagane jest inne podejście. Możesz zbudować półprodukt IServiceProviderz IServiceCollectioninstancji, która zawiera usługi zarejestrowane do tego momentu :

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IFooService, FooService>();

    // Build the intermediate service provider
    var sp = services.BuildServiceProvider();

    // This will succeed.
    var fooService = sp.GetService<IFooService>();
    // This will fail (return null), as IBarService hasn't been registered yet.
    var barService = sp.GetService<IBarService>();
}

Uwaga: Zasadniczo należy unikać rozwiązywania usług w ramach ConfigureServices()metody, ponieważ w rzeczywistości jest to miejsce, w którym konfigurujesz usługi aplikacji. Czasami potrzebujesz tylko dostępu do IOptions<MyOptions>instancji. Możesz to zrobić, wiążąc wartości z IConfigurationinstancji z instancją MyOptions(co jest zasadniczo tym, co robi struktura opcji):

public void ConfigureServices(IServiceCollection services)
{
    var myOptions = new MyOptions();
    Configuration.GetSection("SomeSection").Bind(myOptions);
}

Usługi ręcznego rozwiązywania problemów (zwane też lokalizatorem usług) są ogólnie uważane za anty-wzór . Chociaż ma swoje przypadki użycia (dla frameworków i / lub warstw infrastruktury), powinieneś unikać go w jak największym stopniu.

Henk Mollema
źródło
14
@HenkMollema, ale co jeśli nie mogę wstrzyknąć niczego, to znaczy nie mogę IServiceCollectionwstrzyknąć, jakiejś klasy, która jest tworzona ręcznie ( poza średnim zakresem zastosowania ), w moim przypadku harmonogram, który okresowo potrzebuje pewnych usług do wygenerowania i wyślij e-mail.
Merdan Gochmuradov
52
ostrzeżenie, jeśli musisz rozwiązać usługi ConfigureServiceswewnątrz i ta usługa jest singletonem, będzie to inny singleton niż ten, którego Controllerużywasz! Przypuszczam, że to dlatego, że używa innego IServiceProvider- aby tego uniknąć NIE rozwiązać poprzez BuildServiceProvideri zamiast przenieść odnośnika z Singleton od ConfigureServicesdo Configure(..other params, IServiceProvider serviceProvider)wStartup.cs
Wal
3
@wal dobry punkt. Ponieważ jest to inna IServiceProviderinstancja, utworzy nową instancję singleton. Można tego uniknąć, zwracając instancję usługodawcy z ConfigureServicesmetody, która będzie również kontenerem używanym przez aplikację.
Henk Mollema
1
Wywoływanie collection.BuildServiceProvider();było to, co potrzebne, dzięki!
Chris Marisic
2
@HenkMollema, jak to zrobić, aby działało tylko z jedną instancją usługodawcy? Zazwyczaj należy 1) zarejestrować niektóre ze swoich zależności 2) zbudować instancję tymczasowego dostawcy usług 3) użyć tego usługodawcy, aby rozwiązać problem potrzebny do zarejestrowania innych zależności. Następnie nie można zwrócić instancji tymczasowej, ponieważ brakuje jej niektórych zależności (zarejestrowanych w 3). Czy coś brakuje?
Filip
109

Ręczne rozwiązywanie instancji wymaga użycia IServiceProviderinterfejsu:

Rozwiązywanie zależności w Startup.ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyService, MyService>();

    var serviceProvider = services.BuildServiceProvider();
    var service = serviceProvider.GetService<IMyService>();
}

Rozwiązywanie zależności w Startup.Configure

public void Configure(
    IApplicationBuilder application,
    IServiceProvider serviceProvider)
{
    // By type.
    var service1 = (MyService)serviceProvider.GetService(typeof(MyService));

    // Using extension method.
    var service2 = serviceProvider.GetService<MyService>();

    // ...
}

Rozwiązywanie zależności w Startup.Configure in ASP.NET Core 3

public void Configure(
    IApplicationBuilder application,
    IWebHostEnvironment webHostEnvironment)
{
    app.ApplicationServices.GetService<MyService>();
}

Korzystanie ze wstrzykiwanych usług Runtime

Niektóre typy można wstrzykiwać jako parametry metody:

public class Startup
{
    public Startup(
        IHostingEnvironment hostingEnvironment,
        ILoggerFactory loggerFactory)
    {
    }

    public void ConfigureServices(
        IServiceCollection services)
    {
    }

    public void Configure(
        IApplicationBuilder application,
        IHostingEnvironment hostingEnvironment,
        IServiceProvider serviceProvider,
        ILoggerFactory loggerfactory,
        IApplicationLifetime applicationLifetime)
    {
    }
}

Rozwiązywanie zależności w działaniach kontrolera

[HttpGet("/some-action")]
public string SomeAction([FromServices] IMyService myService) => "Hello";
Muhammad Rehan Saeed
źródło
1
@AfsharMohebbi, GetServicektóry jest ogólny, jest metodą rozszerzenia w Microsoft.Extensions.DependencyInjectionprzestrzeni nazw.
ahmadali shafiee
Informacje o metodach rozszerzeń: Metoda rozszerzenia jest metodą statyczną, która dodaje funkcjonalność do klasy, można zadeklarować publiczną statyczną TheReturnType TheMethodName (to TheTypeYouExtend theTypeYouExtend {// BODY}), a następnie można użyć jej w następujący sposób: TheTypeYouExtend.TheMethodName (); który ma stać się bardzo popularnym podejściem do .NET Core, dzięki czemu programiści mogą rozszerzyć podstawową funkcjonalność ... dobre przykłady tutaj: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/...
Juan
17

Jeśli wygenerujesz aplikację z szablonem, będziesz mieć coś takiego w Startupklasie:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);

    services.AddMvc();
}

Możesz tam dodać zależności, na przykład:

services.AddTransient<ITestService, TestService>();

Jeśli chcesz uzyskać dostęp do ITestServicekontrolera, możesz dodać IServiceProviderdo konstruktora i zostanie on wstrzyknięty:

public HomeController(IServiceProvider serviceProvider)

Następnie możesz rozwiązać dodaną usługę:

var service = serviceProvider.GetService<ITestService>();

Pamiętaj, że aby użyć wersji ogólnej, musisz dołączyć przestrzeń nazw do rozszerzeń:

using Microsoft.Extensions.DependencyInjection;

ITestService.cs

public interface ITestService
{
    int GenerateRandom();
}

TestService.cs

public class TestService : ITestService
{
    public int GenerateRandom()
    {
        return 4;
    }
}

Startup.cs (ConfigureServices)

public void ConfigureServices(IServiceCollection services)
{
    services.AddApplicationInsightsTelemetry(Configuration);
    services.AddMvc();

    services.AddTransient<ITestService, TestService>();
}

HomeController.cs

using Microsoft.Extensions.DependencyInjection;

namespace Core.Controllers
{
    public class HomeController : Controller
    {
        public HomeController(IServiceProvider serviceProvider)
        {
            var service = serviceProvider.GetService<ITestService>();
            int rnd = service.GenerateRandom();
        }
BrunoLM
źródło
10

Jeśli potrzebujesz tylko rozwiązać jedną zależność w celu przekazania jej do konstruktora innej rejestrowanej zależności, możesz to zrobić.

Załóżmy, że masz usługę, która pobierała ciąg i usługę ISomeService.

public class AnotherService : IAnotherService
{
    public AnotherService(ISomeService someService, string serviceUrl)
    {
        ...
    }
}

Kiedy przejdziesz do rejestracji w Startup.cs, musisz to zrobić:

services.AddScoped<IAnotherService>(ctx => 
      new AnotherService(ctx.GetService<ISomeService>(), "https://someservice.com/")
);
raterus
źródło
OP nie
podał
1
W rzeczywistości powinna to być odpowiedź zaakceptowana ... Chociaż odpowiedź Henka Mollemy jest bardzo ilustracyjna, obecnie twoja odpowiedź jest czystsza i nie wprowadza problemów związanych z budowaniem pośredniego dostawcy IServiceProvider (różne przypadki singletonów ...). Prawdopodobnie to rozwiązanie nie było dostępne w 2015 r., Kiedy Henk odpowiedział, ale teraz jest to właściwa droga.
Vi100,
Próbowałem tego, ale ISomeServicewciąż było dla mnie nieważne.
ajbeaven
2 pytania: 1) Jeśli konstruktor parametrów klasy usług AnotherService zmienia się (usługi usunięte lub dodane), to muszę zmodyfikować segment rejestru usługi IAnotherService i ciągle się zmienia? 2) Zamiast tego mogę dodać tylko jeden konstruktor dla AnotherService z 1 parametrem, takim jak public AnotherService (IServiceProvider serviceProvider) i uzyskać potrzebne usługi od konstruktora. Muszę tylko zarejestrować klasę usług AnotherService w klasie Startup, taką jak services.AddTransient <IAnotherService, AnotherService> (sp => {var service = new AnotherService (sp); return service;});
Thomas.Benz
2

W ten sposób można wstrzykiwać zależności w atrybutach takich jak AuthorizeAttribute

var someservice = (ISomeService)context.HttpContext.RequestServices.GetService(typeof(ISomeService));
Bora Aydın
źródło
Tego właśnie szukałem .. Dzięki
Reyan Chougle
0

Wiem, że to stare pytanie, ale jestem zdumiony, że nie ma tu raczej oczywistego i obrzydliwego hacka.

Możesz wykorzystać możliwość zdefiniowania własnej funkcji ctor, aby pobrać niezbędne wartości z twoich usług podczas ich definiowania ... oczywiście będzie to uruchamiane za każdym razem, gdy usługa zostanie zamówiona, chyba że wyraźnie usuniesz / wyczyścisz i ponownie dodasz definicję usługa ta w ramach pierwszej konstrukcji wykorzystującego ctor .

Ta metoda ma tę zaletę, że nie wymaga budowania drzewa usługi ani korzystania z niego podczas konfigurowania usługi. Nadal definiujesz sposób konfigurowania usług.

public void ConfigureServices(IServiceCollection services)
{
    //Prey this doesn't get GC'd or promote to a static class var
    string? somevalue = null;

    services.AddSingleton<IServiceINeedToUse, ServiceINeedToUse>(scope => {
         //create service you need
         var service = new ServiceINeedToUse(scope.GetService<IDependantService>())
         //get the values you need
         somevalue = somevalue ?? service.MyDirtyHack();
         //return the instance
         return service;
    });
    services.AddTransient<IOtherService, OtherService>(scope => {
         //Explicitly ensuring the ctor function above is called, and also showcasing why this is an anti-pattern.
         scope.GetService<IServiceINeedToUse>();
         //TODO: Clean up both the IServiceINeedToUse and IOtherService configuration here, then somehow rebuild the service tree.
         //Wow!
         return new OtherService(somevalue);
    });
}

Sposobem na naprawienie tego wzorca byłoby OtherServicejawne uzależnienie IServiceINeedToUse, a nie pośrednio zależność od niego lub wartości zwracanej przez jego metodę ... lub jawne rozwiązanie tej zależności w inny sposób.

Izzy
źródło
-4
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddDbContext<ConfigurationRepository>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("SqlConnectionString")));

    services.AddScoped<IConfigurationBL, ConfigurationBL>();
    services.AddScoped<IConfigurationRepository, ConfigurationRepository>();
}
Nathan Alard
źródło
5
Twoje odpowiedzi są bardziej prawdopodobne, jeśli zostaną podane krótkie wyjaśnienie, dlaczego jest to dobra odpowiedź, a nie tylko fragment kodu. Pomaga również pytającemu upewnić się, że faktycznie odpowiada na zadane pytanie.
Jim L
Ktoś nieprawidłowo oznaczył twoją odpowiedź jako niskiej jakości. Powinieneś dodać tekst towarzyszący, aby wyjaśnić, jak działa twoja odpowiedź, aby zapobiec dalszemu oznaczaniu i / lub negatywnym ocenom. Odpowiedź tylko na kod nie jest niskiej jakości . Czy próbuje odpowiedzieć na pytanie? Jeśli nie, oznacz jako „brak odpowiedzi” lub zalecenie usunięcia (jeśli jest w kolejce do sprawdzenia). b) Czy jest to technicznie niepoprawne? Głosuj lub komentuj. Z przeglądu .
Wai Ha Lee