Injection de dépendances dans ASP.NET Core

Par Kirk Larkin, Steve Smith, Scott Addieet Brandon Dahler

ASP.NET Core prend en charge le modèle de conception logicielle d’injection de dépendances, technique qui permet d’obtenir une inversion de contrôle entre les classes et leurs dépendances.

Pour plus d’informations spécifiques à l’injection de dépendances au sein des contrôleurs MVC, consultez Injection de dépendances dans les contrôleurs dans ASP.NET Core.

Pour plus d’informations sur l’utilisation de l’injection de dépendances dans les applications autres que Web Apps, consultez injection de dépendances dans .net.

Pour plus d’informations sur l’injection de dépendances d’options, consultez Modèle d’options dans ASP.NET Core .

Cette rubrique fournit des informations sur l’injection de dépendances dans ASP.NET Core. La documentation principale sur l’utilisation de l’injection de dépendances est contenue dans l' injection de dépendances dans .net.

Afficher ou télécharger l’exemple de code (procédure de téléchargement)

Vue d’ensemble de l’injection de dépendances

Une dépendance est un objet dont dépend un autre objet. Examinez la MyDependency classe suivante avec une WriteMessage méthode dont dépendent d’autres classes :

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Une classe peut créer une instance de la MyDependency classe pour utiliser sa WriteMessage méthode. Dans l’exemple suivant, la MyDependency classe est une dépendance de la IndexModel classe :

public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet created this message.");
    }
}

La classe crée et dépend directement de la MyDependency classe. Les dépendances de code, comme dans l’exemple précédent, sont problématiques et doivent être évitées pour les raisons suivantes :

  • Pour remplacer MyDependency par une implémentation différente, vous IndexModel devez modifier la classe.
  • Si MyDependency a des dépendances, ils doivent également être configurés par la IndexModel classe. Dans un grand projet comportant plusieurs classes dépendant de MyDependency, le code de configuration est disséminé dans l’application.
  • Cette implémentation complique le test unitaire. L’application doit utiliser une classe MyDependency fictive ou stub, ce qui est impossible avec cette approche.

L’injection de dépendances résout ces problèmes via :

  • L’utilisation d’une interface ou classe de base pour extraire l’implémentation des dépendances.
  • L’inscription de la dépendance dans un conteneur de service. ASP.NET Core fournit un conteneur de service intégré, IServiceProvider. Les services sont généralement inscrits dans la méthode de l’application Startup.ConfigureServices .
  • Injection du service dans le constructeur de la classe où il est utilisé. Le framework prend la responsabilité de la création d’une instance de la dépendance et de sa suppression lorsqu’elle n’est plus nécessaire.

Dans l' exemple d’application, l' IMyDependency interface définit la WriteMessage méthode :

public interface IMyDependency
{
    void WriteMessage(string message);
}

Cette interface est implémentée par un type concret, MyDependency :

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

L’exemple d’application inscrit le IMyDependency service avec le type concret MyDependency . La AddScoped méthode enregistre le service avec une durée de vie limitée, la durée de vie d’une requête unique. Les durées de vie du service sont décrites plus loin dans cette rubrique.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>();

    services.AddRazorPages();
}

Dans l’exemple d’application, le IMyDependency service est demandé et utilisé pour appeler la WriteMessage méthode :

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

En utilisant le modèle DI, le contrôleur :

  • N’utilise pas le type concret MyDependency , mais uniquement l' IMyDependency interface qu’il implémente. Cela facilite la modification de l’implémentation que le contrôleur utilise sans modifier le contrôleur.
  • Ne crée pas d’instance de MyDependency , elle est créée par le conteneur di.

L’implémentation de l' IMyDependency interface peut être améliorée à l’aide de l’API de journalisation intégrée :

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

La méthode mise à jour ConfigureServices inscrit la nouvelle IMyDependency implémentation :

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency2>();

    services.AddRazorPages();
}

MyDependency2 dépend de ILogger<TCategoryName> , qu’il demande dans le constructeur. ILogger<TCategoryName> est un service fourni par le Framework.

Il n’est pas rare que l’injection de dépendances soit utilisée de manière chaînée. Dans ce cas, chaque dépendance demandée demande à son tour ses propres dépendances. Le conteneur résout les dépendances dans le graphique et retourne le service entièrement résolu. L’ensemble collectif de dépendances qui doivent être résolues est généralement appelé arborescence des dépendances, graphique de dépendance ou graphique d’objet.

Le conteneur se résout ILogger<TCategoryName> en tirant parti de types ouverts (génériques), ce qui évite d’avoir à inscrire chaque type construit (Générique).

Dans la terminologie d’injection de dépendances, un service :

  • Est généralement un objet qui fournit un service à d’autres objets, tels que le IMyDependency service.
  • N’est pas associé à un service Web, bien que le service puisse utiliser un service Web.

Le Framework fournit un système de journalisation robuste. Les IMyDependency implémentations présentées dans les exemples précédents ont été écrites pour illustrer la di de base, et non pour implémenter la journalisation. La plupart des applications n’ont pas besoin d’écrire des enregistreurs. Le code suivant illustre l’utilisation de la journalisation par défaut, qui ne nécessite pas l’inscription de services dans ConfigureServices :

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; }

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

À l’aide du code précédent, il n’est pas nécessaire de mettre à jour ConfigureServices , car la journalisation est assurée par l’infrastructure.

Services injectés au démarrage

Les services peuvent être injectés dans le Startup constructeur et la Startup.Configure méthode.

Seuls les services suivants peuvent être injectés dans le Startup constructeur lors de l’utilisation de l’hôte générique ( IHostBuilder ) :

Tout service enregistré avec le conteneur DI peut être injecté dans la Startup.Configure méthode :

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    ...
}

Pour plus d’informations, consultez Démarrage d’une application dans ASP.NET Core et configuration de l’accès au démarrage.

Inscrire des groupes de services avec des méthodes d’extension

L’infrastructure de ASP.NET Core utilise une convention pour inscrire un groupe de services connexes. La Convention consiste à utiliser une Add{GROUP_NAME} méthode d’extension unique pour inscrire tous les services requis par une fonctionnalité de l’infrastructure. Par exemple, la AddControllers méthode d’extension inscrit les services requis pour les contrôleurs Mvc.

Le code suivant est généré par le Razor modèle pages à l’aide de comptes d’utilisateur individuels et montre comment ajouter des services supplémentaires au conteneur à l’aide des méthodes d’extension AddDbContext et AddDefaultIdentity :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

Considérez la ConfigureServices méthode suivante, qui inscrit les services et configure les options :

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(
        Configuration.GetSection(PositionOptions.Position));
    services.Configure<ColorOptions>(
        Configuration.GetSection(ColorOptions.Color));

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddScoped<IMyDependency2, MyDependency2>();

    services.AddRazorPages();
}

Les groupes d’inscriptions associés peuvent être déplacés vers une méthode d’extension pour inscrire les services. Par exemple, les services de configuration sont ajoutés à la classe suivante :

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }
    }
}

Les services restants sont inscrits dans une classe similaire. La ConfigureServices méthode suivante utilise les nouvelles méthodes d’extension pour inscrire les services :

public void ConfigureServices(IServiceCollection services)
{
    services.AddConfig(Configuration)
            .AddMyDependencyGroup();

    services.AddRazorPages();
}

Remarque : Chaque services.Add{GROUP_NAME} méthode d’extension ajoute et configure éventuellement des services. Par exemple, AddControllersWithViews ajoute les services MVC dont les vues ont besoin et AddRazorPages ajoute les pages de services Razor requis. Nous vous recommandons d’appliquer cette Convention d’affectation de noms aux applications. Placez les méthodes d’extension dans l’espace de noms Microsoft.Extensions.DependencyInjection pour encapsuler des groupes d’inscriptions de service.

Durées de service

Consultez durées de vie des services dans l' injection de dépendances dans .net

Pour utiliser les services délimités dans des intergiciels (middleware), utilisez l’une des approches suivantes :

  • Injectez le service dans la méthode ou l’intergiciel (middleware) Invoke InvokeAsync . L’utilisation de l' injection de constructeur lève une exception Runtime, car elle force le service de portée à se comporter comme un singleton. L’exemple de la section options de durée de vie et d’inscription illustre l' InvokeAsync approche.
  • Utilisez un intergiciel (middleware) basé sur l’usine. Les intergiciels (middleware) inscrits à l’aide de cette approche sont activés par demande du client (connexion), ce qui permet d’injecter des services étendus dans la méthode de l’intergiciel (middleware) InvokeAsync .

Pour plus d’informations, consultez Écrire un intergiciel (middleware) ASP.NET Core personnalisé.

Méthodes d’inscription du service

Consultez les méthodes d’inscription de service dans l' injection de dépendances dans .net

Il est courant d’utiliser plusieurs implémentations lorsqu’il s’agit de simuler des types à des fins de test.

L’inscription d’un service avec un seul type d’implémentation équivaut à inscrire ce service avec le même type de service et l’implémentation. C’est pourquoi plusieurs implémentations d’un service ne peuvent pas être inscrites à l’aide des méthodes qui n’acceptent pas un type de service explicite. Ces méthodes peuvent inscrire plusieurs instances d’un service, mais elles auront toutes le même type d' implémentation .

L’une des méthodes d’inscription de service ci-dessus peut être utilisée pour inscrire plusieurs instances de service du même type de service. Dans l’exemple suivant, AddSingleton est appelé deux fois avec IMyDependency comme type de service. Le deuxième appel à AddSingleton remplace le précédent lorsqu’il est résolu en tant que IMyDependency et ajoute au précédent lorsque plusieurs services sont résolus via IEnumerable<IMyDependency> . Les services s’affichent dans l’ordre dans lequel ils ont été inscrits lorsqu’ils sont résolus via IEnumerable<{SERVICE}> .

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Comportement d’injection de constructeurs

Consultez comportement d’injection de constructeur dans l' injection de dépendances dans .net

Contextes Entity Framework

Par défaut, Entity Framework contextes sont ajoutés au conteneur de service à l’aide de la durée de vie limitée , car les opérations de base de données d’application Web sont normalement étendues à la demande du client. Pour utiliser une durée de vie différente, spécifiez la durée de vie à l’aide d’une AddDbContext surcharge. Les services d’une durée de vie donnée ne doivent pas utiliser un contexte de base de données dont la durée de vie est inférieure à la durée de vie du service.

Options de durée de vie et d’inscription

Pour illustrer la différence entre les durées de vie de service et leurs options d’inscription, considérez les interfaces suivantes qui représentent une tâche comme une opération avec un identificateur, OperationId . Selon la configuration de la durée de vie du service d’une opération pour les interfaces suivantes, le conteneur fournit des instances identiques ou différentes du service lorsqu’il est demandé par une classe :

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

La Operation classe suivante implémente toutes les interfaces précédentes. Le Operation constructeur génère un GUID et stocke les 4 derniers caractères dans la OperationId propriété :

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

La Startup.ConfigureServices méthode crée plusieurs inscriptions de la Operation classe en fonction des durées de vie nommées :

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();

    services.AddRazorPages();
}

L’exemple d’application montre les durées de vie des objets dans et entre les demandes. Le IndexModel et l’intergiciel (middleware) demandent chaque genre de IOperation type et journalisent OperationId pour chacun d’entre eux :

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

À l’instar de IndexModel , l’intergiciel (middleware) résout les mêmes services :

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationTransient transientOperation,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Les services délimités doivent être résolus dans la InvokeAsync méthode :

public async Task InvokeAsync(HttpContext context,
    IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + _transientOperation.OperationId);
    _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

La sortie de l’enregistreur d’événements affiche :

  • Les objets Transient sont toujours différents. La OperationId valeur transitoire est différente dans IndexModel et dans l’intergiciel (middleware).
  • Les objets délimités sont les mêmes pour chaque demande, mais différents dans chaque demande.
  • Les objets Singleton sont les mêmes pour chaque requête.

Pour réduire la sortie de journalisation, définissez « journalisation : LogLevel : Microsoft : erreur » dans le appsettings.Development.jssur le fichier :

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Appeler des services à partir de Main

Créez un IServiceScope avec IServiceScopeFactory.CreateScope pour résoudre un service délimité dans l’étendue de l’application. Cette approche est pratique pour accéder à un service Scoped au démarrage pour exécuter des tâches d’initialisation.

L’exemple suivant montre comment accéder au IMyDependency service étendu et appeler sa WriteMessage méthode dans Program.Main :

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var myDependency = services.GetRequiredService<IMyDependency>();
                myDependency.WriteMessage("Call services from main");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Validation de l’étendue

Consultez comportement d’injection de constructeur dans l' injection de dépendances dans .net

Pour plus d’informations, consultez Validation de l’étendue.

Services de requête

Les services disponibles dans une demande de ASP.NET Core sont exposés via la collection HttpContext. RequestServices . Lorsque des services sont demandés à l’intérieur d’une demande, les services et leurs dépendances sont résolus à partir de la RequestServices collection.

L’infrastructure crée une portée par demande et RequestServices expose le fournisseur de services étendus. Tous les services délimités sont valides tant que la demande est active.

Notes

Préférez demander des dépendances en tant que paramètres de constructeur pour résoudre les services à partir de la RequestServices collection. Il en résulte des classes qui sont plus faciles à tester.

Conception de services pour l’injection de dépendances

Lors de la conception de services pour l’injection de dépendances :

  • Évitez les classes et les membres avec état, static. Évitez de créer un état global en concevant des applications qui utilisent des services Singleton à la place.
  • Éviter une instanciation directe de classes dépendantes au sein de services. L’instanciation directe associe le code à une implémentation particulière.
  • Rendez les services petits, bien factorisés et faciles à tester.

Si une classe possède un grand nombre de dépendances injectées, il est possible qu’il s’agit d’un signe que la classe a trop de responsabilités et viole le principe de responsabilité unique (SRP). Essayez de refactoriser la classe en déplaçant certaines de ses responsabilités dans de nouvelles classes. Gardez à l’esprit que Razor les classes de modèle de page de pages et les classes de contrôleur MVC doivent se concentrer sur les préoccupations de l’interface utilisateur.

Suppression des services

Le conteneur appelle Dispose pour les types IDisposable qu’il crée. Les services résolus à partir du conteneur ne doivent jamais être supprimés par le développeur. Si un type ou une fabrique est inscrit en tant que singleton, le conteneur supprime automatiquement le singleton.

Dans l’exemple suivant, les services sont créés par le conteneur de service et supprimés automatiquement :

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    
    var myKey = Configuration["MyKey"];
    services.AddSingleton<IService3>(sp => new Service3(myKey));

    services.AddRazorPages();
}
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

La console de débogage affiche la sortie suivante après chaque actualisation de la page d’index :

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet
Service1.Dispose

Services non créés par le conteneur de service

Considérez le code suivant :

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());

    services.AddRazorPages();
}

Dans le code précédent :

  • Les instances de service ne sont pas créées par le conteneur de service.
  • L’infrastructure ne supprime pas automatiquement les services.
  • Le développeur est responsable de la suppression des services.

Recommandations IDisposable pour les instances temporaires et partagées

Consultez les recommandations IDisposable pour les instances temporaires et partagées dans l' injection de dépendances dans .net

Remplacement de conteneur de services par défaut

Consultez remplacement de conteneur de service par défaut dans l' injection de dépendances dans .net

Recommandations

Consultez recommandations dans injection de dépendances dans .net

  • Évitez d’utiliser le modèle de localisation de service. Par exemple, n’appelez pas GetService pour obtenir une instance de service si vous pouvez utiliser l’injection de dépendance à la place :

    Incorrect :

    Code incorrect

    Correct:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Une autre variante du localisateur de service à éviter est l’injection d’une fabrique qui résout les dépendances au moment de l’exécution. Ces deux pratiques combinent des stratégies Inversion de contrôle.

  • Évitez l’accès statique à HttpContext (par exemple, IHttpContextAccessor.HttpContext).

  • Évitez les appels à BuildServiceProvider dans ConfigureServices . L’appel de BuildServiceProvider se produit généralement lorsque le développeur souhaite résoudre un service dans ConfigureServices . Par exemple, considérez le cas où le LoginPath est chargé à partir de la configuration. Évitez l’approche suivante :

    code incorrect appelant BuildServiceProvider

    Dans l’image précédente, la sélection de la ligne ondulée verte sous services.BuildServiceProvider affiche l’avertissement ASP0000 suivant :

    ASP0000 l’appel de’BuildServiceProvider’à partir du code de l’application entraîne la création d’une copie supplémentaire des services Singleton. Envisagez des alternatives telles que la dépendance injectant des services en tant que paramètres pour « configurer ».

    L’appel BuildServiceProvider de crée un deuxième conteneur, qui peut créer des singletons endommagés et provoquer des références aux graphiques d’objets sur plusieurs conteneurs.

    Une bonne façon d’obtenir LoginPath est d’utiliser la prise en charge intégrée du modèle d’options pour di :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();
    
        services.AddOptions<CookieAuthenticationOptions>(
                            CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IMyService>((options, myService) =>
            {
                options.LoginPath = myService.GetLoginPath();
            });
    
        services.AddRazorPages();
    }
    
  • Les services transitoires jetables sont capturés par le conteneur pour la suppression. Cela peut entraîner une fuite de mémoire si elle est résolue à partir du conteneur de niveau supérieur.

  • Activez la validation de l’étendue pour vous assurer que l’application n’a pas de singletons qui capturent les services délimités. Pour plus d’informations, consultez Validation de l’étendue.

Comme pour toutes les recommandations, vous pouvez vous trouver dans des situations où il est nécessaire d’ignorer une recommandation. Les exceptions sont rares, principalement des cas spéciaux dans l’infrastructure elle-même.

L’injection de dépendance constitue une alternative aux modèles d’accès aux objets statiques/globaux. Il est possible que vous ne bénéficiez pas des avantages de l’injection de dépendances si vous la combinez avec l’accès aux objets statiques.

Le noyau du verger est une infrastructure d’application permettant de créer des applications modulaires à plusieurs locataires sur ASP.net core. Pour plus d’informations, consultez la documentation sur le noyau du verger.

Consultez les exemples de noyaux de verger pour obtenir des exemples montrant comment créer des applications modulaires et mutualisées à l’aide de l’infrastructure principale du verger sans aucune de ses fonctionnalités propres à CMS.

Services fournis par le framework

La Startup.ConfigureServices méthode enregistre les services utilisés par l’application, y compris les fonctionnalités de plateforme, telles que Entity Framework Core et ASP.net Core Mvc. Initialement, le IServiceCollection fourni pour ConfigureServices possède des services définis par l’infrastructure en fonction de la façon dont l’hôte a été configuré. Pour les applications basées sur les modèles de ASP.NET Core, le Framework inscrit plus de 250 services.

Le tableau suivant répertorie un petit exemple de ces services inscrits au Framework :

Type de service Durée de vie
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Temporaire
IHostApplicationLifetime Singleton
IWebHostEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Temporaire
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Temporaire
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Temporaire
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Ressources supplémentaires

Par Steve Smith, Scott Addieet Brandon Dahler

ASP.NET Core prend en charge le modèle de conception logicielle d’injection de dépendances, technique qui permet d’obtenir une inversion de contrôle entre les classes et leurs dépendances.

Pour plus d’informations spécifiques à l’injection de dépendances au sein des contrôleurs MVC, consultez Injection de dépendances dans les contrôleurs dans ASP.NET Core.

Afficher ou télécharger l’exemple de code (procédure de téléchargement)

Vue d’ensemble de l’injection de dépendances

Une dépendance est un objet qui nécessite un autre objet. Examinez la classe MyDependency suivante avec une méthode WriteMessage dont dépendent d’autres classes dans une application :

public class MyDependency
{
    public MyDependency()
    {
    }

    public Task WriteMessage(string message)
    {
        Console.WriteLine(
            $"MyDependency.WriteMessage called. Message: {message}");

        return Task.FromResult(0);
    }
}

Une instance de la classe MyDependency peut être créée pour rendre la méthode WriteMessage disponible pour une classe. La classe MyDependency est une dépendance de la classe IndexModel :

public class IndexModel : PageModel
{
    MyDependency _dependency = new MyDependency();

    public async Task OnGetAsync()
    {
        await _dependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

La classe est créee et dépend directement de l’instance MyDependency. Les dépendances de code (comme l’exemple précédent) posent problème et doivent être évitées pour les raisons suivantes :

  • Pour remplacer MyDependency par une autre implémentation, la classe doit être modifiée.
  • Si MyDependency possède des dépendances, elles doivent être configurées par la classe. Dans un grand projet comportant plusieurs classes dépendant de MyDependency, le code de configuration est disséminé dans l’application.
  • Cette implémentation complique le test unitaire. L’application doit utiliser une classe MyDependency fictive ou stub, ce qui est impossible avec cette approche.

L’injection de dépendances résout ces problèmes via :

  • L’utilisation d’une interface ou classe de base pour extraire l’implémentation des dépendances.
  • L’inscription de la dépendance dans un conteneur de service. ASP.NET Core fournit un conteneur de service intégré, IServiceProvider. Les services sont inscrits dans la méthode Startup.ConfigureServices de l’application.
  • Injection du service dans le constructeur de la classe où il est utilisé. Le framework prend la responsabilité de la création d’une instance de la dépendance et de sa suppression lorsqu’elle n’est plus nécessaire.

Dans l’exemple d’application, l’interface IMyDependency définit une méthode que le service fournit à l’application :

public interface IMyDependency
{
    Task WriteMessage(string message);
}

Cette interface est implémentée par un type concret, MyDependency :

public class MyDependency : IMyDependency
{
    private readonly ILogger<MyDependency> _logger;

    public MyDependency(ILogger<MyDependency> logger)
    {
        _logger = logger;
    }

    public Task WriteMessage(string message)
    {
        _logger.LogInformation(
            "MyDependency.WriteMessage called. Message: {Message}", 
            message);

        return Task.FromResult(0);
    }
}

MyDependency exige un ILogger<TCategoryName> dans son constructeur. Il n’est pas rare que l’injection de dépendances soit utilisée de manière chaînée. Dans ce cas, chaque dépendance demandée demande à son tour ses propres dépendances. Le conteneur résout les dépendances dans le graphique et retourne le service entièrement résolu. L’ensemble collectif de dépendances qui doivent être résolues est généralement appelé arborescence des dépendances, graphique de dépendance ou graphique d’objet.

IMyDependency et ILogger<TCategoryName> doivent être inscrits dans le conteneur de service. IMyDependency est inscrit dans Startup.ConfigureServices. ILogger<TCategoryName> est inscrit par l’infrastructure d’abstractions de journalisation. Il s’agit donc d’un service fourni par le framework et inscrit par défaut par l’infrastructure.

Le conteneur résout ILogger<TCategoryName> en tirant parti de types ouverts (génériques), ce qui élimine la nécessité d’inscrire chaque type construit (générique) :

services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));

Dans l’exemple d’application, le service IMyDependency est inscrit avec le type concret MyDependency. L’inscription ajuste la durée de vie du service à la durée de vie d’une requête unique. Les durées de vie du service sont décrites plus loin dans cette rubrique.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

Notes

Chaque services.Add{SERVICE_NAME} méthode d’extension ajoute et configure éventuellement des services. Par exemple, services.AddControllersWithViews , services.AddRazorPages et services.AddControllers ajoute les services ASP.net Core les applications requièrent. Il est recommandé que les applications suivent cette convention. Placez les méthodes d’extension dans l’espace de noms Microsoft.Extensions.DependencyInjection pour encapsuler des groupes d’inscriptions de service. L’inclusion de la partie espace Microsoft.Extensions.DependencyInjection de noms pour les méthodes d’extension di est également :

  • Permet de les afficher dans IntelliSense sans ajouter de using blocs supplémentaires.
  • Empêche des using instructions excessives dans la Startup classe où ces méthodes d’extension sont généralement appelées à partir de.

Si le constructeur du service exige un type intégré, comme un string, le type peut être injecté à l’aide de la configuration ou du modèle d’options :

public class MyDependency : IMyDependency
{
    public MyDependency(IConfiguration config)
    {
        var myStringValue = config["MyStringKey"];

        // Use myStringValue
    }

    ...
}

Une instance du service est demandée via le constructeur d’une classe dans laquelle le service est utilisé et assigné à un champ privé. Le champ est utilisé pour accéder au service en fonction des besoins tout au long de la classe.

Dans l’exemple d’application, l’instance IMyDependency est demandée et utilisée pour appeler la méthode WriteMessage du service :

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

Services injectés au démarrage

Seuls les types de service suivants peuvent être injectés dans le Startup constructeur lors de l’utilisation de l’hôte générique ( IHostBuilder ) :

Les services peuvent être injectés dans Startup.Configure :

public void Configure(IApplicationBuilder app, IOptions<MyOptions> options)
{
    ...
}

Pour plus d’informations, consultez Démarrage d’une application dans ASP.NET Core.

Services fournis par le framework

La Startup.ConfigureServices méthode est chargée de définir les services utilisés par l’application, y compris les fonctionnalités de plateforme, telles que Entity Framework Core et ASP.net Core Mvc. Initialement, le IServiceCollection fourni pour ConfigureServices possède des services définis par l’infrastructure en fonction de la façon dont l’hôte a été configuré. Il n’est pas rare qu’une application basée sur un modèle de ASP.NET Core dispose de centaines de services enregistrés par l’infrastructure. Un petit exemple de services inscrits au Framework est répertorié dans le tableau suivant.

Type de service Durée de vie
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Temporaire
Microsoft.AspNetCore.Hosting.IApplicationLifetime Singleton
Microsoft.AspNetCore.Hosting.IHostingEnvironment Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Temporaire
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Http.IHttpContextFactory Temporaire
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Temporaire
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton

Inscrire des services supplémentaires avec les méthodes d’extension

Lorsqu’une méthode d’extension de collection de services est disponible pour inscrire un service (et ses services dépendants, si nécessaire), la convention consiste à utiliser une seule méthode d’extension Add{SERVICE_NAME} pour inscrire tous les services requis par ce service. Le code suivant est un exemple de la façon d’ajouter des services supplémentaires au conteneur à l’aide des méthodes d’extension AddDbContext <TContext> et AddIdentityCore :

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    ...
}

Pour plus d’informations, consultez la classe ServiceCollection dans la documentation de l’API.

Durées de service

Choisissez une durée de vie appropriée pour chaque service inscrit. Vous pouvez configurer les services ASP.NET Core avec les durées de vie suivantes :

Temporaire

Des services à durée de vie temporaire (AddTransient) sont créés chaque fois qu’ils sont demandés à partir du conteneur de service. Cette durée de vie convient parfaitement aux services légers et sans état.

Dans les applications qui traitent les demandes, les services transitoires sont supprimés à la fin de la demande.

Délimité

Les services à durée de vie délimitée (AddScoped) sont créés une seule fois par requête de client (connexion).

Dans les applications qui traitent les requêtes, les services délimités sont supprimés à la fin de la demande.

Avertissement

Si vous utilisez un service Scoped dans un middleware, injectez le service dans la méthode Invoke ou InvokeAsync. N’injectez pas via l' injection de constructeur , car cela force le service à se comporter comme un singleton. Pour plus d’informations, consultez Écrire un intergiciel (middleware) ASP.NET Core personnalisé.

Singleton

Les services avec une durée de vie singleton (AddSingleton) sont créés la première fois qu’ils sont demandés (ou quand Startup.ConfigureServices est exécuté et qu’une instance est spécifiée avec l’inscription du service). Chaque requête ultérieure utilise la même instance. Si l’application exige un comportement singleton, il est recommandé d’autoriser le conteneur de service à gérer la durée de vie du service. N’implémentez pas le modèle de conception singleton et fournissez le code utilisateur pour gérer la durée de vie de l’objet dans la classe.

Dans les applications qui traitent les requêtes, les services Singleton sont supprimés lorsque le ServiceProvider est supprimé au moment de l’arrêt de l’application.

Avertissement

Il est dangereux de résoudre un service délimité depuis un singleton. L’état du service risque de ne pas être correct lors du traitement des requêtes suivantes.

Méthodes d’inscription du service

Les méthodes d’extension d’inscription de service offrent des surcharges qui sont utiles dans des scénarios spécifiques.

Méthode Automatique
object
suppression
Multiple
implémentations
Passage d’args
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()
Exemple :
services.AddSingleton<IMyDep, MyDep>();
Oui Oui Non
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})
Exemples :
services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep("A string!"));
Oui Oui Oui
Add{LIFETIME}<{IMPLEMENTATION}>()
Exemple :
services.AddSingleton<MyDep>();
Oui Non Non
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
Exemples :
services.AddSingleton<IMyDep>(new MyDep());
services.AddSingleton<IMyDep>(new MyDep("A string!"));
Non Oui Oui
AddSingleton(new {IMPLEMENTATION})
Exemples :
services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep("A string!"));
Non Non Oui

Pour plus d’informations sur la suppression de type, consultez la section Suppression des services. Un scénario courant d’implémentations multiples est la simulation de types à des fins de test.

L’inscription d’un service avec un seul type d’implémentation équivaut à inscrire ce service avec le même type de service et l’implémentation. C’est pourquoi plusieurs implémentations d’un service ne peuvent pas être inscrites à l’aide des méthodes qui n’acceptent pas un type de service explicite. Ces méthodes peuvent inscrire plusieurs instances d’un service, mais elles auront toutes le même type d' implémentation .

L’une des méthodes d’inscription de service ci-dessus peut être utilisée pour inscrire plusieurs instances de service du même type de service. Dans l’exemple suivant, AddSingleton est appelé deux fois avec IMyDependency comme type de service. Le deuxième appel à AddSingleton remplace le précédent lorsqu’il est résolu en tant que IMyDependency et ajoute au précédent lorsque plusieurs services sont résolus via IEnumerable<IMyDependency> . Les services s’affichent dans l’ordre dans lequel ils ont été inscrits lorsqu’ils sont résolus via IEnumerable<{SERVICE}> .

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Le Framework fournit également des TryAdd{LIFETIME} méthodes d’extension, qui inscrivent le service uniquement si aucune implémentation n’est déjà inscrite.

Dans l’exemple suivant, l’appel à AddSingleton s’inscrit MyDependency en tant qu’implémentation de IMyDependency . L’appel à n' TryAddSingleton a aucun effet, car IMyDependency a déjà une implémentation inscrite.

services.AddSingleton<IMyDependency, MyDependency>();
// The following line has no effect:
services.TryAddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
        IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is MyDependency);
        Trace.Assert(myDependencies.Single() is MyDependency);
    }
}

Pour plus d'informations, consultez les pages suivantes :

Les méthodes TryAddEnumerable(ServiceDescriptor) inscrivent uniquement le service en l’absence d’une implémentation du même type. Plusieurs services sont résolus par le biais de IEnumerable<{SERVICE}>. Lors de l’inscription de services, le développeur ne souhaite ajouter une instance que si une instance du même type n’a pas déjà été ajoutée. En général, cette méthode est utilisée par les créateurs de bibliothèque pour éviter d’inscrire deux copies d’une instance dans le conteneur.

Dans l’exemple suivant, la première ligne inscrit MyDep pour IMyDep1. La deuxième ligne inscrit MyDep pour IMyDep2. La troisième ligne n’a aucun effet car IMyDep1 a déjà une implémentation inscrite de MyDep :

public interface IMyDep1 {}
public interface IMyDep2 {}

public class MyDep : IMyDep1, IMyDep2 {}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep2, MyDep>());
// Two registrations of MyDep for IMyDep1 is avoided by the following line:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());

Comportement d’injection de constructeurs

Les services peuvent être résolus par deux mécanismes :

  • IServiceProvider
  • ActivatorUtilities: Autorise la création d’objets sans inscription du service dans le conteneur d’injection de dépendances. ActivatorUtilities est utilisé avec les abstractions orientées utilisateur, telles que les Tag Helpers, les contrôleurs MVC et les classeurs de modèles.

Les constructeurs peuvent accepter des arguments qui ne sont pas fournis par l’injection de dépendances, mais les arguments doivent affecter des valeurs par défaut.

Lorsque les services sont résolus par IServiceProvider ou ActivatorUtilities , l' injection de constructeur requiert un constructeur public .

Lorsque les services sont résolus par ActivatorUtilities , l' injection de constructeur requiert l’existence d’un seul constructeur applicable. Les surcharges de constructeurs sont prises en charge, mais une seule peut exister dont les arguments peuvent tous être satisfaits par l’injection de dépendances.

Contextes Entity Framework

Les contextes Entity Framework sont généralement ajoutés au conteneur de service en utilisant la durée de vie limitée, car la portée des opérations de base de données d’application web est normalement limitée à la requête du client. La durée de vie par défaut est définie si une durée de vie n’est pas spécifiée par une surcharge AddDbContext <TContext> lors de l’enregistrement du contexte de base de données. Un service d’une durée de vie donnée ne doit pas utiliser un contexte de base de données dont la durée de vie est plus courte que celle du service.

Options de durée de vie et d’inscription

Pour illustrer la différence entre les options de durée de vie et d’inscription, considérez les interfaces suivantes qui représentent des tâches en tant qu’opération avec un identificateur unique, OperationId. Selon la façon dont la durée de vie d’un service d’opérations est configurée pour les interfaces suivantes, le conteneur fournit la même instance ou une instance différente du service lorsqu’une classe le demande :

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

Les interfaces sont implémentées dans la classe Operation. Le constructeur Operation génère un GUID s’il n’est pas fourni :

public class Operation : IOperationTransient, 
    IOperationScoped, 
    IOperationSingleton, 
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}

Un OperationService est inscrit, dépendant de chacun des autres types Operation. Lorsque OperationService est demandé via l’injection de dépendances, il reçoit une nouvelle instance de chaque service ou une instance existante en fonction de la durée de vie du service dépendant.

  • Quand des services temporaires sont créés à la demande à partir du conteneur, le OperationId du service IOperationTransient est différent du OperationId de OperationService. OperationService reçoit une nouvelle instance de la classe IOperationTransient. La nouvelle instance génère un autre OperationId.
  • Quand des services délimités sont créés pour chaque requête de client, le OperationId du IOperationScoped service est identique à celui de OperationService au sein d’une requête de client. Entre les requêtes de client, les deux services partagent une valeur OperationId différente.
  • Quand des services singleton et d’instances singleton sont créés une fois et utilisés sur toutes les requêtes de client et tous les services, le OperationId est constant entre toutes les requêtes de service.
public class OperationService
{
    public OperationService(
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }

    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }
}

Dans Startup.ConfigureServices, chaque type est ajouté au conteneur en fonction de sa durée de vie nommée :

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

Le service IOperationSingletonInstance utilise une instance spécifique avec un ID connu Guid.Empty. L’utilisation de ce type est facilement identifiable (son GUID n’affiche que des zéros).

L’exemple d’application montre les durées de vie des objets au sein et entre des requêtes individuelles. L’exemple d’application IndexModel demande chaque type IOperation et OperationService. La page affiche ensuite l’ensemble de la classe du modèle de page et des valeurs OperationId du service via des assignations de propriété :

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

Les deux sorties suivantes montrent les résultats de deux requêtes :

Première requête :

Opérations du contrôleur :

Transient: d233e165-f417-469b-a866-1cf1935d2518
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000

Opérations OperationService :

Transient: c6b049eb-1318-4e31-90f1-eb2dd849ff64
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000

Deuxième requête :

Opérations du contrôleur :

Transient: b63bd538-0a37-4ff1-90ba-081c5138dda0
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000

Opérations OperationService :

Transient: c4cbacb8-36a2-436d-81c8-8c1b78808aaf
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000

Observez les valeurs OperationId qui varient au sein d’une requête et entre les requêtes :

  • Les objets Transient sont toujours différents. Les valeurs OperationId transitoires pour la première et la deuxième requêtes de client sont différentes pour les deux opérations OperationService et entre les requêtes de client. Une nouvelle instance est fournie à chaque requête de service et requête de client.
  • Les objets Scoped sont les mêmes au sein d’une requête de client, mais ils diffèrent entre les requêtes de client.
  • Les objets Singleton sont les mêmes pour chaque objet et chaque demande, qu’une Operation instance soit fournie ou non dans Startup.ConfigureServices .

Appeler des services à partir de Main

Créez un IServiceScope avec IServiceScopeFactory.CreateScope pour résoudre un service étendu dans l’étendue de l’application. Cette approche est utile pour accéder à un service étendu au démarrage avec la durée de vie de service correcte pour exécuter des tâches d’initialisation. L’exemple suivant montre comment obtenir un contexte pour MyScopedService dans Program.Main :

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var serviceContext = services.GetRequiredService<MyScopedService>();
                // Use the context here
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        await host.RunAsync();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

Il n’est pas nécessaire de créer une étendue pour les services temporaires, y compris pour ILogger dans l’exemple précédent (voir : Journalisation dans .NET Core et ASP.NET Core ). Les transitoires ne résolvent pas les par inadvertance en tant que singletons lorsqu’ils sont résolus à partir de la racine, comme le ferait les services délimités. Les transitoires sont créées à la demande. Si un service temporaire est jetable, il est enraciné par le conteneur jusqu’à sa suppression. Par exemple, consultez Effectuer des requêtes HTTP en utilisant IHttpClientFactory dans ASP.NET Core.

Validation de l’étendue

Quand l’application s’exécute dans l’environnement de développement, le fournisseur de services par défaut effectue des contrôles pour vérifier que :

  • Les services Scoped ne sont pas résolus directement ou indirectement à partir du fournisseur de services racine.
  • Les services Scoped ne sont pas directement ou indirectement injectés dans des singletons.

Le fournisseur de services racine est créé quand BuildServiceProvider est appelé. La durée de vie du fournisseur de services racine correspond à la durée de vie de l’application/du serveur quand le fournisseur démarre avec l’application et qu’il est supprimé quand l’application s’arrête.

Les services Scoped sont supprimés par le conteneur qui les a créés. Si un service Scoped est créé dans le conteneur racine, la durée de vie du service est promue en singleton, car elle est supprimée par le conteneur racine seulement quand l’application/le serveur est arrêté. La validation des étendues du service permet de traiter ces situations quand BuildServiceProvider est appelé.

Pour plus d’informations, consultez Hôte web ASP.NET Core.

Services de requête

Les services disponibles au sein d’une requête ASP.NET Core à partir de HttpContext sont exposés par le biais de la collection HttpContext.RequestServices.

Les services de requête représentent les services configurés et demandés dans le cadre de l’application. Lorsque les objets spécifient des dépendances, ceux-ci sont satisfaits par les types trouvés dans RequestServices, pas dans ApplicationServices.

En règle générale, l’application ne doit pas utiliser directement ces propriétés. Demandez plutôt les types nécessaires à la classe via des constructeurs de classe et autorisez le framework à injecter les dépendances. Cela génère des classes qui sont plus faciles à tester.

Notes

Préférez demander des dépendances en tant que paramètres de constructeur plutôt qu’accéder à la collection RequestServices.

Conception de services pour l’injection de dépendances

Les bonnes pratiques permettent de :

  • Concevoir des services afin d’utiliser l’injection de dépendances pour obtenir leurs dépendances.
  • Évitez les classes et les membres avec état, static. Concevez des applications pour utiliser des services Singleton à la place, ce qui évite de créer un état global.
  • Éviter une instanciation directe de classes dépendantes au sein de services. L’instanciation directe associe le code à une implémentation particulière.
  • Limiter la taille des classes d’application, faire en sorte qu’elles soient bien factorisées et facilement testées.

Si une classe semble avoir trop de dépendances injectées, cela signifie généralement que la classe a trop de responsabilités et viole le principe de responsabilité unique. Essayez de refactoriser la classe en déplaçant certaines de ses responsabilités dans une nouvelle classe. Gardez à l’esprit que Razor les classes de modèle de page de pages et les classes de contrôleur MVC doivent se concentrer sur les préoccupations de l’interface utilisateur. Les règles d’entreprise et les détails d’implémentation de l’accès aux données doivent être conservés dans les classes appropriées à ces préoccupations distinctes.

Suppression des services

Le conteneur appelle Dispose pour les types IDisposable qu’il crée. Si une instance est ajoutée au conteneur par le code utilisateur, elle n’est pas supprimée automatiquement.

Dans l’exemple suivant, les services sont créés par le conteneur de service et supprimés automatiquement :

public class Service1 : IDisposable {}
public class Service2 : IDisposable {}

public interface IService3 {}
public class Service3 : IService3, IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    services.AddSingleton<IService3>(sp => new Service3());
}

Dans l’exemple suivant :

  • Les instances de service ne sont pas créées par le conteneur de service.
  • Les durées de vie de service prévues ne sont pas connues de l’infrastructure.
  • L’infrastructure ne supprime pas automatiquement les services.
  • Si les services ne sont pas explicitement supprimés dans le code du développeur, ils persistent jusqu’à ce que l’application s’arrête.
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<Service1>(new Service1());
    services.AddSingleton(new Service2());
}

Recommandations IDisposable pour les instances temporaires et partagées

Transitoire, durée de vie limitée

Scénario

L’application requiert une IDisposable instance avec une durée de vie transitoire pour l’un des scénarios suivants :

  • L’instance est résolue dans l’étendue racine.
  • L’instance doit être supprimée avant la fin de l’étendue.

Solution

Utilisez le modèle de fabrique pour créer une instance en dehors de l’étendue parente. Dans ce cas, l’application a généralement une Create méthode qui appelle directement le constructeur du type final. Si le type final a d’autres dépendances, la fabrique peut :

Instance partagée, durée de vie limitée

Scénario

L’application nécessite une IDisposable instance partagée sur plusieurs services, mais le IDisposable doit avoir une durée de vie limitée.

Solution

Inscrivez l’instance avec une durée de vie limitée. Utilisez IServiceScopeFactory.CreateScope pour démarrer et créer un nouveau IServiceScope . Utilisez les étendues IServiceProvider pour accéder aux services requis. Supprimez l’étendue lorsque la durée de vie doit se terminer.

Instructions générales

  • N’inscrivez pas IDisposable les instances avec une étendue transitoire. Utilisez à la place le modèle de fabrique.
  • Ne résolvez pas les instances temporaires ou délimitées IDisposable dans l’étendue racine. La seule exception générale est lorsque l’application crée/recrée et supprime le IServiceProvider , qui n’est pas un modèle idéal.
  • La réception d’une IDisposable dépendance via l’injection de dépendances ne nécessite pas que le récepteur s’implémente IDisposable lui-même. Le récepteur de la IDisposable dépendance ne doit pas appeler Dispose sur cette dépendance.
  • Les portées doivent être utilisées pour contrôler les durées de vie des services. Les étendues ne sont pas hiérarchiques, et il n’existe pas de connexion spéciale entre les étendues.

Remplacement de conteneur de services par défaut

Le conteneur de service intégré est conçu pour répondre aux besoins de l’infrastructure et de la plupart des applications grand public. Nous vous recommandons d’utiliser le conteneur intégré, sauf si vous avez besoin d’une fonctionnalité spécifique que le conteneur intégré ne prend pas en charge, par exemple :

  • Injection de propriétés
  • Injection en fonction du nom
  • Conteneurs enfants
  • Gestion personnalisée de la durée de vie
  • Func<T> prend en charge l’initialisation tardive
  • Inscription basée sur une convention

Les conteneurs tiers suivants peuvent être utilisés avec les applications ASP.NET Core :

Sécurité des threads

Créez des services singleton thread-safe. Si un service singleton a une dépendance vis-à-vis d’un service Transient, ce dernier peut également nécessiter la cohérence de thread, en fonction de la manière dont il est utilisé par le singleton.

La méthode de fabrique d’un service unique, telle que le deuxième argument de AddSingleton <TService> (IServiceCollection, Func <IServiceProvider,TService> ), n’a pas besoin d’être thread-safe. Comme un constructeur de type (static), elle est forcément appelée une fois par un seul thread.

Recommandations

  • La résolution de service basée sur async/await et Task n’est pas prise en charge. C# ne prend pas en charge les constructeurs asynchrones ; par conséquent, le modèle recommandé consiste à utiliser des méthodes asynchrones après avoir résolu le service de façon synchrone.

  • Évitez de stocker des données et des configurations directement dans le conteneur de services. Par exemple, le panier d’achat d’un utilisateur ne doit en général pas être ajouté au conteneur de services. La configuration doit utiliser le modèle d’options. De même, évitez les objets « conteneurs de données » qui n’existent que pour autoriser l’accès à un autre objet. Il est préférable de demander l’élément réel par le biais de l’injection de dépendance.

  • Évitez l’accès statique aux services. Par exemple, évitez de taper statiquement IApplicationBuilder. ApplicationServices pour une utilisation ailleurs.

  • Évitez d’utiliser le modèle de localisation de service, qui combine les stratégies d’inversion de contrôle .

    • N’appelez pas GetService pour obtenir une instance de service lorsque vous pouvez utiliser l’injection de code à la place :

      Incorrect :

      public class MyClass()
      
        public void MyMethod()
        {
            var optionsMonitor = 
                _services.GetService<IOptionsMonitor<MyOptions>>();
            var option = optionsMonitor.CurrentValue.Option;
      
            ...
        }
      

      Correct:

      public class MyClass
      {
          private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
      
          public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
          {
              _optionsMonitor = optionsMonitor;
          }
      
          public void MyMethod()
          {
              var option = _optionsMonitor.CurrentValue.Option;
      
              ...
          }
      }
      
  • Évitez d’injecter une fabrique qui résout les dépendances au moment de l’exécution à l’aide de GetService .

  • Évitez l’accès statique à HttpContext (par exemple, IHttpContextAccessor.HttpContext).

Comme pour toutes les recommandations, vous pouvez vous trouver dans des situations où il est nécessaire d’ignorer une recommandation. Les exceptions sont rares, principalement des cas spéciaux dans l’infrastructure elle-même.

L’injection de dépendance constitue une alternative aux modèles d’accès aux objets statiques/globaux. Il est possible que vous ne bénéficiez pas des avantages de l’injection de dépendances si vous la combinez avec l’accès aux objets statiques.

Ressources supplémentaires