在上一篇文章中,我們讨論了如何通過CallContextInitializer實作Localization的例子,具體的做法是将client端的culture通過SOAP header傳到service端,然後通過自定義的CallContextInitializer設定目前方法執行的線程culture。在client端,目前culture資訊是通過OperationContext.Current.OutgoingMessageHeaders手工至于SOAP Header中的。實際上,我們可以通過基于WCF的另一個可擴充對象來實作這段邏輯,這個可擴充對象就是MessageInspector。我們今天來讨論MessageInspector應用的另外一個場景:如何通過MessageInspector來傳遞Context資訊。
1. Ambient Context
在一個多層結構的應用中,我們需要傳遞一些上下文的資訊在各層之間傳遞,比如:為了進行Audit,需要傳遞一些目前目前user profile的一些資訊。在一些分布式的環境中也可能遇到context資訊從client到server的傳遞。如何實作這種形式的Context資訊的傳遞呢?我們有兩種方案:
一、将Context作為參數傳遞:将context作為API的一部分,context的提供者在調用context接收者的API的時候顯式地設定這些Context資訊,context的接收者則直接通過參數将context取出。這雖然能夠解決問題,但決不是一個好的解決方案,因為API應該隻和具體的業務邏輯有關,而context 一般是與非業務邏輯服務的,比如Audit、Logging等等。此外,将context納入API作為其一部分,将降低API的穩定性, 比如,今天隻需要目前user所在組織的資訊,明天可能需求擷取目前用戶端的IP位址,你的API可以會經常變動,這顯然是不允許的。
二、建立Ambient Context來儲存這些context資訊,Ambient Context可以在不同的層次之間、甚至是分布式環境中每個節點之間共享或者傳遞。比如在ASP.NET 應用中,我們通過SessionSate來存儲目前Session的資訊;通過HttpContext來存儲目前Http request的資訊。在非Web應用中,我們通過CallContext将context資訊存儲在TLS(Thread Local Storage)中,目前線程下執行的所有代碼都可以通路并設定這些context資料。
2、Application Context
介于上面所述,我建立一個名為Application Context的Ambient Context容器,Application Context實際上是一個dictionary對象,通過key-value pair進行context元素的設定,通過key擷取相對應的context元素。Application Context通過CallContext實作,定義很簡單:
namespace Artech.ContextPropagation
{
[Serializable]
public class ApplicationContext:Dictionary<string,object>
{
private const string CallContextKey = "__ApplicationContext";
internal const string ContextHeaderLocalName = "__ApplicationContext";
internal const string ContextHeaderNamespace = "urn:artech.com";
private void EnsureSerializable(object value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
if (!value.GetType().IsSerializable)
{
throw new ArgumentException(string.Format("The argument of the type /"
{0}/" is not serializable!", value.GetType().FullName));
}
}
public new object this[string key]
{
get
{
return base[key];
}
set
{
this.EnsureSerializable(value);
base[key] = value;
}
}
public int Counter
{
get
{
return (int)this["__Count"];
}
set
{
this["__Count"] = value;
}
}
public static ApplicationContext Current
{
get
{
if (CallContext.GetData(CallContextKey) == null)
{
CallContext.SetData(CallContextKey, new ApplicationContext());
}
return CallContext.GetData(CallContextKey) as ApplicationContext;
}
set
{
CallContext.SetData(CallContextKey, value);
}
}
}
}
由于此Context将會置于SOAP Header中從client端向service端進行傳遞,我們需要為此message header指定一個local name和namespace,那麼在service端,才能通過此local name和namespace獲得此message header。同時,在lcoal domain, client或者service,context是通過CallContext進行存取的,CallContext也是一個類似于disctionary的結構,也需要為此定義一個Key:
private const string CallContextKey = "__ApplicationContext"; internal const string ContextHeaderLocalName = "__ApplicationContext";
internal const string ContextHeaderNamespace = "urn:artech.com";
由于ApplicaitonContext直接繼承自Dictionary<string,object>,我們可以通過Index進行元素的設定和提取,考慮到context的跨域傳播,需要進行序列化,是以重寫了Indexer,并添加了可序列化的驗證。為了後面示範方面,我們定義一個context item:Counter。
Static類型的Current屬性通過CallContext的SetData和GetData方法對目前的ApplicationContext進行設定和提取:
public static ApplicationContext Current
{
get
{
if (CallContext.GetData(CallContextKey) == null)
{
CallContext.SetData(CallContextKey, new ApplicationContext());
}
return CallContext.GetData(CallContextKey) as ApplicationContext;
}
set
{
CallContext.SetData(CallContextKey, value);
}
}
3、通過MessageInspector将AppContext置于SOAP header中
通過本系列第3部分對Dispatching system的介紹了,我們知道了在client端和service端,可以通過MessageInspector對request message或者reply message (incoming message或者outgoings message)進行檢驗。MessageInspector可以對MessageHeader進行自由的添加、修改和删除。在service端的MessageInspector被稱為DispatchMessageInspector,相對地,client端被稱為ClientMessageInspector。我們現在自定義我們自己的ClientMessageInspector。
namespace Artech.ContextPropagation
{
public class ContextAttachingMessageInspector:IClientMessageInspector
{
public bool IsBidirectional
{ get; set; }
public ContextAttachingMessageInspector()
: this(false)
{ }
public ContextAttachingMessageInspector(bool isBidirectional)
{
this.IsBidirectional = IsBidirectional;
}
IClientMessageInspector Members#region IClientMessageInspector Members
public void AfterReceiveReply(ref Message reply, object correlationState)
{
if (IsBidirectional)
{
return;
}
if (reply.Headers.FindHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace) < 0)
{
return;
}
ApplicationContext context = reply.Headers.GetHeader<ApplicationContext>(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace);
if (context == null)
{
return;
}
ApplicationContext.Current = context;
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
MessageHeader<ApplicationContext> contextHeader = new MessageHeader<ApplicationContext>(ApplicationContext.Current);
request.Headers.Add(contextHeader.GetUntypedHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace));
return null;
}
#endregion
}
}
一般地,我們僅僅需要Context的單向傳遞,也就是從client端向service端傳遞,而不需要從service端向client端傳遞。不過回來應付将來潛在的需求,也許可能需要這樣的功能:context從client端傳向service端,service對其進行修改後需要将其傳回到client端。為此,我們家了一個屬性:IsBidirectional表明是否支援雙向傳遞。
在BeforeSendRequest,我們将ApplicationContext.Current封裝成一個MessageHeader, 并将此MessageHeader添加到request message 的header集合中,local name和namespace采用的是定義在ApplicationContext中常量:
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
MessageHeader<ApplicationContext> contextHeader = new MessageHeader<ApplicationContext>(ApplicationContext.Current);
request.Headers.Add(contextHeader.GetUntypedHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace));
return null;
}
如何支援context的雙向傳遞,我們在AfterReceiveReply負責從reply message中接收從service傳回的context,并将其設定成目前的context:
public void AfterReceiveReply( ref Message reply, object correlationState)
{
if (IsBidirectional)
{
return;
}
if (reply.Headers.FindHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace) < 0)
{
return;
}
ApplicationContext context = reply.Headers.GetHeader<ApplicationContext>(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace);
if (context == null)
{
return;
}
ApplicationContext.Current = context;
}
4、通過ContextInitializer實作對Context的接收
上面我們介紹了在client端通過ClientMessageInspector将context資訊存儲到request message header中,照理說我們通過可以通過DispatchMessageInspector實作對context資訊的提取,但是考慮到我們設定context是通過CallContext來實作了,我們最好還是使用CallContextInitializer來做比較好一些。CallContextInitializer的定義,我們在上面一章已經作了詳細的介紹了,在這裡就不用多說什麼了。
namespace Artech.ContextPropagation
{
public class ContextReceivalCallContextInitializer : ICallContextInitializer
{
public bool IsBidirectional
{ get; set; }
public ContextReceivalCallContextInitializer()
: this(false)
{ }
public ContextReceivalCallContextInitializer(bool isBidirectional)
{
this.IsBidirectional = isBidirectional;
}
ICallContextInitializer Members#region ICallContextInitializer Members
public void AfterInvoke(object correlationState)
{
if (!this.IsBidirectional)
{
return;
}
ApplicationContext context = correlationState as ApplicationContext;
if (context == null)
{
return;
}
MessageHeader<ApplicationContext> contextHeader = new MessageHeader<ApplicationContext>(context);
OperationContext.Current.OutgoingMessageHeaders.Add(contextHeader.GetUntypedHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace));
ApplicationContext.Current = null;
}
public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
{
ApplicationContext context = message.Headers.GetHeader<ApplicationContext>(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace);
if (context == null)
{
return null;
}
ApplicationContext.Current = context;
return ApplicationContext.Current;
}
#endregion
}
}
代碼其實很簡單,BeforeInvoke中通過local name和namespace提取context對應的message header,并設定目前的ApplicationContext。如果需要雙向傳遞,則通過AfterInvoke方法将context儲存到reply message的header中被送回client端。
5. 為MessageInspector和CallContextInitializer建立behavior:
namespace Artech.ContextPropagation
{
public class ContextPropagationBehavior: IEndpointBehavior
{
public bool IsBidirectional
{ get; set; }
public ContextPropagationBehavior()
: this(false)
{ }
public ContextPropagationBehavior(bool isBidirectional)
{
this.IsBidirectional = isBidirectional;
}
IEndpointBehavior Members#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new ContextAttachingMessageInspector(this.IsBidirectional));
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
foreach (var operation in endpointDispatcher.DispatchRuntime.Operations)
{
operation.CallContextInitializers.Add(new ContextReceivalCallContextInitializer(this.IsBidirectional));
}
}
public void Validate(ServiceEndpoint endpoint)
{
}
#endregion
}
}
在ApplyClientBehavior中,建立我們的ContextAttachingMessageInspector對象,并将其放置到ClientRuntime 的MessageInspectors集合中;在ApplyDispatchBehavior,将ContextReceivalCallContextInitializer對象放到每個DispatchOperation的CallContextInitializers集合中。
因為我們需要通過配置的方式來使用我們的ContextPropagationBehavior,我們還需要定義對應的BehaviorExtensionElement:
namespace Artech.ContextPropagation
{
public class ContextPropagationBehaviorElement:BehaviorExtensionElement
{
[ConfigurationProperty("isBidirectional", DefaultValue = false)]
public bool IsBidirectional
{
get
{
return (bool)this["isBidirectional"];
}
set
{
this["isBidirectional"] = value;
}
}
public override Type BehaviorType
{
get
{
return typeof(ContextPropagationBehavior);
}
}
protected override object CreateBehavior()
{
return new ContextPropagationBehavior(this.IsBidirectional);
}
}
}
我們IsBidirectional則可以通過配置的方式來指定。
6. Context Propagation的運用
我們現在将上面建立的對象應用到真正的WCF調用環境中。我們依然建立我們經典的4層結構:
- Artech.ContextPropagation.Contract:
namespace Artech.ContextPropagation.Contract
{
[ServiceContract]
public interface IContract
{
[OperationContract]
void DoSomething();
}
}
- Artech.ContextPropagation.Services
namespace Artech.ContextPropagation.Services
{
public class Service:IContract
{
IContract Members#region IContract Members
public void DoSomething()
{
Console.WriteLine("ApplicationContext.Current.Count = {0}", ApplicationContext.Current.Counter);
ApplicationContext.Current.Counter++;
}
#endregion
}
}
列印出ApplicationContext.Current.Count 的值,并加1。
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="contextPropagationBehavior">
<contextPropagationElement isBidirectional="true" />
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="http://127.0.0.1/service" behaviorConfiguration="contextPropagationBehavior"
binding="basicHttpBinding" contract="Artech.ContextPropagation.Contract.IContract"
name="service" />
</client>
<extensions>
<behaviorExtensions>
<add name="contextPropagationElement" type="Artech.ContextPropagation.ContextPropagationBehaviorElement, Artech.ContextPropagation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
</system.serviceModel>
</configuration>
Artech.ContextPropagation.Client
namespace Artech.ContextPropagation.Client
{
class Program
{
static void Main(string[] args)
{
using (ChannelFactory<IContract> channelFactory = new ChannelFactory<IContract>("service"))
{
IContract proxy = channelFactory.CreateChannel();
ApplicationContext.Current.Counter = 100;
Console.WriteLine("Brfore service invocation: ApplicationContext.Current.Count = {0}", ApplicationContext.Current.Counter);
proxy.DoSomething();
Console.WriteLine("After service invocation: ApplicationContext.Current.Count = {0}", ApplicationContext.Current.Counter);
Console.Read();
}
}
}
}
以及config:
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="contextPropagationBehavior">
<contextPropagationElement isBidirectional="true" />
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="http://127.0.0.1/service" behaviorConfiguration="contextPropagationBehavior"
binding="basicHttpBinding" contract="Artech.ContextPropagation.Contract.IContract"
name="service" />
</client>
<extensions>
<behaviorExtensions>
<add name="contextPropagationElement" type="Artech.ContextPropagation.ContextPropagationBehaviorElement, Artech.ContextPropagation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
</system.serviceModel>
</configuration>
我們運作整個程式,你将會看到如下的輸出結果:
可見,Context被成功傳播到service端。再看看client端的輸出:
由此可見,在service端設定的context的值也成功傳回到client端,真正實作了雙向傳遞。
P.S: SOA主張Stateless的service,也就是說每次調用service都應該是互相獨立的。context的傳遞實際上卻是讓每次通路有了狀态,這實際上是違背了SOA的原則。是以,如何對于真正的SOA的設計與架構,個人覺得這種方式是不值得推薦的。但是,如何你僅僅是将WCF作為傳統的分布式手段,那麼這可能會給你的應用帶了很大的便利。
WCF後續之旅:
[原創]WCF後續之旅(1): WCF是如何通過Binding進行通信的
[原創]WCF後續之旅(2): 如何對Channel Layer進行擴充——建立自定義Channel
[原創]WCF後續之旅(3): WCF Service Mode Layer 的中樞—Dispatcher
[原創]WCF後續之旅(4):WCF Extension Point 概覽
[原創]WCF後續之旅(5): 通過WCF Extension實作Localization
[原創]WCF後續之旅(6): 通過WCF Extension實作Context資訊的傳遞
[原創]WCF後續之旅(7):通過WCF Extension實作和Enterprise Library Unity Container的內建
[原創]WCF後續之旅(8):通過WCF Extension 實作與MS Enterprise Library Policy Injection Application Block 的內建
[原創]WCF後續之旅(9):通過WCF的雙向通信實作Session管理[Part I]
[原創]WCF後續之旅(9): 通過WCF雙向通信實作Session管理[Part II]
[原創]WCF後續之旅(10): 通過WCF Extension實作以對象池的方式建立Service Instance
我的WCF之旅:
[原創]我的WCF之旅(1):建立一個簡單的WCF程式
[原創]我的WCF之旅(2):Endpoint Overview
[原創]我的WCF之旅(3):在WCF中實作雙向通信(Bi-directional Communication)
[原創]我的WCF之旅(4):WCF中的序列化(Serialization)- Part I
[原創]我的WCF之旅(4):WCF中的序列化(Serialization)- Part II
[原創]我的WCF之旅(5):Service Contract中的重載(Overloading)
[原創]我的WCF之旅(6):在Winform Application中調用Duplex Service出現TimeoutException的原因和解決方案
[原創]我的WCF之旅(7):面向服務架構(SOA)和面向對象程式設計(OOP)的結合——如何實作Service Contract的繼承
[原創]我的WCF之旅(8):WCF中的Session和Instancing Management
[原創]我的WCF之旅(9):如何在WCF中使用tcpTrace來進行Soap Trace
[原創]我的WCF之旅(10): 如何在WCF進行Exception Handling
[原創]我的WCF之旅(11):再談WCF的雙向通訊-基于Http的雙向通訊 V.S. 基于TCP的雙向通訊
[原創]我的WCF之旅(12):使用MSMQ進行Reliable Messaging
[原創]我的WCF之旅(13):建立基于MSMQ的Responsive Service