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

基礎:建構佇列 WCF 回應服務——從單向調用到雙向結果返回

     編輯:關於.NET

文章導讀

本文源自 MSDN Magazine 經典專欄,深入解析 WCF 佇列調用 (NetMsmqBinding) 架構、單向調用限制、回應服務模式設計、訊息標頭傳遞上下文、ResponseContext/ResponseClientBase/ResponseScope 三大輔助類實作。適合架構師與資深工程師掌握離線佇列雙向通訊模式。

核心知識點

  • NetMsmqBinding 佇列調用架構與單向限制
  • 回應服務模式:客戶端佇列接收結果/錯誤
  • 訊息標頭傳遞回應位址與方法 ID
  • ResponseContext 封裝標頭存取
  • ResponseClientBase<T> 簡化客戶端調用
  • ResponseScope<T> 簡化服務端回應
  • 完整程式碼實作與最佳實踐

一、佇列調用架構與單向限制

WCF 佇列調用架構

  • 客戶端 → Proxy → MSMQ 佇列 (非直接呼叫服務)
  • 服務端 → 佇列監聽器 → 出隊 → 調度執行
  • 本質:非連接、異步、單向 — 調用即入隊,無返回值、無例外傳遞
  • 合約限制:所有操作必須 IsOneWay=true
[ServiceContract]
interface IMyContract {
    [OperationContract(IsOneWay = true)]
    void MyMethod();
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract {
    [OperationBehavior(TransactionScopeRequired = true)]
    public void MyMethod() { ... }
}

二、回應服務模式:破解單向無回應困境

回應服務架構

核心思想:服務處理完成後,將結果發送到客戶端提供的回應佇列 (另一個排隊服務)。

1. 回應合約設計

[ServiceContract]
interface ICalculatorResponse {
    [OperationContract(IsOneWay = true)]
    void OnAddCompleted(int result, ExceptionDetail error);
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculatorResponse : ICalculatorResponse {
    [OperationBehavior(TransactionScopeRequired = true)]
    public void OnAddCompleted(int result, ExceptionDetail error) {
        MessageBox.Show($"結果 = {result}");
        if (error != null) { /* 處理錯誤 */ }
    }
}

2. 兩大核心挑戰

挑戰描述解決方案
多重調用區分 同一回應服務處理多個調用,需識別屬於哪個請求 客戶端生成唯一 Method ID,經服務透傳至回應
回應位址發現 服務不知客戶端回應佇列位址,配置檔寫死不靈活 客戶端將位址放入輸出訊息標頭,服務讀取標頭

三、訊息標頭傳遞上下文:最佳實踐

避免汙染業務合約參數,使用 SOAP Header 傳遞管道層資訊。

核心 API

// OperationContext 提供標頭集合
OperationContext.Current.IncomingMessageHeaders  // 讀取
OperationContext.Current.OutgoingMessageHeaders  // 寫入

// MessageHeaders 操作
headers.Add(MessageHeader header);
T header = headers.GetHeader<T>(string name, string ns);

// MessageHeader<T> 型別安全包裝
var typedHeader = new MessageHeader<ResponseContext>(context);
var untypedHeader = typedHeader.GetUntypedHeader("ResponseContext", "ns");
headers.Add(untypedHeader);

四、ResponseContext:封裝標頭存取

ResponseContext 類別圖

[DataContract]
public class ResponseContext {
    [DataMember] public string ResponseAddress { get; set; }
    [DataMember] public string MethodId { get; set; }
    [DataMember] public string FaultAddress { get; set; } // 可選:獨立錯誤佇列

    // 靜態屬性:封裝 Current 訊息標頭存取
    public static ResponseContext Current {
        get {
            var headers = OperationContext.Current.IncomingMessageHeaders;
            return headers.GetHeader<ResponseContext>("ResponseContext", "ns");
        }
        set {
            var headers = OperationContext.Current.OutgoingMessageHeaders;
            var typed = new MessageHeader<ResponseContext>(value);
            headers.Add(typed.GetUntypedHeader("ResponseContext", "ns"));
        }
    }

    // 建構函數:自動生成 GUID MethodId
    public ResponseContext(string responseAddress) : this(responseAddress, Guid.NewGuid().ToString()) {}
    public ResponseContext(string responseAddress, string methodId) { ... }
}

五、客戶端程式設計:OperationContextScope + ResponseClientBase

1. 基礎調用模式

string methodId = Guid.NewGuid().ToString();
string responseQueue = "net.msmq://localhost/private/MyCalculatorResponseQueue";
var proxy = new CalculatorClient();

using (var scope = new OperationContextScope(proxy.InnerChannel)) {
    // 建立新操作上下文,寫入標頭
    ResponseContext.Current = new ResponseContext(responseQueue, methodId);
    proxy.Add(2, 3); // 實際入隊
}
// 此時 methodId 可用於追蹤
proxy.Close();

客戶端程式設計步驟

2. ResponseClientBase<T>:封裝基類

public class ResponseClientBase<T> : ClientBase<T> where T : class {
    public readonly string ResponseAddress;

    public ResponseClientBase(string responseAddress) { ResponseAddress = responseAddress; }
    public ResponseClientBase(string responseAddress, string endpointName) : base(endpointName) { ResponseAddress = responseAddress; }
    public ResponseClientBase(string responseAddress, NetMsmqBinding binding, EndpointAddress remoteAddress) : base(binding, remoteAddress) { ResponseAddress = responseAddress; }

    protected string Enqueue(string operation, params object[] args) {
        using (var scope = new OperationContextScope(InnerChannel)) {
            string methodId = GenerateMethodId(); // 可覆寫
            ResponseContext.Current = new ResponseContext(ResponseAddress, methodId);
            var method = typeof(T).GetMethod(operation);
            method.Invoke(Channel, args); // 反射調用
            return methodId;
        }
    }

    protected virtual string GenerateMethodId() => Guid.NewGuid().ToString();
}

ResponseClientBase 類別圖

3. 具體 Proxy 繼承

public class CalculatorClient : ResponseClientBase<ICalculator> {
    public CalculatorClient(string responseAddress) : base(responseAddress) {}
    public CalculatorClient(string responseAddress, string endpointName) : base(responseAddress, endpointName) {}
    public CalculatorClient(string responseAddress, NetMsmqBinding binding, EndpointAddress remoteAddress) : base(responseAddress, binding, remoteAddress) {}

    public string Add(int n1, int n2) => Enqueue("Add", n1, n2);
    // 其他操作...
}

4. 簡化後的調用

var proxy = new CalculatorClient("net.msmq://localhost/private/MyCalculatorResponseQueue");
string methodId = proxy.Add(2, 3); // 返回 MethodId 供追蹤
proxy.Close();

六、服務端程式設計:ResponseScope<T> 封裝回應邏輯

1. 基礎回應模式

public void Add(int n1, int n2) {
    int result = 0; ExceptionDetail error = null;
    try { result = n1 + n2; }
    catch (Exception ex) { error = new ExceptionDetail(ex); }
    finally {
        var ctx = ResponseContext.Current;
        var address = new EndpointAddress(ctx.ResponseAddress);
        var binding = new NetMsmqBinding();
        var proxy = new CalculatorResponseClient(binding, address);

        using (var scope = new OperationContextScope(proxy.InnerChannel)) {
            ResponseContext.Current = ctx; // 透傳 MethodId
            proxy.OnAddCompleted(result, error);
        }
        proxy.Close();
    }
}

服務端程式設計步驟

2. ResponseScope<T>:自動化上下文與 Proxy 管理

public class ResponseScope<T> : IDisposable where T : class {
    private OperationContextScope _scope;
    public readonly T Response; // 回應服務 Proxy

    public ResponseScope() : this(new NetMsmqBinding()) {}
    public ResponseScope(string bindingConfig) : this(new NetMsmqBinding(bindingConfig)) {}
    public ResponseScope(NetMsmqBinding binding) {
        var ctx = ResponseContext.Current;
        var address = new EndpointAddress(ctx.ResponseAddress);
        var factory = new ChannelFactory<T>(binding, address);
        Response = factory.CreateChannel();

        // 關鍵:建構子中立即切換上下文,寫入標頭
        _scope = new OperationContextScope(Response as IContextChannel);
        ResponseContext.Current = ctx;
    }

    public void Dispose() {
        (Response as IDisposable)?.Dispose();
        _scope?.Dispose(); // 恢復原上下文
    }
}

ResponseScope 實作

3. 簡化後的服務端

finally {
    using (var scope = new ResponseScope<ICalculatorResponse>()) {
        scope.Response.OnAddCompleted(result, error);
    }
}

七、回應服務實作

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculatorResponse : ICalculatorResponse {
    [OperationBehavior(TransactionScopeRequired = true)]
    public void OnAddCompleted(int result, ExceptionDetail error) {
        string methodId = ResponseContext.Current.MethodId;
        if (error == null) {
            // 通知客戶端應用、觸發事件、更新 UI...
        } else {
            // 記錄錯誤、告警、補償邏輯...
        }
    }
}

回應服務實作

八、關鍵設計精要總結

  1. 非連接雙向:佇列調用 + 回應佇列 = 離線環境下的請求-回應
  2. 標頭傳遞上下文:MethodId + ResponseAddress 經 SOAP Header 流轉,不汙染業務合約
  3. OperationContextScope:臨時切換上下文寫入/讀取標頭,using 確保恢復
  4. 事務保護:TransactionScopeRequired=true 確保入隊/出隊/回應原子性
  5. 錯誤不重拋:捕獲業務異常包裝為 ExceptionDetail,切勿 rethrow,否則回應事務中止
  6. 三大輔助類:ResponseContext (標頭封裝)、ResponseClientBase (客戶端簡化)、ResponseScope (服務端簡化)

九、延伸應用

本文技術模式廣泛適用於:

  • 自訂框架的訊息攔截與上下文傳遞
  • 分散式追蹤
  • 長時間作業的狀態輪詢替代方案
  • 微服務異步命令-查詢分離 (CQRS) 回應通道

原文作者:Juval Lowy (IDesign 架構師,《Programming WCF Services》作者)
原文連結:MSDN Magazine

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