Résolution d'instances avec ASP.NET Core DI

302

Comment résoudre manuellement un type à l'aide de l'infrastructure d'injection de dépendances intégrée ASP.NET Core MVC?

La mise en place du conteneur est assez simple:

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

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

Mais comment puis-je résoudre ISomeServicesans effectuer d'injection? Par exemple, je veux faire ceci:

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

Il n'y a pas de telles méthodes dans IServiceCollection.

Dave New
la source
3
Voulez-vous les résoudre dans la ConfigureServices()méthode (avec IServiceCollection) ou n'importe où dans l'application?
Henk Mollema du
2
@HenkMollema: N'importe où dans le démarrage en fait.
Dave New

Réponses:

486

L' IServiceCollectioninterface est utilisée pour créer un conteneur d'injection de dépendances. Une fois entièrement construit, il se compose d'une IServiceProviderinstance que vous pouvez utiliser pour résoudre les services. Vous pouvez injecter un IServiceProviderdans n'importe quelle classe. Les classes IApplicationBuilderet HttpContextpeuvent également fournir le fournisseur de services, via leurs propriétés ApplicationServicesou RequestServices.

IServiceProviderdéfinit une GetService(Type type)méthode pour résoudre un service:

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

Il existe également plusieurs méthodes d'extension pratiques, telles que serviceProvider.GetService<IFooService>()(ajouter un usingpour Microsoft.Extensions.DependencyInjection).

Résolution des services dans la classe de démarrage

Injection de dépendances

Le fournisseur de services d' hébergement de l'exécution peut injecter certains services dans le constructeur de la Startupclasse, par exemple IConfiguration, IWebHostEnvironment( IHostingEnvironmenten version pré-3.0), ILoggerFactoryet IServiceProvider. Notez que cette dernière est une instance construite par la couche d'hébergement et ne contient que les services essentiels pour démarrer une application .

La ConfigureServices()méthode n'autorise pas l'injection de services, elle accepte uniquement un IServiceCollectionargument. Cela est logique car ConfigureServices()c'est là que vous enregistrez les services requis par votre application. Cependant, vous pouvez utiliser ici les services injectés dans le constructeur du démarrage, par exemple:

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

public IConfiguration Configuration { get; }

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

Tous les services enregistrés dans ConfigureServices()peuvent alors être injectés dans la Configure()méthode; vous pouvez ajouter un nombre arbitraire de services après le IApplicationBuilderparamètre:

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

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

Résolution manuelle des dépendances

Si vous devez résoudre manuellement des services, vous devez de préférence utiliser le ApplicationServicesfourni par IApplicationBuilderdans la Configure()méthode:

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

Il est possible de passer et d'utiliser directement un IServiceProviderdans le constructeur de votre Startupclasse, mais comme ci - dessus, cela contiendra un sous-ensemble limité de services , et a donc une utilité limitée:

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

Si vous devez résoudre des services dans la ConfigureServices()méthode, une approche différente est requise. Vous pouvez créer un intermédiaire à IServiceProviderpartir de l' IServiceCollectioninstance qui contient les services qui ont été enregistrés jusqu'à ce point :

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

Remarque: En règle générale, vous devez éviter de résoudre les services dans la ConfigureServices()méthode, car c'est en fait l'endroit où vous configurez les services d'application. Parfois, vous avez juste besoin d'accéder à une IOptions<MyOptions>instance. Vous pouvez accomplir cela en liant les valeurs de l' IConfigurationinstance à une instance de MyOptions(ce qui est essentiellement ce que fait le cadre d'options):

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

Les services de résolution manuelle (aka Service Locator) sont généralement considérés comme un anti-modèle . Bien qu'il ait ses cas d'utilisation (pour les couches d'infrastructure et / ou d'infrastructure), vous devez l'éviter autant que possible.

Henk Mollema
la source
14
@HenkMollema mais que se passe-t-il si je ne peux rien injecter, je veux dire que je ne peux pas avoir IServiceCollectioninjecté, une classe qui est créée manuellement ( hors de la portée du middleware ), un planificateur dans mon cas, qui a périodiquement besoin de certains services pour générer et envoyer un e-mail.
Merdan Gochmuradov
52
avertissement si vous devez résoudre des services dans ConfigureServiceset que ce service est un singleton, ce sera un singleton différent de celui que vous Controllerutilisez! Je suppose que cela est dû au fait qu'il utilise un autre IServiceProvider- pour éviter ce ne résolvent pas par BuildServiceProvideret au lieu déplacer votre recherche du singleton de ConfigureServicesà Configure(..other params, IServiceProvider serviceProvider)enStartup.cs
wal
3
@wal bon point. Parce que c'est une IServiceProviderinstance différente , il créera une nouvelle instance singleton. Vous pouvez éviter cela en renvoyant l'instance de fournisseur de services à partir de la ConfigureServicesméthode afin que ce soit le conteneur que votre application utilise également.
Henk Mollema
1
Invoquer collection.BuildServiceProvider();était ce dont j'avais besoin, merci!
Chris Marisic
2
@HenkMollema comment le faire fonctionner avec une seule instance de fournisseur de services? En règle générale, vous devez 1) enregistrer certaines de vos dépendances 2) créer une instance de fournisseur de services intérimaire 3) utiliser ce fournisseur de services pour résoudre quelque chose dont vous avez besoin pour enregistrer d'autres dépendances. Par la suite, vous ne pouvez pas retourner l'instance intermédiaire, car il manque certaines de vos dépendances (enregistrées en 3). Suis-je en train de manquer quelque chose?
Filip
109

La résolution manuelle des instances implique l'utilisation de l' IServiceProviderinterface:

Résolution de la dépendance dans Startup.ConfigureServices

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

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

Résolution des dépendances au démarrage.

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

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

    // ...
}

Résolution des dépendances dans Startup.Configure dans ASP.NET Core 3

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

Utilisation des services injectés à l'exécution

Certains types peuvent être injectés en tant que paramètres de méthode:

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)
    {
    }
}

Résolution des dépendances dans les actions du contrôleur

[HttpGet("/some-action")]
public string SomeAction([FromServices] IMyService myService) => "Hello";
Muhammad Rehan Saeed
la source
1
@AfsharMohebbi le GetServicequi est générique est une méthode d'extension dans l' Microsoft.Extensions.DependencyInjectionespace de noms.
ahmadali shafiee
À propos des méthodes d'extension: Une méthode d'extension est une méthode statique qui ajoute de la fonctionnalité à une classe, vous pouvez déclarer le TheReturnType TheMethodName public statique (ce TheTypeYouExtend leTypeYouExtend {// BODY} et ensuite vous pouvez l'utiliser comme: TheTypeYouExtend.TheMethodName (); That has has devenir une approche très courante avec .NET Core, afin que les développeurs puissent étendre les fonctionnalités de base ... de bons exemples ici: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
Juan
17

Si vous générez une application avec un modèle, vous allez avoir quelque chose comme ça dans la Startupclasse:

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

    services.AddMvc();
}

Vous pouvez ensuite y ajouter des dépendances, par exemple:

services.AddTransient<ITestService, TestService>();

Si vous souhaitez accéder ITestServicesur votre contrôleur, vous pouvez ajouter IServiceProviderle constructeur et il sera injecté:

public HomeController(IServiceProvider serviceProvider)

Ensuite, vous pouvez résoudre le service que vous avez ajouté:

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

Notez que pour utiliser la version générique, vous devez inclure l'espace de noms avec les extensions:

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
la source
10

Si vous avez juste besoin de résoudre une dépendance dans le but de la transmettre au constructeur d'une autre dépendance que vous enregistrez, vous pouvez le faire.

Supposons que vous disposiez d'un service qui a pris une chaîne et un ISomeService.

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

Lorsque vous allez l'enregistrer dans Startup.cs, vous devrez le faire:

services.AddScoped<IAnotherService>(ctx => 
      new AnotherService(ctx.GetService<ISomeService>(), "https://someservice.com/")
);
raterus
la source
L'OP n'a pas indiqué la raison de la nécessité de résoudre un service dans la méthode ConfigureService, mais c'est probablement la raison pour laquelle quelqu'un penserait à le faire
kilkfoe
1
En fait, cela devrait être la réponse acceptée ... Bien que la réponse de Henk Mollema soit très illustrative, votre réponse est aujourd'hui plus propre et n'introduit pas de problèmes liés à la construction d'un IServiceProvider intermédiaire (différentes instances de singletons ...). Probablement, cette solution n'était pas disponible en 2015 lorsque Henk a répondu, mais c'est maintenant la voie à suivre.
Vi100
J'ai essayé, mais ISomeServicec'était toujours nul pour moi.
ajbeaven
2 questions: 1) Si le constructeur de paramètres de la classe de service AnotherService change (services supprimés ou ajoutés), alors je dois modifier le segment d'enregistrement du service IAnotherService et il continue de changer? 2) Au lieu de cela, je peux ajouter un seul constructeur pour AnotherService avec 1 paramètre comme public AnotherService (IServiceProvider serviceProvider) et obtenir les services dont j'ai besoin auprès du constructeur. Et j'ai juste besoin d'enregistrer la classe de service AnotherService dans la classe de démarrage comme services.AddTransient <IAnotherService, AnotherService> (sp => {var service = new AnotherService (sp); return service;});
Thomas.Benz
2

Vous pouvez injecter des dépendances dans des attributs comme AuthorizeAttribute de cette manière

var someservice = (ISomeService)context.HttpContext.RequestServices.GetService(typeof(ISomeService));
Bora Aydın
la source
C'est ce que je cherchais .. Merci
Reyan Chougle
0

Je sais que c'est une vieille question, mais je suis étonné qu'un hack plutôt évident et dégoûtant ne soit pas là.

Vous pouvez exploiter la possibilité de définir votre propre fonction ctor pour récupérer les valeurs nécessaires de vos services au fur et à mesure que vous les définissez ... évidemment, cela serait exécuté à chaque fois que le service était demandé, sauf si vous supprimez / effacez explicitement et rajoutez la définition de ce service dans la première construction du ctor exploitant .

Cette méthode a l'avantage de ne pas vous obliger à construire l'arborescence du service, ou à l'utiliser, lors de la configuration du service. Vous définissez toujours comment les services seront configurés.

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

La manière de corriger ce modèle serait de donner OtherServiceune dépendance explicite à IServiceINeedToUse, plutôt que de dépendre implicitement de lui ou de la valeur de retour de sa méthode ... ou de résoudre cette dépendance explicitement d'une autre manière.

Izzy
la source
-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
la source
5
Vos réponses sont plus susceptibles d'être acceptées et votées si vous fournissez une brève explication de la raison pour laquelle il s'agit d'une bonne réponse, et pas seulement d'un extrait de code. Cela aide également le demandeur à être sûr que cela répond réellement à la question qu'il a posée.
Jim L
Quelqu'un a incorrectement signalé votre réponse comme étant de mauvaise qualité. Vous devez ajouter du texte d'accompagnement pour expliquer le fonctionnement de votre réponse afin d'éviter d'autres marquages ​​et / ou votes négatifs. Une réponse uniquement codée n'est pas de mauvaise qualité . Tente-t-il de répondre à la question? Si ce n'est pas le cas, indiquez «pas une réponse» ou recommandez la suppression (si dans la file d'attente de révision). b) Est-ce techniquement incorrect? Downvote ou commentaire. De l'avis .
Wai Ha Lee