本文揭露 Enterprise Library Policy Injection Application Block (PIAB) 中 CachingCallHandler 存在多年的嚴重 Bug:目標方法若包含 out 參數且非 void 回傳,拋出 IndexOutOfRangeException;若為 void 回傳,out 參數不被快取。文中完整重現問題、分析根因、提供修復版 FixedCachingCallHandler 完整實作。適合使用 EntLib 快取攔截器的架構師與資深工程師。
public interface IMembershipService {
string GetUserName(string userId);
}
public class MembershipService : IMembershipService {
[CachingCallHandler]
public string GetUserName(string userId) {
return Guid.NewGuid().ToString(); // 每次產生新 GUID
}
}
// 客戶端調用
var svc = PolicyInjection.Create<MembershipService, IMembershipService>();
while (true) {
Console.WriteLine(svc.GetUserName("007")); // 輸出相同 GUID,證明快取生效
Thread.Sleep(1000);
}
輸出:連續相同 GUID,快取正常。
public interface IMembershipService {
bool TryGetUserName(string userId, out string userName);
}
public class MembershipService : IMembershipService {
[CachingCallHandler]
public bool TryGetUserName(string userId, out string userName) {
userName = Guid.NewGuid().ToString();
return true;
}
}
// 客戶端調用
var svc = PolicyInjection.Create<MembershipService, IMembershipService>();
string userName;
while (true) {
svc.TryGetUserName("007", out userName);
Console.WriteLine(userName);
Thread.Sleep(1000);
}
異常:IndexOutOfRangeException

堆疊追蹤:
at System.Runtime.Remoting.Proxies.RealProxy.PropagateOutParameters(IMessage msg, Object[] outArgs, Object returnValue)
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at ...IMembershipService.TryGetUserName(String userId, String& userName)
at ...Program.Main(String[] args) in ...\Program.cs:line 15
原始 CachingCallHandler.Invoke 核心邏輯:
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext) {
if (this.TargetMethodReturnsVoid(input)) { // void 直接執行,不快取
return getNext()(input, getNext);
}
// 建立快取鍵
object[] inputs = ...;
string key = this.keyGenerator.CreateCacheKey(input.MethodBase, inputs);
// 嘗試從快取取得
object[] objArray2 = (object[])HttpRuntime.Cache.Get(key);
if (objArray2 == null) {
// 快取未命中:執行目標方法
IMethodReturn return2 = getNext()(input, getNext);
if (return2.Exception == null) {
// 僅快取回傳值!
this.AddToCache(key, return2.ReturnValue);
}
return return2;
}
// 快取命中:構造回傳訊息
// 錯誤在這裡:objArray2[0] 只有回傳值,缺少 out 參數
return input.CreateMethodReturn(objArray2[0], new object[] { input.Arguments });
}
核心缺陷:
AddToCache(key, return2.ReturnValue) 僅儲存回傳值,忽略 return2.Outputs (out/ref 參數)CreateMethodReturn(objArray2[0], input.Arguments) 用原始輸入參數陣列替代輸出參數,導致參數數量/型別不匹配,觸發 PropagateOutParameters 時的索引越界TargetMethodReturnsVoid 直接返回,out 參數永遠不被快取public class OutputParameter {
public object Value { get; private set; }
public int Index { get; private set; } // 在方法參數列表中的位置
public OutputParameter(object value, int index) { Value = value; Index = index; }
}
public class InvocationResult {
public object ReturnValue { get; private set; }
public OutputParameter[] Outputs { get; set; }
public InvocationResult(object returnValue, OutputParameter[] outputs) {
ReturnValue = returnValue;
Outputs = outputs ?? new OutputParameter[0];
}
// 根據索引查詢參數值
public bool TryGetParameterValue(int index, out object parameterValue) {
parameterValue = null;
var match = Outputs.FirstOrDefault(p => p.Index == index);
if (match != null) { parameterValue = match.Value; return true; }
return false;
}
// 重建完整參數陣列:out 參數用快取值,其餘用原始輸入
public object[] StreamlineArguments(IParameterCollection arguments) {
var list = new List<object>();
for (int i = 0; i < arguments.Count; i++) {
if (TryGetParameterValue(i, out object val)) list.Add(val);
else list.Add(arguments[i]);
}
return list.ToArray();
}
}
[ConfigurationElementType(typeof(CustomCallHandlerData))]
public class FixedCachingCallHandler : ICallHandler {
public int Order { get; set; }
private TimeSpan expirationTime = TimeSpan.FromMinutes(5);
private ICacheKeyGenerator keyGenerator = new DefaultCacheKeyGenerator();
[ConfigurationProperty("expirationTime")]
public TimeSpan ExpirationTime {
get { return expirationTime; }
set { expirationTime = value; }
}
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext) {
// 1. 建立快取鍵 (含所有輸入參數)
object[] inputs = input.Inputs.Cast<object>().ToArray();
string key = keyGenerator.CreateCacheKey(input.MethodBase, inputs);
// 2. 嘗試從快取取得 InvocationResult
var cached = HttpRuntime.Cache.Get(key) as InvocationResult;
if (cached == null) {
// 快取未命中:執行目標方法
IMethodReturn return2 = getNext()(input, getNext);
if (return2.Exception == null) {
// 快取完整結果:回傳值 + out 參數
var result = GetInvocationResult(input, return2);
AddToCache(key, result);
}
return return2;
}
// 3. 快取命中:重建完整回傳訊息
return input.CreateMethodReturn(cached.ReturnValue, cached.StreamlineArguments(input.Arguments));
}
private void AddToCache(string key, InvocationResult result) {
HttpRuntime.Cache.Insert(key, result, null, Cache.NoAbsoluteExpiration, expirationTime, CacheItemPriority.Normal, null);
}
private InvocationResult GetInvocationResult(IMethodInvocation input, IMethodReturn methodReturn) {
var outParms = new List<OutputParameter>();
for (int i = 0; i < input.Arguments.Count; i++) {
var paramInfo = input.Arguments.GetParameterInfo(i);
if (paramInfo.IsOut) { // 僅處理 out 參數 (ref 同理可擴展)
outParms.Add(new OutputParameter(input.Arguments[i], i));
}
}
return new InvocationResult(methodReturn.ReturnValue, outParms.ToArray());
}
}
套用 [FixedCachingCallHandler] 後重新執行含 out 參數測試:
4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189
驗證結論:
userName 正確快取並回傳bool 正確快取IndexOutOfRangeException| 決策點 | 選擇 | 理由 |
|---|---|---|
| 快取儲存結構 | InvocationResult (ReturnValue + OutputParameter[]) | 完整保留調用結果語義,支援任意數量 out 參數 |
| out 參數識別 | ParameterInfo.IsOut | 精確識別 out/ref 參數,避免誤判 |
| 參數重建 | StreamlineArguments 按索引合併 | 保證參數陣列順序/數量與原始簽名完全一致 |
| void 方法處理 | 移除 TargetMethodReturnsVoid 過濾 | 允許 void 方法快取 out 參數 (雖罕見但合法) |
| 快取鍵生成 | 沿用 DefaultCacheKeyGenerator | 相容現有快取鍵策略,無需遷移 |
| 過期策略 | 滑動過期 (ExpirationTime) | 沿用原有配置模式 |
FixedCachingCallHandler、OutputParameter、InvocationResult 至專案[CachingCallHandler] 替換為 [FixedCachingCallHandler]type 屬性指向新類別CachingCallHandler 核心邏輯,納入下一版本ref 參數快取 (本修復易擴展)出處:Artech 博客園