在軟件開發中,依賴注入已成為構建可維護、可測試和可擴展應用程序的核心模式。ASP.NET Core 內置的依賴注入容器為我們管理服務生命周期提供了極大的便利。然而在某些特定場景下,我們可能不希望某個依賴項在宿主對象被創建時立即實例化,而是希望它在首次被使用時才進行實例化。這就是“延遲注入”(Lazy Injection)的概念。
一、延遲注入的原理
延遲注入的核心思想是推遲對象的創建和初始化,直到真正需要使用它的時候。這在以下場景中尤為有用:
- 性能優化:當某個依賴項的創建成本很高(例如,需要進行復雜的計算、數據庫查詢或網絡請求),但并非每次宿主對象被使用時都需要該依賴項時,延遲注入可以避免不必要的資源消耗,從而提升應用程序的啟動速度和整體性能。
- 資源節約:如果某個依賴項會占用大量內存或其他系統資源,并且在應用程序的生命周期中可能只被少數幾次使用,那么延遲注入可以幫助我們更有效地管理和節約資源。
- 解決循環依賴:在某些復雜的依賴關系圖中,可能會出現循環依賴的情況。雖然良好的設計應該避免循環依賴,但在某些不可避免的場景下,延遲注入可以作為一種解決方案,打破循環,允許應用程序正常啟動。
在 .NET 中,實現延遲注入最常用的方式是使用 System.Lazy<T>
類。Lazy<T>
是一個泛型類,它包裝了一個對象,并確保該對象只在首次訪問其 Value
屬性時才被創建。Lazy<T>
的構造函數接受一個 Func<T>
委托,這個委托包含了創建被延遲加載對象的邏輯。當 Value
屬性首次被訪問時,這個委托會被執行,并將其結果緩存起來,后續的訪問將直接返回緩存的值。
ASP.NET Core 的內置依賴注入容器本身并不直接支持 Lazy<T>
的自動解析。這意味著你不能直接在構造函數中注入 Lazy<TService>
,并期望 DI 容器能夠自動為你提供一個 Lazy<TService>
實例。然而,我們可以通過一些簡單的配置和模式來實現延遲注入,尤其是在 .NET 8 環境下,其 DI 容器的性能和功能都有所增強,使得這些模式更加高效。
二、在 ASP.NET Core 中實現延遲注入
雖然 ASP.NET Core 的內置 DI 容器不直接支持 Lazy<T>
的自動解析,但我們可以利用其提供的 IServiceProvider
或通過注冊工廠方法來實現延遲注入。以下將通過一個具體的代碼示例來演示如何在 .NET 8 的 ASP.NET Core 應用程序中實現延遲注入。
假設我們有一個 ExpensiveService
,它的創建成本很高,我們希望只在需要時才實例化它:
public interface IExpensiveService
{string GetData();
}public class ExpensiveService : IExpensiveService
{public ExpensiveService(){// 模擬耗時操作Console.WriteLine("ExpensiveService 實例被創建了!");System.Threading.Thread.Sleep(2000); }public string GetData(){return "這是來自 ExpensiveService 的數據。";}
}
2.1 方法一:通過 IServiceProvider
延遲解析
這是最直接的方法。你可以在需要延遲注入的類中注入 IServiceProvider
,然后在需要時手動從 IServiceProvider
中解析服務。雖然這在一定程度上引入了服務定位器模式的痕跡,但在某些場景下是可接受的。
首先,在 Program.cs
中注冊 ExpensiveService
:
// Program.csusing Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;var builder = WebApplication.CreateBuilder(args);// 注冊 ExpensiveService
builder.Services.AddTransient<IExpensiveService, ExpensiveService>();// 添加控制器支持
builder.Services.AddControllers();var app = builder.Build();// 配置 HTTP 請求管道
if (app.Environment.IsDevelopment())
{app.UseDeveloperExceptionPage();
}app.UseRouting();app.UseEndpoints(endpoints =>
{endpoints.MapControllers();
});app.Run();
然后,在一個控制器中,你可以這樣使用 IServiceProvider
來延遲解析 IExpensiveService
:
using Microsoft.AspNetCore.Mvc;
using System;[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{private readonly IServiceProvider _serviceProvider;public HomeController(IServiceProvider serviceProvider){_serviceProvider = serviceProvider;}[HttpGet("lazy-resolve")]public IActionResult LazyResolve(){Console.WriteLine("進入 LazyResolve 方法");// 只有在需要時才解析 ExpensiveServicevar expensiveService = _serviceProvider.GetService<IExpensiveService>();if (expensiveService != null){var data = expensiveService.GetData();return Ok($"延遲解析成功:{data}");}return NotFound("服務未找到。");}[HttpGet("no-lazy-resolve")]public IActionResult NoLazyResolve(){Console.WriteLine("進入 NoLazyResolve 方法");// 不進行延遲解析,如果 ExpensiveService 在構造函數中被注入,則會立即創建return Ok("未進行延遲解析。");}
}
當你訪問 /home/lazy-resolve
時,ExpensiveService
的構造函數只會在 GetService<IExpensiveService>()
被調用時才執行。而訪問 /home/no-lazy-resolve
則不會觸發 ExpensiveService
的實例化(除非 ExpensiveService
被其他非延遲注入的方式所依賴)。
2.2 方法二:注冊 Lazy<T>
的工廠方法
這種方法更加優雅,它允許你直接在構造函數中注入 Lazy<TService>
,而無需直接暴露 IServiceProvider
。你需要手動注冊一個工廠方法,告訴 DI 容器如何創建 Lazy<TService>
的實例。
在 Program.cs
中,你可以這樣注冊 Lazy<IExpensiveService>
:
// Program.cs (部分代碼)// ...builder.Services.AddTransient<IExpensiveService, ExpensiveService>();// 注冊 Lazy<IExpensiveService>
builder.Services.AddTransient(sp => new Lazy<IExpensiveService>(() => sp.GetRequiredService<IExpensiveService>()));// ...
現在,我們可以在控制器中直接注入 Lazy<IExpensiveService>
:
using Microsoft.AspNetCore.Mvc;
using System;[ApiController]
[Route("[controller]")]
public class LazyInjectionController : ControllerBase
{private readonly Lazy<IExpensiveService> _lazyExpensiveService;public LazyInjectionController(Lazy<IExpensiveService> lazyExpensiveService){_lazyExpensiveService = lazyExpensiveService;Console.WriteLine("LazyInjectionController 構造函數執行,但 ExpensiveService 尚未創建。");}[HttpGet("lazy-injection")]public IActionResult LazyInjection(){Console.WriteLine("進入 LazyInjection 方法");// 首次訪問 .Value 屬性時,ExpensiveService 才會被創建var data = _lazyExpensiveService.Value.GetData();return Ok($"延遲注入成功:{data}");}[HttpGet("no-lazy-injection")]public IActionResult NoLazyInjection(){Console.WriteLine("進入 NoLazyInjection 方法");return Ok("未訪問延遲注入的服務。");}
}
當你訪問 /lazyinjection/lazy-injection
時,ExpensiveService
的構造函數只會在 _lazyExpensiveService.Value
被首次訪問時才執行。而訪問 /lazyinjection/no-lazy-injection
則不會觸發 ExpensiveService
的實例化。
2.3 方法三:使用第三方 DI 容器
一些第三方依賴注入容器,如 Autofac,對 Lazy<T>
有原生支持,使得延遲注入的實現更加簡潔。如果你已經在項目中使用或計劃使用第三方 DI 容器,這可能是一個更方便的選擇。
以 Autofac 為例,你通常只需要注冊你的服務,Autofac 會自動處理 Lazy<T>
的解析:
// Startup.cs (Autofac 配置示例)public class Startup
{public ILifetimeScope AutofacContainer { get; private set; }public void ConfigureServices(IServiceCollection services){services.AddControllers();}public void ConfigureContainer(ContainerBuilder builder){builder.RegisterType<ExpensiveService>().As<IExpensiveService>().InstancePerDependency();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){AutofacContainer = app.ApplicationServices.GetAutofacRoot();if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseRouting();app.UseEndpoints(endpoints =>{endpoints.MapControllers();});}
}
然后,在控制器中直接注入 Lazy<IExpensiveService>
即可:
using Microsoft.AspNetCore.Mvc;
using System;[ApiController]
[Route("[controller]")]
public class AutofacLazyInjectionController : ControllerBase
{private readonly Lazy<IExpensiveService> _lazyExpensiveService;public AutofacLazyInjectionController(Lazy<IExpensiveService> lazyExpensiveService){_lazyExpensiveService = lazyExpensiveService;Console.WriteLine("AutofacLazyInjectionController 構造函數執行,但 ExpensiveService 尚未創建。");}[HttpGet("autofac-lazy-injection")]public IActionResult AutofacLazyInjection(){Console.WriteLine("進入 AutofacLazyInjection 方法");var data = _lazyExpensiveService.Value.GetData();return Ok($"Autofac 延遲注入成功:{data}");}
}
TIP:使用 Autofac 需要額外的配置步驟來集成到 ASP.NET Core 中,這里僅展示了核心的延遲注入部分。
三、總結
延遲注入是 ASP.NET Core 中一種重要的性能優化和資源管理策略,尤其適用于那些創建成本高昂或不總是需要的服務。通過 System.Lazy<T>
類,我們可以有效地推遲對象的實例化,直到它們真正被使用。雖然 ASP.NET Core 的內置 DI 容器不直接支持 Lazy<T>
的自動解析,但我們可以通過注冊工廠方法或注入 IServiceProvider
來實現這一目標。對于更復雜的場景,或者如果你已經在使用第三方 DI 容器,它們通常會提供更簡潔的 Lazy<T>
解析支持。在 .NET 8 及其后續版本中,隨著框架性能的不斷提升,合理地運用延遲注入將有助于構建更高效、響應更迅速的應用程序。