.NET Core 中的相依性插入

Kirk LarkinSteve SmithBrandon Dahler

ASP.NET Core 支援相依性插入 (DI) 軟體設計模式,這是在類別及其相依性之間達成控制反轉 (IoC) 的技術。

如需 MVC 控制器內相依性插入的特定詳細資訊,請參閱ASP.NET Core 中的控制項插入至控制器

如需在 web 應用程式以外的應用程式中使用相依性插入的相關資訊,請參閱 .net 中的相依性插入。

如需有關選項之相依性插入的詳細資訊,請參閱ASP.NET Core 中的選項模式

本主題提供 ASP.NET Core 中的相依性插入的相關資訊。 使用相依性插入的主要檔包含在 .NET 的相依性 插入中。

查看或下載範例程式碼 (如何下載)

相依性插入概觀

相依 性是另 一個物件所依存的物件。 MyDependency使用其他類別所依存的方法檢查下列類別 WriteMessage

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

類別可以建立類別的實例 MyDependency ,以使用其 WriteMessage 方法。 在下列範例中, MyDependency 類別是類別的相依性 IndexModel


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

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

類別會建立和直接相依于 MyDependency 類別。 程式碼相依性(如上述範例所示)有問題,而且應該避免因下列原因:

  • 若要以 MyDependency 不同的實作為取代, IndexModel 必須修改該類別。
  • 如果 MyDependency 有相依性,則也必須由類別設定 IndexModel 。 在具有多個相依於 MyDependency 之多個類別的大型專案中,設定程式碼在不同的應用程式之間會變得鬆散。
  • 此實作難以進行單元測試。

相依性插入可透過下列方式解決這些問題:

  • 使用介面或基底類別來將相依性資訊抽象化。
  • 在服務容器中註冊相依性。 ASP.NET Core 提供內建服務容器 IServiceProvider。 服務通常會在應用 程式的 .cs 檔案中註冊。
  • 將服務「插入」到服務使用位置之類別的建構函式。 架構會負責建立相依性的執行個體,並在不再需要時將它捨棄。

範例應用程式中, 介面會定義 WriteMessage 方法:

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

這個介面是由具象型別 MyDependency 所實作:

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

範例應用程式會 IMyDependency 使用具象類型來註冊服務 MyDependencyAddScoped方法會使用範圍存留期(單一要求的存留期)來註冊服務。 將在此主題稍後將說明服務存留期

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

在範例應用程式中, IMyDependency 會要求服務並用來呼叫 WriteMessage 方法:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

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

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

使用 DI 模式,即控制器或 Razor 頁面:

  • 不使用具象型別 MyDependency ,只使用它所執行的 IMyDependency 介面。 這可讓您輕鬆地變更執行,而不需要修改控制器或 Razor 頁面。
  • 不會建立的實例 MyDependency ,而是由 DI 容器所建立。

IMyDependency 可以使用內建的記錄 API 來改善介面的實作為:

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

更新的 程式。 cs 會註冊新的 實作為:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

MyDependency2 相依于 ILogger<TCategoryName> 它在函式中要求的。 ILogger<TCategoryName>ILogger<TCategoryName>

以鏈結方式使用相依性插入並非不尋常。 每個要求的相依性接著會要求其自己的相依性。 容器會解決圖形中的相依性,並傳回完全解析的服務。 必須先解析的相依性集合組通常稱為「相依性樹狀結構」、「相依性圖形」或「物件圖形」

容器會 ILogger<TCategoryName> 利用 ILogger<TCategoryName>型別來解析,因此不需要註冊每個 (泛型) 結構類型

在相依性插入術語中,服務:

  • 通常是為其他物件(例如服務)提供服務的物件 IMyDependency
  • 與 web 服務無關,但服務可能會使用 web 服務。

架構會提供健全的 記錄 系統。 IMyDependency上述範例中所示的執行是為了示範基本 DI 而撰寫的,而不是用來執行記錄。 大部分的應用程式都不需要撰寫記錄器。 下列程式碼示範如何使用預設記錄,這不需要註冊任何服務:

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

使用上述程式碼,就不需要更新 程式 .cs,因為 記錄 是由架構所提供。

插入至 Program .cs 的服務

任何向 DI 容器註冊的服務都可以從 app.Servicesapp.Services中解析:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

使用擴充方法來註冊服務群組

ASP.NET Core 架構使用註冊一組相關服務的慣例。 慣例是使用單一 Add{GROUP_NAME} 擴充方法來註冊架構功能所需的所有服務。 例如, AddControllers 擴充方法會註冊 MVC 控制器所需的服務。

下列程式碼是由 Razor 使用個別使用者帳戶的 Pages 範本所產生,並示範如何使用擴充方法和來將其他服務新增至容器 AddDbContextAddDefaultIdentity

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

請考慮下列各項,以註冊服務並設定選項:

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

您可以將相關的註冊群組移至擴充方法,以註冊服務。 例如,設定服務會新增至下列類別:

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

其餘的服務會註冊在類似的類別中。 下列程式碼使用新的擴充方法來註冊服務:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddRazorPages();

var app = builder.Build();

注意: 每個 擴充方法會新增並可能設定服務。 例如, AddControllersWithViews 新增具有 views 需求的服務 MVC 控制器,並 AddRazorPages 新增服務頁面所 Razor 需的服務。 建議應用程式遵循在命名空間中建立擴充方法的命名慣例 Microsoft.Extensions.DependencyInjection 。 在命名空間中建立擴充方法 Microsoft.Extensions.DependencyInjection

  • 封裝服務註冊的群組。
  • 提供對服務的方便 IntelliSense 存取。

服務存留期

.net 中查看相依性插入中的服務存留期

若要在中介軟體中使用範圍服務,請使用下列其中一種方法:

  • 將服務插入中介軟體的 InvokeInvokeAsync 方法。 使用函式 插入 會擲回執行時間例外狀況,因為它會強制範圍服務的行為就像 singleton 一樣。 [ 存留期和註冊選項 ] 區段中的範例會示範 方法。
  • 使用以 Factory 為基礎的中介軟體。 使用此方法註冊的中介軟體會根據用戶端要求啟動 (連接) ,可讓範圍服務插入中介軟體的函式。

如需詳細資訊,請參閱撰寫自訂 ASP.NET Core 中介軟體

服務註冊方法

.net 中查看相依性插入中的服務註冊方法

模擬類型以進行測試時,通常會使用多個實作為。

註冊只有實作為型別的服務,相當於向相同的實和服務型別註冊該服務。 這就是為什麼無法使用未採用明確服務類型的方法來註冊多個服務的服務。 這些方法可以註冊服務的多個 實例 ,但它們都有相同的 作為型別。

您可以使用上述任何一種服務註冊方法來註冊相同服務類型的多個服務實例。 在下列範例中, AddSingleton 使用 IMyDependency 做為服務類型呼叫兩次。 第二個呼叫會 AddSingleton 在解析為時覆寫上一個 IMyDependency ,並在透過來解析多個服務時加入至上一個 IEnumerable<IMyDependency> 。 服務會依照其在解析時所註冊的順序顯示 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);
    }
}

建構函式插入行為

.net 中查看相依性插入中的函式插入行為

Entity Framework 內容

根據預設,Entity Framework 內容會使用限 域存留期 新增至服務容器,因為 web 應用程式資料庫作業的範圍通常是用戶端要求。 若要使用不同的存留期,請使用多載來指定存留期 AddDbContext 。 給定存留期的服務不應使用存留期短于服務存留期的資料庫內容。

留期和註冊選項

若要示範服務存留期與其註冊選項之間的差異,請考慮使用下列介面,將工作表示為具有識別碼的作業 OperationId 。 根據作業的服務存留期如何針對下列介面進行設定,容器會在要求類別時提供相同或不同的服務實例:

public interface IOperation
{
    string OperationId { get; }
}

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

下列類別會執行 Operation 上述所有介面。 此函式會 Operation 產生 GUID,並將最後4個字元儲存在 OperationId 屬性中:

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

    public string OperationId { get; }
}

下列程式碼會根據指定的存留期,建立類別的多個註冊 Operation

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

範例應用程式會示範在要求內和之間的物件存留期。 IndexModel和中介軟體會要求每種 IOperation 類型,並記錄 OperationId 每個類型的:

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

類似于 IndexModel ,中介軟體會解析相同的服務:

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

您必須在方法中解析已設定範圍的服務 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);
}

記錄器輸出顯示:

  • 「暫時性」 物件一律不同。 在 OperationId 中介軟體的和中,暫時性值是不同的 IndexModel
  • 指定之要求的範圍物件會相同,但在每個新的要求中都不同。
  • 每個要求的單一物件都相同。

若要減少記錄輸出,請在 appsettings 中設定 "記錄: LogLevel: Microsoft: Error" 。開發的 json 檔案:

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

在應用程式啟動時解析服務

下列程式碼示範如何在應用程式啟動時,解析有限期間內的範圍服務:

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

範圍驗證

.net 中查看相依性插入中的函式插入行為

如需詳細資訊,請參閱範圍驗證

要求服務

服務及其在 ASP.NET Core 要求內的相依性會透過公開 HttpContext.RequestServices

架構會建立每個要求的範圍,並 RequestServices 公開範圍服務提供者。 只要要求為作用中狀態,所有範圍的服務都有效。

注意

偏好將相依性要求為函式參數,而不是從解析服務 RequestServices 。 將相依性要求為函式參數,會產生更容易測試的類別。

針對相依性插入設計服務

針對相依性插入設計服務時:

  • 避免具狀態、靜態類別和成員。 請改為使用單一服務來設計應用程式,以避免建立全域狀態。
  • 避免直接在服務內具現化相依類別。 直接具現化會將程式碼耦合到特定實作。
  • 使服務更小、妥善組成且輕鬆地進行測試。

如果類別有許多插入的相依性,可能是因為類別具有太多責任,而且違反了 單一責任原則 (SRP) 。 嘗試將其部分責任移至新的類別,以重構類別。 請記住, Razor 頁面頁面模型類別和 MVC 控制器類別應該專注于 UI 的考慮。

處置服務

容器會為它建立的 IDisposable 類型呼叫 Dispose。 從容器解析的服務絕對不能由開發人員處置。 如果類型或 factory 註冊為 singleton,則容器會自動處置 singleton。

在下列範例中,服務是由服務容器建立並自動處置: dependency-injection\samples\6.x\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");
    }
}

在每次重新整理 [索引] 頁面之後,debug 主控台會顯示下列輸出:

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

服務容器未建立的服務

請考慮下列程式碼:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

在上述程式碼中:

  • 服務實例不是由服務容器所建立。
  • 架構不會自動處置服務。
  • 開發人員負責處置服務。

暫時性和共用實例的 IDisposable 指引

在 .NET 中的相依性插入中查看暫時性和共用實例的 IDisposable 指導方針

預設服務容器取代

查看 .NET 中的相依性插入中的預設服務容器取代

建議

請參閱 .net 中的相依性插入建議

  • 避免使用「服務定位器模式」。 例如,當您可以改用 DI 時,請勿叫用 GetService 來取得服務執行個體:

    不正確:

    Incorrect code

    正確

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • 另一個要避免的服務定位器變化是插入在執行階段解析相依性的處理站。 這兩種做法都會混用控制反轉策略。

  • 避免以靜態方式存取 HttpContext (例如 HttpContext)。

DI 是靜態/全域物件存取模式的「替代」選項。 如果您將 DI 與靜態物件存取混合,則可能無法實現 DI 的優點。

Orchard Core是一種應用程式架構,可在 ASP.NET Core 上建立模組化、多租使用者應用程式。 如需詳細資訊,請參閱 Orchard Core 檔

如需如何使用 Orchard Core Framework 來建立模組化和多租使用者應用程式的範例,請參閱 Orchard core 範例 ,而不需要任何 CMS 專屬的功能。

架構提供的服務

Program會註冊應用程式所使用的服務,包括平臺功能,例如 Entity Framework Core 和 ASP.NET Core MVC。 一開始, IServiceCollection 提供給 IServiceCollection 的會根據 設定主機的方式,提供由架構定義的服務。 針對以 ASP.NET Core 範本為基礎的應用程式,架構會註冊250以上的服務。

下表列出這些架構註冊服務的小型範例:

服務類型 存留期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory 暫時性
IHostApplicationLifetime 單一
IWebHostEnvironment 單一
Microsoft.AspNetCore.Hosting.IStartup 單一
Microsoft.AspNetCore.Hosting.IStartupFilter 暫時性
Microsoft.AspNetCore.Hosting.Server.IServer 單一
Microsoft.AspNetCore.Http.IHttpContextFactory 暫時性
Microsoft.Extensions.Logging.ILogger<TCategoryName> 單一
Microsoft.Extensions.Logging.ILoggerFactory 單一
Microsoft.Extensions.ObjectPool.ObjectPoolProvider 單一
Microsoft.Extensions.Options.IConfigureOptions<TOptions> 暫時性
Microsoft.Extensions.Options.IOptions<TOptions> 單一
System.Diagnostics.DiagnosticSource 單一
System.Diagnostics.DiagnosticListener 單一

其他資源

Kirk LarkinSteve SmithScott AddieBrandon Dahler

ASP.NET Core 支援相依性插入 (DI) 軟體設計模式,這是在類別及其相依性之間達成控制反轉 (IoC) 的技術。

如需 MVC 控制器內相依性插入的特定詳細資訊,請參閱ASP.NET Core 中的控制項插入至控制器

如需在 web 應用程式以外的應用程式中使用相依性插入的相關資訊,請參閱 .net 中的相依性插入。

如需有關選項之相依性插入的詳細資訊,請參閱ASP.NET Core 中的選項模式

本主題提供 ASP.NET Core 中的相依性插入的相關資訊。 使用相依性插入的主要檔包含在 .NET 的相依性 插入中。

查看或下載範例程式碼 (如何下載)

相依性插入概觀

相依 性是另 一個物件所依存的物件。 MyDependency使用其他類別所依存的方法檢查下列類別 WriteMessage

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

類別可以建立類別的實例 MyDependency ,以使用其 WriteMessage 方法。 在下列範例中, MyDependency 類別是類別的相依性 IndexModel

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

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

類別會建立和直接相依于 MyDependency 類別。 程式碼相依性(如上述範例所示)有問題,而且應該避免因下列原因:

  • 若要以 MyDependency 不同的實作為取代, IndexModel 必須修改該類別。
  • 如果 MyDependency 有相依性,則也必須由類別設定 IndexModel 。 在具有多個相依於 MyDependency 之多個類別的大型專案中,設定程式碼在不同的應用程式之間會變得鬆散。
  • 此實作難以進行單元測試。 應用程式應該使用模擬 (Mock) 或虛設常式 (Stub) MyDependency 類別,這在使用此方法時無法使用。

相依性插入可透過下列方式解決這些問題:

  • 使用介面或基底類別來將相依性資訊抽象化。
  • 在服務容器中註冊相依性。 ASP.NET Core 提供內建服務容器 IServiceProvider。 服務通常會在應用程式的方法中註冊 Startup.ConfigureServices
  • 將服務「插入」到服務使用位置之類別的建構函式。 架構會負責建立相依性的執行個體,並在不再需要時將它捨棄。

範例應用程式中, 介面會定義 WriteMessage 方法:

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

這個介面是由具象型別 MyDependency 所實作:

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

範例應用程式會 IMyDependency 使用具象類型來註冊服務 MyDependencyAddScoped方法會使用範圍存留期(單一要求的存留期)來註冊服務。 將在此主題稍後將說明服務存留期

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

    services.AddRazorPages();
}

在範例應用程式中, IMyDependency 會要求服務並用來呼叫 WriteMessage 方法:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

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

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

藉由使用 DI 模式,控制器:

  • 不使用具象型別 MyDependency ,只使用它所執行的 IMyDependency 介面。 這可讓您輕鬆地變更控制器所使用的執行,而不需要修改控制器。
  • 不會建立的實例 MyDependency ,而是由 DI 容器所建立。

IMyDependency 可以使用內建的記錄 API 來改善介面的實作為:

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

更新的 ConfigureServices 方法會註冊新的 IMyDependency 實作為:

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

    services.AddRazorPages();
}

MyDependency2 相依于 ILogger<TCategoryName> 它在函式中要求的。 ILogger<TCategoryName>ILogger<TCategoryName>

以鏈結方式使用相依性插入並非不尋常。 每個要求的相依性接著會要求其自己的相依性。 容器會解決圖形中的相依性,並傳回完全解析的服務。 必須先解析的相依性集合組通常稱為「相依性樹狀結構」、「相依性圖形」或「物件圖形」

容器會 ILogger<TCategoryName> 利用 ILogger<TCategoryName>型別來解析,因此不需要註冊每個 (泛型) 結構類型

在相依性插入術語中,服務:

  • 通常是為其他物件(例如服務)提供服務的物件 IMyDependency
  • 與 web 服務無關,但服務可能會使用 web 服務。

架構會提供健全的 記錄 系統。 IMyDependency上述範例中所示的執行是為了示範基本 DI 而撰寫的,而不是用來執行記錄。 大部分的應用程式都不需要撰寫記錄器。 下列程式碼示範如何使用預設記錄,這不需要在中註冊任何服務 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);
    }
}

使用上述程式碼不需要更新 ConfigureServices ,因為 ConfigureServices 是由架構提供。

插入啟動的服務

服務可以插入至函式 StartupStartup.Configure 方法。

Startup使用泛型主機 () 時,只可將下列服務插入至函式 IHostBuilder

任何向 DI 容器註冊的服務都可以插入方法中 Startup.Configure

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

如需詳細資訊,請參閱啟動時 ASP.NET Core 和存取設定中的應用程式啟動

使用擴充方法來註冊服務群組

ASP.NET Core 架構使用註冊一組相關服務的慣例。 慣例是使用單一 Add{GROUP_NAME} 擴充方法來註冊架構功能所需的所有服務。 例如, AddControllers 擴充方法會註冊 MVC 控制器所需的服務。

下列程式碼是由 Razor 使用個別使用者帳戶的 Pages 範本所產生,並示範如何使用擴充方法和來將其他服務新增至容器 AddDbContextAddDefaultIdentity

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

請考慮下列 ConfigureServices 方法,它會註冊服務並設定選項:

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

您可以將相關的註冊群組移至擴充方法,以註冊服務。 例如,設定服務會新增至下列類別:

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

其餘的服務會註冊在類似的類別中。 下列 ConfigureServices 方法會使用新的擴充方法來註冊服務:

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

    services.AddRazorPages();
}

注意: 每個 擴充方法會新增並可能設定服務。 例如, AddControllersWithViews 新增具有 views 需求的服務 MVC 控制器,並 AddRazorPages 新增服務頁面所 Razor 需的服務。 建議應用程式遵循在命名空間中建立擴充方法的命名慣例 Microsoft.Extensions.DependencyInjection 。 在命名空間中建立擴充方法 Microsoft.Extensions.DependencyInjection

  • 封裝服務註冊的群組。
  • 提供對服務的方便 IntelliSense 存取。

服務存留期

.net 中查看相依性插入中的服務存留期

若要在中介軟體中使用範圍服務,請使用下列其中一種方法:

  • 將服務插入中介軟體的 InvokeInvokeAsync 方法。 使用函式 插入 會擲回執行時間例外狀況,因為它會強制範圍服務的行為就像 singleton 一樣。 [ 存留期和註冊選項 ] 區段中的範例會示範 方法。
  • 使用以 Factory 為基礎的中介軟體。 使用此方法註冊的中介軟體會根據用戶端要求啟動 (連接) ,可讓範圍服務插入中介軟體的 InvokeAsync 方法。

如需詳細資訊,請參閱撰寫自訂 ASP.NET Core 中介軟體

服務註冊方法

.net 中查看相依性插入中的服務註冊方法

模擬類型以進行測試時,通常會使用多個實作為。

註冊只有實作為型別的服務,相當於向相同的實和服務型別註冊該服務。 這就是為什麼無法使用未採用明確服務類型的方法來註冊多個服務的服務。 這些方法可以註冊服務的多個 實例 ,但它們都有相同的 作為型別。

您可以使用上述任何一種服務註冊方法來註冊相同服務類型的多個服務實例。 在下列範例中, AddSingleton 使用 IMyDependency 做為服務類型呼叫兩次。 第二個呼叫會 AddSingleton 在解析為時覆寫上一個 IMyDependency ,並在透過來解析多個服務時加入至上一個 IEnumerable<IMyDependency> 。 服務會依照其在解析時所註冊的順序顯示 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);
    }
}

建構函式插入行為

.net 中查看相依性插入中的函式插入行為

Entity Framework 內容

根據預設,Entity Framework 內容會使用限 域存留期 新增至服務容器,因為 web 應用程式資料庫作業的範圍通常是用戶端要求。 若要使用不同的存留期,請使用多載來指定存留期 AddDbContext 。 給定存留期的服務不應使用存留期短于服務存留期的資料庫內容。

留期和註冊選項

若要示範服務存留期與其註冊選項之間的差異,請考慮使用下列介面,將工作表示為具有識別碼的作業 OperationId 。 根據作業的服務存留期如何針對下列介面進行設定,容器會在要求類別時提供相同或不同的服務實例:

public interface IOperation
{
    string OperationId { get; }
}

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

下列類別會執行 Operation 上述所有介面。 此函式會 Operation 產生 GUID,並將最後4個字元儲存在 OperationId 屬性中:

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

    public string OperationId { get; }
}

Startup.ConfigureServices方法會根據指定的存留期,建立類別的多個註冊 Operation

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

    services.AddRazorPages();
}

範例應用程式會示範在要求內和之間的物件存留期。 IndexModel和中介軟體會要求每種 IOperation 類型,並記錄 OperationId 每個類型的:

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

類似于 IndexModel ,中介軟體會解析相同的服務:

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

您必須在方法中解析已設定範圍的服務 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);
}

記錄器輸出顯示:

  • 「暫時性」 物件一律不同。 在 OperationId 中介軟體的和中,暫時性值是不同的 IndexModel
  • 指定之要求的範圍物件會相同,但在每個新的要求中都不同。
  • 每個要求的單一物件都相同。

若要減少記錄輸出,請在 appsettings 中設定 "記錄: LogLevel: Microsoft: Error" 。開發的 json 檔案:

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

從主要呼叫服務

使用 IServiceScope 建立 IServiceScope,以解析應用程式範圍中的範圍服務。 此法可用於在開機時存取範圍服務,以執行初始化工作。

下列範例示範如何存取範圍 IMyDependency 服務,並在中呼叫其 WriteMessage 方法 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>();
            });
}

範圍驗證

.net 中查看相依性插入中的函式插入行為

如需詳細資訊,請參閱範圍驗證

要求服務

服務及其在 ASP.NET Core 要求內的相依性會透過公開 HttpContext.RequestServices

架構會建立每個要求的範圍,並 RequestServices 公開範圍服務提供者。 只要要求為作用中狀態,所有範圍的服務都有效。

注意

偏好將相依性要求為函式參數,而不是從解析服務 RequestServices 。 將相依性要求為函式參數,會產生更容易測試的類別。

針對相依性插入設計服務

針對相依性插入設計服務時:

  • 避免具狀態、靜態類別和成員。 請改為使用單一服務來設計應用程式,以避免建立全域狀態。
  • 避免直接在服務內具現化相依類別。 直接具現化會將程式碼耦合到特定實作。
  • 使服務更小、妥善組成且輕鬆地進行測試。

如果類別有許多插入的相依性,可能是因為類別具有太多責任,而且違反了 單一責任原則 (SRP) 。 嘗試將其部分責任移至新的類別,以重構類別。 請記住, Razor 頁面頁面模型類別和 MVC 控制器類別應該專注于 UI 的考慮。

處置服務

容器會為它建立的 IDisposable 類型呼叫 Dispose。 從容器解析的服務絕對不能由開發人員處置。 如果類型或 factory 註冊為 singleton,則容器會自動處置 singleton。

在下列範例中,服務是由服務容器建立並自動處置:

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

在每次重新整理 [索引] 頁面之後,debug 主控台會顯示下列輸出:

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

服務容器未建立的服務

請考慮下列程式碼:

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

    services.AddRazorPages();
}

在上述程式碼中:

  • 服務實例不是由服務容器所建立。
  • 架構不會自動處置服務。
  • 開發人員負責處置服務。

暫時性和共用實例的 IDisposable 指引

在 .NET 中的相依性插入中查看暫時性和共用實例的 IDisposable 指導方針

預設服務容器取代

查看 .NET 中的相依性插入中的預設服務容器取代

建議

請參閱 .net 中的相依性插入建議

  • 避免使用「服務定位器模式」。 例如,當您可以改用 DI 時,請勿叫用 GetService 來取得服務執行個體:

    不正確:

    Incorrect code

    正確

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • 另一個要避免的服務定位器變化是插入在執行階段解析相依性的處理站。 這兩種做法都會混用控制反轉策略。

  • 避免以靜態方式存取 HttpContext (例如 HttpContext)。

  • 避免 BuildServiceProvider 在中呼叫 ConfigureServices 。 呼叫 BuildServiceProvider 通常會在開發人員想要解析中的服務時發生 ConfigureServices 。 例如,請考慮從設定載入的情況 LoginPath 。 請避免下列方法:

    bad code calling BuildServiceProvider

    在上圖中,選取下綠色波浪線會 services.BuildServiceProvider 顯示下列 ASP0000 警告:

    從應用程式程式碼呼叫 ' BuildServiceProvider ' ASP0000 時,會產生另一個要建立的單一服務複本。 請考慮將相依性插入服務作為「設定」參數的替代方案。

    呼叫 BuildServiceProvider 會建立第二個容器,它可以建立損毀的 singleton,並導致跨多個容器的物件圖形參考。

    若要取得正確的方式 LoginPath ,請使用選項模式內建的 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();
    }
    
  • 容器會捕獲可處置的暫時性服務以供處置。 如果從最上層容器解析,這可能會導致記憶體流失。

  • 啟用範圍驗證,以確定應用程式沒有可捕獲範圍服務的 singleton。 如需詳細資訊,請參閱範圍驗證

就像所有的建議集,您可能會遇到需要忽略建議的情況。 例外狀況很罕見,大多是架構本身內的特殊案例。

DI 是靜態/全域物件存取模式的「替代」選項。 如果您將 DI 與靜態物件存取混合,則可能無法實現 DI 的優點。

Orchard Core是一種應用程式架構,可在 ASP.NET Core 上建立模組化、多租使用者應用程式。 如需詳細資訊,請參閱 Orchard Core 檔

如需如何使用 Orchard Core Framework 來建立模組化和多租使用者應用程式的範例,請參閱 Orchard core 範例 ,而不需要任何 CMS 專屬的功能。

架構提供的服務

Startup.ConfigureServices方法會註冊應用程式所使用的服務,包括平臺功能,例如 Entity Framework Core 和 ASP.NET Core MVC。 一開始, IServiceCollection 提供給的是 ConfigureServices 由架構定義的服務,視 IServiceCollection而定。 針對以 ASP.NET Core 範本為基礎的應用程式,架構會註冊250以上的服務。

下表列出這些架構註冊服務的小型範例:

服務類型 存留期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory 暫時性
IHostApplicationLifetime 單一
IWebHostEnvironment 單一
Microsoft.AspNetCore.Hosting.IStartup 單一
Microsoft.AspNetCore.Hosting.IStartupFilter 暫時性
Microsoft.AspNetCore.Hosting.Server.IServer 單一
Microsoft.AspNetCore.Http.IHttpContextFactory 暫時性
Microsoft.Extensions.Logging.ILogger<TCategoryName> 單一
Microsoft.Extensions.Logging.ILoggerFactory 單一
Microsoft.Extensions.ObjectPool.ObjectPoolProvider 單一
Microsoft.Extensions.Options.IConfigureOptions<TOptions> 暫時性
Microsoft.Extensions.Options.IOptions<TOptions> 單一
System.Diagnostics.DiagnosticSource 單一
System.Diagnostics.DiagnosticListener 單一

其他資源