Partager via


Injection de dépendances dans ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Par Kirk Larkin, Steve Smith et Brandon Dahler

ASP.NET Core prend en charge le modèle de conception de logiciel avec injection de dépendances (DI), une technique permettant d’obtenir une inversion de contrôle (IoC) entre les classes et leurs dépendances.

Pour plus d’informations sur l’injection de dépendances dans les contrôleurs MVC, consultez Injection de dépendances dans les contrôleurs dans ASP.NET Core.

Pour obtenir des informations sur l’utilisation de l’injection de dépendances dans des applications autres que des applications web, 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 incluse dans Injection de dépendances dans .NET.

Pour obtenir des instructions sur l’injection de dépendance Blazor, en complément ou en remplacement des instructions de cet article, consultez Injection de dépendance Blazor ASP.NET Core.

Affichez ou téléchargez 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 classe MyDependency suivante avec une méthode WriteMessage 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 classe MyDependency pour utiliser sa méthode WriteMessage. Dans l’exemple suivant, la classe MyDependency est une dépendance de la classe IndexModel :


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

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

La classe est créee et dépend directement de la classe MyDependency. Les dépendances de code, comme dans 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 IndexModel doit être modifiée.
  • Si MyDependency possède des dépendances, elles doivent être configurées par la classe IndexModel. 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’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 le fichier Program.cs 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 la méthode WriteMessage :

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 service IMyDependency avec le type MyDependency concret. La méthode AddScoped inscrit le service avec une durée de vie dé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.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

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

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 d’injection de dépendances, le contrôleur ou la page Razor :

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

L’implémentation de l’interface IMyDependency 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}");
    }
}

Le fichier Program.cs mis à jour inscrit la nouvelle implémentation IMyDependency :

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

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

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

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 service IMyDependency.
  • N’est pas lié à un service web, bien que le service puisse utiliser un service web.

L’infrastructure fournit un système de journalisation robuste. Les implémentations IMyDependency indiquées dans les exemples précédents ont été écrites pour démontrer l’injection de dépendances de base, et non pour implémenter la journalisation. La plupart des applications ne doivent pas avoir besoin d’écrire des enregistreurs d’événements. Le code suivant illustre l’utilisation de la journalisation par défaut, qui ne nécessite l’inscription d’aucun service :

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

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

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

En utilisant le code précédent, il n’est pas nécessaire de mettre à jour Program.cs, car la journalisation est fournie par l’infrastructure.

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

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

Le code suivant est généré par le modèle des pages Razor à 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 :

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Examinons les éléments suivants permettant d’inscrire les services et de configurer les options :

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

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

var app = builder.Build();

Les groupes d’inscriptions associés peuvent être déplacés vers une méthode d’extension pour inscrire les services. Les services de configuration sont par exemple 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;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Les services qui restent sont inscrits dans une classe similaire. Le code suivant utilise les nouvelles méthodes d’extension pour inscrire les services :

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Remarque : chaque méthode d’extension services.Add{GROUP_NAME} ajoute des services et les configure éventuellement. Par exemple, AddControllersWithViews ajoute les services nécessaires à MVC avec vues et AddRazorPages ajoute les services nécessaires aux pages Razor.

Durées de service

Consultez Durées de service dans Injection de dépendances dans .NET

Pour utiliser des services délimités dans un intergiciel, utilisez l’une des approches suivantes :

  • Injectez le service dans la méthode Invoke ou InvokeAsync de l’intergiciel. L’utilisation de l’injection de constructeur lève une exception de runtime, car elle force le service délimité à se comporter comme un singleton. L’exemple utilisé dans la section Options de durée de vie et d’inscription illustre l’approche InvokeAsync.
  • Utilisez un intergiciel basé sur une fabrique. L’intergiciel inscrit à l’aide de cette approche est activé par demande client (connexion), ce qui permet d’injecter des services délimités dans le constructeur de l’intergiciel.

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

Méthodes d’inscription du service

Consultez Méthodes d’inscription de service dans Injection de dépendances dans .NET

Il est courant d’utiliser plusieurs implémentations lors d’une simulation de types à des fins de test.

L’inscription d’un service avec uniquement un type d’implémentation revient à inscrire ce service avec le même type d’implémentation et de service. C’est pourquoi plusieurs implémentations d’un service ne peuvent pas être inscrites à l’aide des méthodes qui n’acceptent pas de 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 s’ajoute au précédent lorsque plusieurs services sont résolus via IEnumerable<IMyDependency>. Les services apparaissent 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);
    }
}

Services à clé

Les services à clé désigne un mécanisme d’inscription et de récupération des services d’injection de dépendances (DI) à l’aide de clés. Un service est associé à une clé en appelant AddKeyedSingleton (ou AddKeyedScoped ou AddKeyedTransient) pour l’inscrire. Accédez à un service inscrit en spécifiant la clé avec l’attribut [FromKeyedServices]. Le code suivant montre comment utiliser les services à clé :

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big-cache")]
    public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}

public class MyHub : Hub
{
    public void Method([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}

Services clés dans l’intergiciel

L’intergiciel prend en charge les services à clé dans le constructeur et la méthode Invoke/InvokeAsync :

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");

var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();

internal class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next,
        [FromKeyedServices("test")] MySingletonClass service)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context,
        [FromKeyedServices("test2")]
            MyScopedClass scopedService) => _next(context);
}

Pour plus d’informations sur la création d’intergiciel, consultez Écrire un intergiciel ASP.NET Core personnalisé.

Comportement d’injection de constructeurs

Consultez Comportement d’injection de constructeurs dans Injection de dépendances dans .NET

Contextes Entity Framework

Par défaut, les contextes Entity Framework sont 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 demande du client. Pour utiliser une durée de vie différente, spécifiez la durée de vie à l’aide d’une surcharge AddDbContext. 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 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 de service, considérez les interfaces suivantes qui représentent une tâche en tant qu’opération avec un identificateur, 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
{
    string OperationId { get; }
}

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

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

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

    public string OperationId { get; }
}

Le code suivant crée plusieurs inscriptions de la classe Operation en fonction des durées de vie nommées :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

L’exemple d’application illustre les durées de vie des objets au sein et entre des demandes. IndexModel et l’intergiciel demandent chaque type de IOperation et consignent OperationId pour chacun :

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

Comme IndexModel, l’intergiciel résout les mêmes services :

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

    private readonly IOperationSingleton _singletonOperation;

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

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, 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 et temporaires doivent être résolus dans la méthode InvokeAsync :

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, 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 montre :

  • Les objets Transient sont toujours différents. La valeur OperationId temporaire est différente dans IndexModel et dans l’intergiciel.
  • Les objets délimités sont les mêmes pour une demande donnée, mais diffèrent pour chaque nouvelle demande.
  • Les objets singleton sont identiques pour chaque demande.

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

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

Résoudre un service au démarrage de l’application

Le code suivant montre comment résoudre un service délimité pendant une durée limitée quand l’application démarre :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

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

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Validation de l’étendue

Consultez Comportement d’injection de constructeurs dans Injection de dépendances dans .NET

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

Services de requête

Les services et leurs dépendances au sein d’une demande ASP.NET Core sont exposés via HttpContext.RequestServices.

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

Remarque

Préférez demander des dépendances en tant que paramètres de constructeur plutôt que résoudre les services à partir de RequestServices. La demande de dépendances en tant que paramètres de constructeur génère des classes 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 statiques avec état. Évitez de créer un état global en concevant des applications pour utiliser 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.
  • Limitez la taille des services, faites en sorte qu’elles soient bien factorisées et facilement testées.

Si une classe possède de nombreuses dépendances injectées, il peut s’agir 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. N’oubliez pas que les classes du modèle de page Razor Pages et les classes du contrôleur MVC doivent se concentrer sur les problèmes d’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 : dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

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;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
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 Index :

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Services non créés par le conteneur de services

Prenez le code suivant :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

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.

Conseils sur l’interface IDisposable pour les instances temporaires et partagées

Consultez Conseils sur l’interface IDisposable pour les instances temporaires et partagées dans Injection de dépendances dans .NET

Remplacement de conteneur de services par défaut

Consultez Remplacement de conteneur de services par défaut dans Injection de dépendances dans .NET

Recommandations

Consultez Recommandations dans Injection de dépendances dans .NET

  • Évitez d’utiliser le modèle de localisateur 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).

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.

Orchard Core est une infrastructure d’application permettant de créer des applications modulaires multilocataires sur ASP.NET Core. Pour plus d’informations, consultez la documentation Orchard Core.

Consultez les exemples Orchard Core pour obtenir des exemples de création d’applications modulaires et multilocataires utilisant simplement l’infrastructure Orchard Core sans aucune de ses fonctionnalités spécifiques à CMS.

Services fournis par le framework

Program.cs inscrit les services utilisés par l’application, notamment les fonctionnalités de plateforme comme Entity Framework Core et ASP.NET Core MVC. Au départ, IServiceCollection fournie à Program.cs a les services définis par l’infrastructure en fonction de la manière dont l’hôte a été configuré. Pour les applications basées sur les modèles ASP.NET Core, l’infrastructure inscrit plus de 250 services.

Le tableau suivant répertorie un petit exemple de ces services inscrits dans l’infrastructure :

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 Kirk Larkin, Steve Smith et Brandon Dahler

ASP.NET Core prend en charge le modèle de conception de logiciel avec injection de dépendances (DI), une technique permettant d’obtenir une inversion de contrôle (IoC) entre les classes et leurs dépendances.

Pour plus d’informations sur l’injection de dépendances dans les contrôleurs MVC, consultez Injection de dépendances dans les contrôleurs dans ASP.NET Core.

Pour obtenir des informations sur l’utilisation de l’injection de dépendances dans des applications autres que des applications web, 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 incluse dans Injection de dépendances dans .NET.

Affichez ou téléchargez 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 classe MyDependency suivante avec une méthode WriteMessage 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 classe MyDependency pour utiliser sa méthode WriteMessage. Dans l’exemple suivant, la classe MyDependency est une dépendance de la classe IndexModel :


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

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

La classe est créee et dépend directement de la classe MyDependency. Les dépendances de code, comme dans 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 IndexModel doit être modifiée.
  • Si MyDependency possède des dépendances, elles doivent être configurées par la classe IndexModel. 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’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 le fichier Program.cs 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 la méthode WriteMessage :

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 service IMyDependency avec le type MyDependency concret. La méthode AddScoped inscrit le service avec une durée de vie dé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.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

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

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 d’injection de dépendances, le contrôleur ou la page Razor :

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

L’implémentation de l’interface IMyDependency 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}");
    }
}

Le fichier Program.cs mis à jour inscrit la nouvelle implémentation IMyDependency :

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

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

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

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 service IMyDependency.
  • N’est pas lié à un service web, bien que le service puisse utiliser un service web.

L’infrastructure fournit un système de journalisation robuste. Les implémentations IMyDependency indiquées dans les exemples précédents ont été écrites pour démontrer l’injection de dépendances de base, et non pour implémenter la journalisation. La plupart des applications ne doivent pas avoir besoin d’écrire des enregistreurs d’événements. Le code suivant illustre l’utilisation de la journalisation par défaut, qui ne nécessite l’inscription d’aucun service :

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

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

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

En utilisant le code précédent, il n’est pas nécessaire de mettre à jour Program.cs, car la journalisation est fournie par l’infrastructure.

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

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

Le code suivant est généré par le modèle des pages Razor à 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 :

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Examinons les éléments suivants permettant d’inscrire les services et de configurer les options :

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

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

var app = builder.Build();

Les groupes d’inscriptions associés peuvent être déplacés vers une méthode d’extension pour inscrire les services. Les services de configuration sont par exemple 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;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Les services qui restent sont inscrits dans une classe similaire. Le code suivant utilise les nouvelles méthodes d’extension pour inscrire les services :

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Remarque : chaque méthode d’extension services.Add{GROUP_NAME} ajoute des services et les configure éventuellement. Par exemple, AddControllersWithViews ajoute les services nécessaires à MVC avec vues et AddRazorPages ajoute les services nécessaires aux pages Razor.

Durées de service

Consultez Durées de service dans Injection de dépendances dans .NET

Pour utiliser des services délimités dans un intergiciel, utilisez l’une des approches suivantes :

  • Injectez le service dans la méthode Invoke ou InvokeAsync de l’intergiciel. L’utilisation de l’injection de constructeur lève une exception de runtime, car elle force le service délimité à se comporter comme un singleton. L’exemple utilisé dans la section Options de durée de vie et d’inscription illustre l’approche InvokeAsync.
  • Utilisez un intergiciel basé sur une fabrique. L’intergiciel inscrit à l’aide de cette approche est activé par demande client (connexion), ce qui permet d’injecter des services délimités dans le constructeur de l’intergiciel.

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

Méthodes d’inscription du service

Consultez Méthodes d’inscription de service dans Injection de dépendances dans .NET

Il est courant d’utiliser plusieurs implémentations lors d’une simulation de types à des fins de test.

L’inscription d’un service avec uniquement un type d’implémentation revient à inscrire ce service avec le même type d’implémentation et de service. C’est pourquoi plusieurs implémentations d’un service ne peuvent pas être inscrites à l’aide des méthodes qui n’acceptent pas de 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 s’ajoute au précédent lorsque plusieurs services sont résolus via IEnumerable<IMyDependency>. Les services apparaissent 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);
    }
}

Services à clé

Les services à clé désigne un mécanisme d’inscription et de récupération des services d’injection de dépendances (DI) à l’aide de clés. Un service est associé à une clé en appelant AddKeyedSingleton (ou AddKeyedScoped ou AddKeyedTransient) pour l’inscrire. Accédez à un service inscrit en spécifiant la clé avec l’attribut [FromKeyedServices]. Le code suivant montre comment utiliser les services à clé :

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big-cache")]
    public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}

public class MyHub : Hub
{
    public void Method([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}

Comportement d’injection de constructeurs

Consultez Comportement d’injection de constructeurs dans Injection de dépendances dans .NET

Contextes Entity Framework

Par défaut, les contextes Entity Framework sont 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 demande du client. Pour utiliser une durée de vie différente, spécifiez la durée de vie à l’aide d’une surcharge AddDbContext. 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 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 de service, considérez les interfaces suivantes qui représentent une tâche en tant qu’opération avec un identificateur, 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
{
    string OperationId { get; }
}

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

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

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

    public string OperationId { get; }
}

Le code suivant crée plusieurs inscriptions de la classe Operation en fonction des durées de vie nommées :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

L’exemple d’application illustre les durées de vie des objets au sein et entre des demandes. IndexModel et l’intergiciel demandent chaque type de IOperation et consignent OperationId pour chacun :

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

Comme IndexModel, l’intergiciel résout les mêmes services :

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

    private readonly IOperationSingleton _singletonOperation;

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

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, 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 et temporaires doivent être résolus dans la méthode InvokeAsync :

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, 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 montre :

  • Les objets Transient sont toujours différents. La valeur OperationId temporaire est différente dans IndexModel et dans l’intergiciel.
  • Les objets délimités sont les mêmes pour une demande donnée, mais diffèrent pour chaque nouvelle demande.
  • Les objets singleton sont identiques pour chaque demande.

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

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

Résoudre un service au démarrage de l’application

Le code suivant montre comment résoudre un service délimité pendant une durée limitée quand l’application démarre :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

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

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Validation de l’étendue

Consultez Comportement d’injection de constructeurs dans Injection de dépendances dans .NET

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

Services de requête

Les services et leurs dépendances au sein d’une demande ASP.NET Core sont exposés via HttpContext.RequestServices.

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

Remarque

Préférez demander des dépendances en tant que paramètres de constructeur plutôt que résoudre les services à partir de RequestServices. La demande de dépendances en tant que paramètres de constructeur génère des classes 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 statiques avec état. Évitez de créer un état global en concevant des applications pour utiliser 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.
  • Limitez la taille des services, faites en sorte qu’elles soient bien factorisées et facilement testées.

Si une classe possède de nombreuses dépendances injectées, il peut s’agir 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. N’oubliez pas que les classes du modèle de page Razor Pages et les classes du contrôleur MVC doivent se concentrer sur les problèmes d’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 : dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

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;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
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 Index :

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Services non créés par le conteneur de services

Prenez le code suivant :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

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.

Conseils sur l’interface IDisposable pour les instances temporaires et partagées

Consultez Conseils sur l’interface IDisposable pour les instances temporaires et partagées dans Injection de dépendances dans .NET

Remplacement de conteneur de services par défaut

Consultez Remplacement de conteneur de services par défaut dans Injection de dépendances dans .NET

Recommandations

Consultez Recommandations dans Injection de dépendances dans .NET

  • Évitez d’utiliser le modèle de localisateur 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).

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.

Orchard Core est une infrastructure d’application permettant de créer des applications modulaires multilocataires sur ASP.NET Core. Pour plus d’informations, consultez la documentation Orchard Core.

Consultez les exemples Orchard Core pour obtenir des exemples de création d’applications modulaires et multilocataires utilisant simplement l’infrastructure Orchard Core sans aucune de ses fonctionnalités spécifiques à CMS.

Services fournis par le framework

Program.cs inscrit les services utilisés par l’application, notamment les fonctionnalités de plateforme comme Entity Framework Core et ASP.NET Core MVC. Au départ, IServiceCollection fournie à Program.cs a les services définis par l’infrastructure en fonction de la manière dont l’hôte a été configuré. Pour les applications basées sur les modèles ASP.NET Core, l’infrastructure inscrit plus de 250 services.

Le tableau suivant répertorie un petit exemple de ces services inscrits dans l’infrastructure :

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 Kirk Larkin, Steve Smith et Brandon Dahler

ASP.NET Core prend en charge le modèle de conception de logiciel avec injection de dépendances (DI), une technique permettant d’obtenir une inversion de contrôle (IoC) entre les classes et leurs dépendances.

Pour plus d’informations sur l’injection de dépendances dans les contrôleurs MVC, consultez Injection de dépendances dans les contrôleurs dans ASP.NET Core.

Pour obtenir des informations sur l’utilisation de l’injection de dépendances dans des applications autres que des applications web, 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 incluse dans Injection de dépendances dans .NET.

Affichez ou téléchargez 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 classe MyDependency suivante avec une méthode WriteMessage 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 classe MyDependency pour utiliser sa méthode WriteMessage. Dans l’exemple suivant, la classe MyDependency est une dépendance de la classe IndexModel :


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

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

La classe est créee et dépend directement de la classe MyDependency. Les dépendances de code, comme dans 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 IndexModel doit être modifiée.
  • Si MyDependency possède des dépendances, elles doivent être configurées par la classe IndexModel. 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’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 le fichier Program.cs 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 la méthode WriteMessage :

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 service IMyDependency avec le type MyDependency concret. La méthode AddScoped inscrit le service avec une durée de vie dé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.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

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

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 d’injection de dépendances, le contrôleur ou la page Razor :

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

L’implémentation de l’interface IMyDependency 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}");
    }
}

Le fichier Program.cs mis à jour inscrit la nouvelle implémentation IMyDependency :

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

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

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

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 service IMyDependency.
  • N’est pas lié à un service web, bien que le service puisse utiliser un service web.

L’infrastructure fournit un système de journalisation robuste. Les implémentations IMyDependency indiquées dans les exemples précédents ont été écrites pour démontrer l’injection de dépendances de base, et non pour implémenter la journalisation. La plupart des applications ne doivent pas avoir besoin d’écrire des enregistreurs d’événements. Le code suivant illustre l’utilisation de la journalisation par défaut, qui ne nécessite l’inscription d’aucun service :

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

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

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

En utilisant le code précédent, il n’est pas nécessaire de mettre à jour Program.cs, car la journalisation est fournie par l’infrastructure.

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

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

Le code suivant est généré par le modèle des pages Razor à 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 :

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Examinons les éléments suivants permettant d’inscrire les services et de configurer les options :

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

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

var app = builder.Build();

Les groupes d’inscriptions associés peuvent être déplacés vers une méthode d’extension pour inscrire les services. Les services de configuration sont par exemple 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;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Les services qui restent sont inscrits dans une classe similaire. Le code suivant utilise les nouvelles méthodes d’extension pour inscrire les services :

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Remarque : chaque méthode d’extension services.Add{GROUP_NAME} ajoute des services et les configure éventuellement. Par exemple, AddControllersWithViews ajoute les services nécessaires à MVC avec vues et AddRazorPages ajoute les services nécessaires aux pages Razor.

Durées de service

Consultez Durées de service dans Injection de dépendances dans .NET

Pour utiliser des services délimités dans un intergiciel, utilisez l’une des approches suivantes :

  • Injectez le service dans la méthode Invoke ou InvokeAsync de l’intergiciel. L’utilisation de l’injection de constructeur lève une exception de runtime, car elle force le service délimité à se comporter comme un singleton. L’exemple utilisé dans la section Options de durée de vie et d’inscription illustre l’approche InvokeAsync.
  • Utilisez un intergiciel basé sur une fabrique. L’intergiciel inscrit à l’aide de cette approche est activé par demande client (connexion), ce qui permet d’injecter des services délimités dans le constructeur de l’intergiciel.

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

Méthodes d’inscription du service

Consultez Méthodes d’inscription de service dans Injection de dépendances dans .NET

Il est courant d’utiliser plusieurs implémentations lors d’une simulation de types à des fins de test.

L’inscription d’un service avec uniquement un type d’implémentation revient à inscrire ce service avec le même type d’implémentation et de service. C’est pourquoi plusieurs implémentations d’un service ne peuvent pas être inscrites à l’aide des méthodes qui n’acceptent pas de 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 s’ajoute au précédent lorsque plusieurs services sont résolus via IEnumerable<IMyDependency>. Les services apparaissent 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 constructeurs dans Injection de dépendances dans .NET

Contextes Entity Framework

Par défaut, les contextes Entity Framework sont 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 demande du client. Pour utiliser une durée de vie différente, spécifiez la durée de vie à l’aide d’une surcharge AddDbContext. 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 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 de service, considérez les interfaces suivantes qui représentent une tâche en tant qu’opération avec un identificateur, 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
{
    string OperationId { get; }
}

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

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

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

    public string OperationId { get; }
}

Le code suivant crée plusieurs inscriptions de la classe Operation en fonction des durées de vie nommées :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

L’exemple d’application illustre les durées de vie des objets au sein et entre des demandes. IndexModel et l’intergiciel demandent chaque type de IOperation et consignent OperationId pour chacun :

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

Comme IndexModel, l’intergiciel résout les mêmes services :

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

    private readonly IOperationSingleton _singletonOperation;

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

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, 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 et temporaires doivent être résolus dans la méthode InvokeAsync :

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, 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 montre :

  • Les objets Transient sont toujours différents. La valeur OperationId temporaire est différente dans IndexModel et dans l’intergiciel.
  • Les objets délimités sont les mêmes pour une demande donnée, mais diffèrent pour chaque nouvelle demande.
  • Les objets singleton sont identiques pour chaque demande.

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

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

Résoudre un service au démarrage de l’application

Le code suivant montre comment résoudre un service délimité pendant une durée limitée quand l’application démarre :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

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

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Validation de l’étendue

Consultez Comportement d’injection de constructeurs dans Injection de dépendances dans .NET

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

Services de requête

Les services et leurs dépendances au sein d’une demande ASP.NET Core sont exposés via HttpContext.RequestServices.

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

Remarque

Préférez demander des dépendances en tant que paramètres de constructeur plutôt que résoudre les services à partir de RequestServices. La demande de dépendances en tant que paramètres de constructeur génère des classes 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 statiques avec état. Évitez de créer un état global en concevant des applications pour utiliser 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.
  • Limitez la taille des services, faites en sorte qu’elles soient bien factorisées et facilement testées.

Si une classe possède de nombreuses dépendances injectées, il peut s’agir 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. N’oubliez pas que les classes du modèle de page Razor Pages et les classes du contrôleur MVC doivent se concentrer sur les problèmes d’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 : dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

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;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
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 Index :

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Services non créés par le conteneur de services

Prenez le code suivant :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

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.

Conseils sur l’interface IDisposable pour les instances temporaires et partagées

Consultez Conseils sur l’interface IDisposable pour les instances temporaires et partagées dans Injection de dépendances dans .NET

Remplacement de conteneur de services par défaut

Consultez Remplacement de conteneur de services par défaut dans Injection de dépendances dans .NET

Recommandations

Consultez Recommandations dans Injection de dépendances dans .NET

  • Évitez d’utiliser le modèle de localisateur 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).

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.

Orchard Core est une infrastructure d’application permettant de créer des applications modulaires multilocataires sur ASP.NET Core. Pour plus d’informations, consultez la documentation Orchard Core.

Consultez les exemples Orchard Core pour obtenir des exemples de création d’applications modulaires et multilocataires utilisant simplement l’infrastructure Orchard Core sans aucune de ses fonctionnalités spécifiques à CMS.

Services fournis par le framework

Program.cs inscrit les services utilisés par l’application, notamment les fonctionnalités de plateforme comme Entity Framework Core et ASP.NET Core MVC. Au départ, IServiceCollection fournie à Program.cs a les services définis par l’infrastructure en fonction de la manière dont l’hôte a été configuré. Pour les applications basées sur les modèles ASP.NET Core, l’infrastructure inscrit plus de 250 services.

Le tableau suivant répertorie un petit exemple de ces services inscrits dans l’infrastructure :

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 Kirk Larkin, Steve Smith, Scott Addie et Brandon Dahler

ASP.NET Core prend en charge le modèle de conception de logiciel avec injection de dépendances (DI), une technique permettant d’obtenir une inversion de contrôle (IoC) entre les classes et leurs dépendances.

Pour plus d’informations sur l’injection de dépendances dans les contrôleurs MVC, consultez Injection de dépendances dans les contrôleurs dans ASP.NET Core.

Pour obtenir des informations sur l’utilisation de l’injection de dépendances dans des applications autres que des applications web, 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 incluse dans Injection de dépendances dans .NET.

Affichez ou téléchargez 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 classe MyDependency suivante avec une méthode WriteMessage 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 classe MyDependency pour utiliser sa méthode WriteMessage. Dans l’exemple suivant, la classe MyDependency est une dépendance de la classe IndexModel :

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

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

La classe est créee et dépend directement de la classe MyDependency. Les dépendances de code, comme dans 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 IndexModel doit être modifiée.
  • Si MyDependency possède des dépendances, elles doivent être configurées par la classe IndexModel. 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 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 la méthode WriteMessage :

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 service IMyDependency avec le type MyDependency concret. La méthode AddScoped inscrit le service avec une durée de vie dé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 service IMyDependency est demandé et utilisé pour appeler la méthode WriteMessage :

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 d’injection de dépendances, le contrôleur :

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

L’implémentation de l’interface IMyDependency 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 ConfigureServices mise à jour inscrit la nouvelle implémentation IMyDependency :

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 l’infrastructure.

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

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 service IMyDependency.
  • N’est pas lié à un service web, bien que le service puisse utiliser un service web.

L’infrastructure fournit un système de journalisation robuste. Les implémentations IMyDependency indiquées dans les exemples précédents ont été écrites pour démontrer l’injection de dépendances de base, et non pour implémenter la journalisation. La plupart des applications ne doivent pas avoir besoin d’écrire des enregistreurs d’événements. Le code suivant illustre l’utilisation de la journalisation par défaut, qui ne nécessite l’inscription d’aucun service 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);
    }
}

En utilisant le code précédent, il n’est pas nécessaire de mettre à jour ConfigureServices, car la journalisation est fournie par l’infrastructure.

Services injectés au démarrage

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

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

Tout service inscrit auprès du conteneur d’injection de dépendances peut être injecté dans la méthode Startup.Configure :

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 ASP.NET Core utilise une convention pour l’inscription d’un groupe de services associés. La convention consiste à utiliser une seule méthode d’extension Add{GROUP_NAME} pour inscrire tous les services requis par une fonctionnalité d’infrastructure. Par exemple, la méthode d’extension AddControllers inscrit les services requis pour les contrôleurs MVC.

Le code suivant est généré par le modèle des pages Razor à 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();
}

Examinons la méthode ConfigureServices 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. Les services de configuration sont par exemple 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;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Les services qui restent sont inscrits dans une classe similaire. La méthode ConfigureServices 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 méthode d’extension services.Add{GROUP_NAME} ajoute des services et les configure éventuellement. Par exemple, AddControllersWithViews ajoute les services nécessaires à MVC avec vues et AddRazorPages ajoute les services nécessaires aux pages Razor. Nous recommandons que les applications suivent la convention de nommage associée à la création des méthodes d’extension dans l’espace de noms Microsoft.Extensions.DependencyInjection. Création de méthodes d’extension dans l’espace de noms Microsoft.Extensions.DependencyInjection :

  • Encapsule des groupes d’inscriptions de service.
  • Fournit un accès IntelliSense pratique au service.

Durées de service

Consultez Durées de service dans Injection de dépendances dans .NET

Pour utiliser des services délimités dans un intergiciel, utilisez l’une des approches suivantes :

  • Injectez le service dans la méthode Invoke ou InvokeAsync de l’intergiciel. L’utilisation de l’injection de constructeur lève une exception de runtime, car elle force le service délimité à se comporter comme un singleton. L’exemple utilisé dans la section Options de durée de vie et d’inscription illustre l’approche InvokeAsync.
  • Utilisez un intergiciel basé sur une fabrique. L’intergiciel inscrit à l’aide de cette approche est activé par demande client (connexion), ce qui permet d’injecter des services délimités dans la méthode InvokeAsync de l’intergiciel.

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

Méthodes d’inscription du service

Consultez Méthodes d’inscription de service dans Injection de dépendances dans .NET

Il est courant d’utiliser plusieurs implémentations lors d’une simulation de types à des fins de test.

L’inscription d’un service avec uniquement un type d’implémentation revient à inscrire ce service avec le même type d’implémentation et de service. C’est pourquoi plusieurs implémentations d’un service ne peuvent pas être inscrites à l’aide des méthodes qui n’acceptent pas de 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 s’ajoute au précédent lorsque plusieurs services sont résolus via IEnumerable<IMyDependency>. Les services apparaissent 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 constructeurs dans Injection de dépendances dans .NET

Contextes Entity Framework

Par défaut, les contextes Entity Framework sont 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 demande du client. Pour utiliser une durée de vie différente, spécifiez la durée de vie à l’aide d’une surcharge AddDbContext. 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 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 de service, considérez les interfaces suivantes qui représentent une tâche en tant qu’opération avec un identificateur, 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
{
    string OperationId { get; }
}

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

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

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

    public string OperationId { get; }
}

La méthode Startup.ConfigureServices crée plusieurs inscriptions de la classe Operation 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 illustre les durées de vie des objets au sein et entre des demandes. IndexModel et l’intergiciel demandent chaque type de IOperation et consignent OperationId pour chacun :

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

Comme IndexModel, l’intergiciel 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 méthode InvokeAsync :

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 montre :

  • Les objets Transient sont toujours différents. La valeur OperationId temporaire est différente dans IndexModel et dans l’intergiciel.
  • Les objets délimités sont les mêmes pour une demande donnée, mais diffèrent pour chaque nouvelle demande.
  • Les objets singleton sont identiques pour chaque demande.

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

{
  "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 service IMyDependency délimité et appeler sa méthode WriteMessage 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 constructeurs dans Injection de dépendances dans .NET

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

Services de requête

Les services et leurs dépendances au sein d’une demande ASP.NET Core sont exposés via HttpContext.RequestServices.

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

Remarque

Préférez demander des dépendances en tant que paramètres de constructeur plutôt que résoudre les services à partir de RequestServices. La demande de dépendances en tant que paramètres de constructeur génère des classes 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 statiques avec état. Évitez de créer un état global en concevant des applications pour utiliser 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.
  • Limitez la taille des services, faites en sorte qu’elles soient bien factorisées et facilement testées.

Si une classe possède de nombreuses dépendances injectées, il peut s’agir 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. N’oubliez pas que les classes du modèle de page Razor Pages et les classes du contrôleur MVC doivent se concentrer sur les problèmes d’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 Index :

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = My Key from config
Service1.Dispose

Services non créés par le conteneur de services

Prenez 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.

Conseils sur l’interface IDisposable pour les instances temporaires et partagées

Consultez Conseils sur l’interface IDisposable pour les instances temporaires et partagées dans Injection de dépendances dans .NET

Remplacement de conteneur de services par défaut

Consultez Remplacement de conteneur de services par défaut dans Injection de dépendances dans .NET

Recommandations

Consultez Recommandations dans Injection de dépendances dans .NET

  • Évitez d’utiliser le modèle de localisateur 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ù 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 d’application entraîne la création d’une copie supplémentaire des services singleton. Envisagez des alternatives telles que l’injection de dépendances de services en tant que paramètres pour « Configurer ».

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

    Une bonne façon d’obtenir LoginPath consiste à utiliser la prise en charge intégrée du modèle d’options pour l’injection de dépendances :

    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 temporaires supprimables sont capturés par le conteneur à des fins d’élimination. Cela peut se transformer en fuite de mémoire si le service est résolu à 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 des services 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 et sont principalement des cas particuliers 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.

Orchard Core est une infrastructure d’application permettant de créer des applications modulaires multilocataires sur ASP.NET Core. Pour plus d’informations, consultez la documentation Orchard Core.

Consultez les exemples Orchard Core pour obtenir des exemples de création d’applications modulaires et multilocataires utilisant simplement l’infrastructure Orchard Core sans aucune de ses fonctionnalités spécifiques à CMS.

Services fournis par le framework

La méthode Startup.ConfigureServices inscrit les services utilisés par l’application, notamment les fonctionnalités de plateforme comme Entity Framework Core et ASP.NET Core MVC. Au départ, IServiceCollection fournie à ConfigureServices a les services définis par l’infrastructure en fonction de la manière dont l’hôte a été configuré. Pour les applications basées sur les modèles ASP.NET Core, l’infrastructure inscrit plus de 250 services.

Le tableau suivant répertorie un petit exemple de ces services inscrits dans l’infrastructure :

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