.NET Core 中的相依性插入

作者: Kirk LarkinSteve SmithBrandon Dahler

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

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

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

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

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

檢視或下載範例程式碼 (如何下載)

相依性插入概觀

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

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 類別。 如上一個範例中的程式碼相依性有問題,因此應避免下列原因:

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

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

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

範例應用程式中IMyDependency 介面會 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 向具體類型 MyDependency 註冊服務。 方法 AddScoped 會向範圍存留期、單一要求的存留期註冊服務。 將在此主題稍後將說明服務存留期

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 Page。
  • 不會建立 的 MyDependency 實例,它會由 DI 容器建立。

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

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

更新的 Program.cs 會註冊新的 IMyDependency 實作:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

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

var app = builder.Build();

MyDependency2 相依于 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);
    }
}

使用上述程式碼不需要更新 Program.cs ,因為架構會提供 記錄

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

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

注意: 每個 services.Add{GROUP_NAME} 擴充方法都會新增並可能設定服務。 例如, AddControllersWithViews 新增具有檢視所需的 MVC 控制器服務,並 AddRazorPages 新增 Pages 所需的服務 Razor 。 我們建議應用程式遵循在命名空間中建立擴充方法的 Microsoft.Extensions.DependencyInjection 命名慣例。 在命名空間中 Microsoft.Extensions.DependencyInjection 建立擴充方法:

服務存留期

請參閱.NET 中相依性插入的服務存留期

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

  • 將服務插入中介軟體的 InvokeInvokeAsync 方法。 使用 建構函式插入 會擲回執行時間例外狀況,因為它強制限定範圍服務的行為就像單一。 存 留期和註冊選項 一節中的範例示範 InvokeAsync 方法。
  • 使用 Factory 型中介軟體。 使用此方法註冊的中介軟體會針對每個用戶端要求啟用 (連線) ,這可讓範圍服務插入中介軟體的建構函式中。

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

服務註冊方法

請參閱.NET 中相依性插入的服務註冊方法

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

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

上述任何服務註冊方法都可以用來註冊相同服務類型的多個服務實例。 在下列範例中, AddSingleton 會呼叫 兩次做 IMyDependency 為服務類型。 當解析為 IMyDependency 時覆寫前一個呼叫的第二個呼叫 AddSingleton ,並在透過 解析 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();

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

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

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

app.UseAuthorization();

app.MapRazorPages();

app.Run();

範例應用程式示範要求內和之間的物件存留期。 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 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>();
    }
}

必須在 方法中 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);
}

記錄器輸出會顯示:

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

若要減少記錄輸出,請在檔案中 appsettings.Development.json 設定 「Logging:LogLevel:Microsoft:Error」 :

{
  "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。 開發人員不應處置從容器解析的服務。 如果類型或處理站註冊為單一,容器會自動處置單一。

在下列範例中,服務會由服務容器建立並自動處置: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");
    }
}

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

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 (例如 IHttpContextAccessor.HttpContext)。

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

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

如需如何只使用 Orchard Core Framework 建置模組化和多租使用者應用程式的範例,而不需具備任何 CMS 特定功能的範例,請參閱 範例。

架構提供的服務

Program.cs 註冊應用程式使用的服務,包括 Entity Framework Core 和 ASP.NET Core MVC 等平臺功能。 一開始,提供給 Program.cs 的 具有 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 中的相依性插入中

檢視或下載範例程式碼 (如何下載)

相依性插入概觀

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

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 類別。 如上一個範例中的程式碼相依性有問題,因此應避免下列原因:

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

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

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

範例應用程式中IMyDependency 介面會 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 向具體類型 MyDependency 註冊服務。 方法 AddScoped 會向範圍存留期、單一要求的存留期註冊服務。 將在此主題稍後將說明服務存留期

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 容器建立。

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

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> 利用 (泛型) 開放型別來解決,而不需要註冊每個 (泛型) 建構型別

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

  • 通常是提供服務給其他物件的 物件,例如 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 ,因為架構會提供 記錄

插入啟動的服務

服務可以插入建 Startup 構函式和 Startup.Configure 方法。

使用泛型主機 () IHostBuilder 時,只能將下列服務插入 Startup 建構函式:

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

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

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

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

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

注意: 每個 services.Add{GROUP_NAME} 擴充方法都會新增並可能設定服務。 例如, AddControllersWithViews 新增具有檢視所需的 MVC 控制器服務,並 AddRazorPages 新增 Pages 所需的服務 Razor 。 我們建議應用程式遵循在命名空間中建立擴充方法的 Microsoft.Extensions.DependencyInjection 命名慣例。 在命名空間中 Microsoft.Extensions.DependencyInjection 建立擴充方法:

服務存留期

請參閱.NET 中相依性插入的服務存留期

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

  • 將服務插入中介軟體的 InvokeInvokeAsync 方法。 使用 建構函式插入 會擲回執行時間例外狀況,因為它強制限定範圍服務的行為就像單一。 存 留期和註冊選項 一節中的範例示範 InvokeAsync 方法。
  • 使用 Factory 型中介軟體。 使用此方法註冊的中介軟體會針對每個用戶端要求啟用 (連線) ,這可讓範圍服務插入中介軟體 InvokeAsync 的 方法中。

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

服務註冊方法

請參閱.NET 中相依性插入的服務註冊方法

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

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

上述任何服務註冊方法都可以用來註冊相同服務類型的多個服務實例。 在下列範例中, AddSingleton 會呼叫 兩次做 IMyDependency 為服務類型。 當解析為 IMyDependency 時覆寫前一個呼叫的第二個呼叫 AddSingleton ,並在透過 解析 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.Development.json 設定 「Logging:LogLevel:Microsoft:Error」 :

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

從主要呼叫服務

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

下列範例示範如何存取範圍 IMyDependency 服務,並在 中 Program.Main 呼叫其 WriteMessage 方法:

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。 開發人員不應處置從容器解析的服務。 如果類型或處理站註冊為單一,容器會自動處置單一。

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

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

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

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 (例如 IHttpContextAccessor.HttpContext)。

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

    bad code calling BuildServiceProvider

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

    ASP0000 從應用程式程式碼呼叫 'BuildServiceProvider' 會導致建立額外的單一服務複本。 請考慮將服務作為參數插入至 'Configure' 的替代專案,例如相依性插入服務。

    呼叫 BuildServiceProvider 會建立第二個容器,這會建立損毀的單一物件,並造成跨多個容器的物件圖形參考。

    取得的正確方法是 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();
    }
    
  • 容器會擷取可處置暫時性服務。 如果從最上層容器解析,這可能會變成記憶體流失。

  • 啟用範圍驗證,以確定應用程式沒有擷取範圍服務的單一。 如需詳細資訊,請參閱範圍驗證

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

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

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

如需如何只使用 Orchard Core Framework 建置模組化和多租使用者應用程式的範例,而不需具備任何 CMS 特定功能的範例,請參閱 範例。

架構提供的服務

方法 Startup.ConfigureServices 會註冊應用程式所使用的服務,包括 Entity Framework Core 和 ASP.NET Core MVC 等平臺功能。 一開始,提供給 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 單一

其他資源