Dependency injection in ASP.NET Core (Wstrzykiwanie zależności na platformie ASP.NET Core)

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz ASP.NET Core 8.0 tego artykułu.

Przez Kirk Larkin, Steve Smith i Brandon Dahler

ASP.NET Core obsługuje wzorzec projektowania oprogramowania iniekcji zależności (DI), który jest techniką osiągnięcia inwersji kontroli (IoC) między klasami i ich zależnościami.

Aby uzyskać więcej informacji specyficznych dla wstrzykiwania zależności w kontrolerach MVC, zobacz Wstrzykiwanie zależności do kontrolerów w ASP.NET Core.

Aby uzyskać informacje na temat używania wstrzykiwania zależności w aplikacjach innych niż aplikacje internetowe, zobacz Wstrzykiwanie zależności na platformie .NET.

Aby uzyskać więcej informacji na temat wstrzykiwania zależności opcji, zobacz Wzorzec opcji w ASP.NET Core.

Ten temat zawiera informacje na temat wstrzykiwania zależności w programie ASP.NET Core. Podstawowa dokumentacja dotycząca używania wstrzykiwania zależności znajduje się w iniekcji zależności na platformie .NET.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Omówienie wstrzykiwania zależności

Zależność to obiekt, od którego zależy inny obiekt. Zbadaj następującą MyDependency klasę przy użyciu metody, od których WriteMessage zależą inne klasy:

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

Klasa może utworzyć wystąpienie MyDependency klasy w celu użycia jej WriteMessage metody. W poniższym przykładzie MyDependency klasa jest zależnością IndexModel klasy:


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

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

Klasa tworzy klasę MyDependency i zależy bezpośrednio od klasy. Zależności kodu, takie jak w poprzednim przykładzie, są problematyczne i należy unikać z następujących powodów:

  • Aby zastąpić MyDependency inną implementacją, należy zmodyfikować klasę IndexModel .
  • Jeśli MyDependency ma zależności, muszą być również skonfigurowane przez klasę IndexModel . W dużym projekcie z wieloma klasami w zależności od MyDependencymetody kod konfiguracji staje się rozproszony w całej aplikacji.
  • Ta implementacja jest trudna do testowania jednostkowego.

Wstrzykiwanie zależności rozwiązuje te problemy za pomocą następujących elementów:

  • Użycie interfejsu lub klasy bazowej do abstrakcji implementacji zależności.
  • Rejestracja zależności w kontenerze usługi. ASP.NET Core udostępnia wbudowany kontener usługi. IServiceProvider Usługi są zwykle rejestrowane w pliku aplikacji Program.cs .
  • Iniekcja usługi do konstruktora klasy, w której jest używana. Struktura bierze na siebie odpowiedzialność za utworzenie wystąpienia zależności i usunięcie go, gdy nie jest już potrzebne.

W przykładowej aplikacjiIMyDependency interfejs definiuje metodęWriteMessage:

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

Ten interfejs jest implementowany przez konkretny typ: MyDependency

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

Przykładowa aplikacja rejestruje usługę IMyDependency przy użyciu konkretnego typu MyDependency. Metoda AddScoped rejestruje usługę z okresem istnienia o określonym zakresie, okres istnienia pojedynczego żądania. Okresy istnienia usługi zostały opisane w dalszej części tego tematu.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

W przykładowej aplikacji IMyDependency usługa jest żądana i używana do wywoływania WriteMessage metody:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

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

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

Korzystając ze wzorca DI, kontroler lub Razor strona:

  • Nie używa konkretnego typu MyDependency, tylko IMyDependency interfejs, który implementuje. Ułatwia to zmianę implementacji bez modyfikowania kontrolera lub Razor strony.
  • Nie tworzy wystąpienia klasy MyDependency, które jest tworzone przez kontener DI.

Implementację interfejsu IMyDependency można ulepszyć przy użyciu wbudowanego interfejsu API rejestrowania:

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

Zaktualizowana Program.cs wersja rejestruje nową IMyDependency implementację:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

MyDependency2ILogger<TCategoryName>zależy od elementu , który żąda w konstruktorze. ILogger<TCategoryName>jest usługą zapewnianą przez platformę.

Nie jest niczym niezwykłym, aby używać wstrzykiwania zależności w sposób łańcuchowy. Każda żądana zależność z kolei żąda własnych zależności. Kontener rozpoznaje zależności na grafie i zwraca w pełni rozwiązaną usługę. Zbiorczy zestaw zależności, które należy rozpoznać, jest zwykle określany jako drzewo zależności, graf zależności lub graf obiektu.

Kontener rozwiązuje problem ILogger<TCategoryName> dzięki wykorzystaniu (ogólnych) typów otwartych, eliminując konieczność rejestrowania każdego typu skonstruowanego (ogólnego).

W terminologii iniekcji zależności usługa:

  • Jest to zazwyczaj obiekt, który zapewnia usługę innym obiektom, takim jak IMyDependency usługa.
  • Nie jest powiązana z usługą internetową, chociaż usługa może używać usługi internetowej.

Platforma zapewnia niezawodny system rejestrowania . Implementacje IMyDependency pokazane w poprzednich przykładach zostały napisane, aby zademonstrować podstawowe di, a nie zaimplementować rejestrowania. Większość aplikacji nie powinna pisać rejestratorów. Poniższy kod demonstruje użycie domyślnego rejestrowania, które nie wymaga zarejestrowania żadnych usług:

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

Korzystając z powyższego kodu, nie ma potrzeby aktualizowania Program.cselementu , ponieważ rejestrowanie jest udostępniane przez platformę.

Rejestrowanie grup usług za pomocą metod rozszerzeń

Platforma ASP.NET Core używa konwencji do rejestrowania grupy powiązanych usług. Konwencja polega na użyciu jednej Add{GROUP_NAME} metody rozszerzenia do rejestrowania wszystkich usług wymaganych przez funkcję platformy. Na przykład AddControllers metoda rozszerzenia rejestruje usługi wymagane dla kontrolerów MVC.

Poniższy kod jest generowany przez Razor szablon Pages przy użyciu poszczególnych kont użytkowników i pokazuje, jak dodać dodatkowe usługi do kontenera przy użyciu metod AddDbContext rozszerzeń i 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();

Rozważ użycie następujących elementów, które rejestrują usługi i konfigurują opcje:

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

Powiązane grupy rejestracji można przenieść do metody rozszerzenia w celu zarejestrowania usług. Na przykład usługi konfiguracji są dodawane do następującej klasy:

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

Pozostałe usługi są rejestrowane w podobnej klasie. Poniższy kod używa nowych metod rozszerzenia do rejestrowania usług:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddRazorPages();

var app = builder.Build();

Uwaga: każda metoda rozszerzenia services.Add{GROUP_NAME} dodaje i potencjalnie konfiguruje usługi. Na przykład metoda AddControllersWithViews dodaje kontrolery MVC usług z wymaganymi widokami, a metoda AddRazorPages dodaje usługi wymagane przez usługę Razor Pages.

Okresy istnienia usługi

Zobacz Okresy istnienia usługi w iniekcji zależności na platformie .NET

Aby użyć usług o określonym zakresie w programie pośredniczącym, użyj jednej z następujących metod:

  • Wstrzykiwanie usługi do metody lub InvokeAsync oprogramowania pośredniczącegoInvoke. Użycie iniekcji konstruktora zgłasza wyjątek środowiska uruchomieniowego, ponieważ wymusza zachowanie usługi o określonym zakresie jak pojedyncza. W przykładzie w sekcji Opcje okresu istnienia i rejestracji przedstawiono InvokeAsync podejście.
  • Użyj oprogramowania pośredniczącego opartego na fabryce. Oprogramowanie pośredniczące zarejestrowane przy użyciu tego podejścia jest aktywowane na żądanie klienta (połączenie), które umożliwia wstrzyknięcie usług o określonym zakresie do konstruktora oprogramowania pośredniczącego.

Aby uzyskać więcej informacji, zobacz Pisanie niestandardowego oprogramowania pośredniczącego ASP.NET Core.

Metody rejestracji usługi

Zobacz Metody rejestracji usługi w iniekcji zależności na platformie .NET

Często używa się wielu implementacji podczas pozorowania typów do testowania.

Zarejestrowanie usługi tylko z typem implementacji jest równoważne zarejestrowaniu tej usługi z tą samą implementacją i typem usługi. Dlatego nie można zarejestrować wielu implementacji usługi przy użyciu metod, które nie przyjmują jawnego typu usługi. Te metody mogą rejestrować wiele wystąpień usługi, ale wszystkie te metody będą miały ten sam typ implementacji .

Każda z powyższych metod rejestracji usług może służyć do rejestrowania wielu wystąpień usługi tego samego typu usługi. W poniższym przykładzie AddSingleton jest wywoływana dwukrotnie z IMyDependency typem usługi. Drugie wywołanie AddSingleton przesłonięcia poprzedniego, gdy zostanie rozpoznane jako IMyDependency i dodaje do poprzedniego, gdy wiele usług jest rozpoznawanych za pośrednictwem metody IEnumerable<IMyDependency>. Usługi są wyświetlane w kolejności, w której zostały zarejestrowane po rozwiązaniu za pośrednictwem .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);
    }
}

Usługi kluczy

Usługi kluczy odnoszą się do mechanizmu rejestrowania i pobierania usług wstrzykiwania zależności (DI) przy użyciu kluczy. Usługa jest skojarzona z kluczem przez wywołanie AddKeyedSingleton metody (lub AddKeyedScopedAddKeyedTransient) w celu jej zarejestrowania. Uzyskaj dostęp do zarejestrowanej usługi, określając klucz za pomocą atrybutu [FromKeyedServices] . Poniższy kod pokazuje, jak używać usług kluczy:

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

Zachowanie iniekcji konstruktora

Zobacz Zachowanie iniekcji konstruktora w iniekcji zależności na platformie .NET

Konteksty programu Entity Framework

Domyślnie konteksty programu Entity Framework są dodawane do kontenera usługi przy użyciu okresu istnienia o określonym zakresie, ponieważ operacje bazy danych aplikacji internetowej są zwykle ograniczone do żądania klienta. Aby użyć innego okresu istnienia, określ okres istnienia przy użyciu AddDbContext przeciążenia. Usługi danego okresu istnienia nie powinny używać kontekstu bazy danych z okresem istnienia krótszym niż okres istnienia usługi.

Opcje okresu istnienia i rejestracji

Aby zademonstrować różnicę między okresami istnienia usługi a ich opcjami rejestracji, należy wziąć pod uwagę następujące interfejsy reprezentujące zadanie jako operację z identyfikatorem OperationId. W zależności od tego, jak okres istnienia usługi operacji jest skonfigurowany dla następujących interfejsów, kontener udostępnia te same lub różne wystąpienia usługi w przypadku żądania przez klasę:

public interface IOperation
{
    string OperationId { get; }
}

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

Poniższa Operation klasa implementuje wszystkie poprzednie interfejsy. Konstruktor Operation generuje identyfikator GUID i przechowuje ostatnie 4 znaki we OperationId właściwości :

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

    public string OperationId { get; }
}

Poniższy kod tworzy wiele rejestracji Operation klasy zgodnie z nazwanych okresów istnienia:

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

Przykładowa aplikacja demonstruje okresy istnienia obiektów zarówno wewnątrz, jak i między żądaniami. Oprogramowanie IndexModel pośredniczące i żądają każdego typu IOperation i rejestrują OperationId dla każdego typu:

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

Podobnie jak w przypadku IndexModelprogramu , oprogramowanie pośredniczące rozwiązuje te same usługi:

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

Usługi o określonym zakresie i przejściowym muszą zostać rozwiązane w metodzie 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);
}

Dane wyjściowe rejestratora pokazują:

  • Obiekty przejściowe są zawsze różne. Wartość przejściowa OperationId różni się w IndexModel i w oprogramowania pośredniczącego.
  • Obiekty o określonym zakresie są takie same dla danego żądania, ale różnią się w zależności od każdego nowego żądania.
  • Pojedyncze obiekty są takie same dla każdego żądania.

Aby zmniejszyć dane wyjściowe rejestrowania, w pliku ustaw wartość "Logging:LogLevel:Microsoft:Error" appsettings.Development.json :

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

Rozwiązywanie problemów z usługą podczas uruchamiania aplikacji

Poniższy kod pokazuje, jak rozwiązać problem z usługą o określonym zakresie przez ograniczony czas trwania podczas uruchamiania aplikacji:

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

Walidacja zakresu

Zobacz Zachowanie iniekcji konstruktora w iniekcji zależności na platformie .NET

Aby uzyskać więcej informacji, zobacz Walidacja zakresu.

Żądanie usług

Usługi i ich zależności w ramach żądania ASP.NET Core są udostępniane za pośrednictwem usługi HttpContext.RequestServices.

Platforma tworzy zakres na żądanie i RequestServices uwidacznia dostawcę usług o określonym zakresie. Wszystkie usługi o określonym zakresie są prawidłowe tak długo, jak żądanie jest aktywne.

Uwaga

Preferuj żądanie zależności jako parametrów konstruktora dla rozpoznawania usług z .RequestServices Żądanie zależności jako parametrów konstruktora daje klasy, które są łatwiejsze do przetestowania.

Usługi projektowe do wstrzykiwania zależności

Podczas projektowania usług do wstrzykiwania zależności:

  • Unikaj stanowych, statycznych klas i składowych. Unikaj tworzenia stanu globalnego, projektując aplikacje do korzystania z usług jednotonowych.
  • Unikaj bezpośredniego tworzenia wystąpień klas zależnych w ramach usług. Bezpośrednie tworzenie wystąpienia łączy kod z określoną implementacją.
  • Umożliwianie usługom małych, dobrze ocenianych i łatwych do testowania.

Jeśli klasa ma wiele wstrzykiwanych zależności, może to być znak, że klasa ma zbyt wiele obowiązków i narusza zasadę o pojedynczej odpowiedzialności (SRP). Spróbuj refaktoryzować klasę, przenosząc część swoich obowiązków do nowych klas. Należy pamiętać, że Razor klasy modeli stron stron i klasy kontrolerów MVC powinny skupić się na problemach interfejsu użytkownika.

Usuwanie usług

Kontener wywołuje Dispose typy, które IDisposable tworzy. Usługi rozwiązane z kontenera nigdy nie powinny być usuwane przez dewelopera. Jeśli typ lub fabryka jest rejestrowana jako pojedyncza, kontener automatycznie usuwa pojedynczyton.

W poniższym przykładzie usługi są tworzone przez kontener usługi i usuwane automatycznie: 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");
    }
}

Konsola debugowania wyświetla następujące dane wyjściowe po każdym odświeżeniu strony Indeks:

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

Usługi nieutworowane przez kontener usługi

Spójrzmy na poniższy kod:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

Powyższy kod:

  • Wystąpienia usługi nie są tworzone przez kontener usługi.
  • Platforma nie usuwa automatycznie usług.
  • Deweloper jest odpowiedzialny za rozpowszechnianie usług.

Wskazówki dotyczące interfejsu IDisposable dla wystąpień przejściowych i udostępnionych

Zobacz IDisposable guidance for Transient and shared instance in Dependency injection in .NET (Wskazówki dotyczące funkcji IDisposable dla przejściowego i współużytkowanego wystąpienia w iniekcji zależności na platformie .NET)

Domyślne zastąpienie kontenera usługi

Zobacz Domyślne zastępowanie kontenera usługi w iniekcji zależności na platformie .NET

Zalecenia

Zobacz Rekomendacje iniekcji zależności na platformie .NET

  • Unikaj używania wzorca lokalizatora usług. Na przykład nie należy wywoływać GetService w celu uzyskania wystąpienia usługi, gdy można użyć di:

    Odpowiedź nieprawidłowa:

    Incorrect code

    Odpowiedź prawidłowa:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Kolejna odmiana lokalizatora usług, aby uniknąć, to wstrzykiwanie fabryki, która rozwiązuje zależności w czasie wykonywania. Obie te praktyki mieszają inwersję strategii kontroli .

  • Unikaj dostępu statycznego do HttpContext (na przykład IHttpContextAccessor.HttpContext).

Di to alternatywa dla wzorców dostępu do obiektów statycznych/globalnych. Jeśli połączysz go z dostępem do obiektów statycznych, możesz nie być w stanie zrealizować korzyści z di.

Orchard Core to struktura aplikacji do tworzenia modułowych, wielodostępnych aplikacji na platformie ASP.NET Core. Aby uzyskać więcej informacji, zobacz dokumentację Sad Core.

Zapoznaj się z przykładami aplikacji Orchard Core, aby zapoznać się z przykładami tworzenia modułowych i wielodostępnych aplikacji przy użyciu platformy Sad Core Framework bez żadnych funkcji specyficznych dla programu CMS.

Usługi dostarczane przez platformę

Program.cs rejestruje usługi używane przez aplikację, w tym funkcje platformy, takie jak Entity Framework Core i ASP.NET Core MVC. Początkowo udostępniany Program.cs element IServiceCollection zawiera usługi zdefiniowane przez platformę w zależności od sposobu konfigurowania hosta. W przypadku aplikacji opartych na szablonach ASP.NET Core platforma rejestruje ponad 250 usług.

W poniższej tabeli wymieniono niewielką próbkę tych usług zarejestrowanych w strukturze:

Typ usługi Okres istnienia
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Przejściowy
IHostApplicationLifetime Pojedyncze
IWebHostEnvironment Pojedyncze
Microsoft.AspNetCore.Hosting.IStartup Pojedyncze
Microsoft.AspNetCore.Hosting.IStartupFilter Przejściowy
Microsoft.AspNetCore.Hosting.Server.IServer Pojedyncze
Microsoft.AspNetCore.Http.IHttpContextFactory Przejściowy
Microsoft.Extensions.Logging.ILogger<TCategoryName> Pojedyncze
Microsoft.Extensions.Logging.ILoggerFactory Pojedyncze
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Pojedyncze
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Przejściowy
Microsoft.Extensions.Options.IOptions<TOptions> Pojedyncze
System.Diagnostics.DiagnosticSource Pojedyncze
System.Diagnostics.DiagnosticListener Pojedyncze

Dodatkowe zasoby

Przez Kirk Larkin, Steve Smith i Brandon Dahler

ASP.NET Core obsługuje wzorzec projektowania oprogramowania iniekcji zależności (DI), który jest techniką osiągnięcia inwersji kontroli (IoC) między klasami i ich zależnościami.

Aby uzyskać więcej informacji specyficznych dla wstrzykiwania zależności w kontrolerach MVC, zobacz Wstrzykiwanie zależności do kontrolerów w ASP.NET Core.

Aby uzyskać informacje na temat używania wstrzykiwania zależności w aplikacjach innych niż aplikacje internetowe, zobacz Wstrzykiwanie zależności na platformie .NET.

Aby uzyskać więcej informacji na temat wstrzykiwania zależności opcji, zobacz Wzorzec opcji w ASP.NET Core.

Ten temat zawiera informacje na temat wstrzykiwania zależności w programie ASP.NET Core. Podstawowa dokumentacja dotycząca używania wstrzykiwania zależności znajduje się w iniekcji zależności na platformie .NET.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Omówienie wstrzykiwania zależności

Zależność to obiekt, od którego zależy inny obiekt. Zbadaj następującą MyDependency klasę przy użyciu metody, od których WriteMessage zależą inne klasy:

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

Klasa może utworzyć wystąpienie MyDependency klasy w celu użycia jej WriteMessage metody. W poniższym przykładzie MyDependency klasa jest zależnością IndexModel klasy:


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

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

Klasa tworzy klasę MyDependency i zależy bezpośrednio od klasy. Zależności kodu, takie jak w poprzednim przykładzie, są problematyczne i należy unikać z następujących powodów:

  • Aby zastąpić MyDependency inną implementacją, należy zmodyfikować klasę IndexModel .
  • Jeśli MyDependency ma zależności, muszą być również skonfigurowane przez klasę IndexModel . W dużym projekcie z wieloma klasami w zależności od MyDependencymetody kod konfiguracji staje się rozproszony w całej aplikacji.
  • Ta implementacja jest trudna do testowania jednostkowego.

Wstrzykiwanie zależności rozwiązuje te problemy za pomocą następujących elementów:

  • Użycie interfejsu lub klasy bazowej do abstrakcji implementacji zależności.
  • Rejestracja zależności w kontenerze usługi. ASP.NET Core udostępnia wbudowany kontener usługi. IServiceProvider Usługi są zwykle rejestrowane w pliku aplikacji Program.cs .
  • Iniekcja usługi do konstruktora klasy, w której jest używana. Struktura bierze na siebie odpowiedzialność za utworzenie wystąpienia zależności i usunięcie go, gdy nie jest już potrzebne.

W przykładowej aplikacjiIMyDependency interfejs definiuje metodęWriteMessage:

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

Ten interfejs jest implementowany przez konkretny typ: MyDependency

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

Przykładowa aplikacja rejestruje usługę IMyDependency przy użyciu konkretnego typu MyDependency. Metoda AddScoped rejestruje usługę z okresem istnienia o określonym zakresie, okres istnienia pojedynczego żądania. Okresy istnienia usługi zostały opisane w dalszej części tego tematu.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

W przykładowej aplikacji IMyDependency usługa jest żądana i używana do wywoływania WriteMessage metody:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

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

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

Korzystając ze wzorca DI, kontroler lub Razor strona:

  • Nie używa konkretnego typu MyDependency, tylko IMyDependency interfejs, który implementuje. Ułatwia to zmianę implementacji bez modyfikowania kontrolera lub Razor strony.
  • Nie tworzy wystąpienia klasy MyDependency, które jest tworzone przez kontener DI.

Implementację interfejsu IMyDependency można ulepszyć przy użyciu wbudowanego interfejsu API rejestrowania:

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

Zaktualizowana Program.cs wersja rejestruje nową IMyDependency implementację:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

MyDependency2ILogger<TCategoryName>zależy od elementu , który żąda w konstruktorze. ILogger<TCategoryName>jest usługą zapewnianą przez platformę.

Nie jest niczym niezwykłym, aby używać wstrzykiwania zależności w sposób łańcuchowy. Każda żądana zależność z kolei żąda własnych zależności. Kontener rozpoznaje zależności na grafie i zwraca w pełni rozwiązaną usługę. Zbiorczy zestaw zależności, które należy rozpoznać, jest zwykle określany jako drzewo zależności, graf zależności lub graf obiektu.

Kontener rozwiązuje problem ILogger<TCategoryName> dzięki wykorzystaniu (ogólnych) typów otwartych, eliminując konieczność rejestrowania każdego typu skonstruowanego (ogólnego).

W terminologii iniekcji zależności usługa:

  • Jest to zazwyczaj obiekt, który zapewnia usługę innym obiektom, takim jak IMyDependency usługa.
  • Nie jest powiązana z usługą internetową, chociaż usługa może używać usługi internetowej.

Platforma zapewnia niezawodny system rejestrowania . Implementacje IMyDependency pokazane w poprzednich przykładach zostały napisane, aby zademonstrować podstawowe di, a nie zaimplementować rejestrowania. Większość aplikacji nie powinna pisać rejestratorów. Poniższy kod demonstruje użycie domyślnego rejestrowania, które nie wymaga zarejestrowania żadnych usług:

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

Korzystając z powyższego kodu, nie ma potrzeby aktualizowania Program.cselementu , ponieważ rejestrowanie jest udostępniane przez platformę.

Rejestrowanie grup usług za pomocą metod rozszerzeń

Platforma ASP.NET Core używa konwencji do rejestrowania grupy powiązanych usług. Konwencja polega na użyciu jednej Add{GROUP_NAME} metody rozszerzenia do rejestrowania wszystkich usług wymaganych przez funkcję platformy. Na przykład AddControllers metoda rozszerzenia rejestruje usługi wymagane dla kontrolerów MVC.

Poniższy kod jest generowany przez Razor szablon Pages przy użyciu poszczególnych kont użytkowników i pokazuje, jak dodać dodatkowe usługi do kontenera przy użyciu metod AddDbContext rozszerzeń i 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();

Rozważ użycie następujących elementów, które rejestrują usługi i konfigurują opcje:

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

Powiązane grupy rejestracji można przenieść do metody rozszerzenia w celu zarejestrowania usług. Na przykład usługi konfiguracji są dodawane do następującej klasy:

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

Pozostałe usługi są rejestrowane w podobnej klasie. Poniższy kod używa nowych metod rozszerzenia do rejestrowania usług:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddRazorPages();

var app = builder.Build();

Uwaga: każda metoda rozszerzenia services.Add{GROUP_NAME} dodaje i potencjalnie konfiguruje usługi. Na przykład metoda AddControllersWithViews dodaje kontrolery MVC usług z wymaganymi widokami, a metoda AddRazorPages dodaje usługi wymagane przez usługę Razor Pages.

Okresy istnienia usługi

Zobacz Okresy istnienia usługi w iniekcji zależności na platformie .NET

Aby użyć usług o określonym zakresie w programie pośredniczącym, użyj jednej z następujących metod:

  • Wstrzykiwanie usługi do metody lub InvokeAsync oprogramowania pośredniczącegoInvoke. Użycie iniekcji konstruktora zgłasza wyjątek środowiska uruchomieniowego, ponieważ wymusza zachowanie usługi o określonym zakresie jak pojedyncza. W przykładzie w sekcji Opcje okresu istnienia i rejestracji przedstawiono InvokeAsync podejście.
  • Użyj oprogramowania pośredniczącego opartego na fabryce. Oprogramowanie pośredniczące zarejestrowane przy użyciu tego podejścia jest aktywowane na żądanie klienta (połączenie), które umożliwia wstrzyknięcie usług o określonym zakresie do konstruktora oprogramowania pośredniczącego.

Aby uzyskać więcej informacji, zobacz Pisanie niestandardowego oprogramowania pośredniczącego ASP.NET Core.

Metody rejestracji usługi

Zobacz Metody rejestracji usługi w iniekcji zależności na platformie .NET

Często używa się wielu implementacji podczas pozorowania typów do testowania.

Zarejestrowanie usługi tylko z typem implementacji jest równoważne zarejestrowaniu tej usługi z tą samą implementacją i typem usługi. Dlatego nie można zarejestrować wielu implementacji usługi przy użyciu metod, które nie przyjmują jawnego typu usługi. Te metody mogą rejestrować wiele wystąpień usługi, ale wszystkie te metody będą miały ten sam typ implementacji .

Każda z powyższych metod rejestracji usług może służyć do rejestrowania wielu wystąpień usługi tego samego typu usługi. W poniższym przykładzie AddSingleton jest wywoływana dwukrotnie z IMyDependency typem usługi. Drugie wywołanie AddSingleton przesłonięcia poprzedniego, gdy zostanie rozpoznane jako IMyDependency i dodaje do poprzedniego, gdy wiele usług jest rozpoznawanych za pośrednictwem metody IEnumerable<IMyDependency>. Usługi są wyświetlane w kolejności, w której zostały zarejestrowane po rozwiązaniu za pośrednictwem .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);
    }
}

Zachowanie iniekcji konstruktora

Zobacz Zachowanie iniekcji konstruktora w iniekcji zależności na platformie .NET

Konteksty programu Entity Framework

Domyślnie konteksty programu Entity Framework są dodawane do kontenera usługi przy użyciu okresu istnienia o określonym zakresie, ponieważ operacje bazy danych aplikacji internetowej są zwykle ograniczone do żądania klienta. Aby użyć innego okresu istnienia, określ okres istnienia przy użyciu AddDbContext przeciążenia. Usługi danego okresu istnienia nie powinny używać kontekstu bazy danych z okresem istnienia krótszym niż okres istnienia usługi.

Opcje okresu istnienia i rejestracji

Aby zademonstrować różnicę między okresami istnienia usługi a ich opcjami rejestracji, należy wziąć pod uwagę następujące interfejsy reprezentujące zadanie jako operację z identyfikatorem OperationId. W zależności od tego, jak okres istnienia usługi operacji jest skonfigurowany dla następujących interfejsów, kontener udostępnia te same lub różne wystąpienia usługi w przypadku żądania przez klasę:

public interface IOperation
{
    string OperationId { get; }
}

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

Poniższa Operation klasa implementuje wszystkie poprzednie interfejsy. Konstruktor Operation generuje identyfikator GUID i przechowuje ostatnie 4 znaki we OperationId właściwości :

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

    public string OperationId { get; }
}

Poniższy kod tworzy wiele rejestracji Operation klasy zgodnie z nazwanych okresów istnienia:

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

Przykładowa aplikacja demonstruje okresy istnienia obiektów zarówno wewnątrz, jak i między żądaniami. Oprogramowanie IndexModel pośredniczące i żądają każdego typu IOperation i rejestrują OperationId dla każdego typu:

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

Podobnie jak w przypadku IndexModelprogramu , oprogramowanie pośredniczące rozwiązuje te same usługi:

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

Usługi o określonym zakresie i przejściowym muszą zostać rozwiązane w metodzie 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);
}

Dane wyjściowe rejestratora pokazują:

  • Obiekty przejściowe są zawsze różne. Wartość przejściowa OperationId różni się w IndexModel i w oprogramowania pośredniczącego.
  • Obiekty o określonym zakresie są takie same dla danego żądania, ale różnią się w zależności od każdego nowego żądania.
  • Pojedyncze obiekty są takie same dla każdego żądania.

Aby zmniejszyć dane wyjściowe rejestrowania, w pliku ustaw wartość "Logging:LogLevel:Microsoft:Error" appsettings.Development.json :

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

Rozwiązywanie problemów z usługą podczas uruchamiania aplikacji

Poniższy kod pokazuje, jak rozwiązać problem z usługą o określonym zakresie przez ograniczony czas trwania podczas uruchamiania aplikacji:

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

Walidacja zakresu

Zobacz Zachowanie iniekcji konstruktora w iniekcji zależności na platformie .NET

Aby uzyskać więcej informacji, zobacz Walidacja zakresu.

Żądanie usług

Usługi i ich zależności w ramach żądania ASP.NET Core są udostępniane za pośrednictwem usługi HttpContext.RequestServices.

Platforma tworzy zakres na żądanie i RequestServices uwidacznia dostawcę usług o określonym zakresie. Wszystkie usługi o określonym zakresie są prawidłowe tak długo, jak żądanie jest aktywne.

Uwaga

Preferuj żądanie zależności jako parametrów konstruktora dla rozpoznawania usług z .RequestServices Żądanie zależności jako parametrów konstruktora daje klasy, które są łatwiejsze do przetestowania.

Usługi projektowe do wstrzykiwania zależności

Podczas projektowania usług do wstrzykiwania zależności:

  • Unikaj stanowych, statycznych klas i składowych. Unikaj tworzenia stanu globalnego, projektując aplikacje do korzystania z usług jednotonowych.
  • Unikaj bezpośredniego tworzenia wystąpień klas zależnych w ramach usług. Bezpośrednie tworzenie wystąpienia łączy kod z określoną implementacją.
  • Umożliwianie usługom małych, dobrze ocenianych i łatwych do testowania.

Jeśli klasa ma wiele wstrzykiwanych zależności, może to być znak, że klasa ma zbyt wiele obowiązków i narusza zasadę o pojedynczej odpowiedzialności (SRP). Spróbuj refaktoryzować klasę, przenosząc część swoich obowiązków do nowych klas. Należy pamiętać, że Razor klasy modeli stron stron i klasy kontrolerów MVC powinny skupić się na problemach interfejsu użytkownika.

Usuwanie usług

Kontener wywołuje Dispose typy, które IDisposable tworzy. Usługi rozwiązane z kontenera nigdy nie powinny być usuwane przez dewelopera. Jeśli typ lub fabryka jest rejestrowana jako pojedyncza, kontener automatycznie usuwa pojedynczyton.

W poniższym przykładzie usługi są tworzone przez kontener usługi i usuwane automatycznie: 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");
    }
}

Konsola debugowania wyświetla następujące dane wyjściowe po każdym odświeżeniu strony Indeks:

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

Usługi nieutworowane przez kontener usługi

Spójrzmy na poniższy kod:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

Powyższy kod:

  • Wystąpienia usługi nie są tworzone przez kontener usługi.
  • Platforma nie usuwa automatycznie usług.
  • Deweloper jest odpowiedzialny za rozpowszechnianie usług.

Wskazówki dotyczące interfejsu IDisposable dla wystąpień przejściowych i udostępnionych

Zobacz IDisposable guidance for Transient and shared instance in Dependency injection in .NET (Wskazówki dotyczące funkcji IDisposable dla przejściowego i współużytkowanego wystąpienia w iniekcji zależności na platformie .NET)

Domyślne zastąpienie kontenera usługi

Zobacz Domyślne zastępowanie kontenera usługi w iniekcji zależności na platformie .NET

Zalecenia

Zobacz Rekomendacje iniekcji zależności na platformie .NET

  • Unikaj używania wzorca lokalizatora usług. Na przykład nie należy wywoływać GetService w celu uzyskania wystąpienia usługi, gdy można użyć di:

    Odpowiedź nieprawidłowa:

    Incorrect code

    Odpowiedź prawidłowa:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Kolejna odmiana lokalizatora usług, aby uniknąć, to wstrzykiwanie fabryki, która rozwiązuje zależności w czasie wykonywania. Obie te praktyki mieszają inwersję strategii kontroli .

  • Unikaj dostępu statycznego do HttpContext (na przykład IHttpContextAccessor.HttpContext).

Di to alternatywa dla wzorców dostępu do obiektów statycznych/globalnych. Jeśli połączysz go z dostępem do obiektów statycznych, możesz nie być w stanie zrealizować korzyści z di.

Orchard Core to struktura aplikacji do tworzenia modułowych, wielodostępnych aplikacji na platformie ASP.NET Core. Aby uzyskać więcej informacji, zobacz dokumentację Sad Core.

Zapoznaj się z przykładami aplikacji Orchard Core, aby zapoznać się z przykładami tworzenia modułowych i wielodostępnych aplikacji przy użyciu platformy Sad Core Framework bez żadnych funkcji specyficznych dla programu CMS.

Usługi dostarczane przez platformę

Program.cs rejestruje usługi używane przez aplikację, w tym funkcje platformy, takie jak Entity Framework Core i ASP.NET Core MVC. Początkowo udostępniany Program.cs element IServiceCollection zawiera usługi zdefiniowane przez platformę w zależności od sposobu konfigurowania hosta. W przypadku aplikacji opartych na szablonach ASP.NET Core platforma rejestruje ponad 250 usług.

W poniższej tabeli wymieniono niewielką próbkę tych usług zarejestrowanych w strukturze:

Typ usługi Okres istnienia
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Przejściowy
IHostApplicationLifetime Pojedyncze
IWebHostEnvironment Pojedyncze
Microsoft.AspNetCore.Hosting.IStartup Pojedyncze
Microsoft.AspNetCore.Hosting.IStartupFilter Przejściowy
Microsoft.AspNetCore.Hosting.Server.IServer Pojedyncze
Microsoft.AspNetCore.Http.IHttpContextFactory Przejściowy
Microsoft.Extensions.Logging.ILogger<TCategoryName> Pojedyncze
Microsoft.Extensions.Logging.ILoggerFactory Pojedyncze
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Pojedyncze
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Przejściowy
Microsoft.Extensions.Options.IOptions<TOptions> Pojedyncze
System.Diagnostics.DiagnosticSource Pojedyncze
System.Diagnostics.DiagnosticListener Pojedyncze

Dodatkowe zasoby

Autor : Kirk Larkin, Steve Smith, Scott Addie i Brandon Dahler

ASP.NET Core obsługuje wzorzec projektowania oprogramowania iniekcji zależności (DI), który jest techniką osiągnięcia inwersji kontroli (IoC) między klasami i ich zależnościami.

Aby uzyskać więcej informacji specyficznych dla wstrzykiwania zależności w kontrolerach MVC, zobacz Wstrzykiwanie zależności do kontrolerów w ASP.NET Core.

Aby uzyskać informacje na temat używania wstrzykiwania zależności w aplikacjach innych niż aplikacje internetowe, zobacz Wstrzykiwanie zależności na platformie .NET.

Aby uzyskać więcej informacji na temat wstrzykiwania zależności opcji, zobacz Wzorzec opcji w ASP.NET Core.

Ten temat zawiera informacje na temat wstrzykiwania zależności w programie ASP.NET Core. Podstawowa dokumentacja dotycząca używania wstrzykiwania zależności znajduje się w iniekcji zależności na platformie .NET.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Omówienie wstrzykiwania zależności

Zależność to obiekt, od którego zależy inny obiekt. Zbadaj następującą MyDependency klasę przy użyciu metody, od których WriteMessage zależą inne klasy:

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

Klasa może utworzyć wystąpienie MyDependency klasy w celu użycia jej WriteMessage metody. W poniższym przykładzie MyDependency klasa jest zależnością IndexModel klasy:

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

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

Klasa tworzy klasę MyDependency i zależy bezpośrednio od klasy. Zależności kodu, takie jak w poprzednim przykładzie, są problematyczne i należy unikać z następujących powodów:

  • Aby zastąpić MyDependency inną implementacją, należy zmodyfikować klasę IndexModel .
  • Jeśli MyDependency ma zależności, muszą być również skonfigurowane przez klasę IndexModel . W dużym projekcie z wieloma klasami w zależności od MyDependencymetody kod konfiguracji staje się rozproszony w całej aplikacji.
  • Ta implementacja jest trudna do testowania jednostkowego. Aplikacja powinna używać makiety lub klasy wycinkowej MyDependency , która nie jest możliwa w przypadku tego podejścia.

Wstrzykiwanie zależności rozwiązuje te problemy za pomocą następujących elementów:

  • Użycie interfejsu lub klasy bazowej do abstrakcji implementacji zależności.
  • Rejestracja zależności w kontenerze usługi. ASP.NET Core udostępnia wbudowany kontener usługi. IServiceProvider Usługi są zwykle rejestrowane w metodzie aplikacji Startup.ConfigureServices .
  • Iniekcja usługi do konstruktora klasy, w której jest używana. Struktura bierze na siebie odpowiedzialność za utworzenie wystąpienia zależności i usunięcie go, gdy nie jest już potrzebne.

W przykładowej aplikacjiIMyDependency interfejs definiuje metodęWriteMessage:

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

Ten interfejs jest implementowany przez konkretny typ: MyDependency

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

Przykładowa aplikacja rejestruje usługę IMyDependency przy użyciu konkretnego typu MyDependency. Metoda AddScoped rejestruje usługę z okresem istnienia o określonym zakresie, okres istnienia pojedynczego żądania. Okresy istnienia usługi zostały opisane w dalszej części tego tematu.

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

    services.AddRazorPages();
}

W przykładowej aplikacji IMyDependency usługa jest żądana i używana do wywoływania WriteMessage metody:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

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

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

Korzystając ze wzorca di, kontroler:

  • Nie używa konkretnego typu MyDependency, tylko IMyDependency interfejs, który implementuje. Ułatwia to zmianę implementacji używanej przez kontroler bez modyfikowania kontrolera.
  • Nie tworzy wystąpienia klasy MyDependency, które jest tworzone przez kontener DI.

Implementację interfejsu IMyDependency można ulepszyć przy użyciu wbudowanego interfejsu API rejestrowania:

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

Zaktualizowana ConfigureServices metoda rejestruje nową IMyDependency implementację:

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

    services.AddRazorPages();
}

MyDependency2ILogger<TCategoryName>zależy od elementu , który żąda w konstruktorze. ILogger<TCategoryName>jest usługą zapewnianą przez platformę.

Nie jest niczym niezwykłym, aby używać wstrzykiwania zależności w sposób łańcuchowy. Każda żądana zależność z kolei żąda własnych zależności. Kontener rozpoznaje zależności na grafie i zwraca w pełni rozwiązaną usługę. Zbiorczy zestaw zależności, które należy rozpoznać, jest zwykle określany jako drzewo zależności, graf zależności lub graf obiektu.

Kontener rozwiązuje problem ILogger<TCategoryName> dzięki wykorzystaniu (ogólnych) typów otwartych, eliminując konieczność rejestrowania każdego typu skonstruowanego (ogólnego).

W terminologii iniekcji zależności usługa:

  • Jest to zazwyczaj obiekt, który zapewnia usługę innym obiektom, takim jak IMyDependency usługa.
  • Nie jest powiązana z usługą internetową, chociaż usługa może używać usługi internetowej.

Platforma zapewnia niezawodny system rejestrowania . Implementacje IMyDependency pokazane w poprzednich przykładach zostały napisane, aby zademonstrować podstawowe di, a nie zaimplementować rejestrowania. Większość aplikacji nie powinna pisać rejestratorów. Poniższy kod demonstruje użycie domyślnego rejestrowania, które nie wymaga zarejestrowania żadnych usług w programie 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);
    }
}

Korzystając z powyższego kodu, nie ma potrzeby aktualizowania ConfigureServiceselementu , ponieważ rejestrowanie jest udostępniane przez platformę.

Usługi wprowadzone do uruchamiania

Usługi można wstrzykiwać do konstruktora Startup i Startup.Configure metody.

Tylko następujące usługi można wstrzyknąć do konstruktora Startup podczas korzystania z hosta ogólnego (IHostBuilder):

Do metody można wprowadzić dowolną usługę zarejestrowaną w kontenerze Startup.Configure DI:

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

Aby uzyskać więcej informacji, zobacz Uruchamianie aplikacji w ASP.NET Core i Konfiguracja dostępu w uruchamianiu.

Rejestrowanie grup usług za pomocą metod rozszerzeń

Platforma ASP.NET Core używa konwencji do rejestrowania grupy powiązanych usług. Konwencja polega na użyciu jednej Add{GROUP_NAME} metody rozszerzenia do rejestrowania wszystkich usług wymaganych przez funkcję platformy. Na przykład AddControllers metoda rozszerzenia rejestruje usługi wymagane dla kontrolerów MVC.

Poniższy kod jest generowany przez Razor szablon Pages przy użyciu poszczególnych kont użytkowników i pokazuje, jak dodać dodatkowe usługi do kontenera przy użyciu metod AddDbContext rozszerzeń i 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();
}

Rozważ użycie następującej metody ConfigureServices, która rejestruje usługi i konfiguruje opcje:

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

Powiązane grupy rejestracji można przenieść do metody rozszerzenia w celu zarejestrowania usług. Na przykład usługi konfiguracji są dodawane do następującej klasy:

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

Pozostałe usługi są rejestrowane w podobnej klasie. Poniższa metoda ConfigureServices używa nowych metod rozszerzenia do rejestrowania usług:

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

    services.AddRazorPages();
}

Uwaga: każda metoda rozszerzenia services.Add{GROUP_NAME} dodaje i potencjalnie konfiguruje usługi. Na przykład metoda AddControllersWithViews dodaje kontrolery MVC usług z wymaganymi widokami, a metoda AddRazorPages dodaje usługi wymagane przez usługę Razor Pages. Zalecamy, aby w przypadku aplikacji stosować konwencję nazewnictwa podczas tworzenia metod rozszerzenia w przestrzeni nazw Microsoft.Extensions.DependencyInjection. Tworzenie metod rozszerzenia w przestrzeni nazw Microsoft.Extensions.DependencyInjection:

  • Hermetyzuje grupy rejestracji usług.
  • Zapewnia wygodny dostęp do usługi za pomocą funkcji IntelliSense.

Okresy istnienia usługi

Zobacz Okresy istnienia usługi w iniekcji zależności na platformie .NET

Aby użyć usług o określonym zakresie w programie pośredniczącym, użyj jednej z następujących metod:

  • Wstrzykiwanie usługi do metody lub InvokeAsync oprogramowania pośredniczącegoInvoke. Użycie iniekcji konstruktora zgłasza wyjątek środowiska uruchomieniowego, ponieważ wymusza zachowanie usługi o określonym zakresie jak pojedyncza. W przykładzie w sekcji Opcje okresu istnienia i rejestracji przedstawiono InvokeAsync podejście.
  • Użyj oprogramowania pośredniczącego opartego na fabryce. Oprogramowanie pośredniczące zarejestrowane przy użyciu tego podejścia jest aktywowane na żądanie klienta (połączenie), co umożliwia wstrzyknięcie usług o określonym zakresie do metody oprogramowania pośredniczącego InvokeAsync .

Aby uzyskać więcej informacji, zobacz Pisanie niestandardowego oprogramowania pośredniczącego ASP.NET Core.

Metody rejestracji usługi

Zobacz Metody rejestracji usługi w iniekcji zależności na platformie .NET

Często używa się wielu implementacji podczas pozorowania typów do testowania.

Zarejestrowanie usługi tylko z typem implementacji jest równoważne zarejestrowaniu tej usługi z tą samą implementacją i typem usługi. Dlatego nie można zarejestrować wielu implementacji usługi przy użyciu metod, które nie przyjmują jawnego typu usługi. Te metody mogą rejestrować wiele wystąpień usługi, ale wszystkie te metody będą miały ten sam typ implementacji .

Każda z powyższych metod rejestracji usług może służyć do rejestrowania wielu wystąpień usługi tego samego typu usługi. W poniższym przykładzie AddSingleton jest wywoływana dwukrotnie z IMyDependency typem usługi. Drugie wywołanie AddSingleton przesłonięcia poprzedniego, gdy zostanie rozpoznane jako IMyDependency i dodaje do poprzedniego, gdy wiele usług jest rozpoznawanych za pośrednictwem metody IEnumerable<IMyDependency>. Usługi są wyświetlane w kolejności, w której zostały zarejestrowane po rozwiązaniu za pośrednictwem .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);
    }
}

Zachowanie iniekcji konstruktora

Zobacz Zachowanie iniekcji konstruktora w iniekcji zależności na platformie .NET

Konteksty programu Entity Framework

Domyślnie konteksty programu Entity Framework są dodawane do kontenera usługi przy użyciu okresu istnienia o określonym zakresie, ponieważ operacje bazy danych aplikacji internetowej są zwykle ograniczone do żądania klienta. Aby użyć innego okresu istnienia, określ okres istnienia przy użyciu AddDbContext przeciążenia. Usługi danego okresu istnienia nie powinny używać kontekstu bazy danych z okresem istnienia krótszym niż okres istnienia usługi.

Opcje okresu istnienia i rejestracji

Aby zademonstrować różnicę między okresami istnienia usługi a ich opcjami rejestracji, należy wziąć pod uwagę następujące interfejsy reprezentujące zadanie jako operację z identyfikatorem OperationId. W zależności od tego, jak okres istnienia usługi operacji jest skonfigurowany dla następujących interfejsów, kontener udostępnia te same lub różne wystąpienia usługi w przypadku żądania przez klasę:

public interface IOperation
{
    string OperationId { get; }
}

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

Poniższa Operation klasa implementuje wszystkie poprzednie interfejsy. Konstruktor Operation generuje identyfikator GUID i przechowuje ostatnie 4 znaki we OperationId właściwości :

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

    public string OperationId { get; }
}

Metoda Startup.ConfigureServices tworzy wiele rejestracji Operation klasy zgodnie z nazwanych okresów istnienia:

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

    services.AddRazorPages();
}

Przykładowa aplikacja demonstruje okresy istnienia obiektów zarówno wewnątrz, jak i między żądaniami. Oprogramowanie IndexModel pośredniczące i żądają każdego typu IOperation i rejestrują OperationId dla każdego typu:

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

Podobnie jak w przypadku IndexModelprogramu , oprogramowanie pośredniczące rozwiązuje te same usługi:

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

Usługi o określonym zakresie muszą zostać rozwiązane w metodzie 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);
}

Dane wyjściowe rejestratora pokazują:

  • Obiekty przejściowe są zawsze różne. Wartość przejściowa OperationId różni się w IndexModel i w oprogramowania pośredniczącego.
  • Obiekty o określonym zakresie są takie same dla danego żądania, ale różnią się w zależności od każdego nowego żądania.
  • Pojedyncze obiekty są takie same dla każdego żądania.

Aby zmniejszyć dane wyjściowe rejestrowania, w pliku ustaw wartość "Logging:LogLevel:Microsoft:Error" appsettings.Development.json :

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

Call services from main (Wywołanie usług z sieci głównej)

Utwórz element IServiceScope z elementem IServiceScopeFactory.CreateScope , aby rozpoznać usługę o określonym zakresie w zakresie aplikacji. Takie podejście jest przydatne do uzyskiwania dostępu do usługi o określonym zakresie podczas uruchamiania w celu uruchamiania zadań inicjowania.

W poniższym przykładzie pokazano, jak uzyskać dostęp do usługi o IMyDependency określonym zakresie i wywołać jej WriteMessage metodę w pliku 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>();
            });
}

Walidacja zakresu

Zobacz Zachowanie iniekcji konstruktora w iniekcji zależności na platformie .NET

Aby uzyskać więcej informacji, zobacz Walidacja zakresu.

Żądanie usług

Usługi i ich zależności w ramach żądania ASP.NET Core są udostępniane za pośrednictwem usługi HttpContext.RequestServices.

Platforma tworzy zakres na żądanie i RequestServices uwidacznia dostawcę usług o określonym zakresie. Wszystkie usługi o określonym zakresie są prawidłowe tak długo, jak żądanie jest aktywne.

Uwaga

Preferuj żądanie zależności jako parametrów konstruktora dla rozpoznawania usług z .RequestServices Żądanie zależności jako parametrów konstruktora daje klasy, które są łatwiejsze do przetestowania.

Usługi projektowe do wstrzykiwania zależności

Podczas projektowania usług do wstrzykiwania zależności:

  • Unikaj stanowych, statycznych klas i składowych. Unikaj tworzenia stanu globalnego, projektując aplikacje do korzystania z usług jednotonowych.
  • Unikaj bezpośredniego tworzenia wystąpień klas zależnych w ramach usług. Bezpośrednie tworzenie wystąpienia łączy kod z określoną implementacją.
  • Umożliwianie usługom małych, dobrze ocenianych i łatwych do testowania.

Jeśli klasa ma wiele wstrzykiwanych zależności, może to być znak, że klasa ma zbyt wiele obowiązków i narusza zasadę o pojedynczej odpowiedzialności (SRP). Spróbuj refaktoryzować klasę, przenosząc część swoich obowiązków do nowych klas. Należy pamiętać, że Razor klasy modeli stron stron i klasy kontrolerów MVC powinny skupić się na problemach interfejsu użytkownika.

Usuwanie usług

Kontener wywołuje Dispose typy, które IDisposable tworzy. Usługi rozwiązane z kontenera nigdy nie powinny być usuwane przez dewelopera. Jeśli typ lub fabryka jest rejestrowana jako pojedyncza, kontener automatycznie usuwa pojedynczyton.

W poniższym przykładzie usługi są tworzone przez kontener usługi i usuwane automatycznie:

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

Konsola debugowania wyświetla następujące dane wyjściowe po każdym odświeżeniu strony Indeks:

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

Usługi nieutworowane przez kontener usługi

Spójrzmy na poniższy kod:

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

    services.AddRazorPages();
}

Powyższy kod:

  • Wystąpienia usługi nie są tworzone przez kontener usługi.
  • Platforma nie usuwa automatycznie usług.
  • Deweloper jest odpowiedzialny za rozpowszechnianie usług.

Wskazówki dotyczące interfejsu IDisposable dla wystąpień przejściowych i udostępnionych

Zobacz IDisposable guidance for Transient and shared instance in Dependency injection in .NET (Wskazówki dotyczące funkcji IDisposable dla przejściowego i współużytkowanego wystąpienia w iniekcji zależności na platformie .NET)

Domyślne zastąpienie kontenera usługi

Zobacz Domyślne zastępowanie kontenera usługi w iniekcji zależności na platformie .NET

Zalecenia

Zobacz Rekomendacje iniekcji zależności na platformie .NET

  • Unikaj używania wzorca lokalizatora usług. Na przykład nie należy wywoływać GetService w celu uzyskania wystąpienia usługi, gdy można użyć di:

    Odpowiedź nieprawidłowa:

    Incorrect code

    Odpowiedź prawidłowa:

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Kolejna odmiana lokalizatora usług, aby uniknąć, to wstrzykiwanie fabryki, która rozwiązuje zależności w czasie wykonywania. Obie te praktyki mieszają inwersję strategii kontroli .

  • Unikaj dostępu statycznego do HttpContext (na przykład IHttpContextAccessor.HttpContext).

  • Unikaj wywołań w BuildServiceProvider pliku ConfigureServices. Wywołanie BuildServiceProvider zwykle występuje, gdy deweloper chce rozwiązać problem z usługą w programie ConfigureServices. Rozważmy na przykład przypadek, w którym element LoginPath jest ładowany z konfiguracji. Unikaj następującego podejścia:

    bad code calling BuildServiceProvider

    Na poprzedniej ilustracji wybranie zielonej linii falistej poniżej services.BuildServiceProvider pokazuje następujące ostrzeżenie ASP0000:

    ASP0000 wywoływanie elementu "BuildServiceProvider" z kodu aplikacji powoduje utworzenie dodatkowej kopii usług jednotonowych. Rozważ alternatywy, takie jak wstrzykiwanie usług zależności jako parametry do "Konfiguruj".

    Wywołanie BuildServiceProvider tworzy drugi kontener, który może tworzyć rozdarte pojedynczetony i powodować odwołania do grafów obiektów w wielu kontenerach.

    Prawidłowym sposobem uzyskania LoginPath jest użycie wbudowanej obsługi wzorca opcji dla di:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();
    
        services.AddOptions<CookieAuthenticationOptions>(
                            CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IMyService>((options, myService) =>
            {
                options.LoginPath = myService.GetLoginPath();
            });
    
        services.AddRazorPages();
    }
    
  • Jednorazowe usługi przejściowe są przechwytywane przez pojemnik do dyspozycji. Może to przekształcić się w przeciek pamięci, jeśli zostanie rozwiązany z kontenera najwyższego poziomu.

  • Włącz walidację zakresu, aby upewnić się, że aplikacja nie ma pojedynczych dysków, które przechwytują usługi o określonym zakresie. Aby uzyskać więcej informacji, zobacz Walidacja zakresu.

Podobnie jak w przypadku wszystkich zestawów zaleceń, mogą wystąpić sytuacje, w których wymagane jest ignorowanie rekomendacji. Wyjątki są rzadkie, głównie specjalne przypadki w ramach samej struktury.

Di to alternatywa dla wzorców dostępu do obiektów statycznych/globalnych. Jeśli połączysz go z dostępem do obiektów statycznych, możesz nie być w stanie zrealizować korzyści z di.

Orchard Core to struktura aplikacji do tworzenia modułowych, wielodostępnych aplikacji na platformie ASP.NET Core. Aby uzyskać więcej informacji, zobacz dokumentację Sad Core.

Zapoznaj się z przykładami aplikacji Orchard Core, aby zapoznać się z przykładami tworzenia modułowych i wielodostępnych aplikacji przy użyciu platformy Sad Core Framework bez żadnych funkcji specyficznych dla programu CMS.

Usługi dostarczane przez platformę

Metoda Startup.ConfigureServices rejestruje usługi używane przez aplikację, w tym funkcje platformy, takie jak Entity Framework Core i ASP.NET Core MVC. Początkowo udostępniany ConfigureServices element IServiceCollection zawiera usługi zdefiniowane przez platformę w zależności od sposobu konfigurowania hosta. W przypadku aplikacji opartych na szablonach ASP.NET Core platforma rejestruje ponad 250 usług.

W poniższej tabeli wymieniono niewielką próbkę tych usług zarejestrowanych w strukturze:

Typ usługi Okres istnienia
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Przejściowy
IHostApplicationLifetime Pojedyncze
IWebHostEnvironment Pojedyncze
Microsoft.AspNetCore.Hosting.IStartup Pojedyncze
Microsoft.AspNetCore.Hosting.IStartupFilter Przejściowy
Microsoft.AspNetCore.Hosting.Server.IServer Pojedyncze
Microsoft.AspNetCore.Http.IHttpContextFactory Przejściowy
Microsoft.Extensions.Logging.ILogger<TCategoryName> Pojedyncze
Microsoft.Extensions.Logging.ILoggerFactory Pojedyncze
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Pojedyncze
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Przejściowy
Microsoft.Extensions.Options.IOptions<TOptions> Pojedyncze
System.Diagnostics.DiagnosticSource Pojedyncze
System.Diagnostics.DiagnosticListener Pojedyncze

Dodatkowe zasoby