首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Enterprise Library 深度解析 (九):CachingCallHandler Out 參數 Bug 根因與修復方案

     編輯:關於.NET

文章導讀

本文揭露 Enterprise Library Policy Injection Application Block (PIAB) 中 CachingCallHandler 存在多年的嚴重 Bug:目標方法若包含 out 參數且非 void 回傳,拋出 IndexOutOfRangeException;若為 void 回傳,out 參數不被快取。文中完整重現問題、分析根因、提供修復版 FixedCachingCallHandler 完整實作。適合使用 EntLib 快取攔截器的架構師與資深工程師。

核心知識點

  • Bug 重現:含 out 參數方法套用 CachingCallHandler 導致異常
  • 根因分析:原始碼僅快取回傳值,忽略 out 參數,導致回傳訊息與參數列表不匹配
  • 修復方案:InvocationResult 封裝回傳值 + out 參數,StreamlineArguments 重建參數陣列
  • 完整 FixedCachingCallHandler 實作程式碼
  • 驗證結果:修復後正常快取 out 參數與回傳值

一、問題重現

1. 正常情況:無 out 參數,快取運作正常

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,快取正常。

2. Bug 觸發:含 out 參數

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

核心缺陷:

  1. 快取內容不完整:AddToCache(key, return2.ReturnValue) 僅儲存回傳值,忽略 return2.Outputs (out/ref 參數)
  2. 回傳訊息構造錯誤:CreateMethodReturn(objArray2[0], input.Arguments) 用原始輸入參數陣列替代輸出參數,導致參數數量/型別不匹配,觸發 PropagateOutParameters 時的索引越界
  3. void 方法完全不快取:TargetMethodReturnsVoid 直接返回,out 參數永遠不被快取

三、修復方案設計

1. OutputParameter:表示單一 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; }
}

2. InvocationResult:封裝完整調用結果

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

3. FixedCachingCallHandler 完整實作

[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

驗證結論:

  • out 參數 userName 正確快取並回傳
  • 回傳值 bool 正確快取
  • 無 IndexOutOfRangeException
  • 多次調用返回相同快取結果,證明快取生效

五、關鍵設計決策說明

決策點選擇理由
快取儲存結構InvocationResult (ReturnValue + OutputParameter[])完整保留調用結果語義,支援任意數量 out 參數
out 參數識別ParameterInfo.IsOut精確識別 out/ref 參數,避免誤判
參數重建StreamlineArguments 按索引合併保證參數陣列順序/數量與原始簽名完全一致
void 方法處理移除 TargetMethodReturnsVoid 過濾允許 void 方法快取 out 參數 (雖罕見但合法)
快取鍵生成沿用 DefaultCacheKeyGenerator相容現有快取鍵策略,無需遷移
過期策略滑動過期 (ExpirationTime)沿用原有配置模式

六、遷移指南

  1. 複製 FixedCachingCallHandler、OutputParameter、InvocationResult 至專案
  2. 將原 [CachingCallHandler] 替換為 [FixedCachingCallHandler]
  3. 若使用配置檔註冊,更新 type 屬性指向新類別
  4. 重新編譯部署,現有快取鍵相容 (建議清空快取重建)

七、給微軟 EntLib 團隊的建議

  1. 官方修復 CachingCallHandler 核心邏輯,納入下一版本
  2. 增加單元測試覆蓋 out/ref 參數場景
  3. 文件化快取行為限制與最佳實踐
  4. 考慮支援 ref 參數快取 (本修復易擴展)

出處:Artech 博客園

本文配套源碼下載
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved