Unity 內建 PerThreadLifetimeManager 基於 [ThreadStatic] TLS(Thread Local Storage),同一 ManagedThreadId 共用實例。然而 CLR ThreadPool 會重用執行緒:
這在 ASP.NET Web Forms / MVC / WCF PerCall 託管模式下極為常見,**PerThread ≠ PerRequest**。
| 方案 | 可行性 | 缺點 |
|---|---|---|
| 停用 ThreadPool,手動建立/銷毀執行緒 | 理論可行 | 效能災難、擴展性極差、違背 .NET 執行緒模型 |
| 每 Request 建立子容器 | 成熟方案 | 需自行管理子容器生命週期(HttpModule/Global.asax) |
| 自訂 PerCallContextLifetimeManager | **優雅、零侵入** | 需理解 CallContext 流動機制 |
using System;
using System.Globalization;
using System.Runtime.Remoting.Messaging;
using Microsoft.Practices.Unity;
public class PerCallContextLifetimeManager : LifetimeManager
{
private readonly string _key = string.Format(CultureInfo.InvariantCulture,
"PerCallContextLifetimeManager_{0}", Guid.NewGuid());
public override object GetValue() => CallContext.LogicalGetData(_key);
public override void SetValue(object newValue) => CallContext.LogicalSetData(_key, newValue);
public override void RemoveValue() => CallContext.FreeNamedDataSlot(_key);
}
關鍵點:使用 CallContext.LogicalGetData/SetData 而非 CallContext.GetData/SetData。Logical 版本會隨 ExecutionContext 跨非同步點(await、Task.Run、WCF 處理鏈)流動,完美對齊「邏輯呼叫鏈」邊界。
container.RegisterType<IExample, Example>(new PerCallContextLifetimeManager());
private static void TestPerCallContextLifetimeManager()
{
IExample example;
using (var container = new UnityContainer())
{
container.RegisterType<IExample, Example>(new PerCallContextLifetimeManager());
// 同一 CallContext 內多次 Resolve → 同一實例
container.Resolve<IExample>().SayHello();
container.Resolve<IExample>().SayHello();
// 模擬多執行緒併發 Request
Action act = sleep => {
container.Resolve<IExample>().SayHello();
Thread.Sleep(sleep);
container.Resolve<IExample>().SayHello();
};
new Thread(() => act(50)).Start();
new Thread(() => act(55)).Start();
// ThreadPool 佇列工作項目(關鍵測試)
ThreadPool.QueueUserWorkItem(_ => act(50));
ThreadPool.QueueUserWorkItem(_ => act(55));
Thread.Sleep(200);
example = container.Resolve<IExample>();
}
// 容器 Dispose 後,外部參考實例仍可用(未被容器銷毀)
example.SayHello();
Console.ReadKey();
}
在 Global.asax 或 HttpModule 中,於 Application_BeginRequest 建立子容器或標記 CallContext,Application_EndRequest 呼叫 CallContext.FreeNamedDataSlot 清理(雖然 LogicalSetData 隨 ExecutionContext 結束自然失效,顯式清理更保險)。
ASP.NET Core 內建 DI 容器提供 ServiceLifetime.Scoped,預設對應 HTTP Request Scope,實作原理同樣基於 AsyncLocal<T>(.NET Core 取代 CallContext)。遷移專案時,PerCallContextLifetimeManager 可視為 Scoped 的 .NET Framework 對應物。
PerCallContextLifetimeManager 以約 20 行程式碼解決 Unity 在 ThreadPool 環境下長期存在的 PerRequest 偽單例問題,零配置、零侵入、相容非同步流程,是維護舊版 .NET Framework + Unity 專案的最佳實踐之一。