简介:这篇文章想简单谈一下在WCF下如何实现持久服务问题。什么是持久服务?其实这个话题和技术本身在很多其他已经流行的技术,譬如BizTalk Server,Windows Workflow foundation已经很为普遍了。持久服务是指稳定可靠的服务,这中服务能够坦然面对譬如主机重启,意外断电的异常状况,而即使发生了异常状况也能够自然的恢复到原先的状态而对客户端又能保持一定的透明性。对WCF来说,实现持久服务,意味着需要持续的消息和服务状态的稳定。在framework3.0下,WCF提出了有状态的服务,这中状态是以Sessions来实现的。但是,请注意,在3.0下,这中有状态的服务是不能成为持久的服务,因为WCF并没有提供足够的支持来实现这中服务。然而,持久的服务是几乎所有的企业都需要的服务,因而3.0支持下的WCF不能满足很多特定场景的需要。特别的,WF(Windows Workflow foundation) 提供了基于持久服务的昂贵模型,这允许开发者创建他们自己的机制来维护一个WF示例状态的持久性。
不过,在.net framework3.5下,它提供了一种简单的机制来维护the state of WCF Service的持久性。接下来的部分将简单阐述必须的步骤去实现持久服务。首先,我门定义一个具有状态交互的服务契约。
[ServiceContract]
public interface ITextComposer
{
[OperationContract]
string PowerOn(string text); 
[OperationContract]
string InsertText(string text); 
[OperationContract]
string DeleteText(string text); 
[OperationContract]
void PowerOff();
}
然后,我门要定义一个实现上述契约的可持久性服务,其实很简单,我门要利用 DurableService 和 DurableOperation 行为。象在3.0下的会话管理一样,我门可以用行为参数来配置会话示例的生命周期。在我门的例子,我门定义 PowerON and powerOff 操作来实现一个服务实例的开启和完成。
[Serializable]
[DurableServiceBehavior]
public class TextComposer : ITextComposer
{ private string CurrentText ;
[DurableOperationBehavior(CanCreateInstance = true)]
public string PowerOn(string text)
{
CurrentText = text;
return CurrentText;
} 
[DurableOperationBehavior()]
public string InsertText(string text) {
CurrentText += " " + text;
return CurrentText;
}
[DurableOperationBehavior()]
public string DeleteText(string text)
{
CurrentText = CurrentText.Replace(text, "");
return CurrentText;
}
[DurableOperationBehavior(CompletesInstance=true)]
public void PowerOff()
{
}
}
现在为了能够实现服务实例的序列化,我门还需要配置持久服务。为了那,我门需要用新的WsHttpContextBinding邦定,它允许以http headers的形式来传递状态信息。另外,The SDK包含了一些脚本,这些脚本能够创造存贮持久服务的数据库。这个配置的过程与配置 WF 的持久服务很相似。
<?xml version="1.0"?>
<configuration>
<system.serviceModel>
<services>
<service name="TextComposer" behaviorConfiguration="ServiceBehavior">
<endpoint address="ContextOverHttp" binding="wsHttpContextBinding" bindingConfiguration="DemoBinding" contract="ITextComposer" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehavior">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceCredentials>
<windowsAuthentication allowAnonymousLogons="false" includeWindowsGroups="true"/>
</serviceCredentials>
<persistenceProvider type="System.ServiceModel.Persistence.SqlPersistenceProvider, System.WorkflowServices, Version=3.5.0.0,
culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="DurableServiceStore" persistenceOperationTimeout="00:00:10"
lockTimeout="00:01:00" serializeAsText="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<wsHttpContextBinding>
<binding name="DemoBinding">
<security mode="None" />
</binding>
</wsHttpContextBinding>
</bindings>
</system.serviceModel>
<connectionStrings>
<add name="DurableServiceStore" connectionString="Data Source=localhost\SQLEXPRESS;Initial Catalog=ServiceState;Integrated Security=SSPI"/>
</connectionStrings>
<system.web>
<compilation debug="true">
<assemblies>
<add assembly="System.WorkflowServices, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</assemblies>
</compilation>
</system.web>
</configuration>
现在我门准备创造我门的WCF可户端。基本上跟3.0下的实现差不多,不过有一点不同。
ServiceReference.TextComposerClient proxy = new ClientApp.ServiceReference.TextComposerClient();
using(new OperationContextScope((IContextChannel)proxy.InnerChannel))
{
string text= proxy.PowerOn("Text
");
context = ContextManager.ExtractContextFromChannel(proxy.InnerChannel);
ContextManager.ApplyContextToChannel(Context, proxy.InnerChannel);
text = proxy.InsertText("First line ");
text = proxy.InsertText("Second line");
}
对PowerOn方法的调用回产生如下的 Http-Soap请求 和response 消息。
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://tempuri.org/ITextComposer/PowerOn</a:Action>
<a:MessageID>urn:uuid:4bbd8b6e-8a27-4515-8856-f4e3aa684983</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">http://localhost:1242/TextComposer/Service1.svc/ContextOverHttp</a:To>
</s:Header>
<s:Body>
<PowerOn xmlns="http://tempuri.org/">
<text>Text
</text>
</PowerOn>
</s:Body>
</s:Envelope>

HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Tue, 12 Jun 2007 05:08:27 GMT
X-AspNet-Version: 2.0.50727
Set-Cookie: WscContext=PEFycmF5T2ZLZXlWYWx1ZU9mUU5hbWVzdHJpbmcgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzL…context state
Cache-Control: private
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 559
Connection: Close
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://tempuri.org/ITextComposer/PowerOnResponse</a:Action>
<a:RelatesTo>urn:uuid:4bbd8b6e-8a27-4515-8856-f4e3aa684983</a:RelatesTo>
<Context xmlns="http://schemas.microsoft.com/ws/2006/05/context">
<InstanceId>e32c3fd9-8f56-4024-b818-4f920813d313</InstanceId>
</Context>
</s:Header>
<s:Body>
<PowerOnResponse xmlns="http://tempuri.org/">
<PowerOnResult>Text
</PowerOnResult>
</PowerOnResponse>
</s:Body>
</s:Envelope>

The highlighted lines on figure 4 leverage a helper class for manipulating the service state headers provided as part of the .NET Framework 3.5 SDK. Specifically, the ExtractContectFromChannel operation retrieves the instance key from the SOAP headers .
static public IDictionary<XmlQualifiedName, string> ExtractContextFromChannel(IClientChannel channel)
{ // extract context from channel
IContextManager cm = channel.GetProperty<IContextManager>();
if (cm != null)
{ // attempt to extract context from channel
return cm.GetContext();
}
else if (OperationContext.Current != null)
{ // attempt to extract context from HttpCookie
CookieContainer cookies = new CookieContainer();
if (OperationContext.Current.IncomingMessageProperties.ContainsKey(HttpResponseMessageProperty.Name))
{
HttpResponseMessageProperty httpResponse = (HttpResponseMessageProperty)OperationContext.Current.IncomingMessageProperties[HttpResponseMessageProperty.Name];
cookies.SetCookies(ccUri, httpResponse.Headers[HttpResponseHeader.SetCookie]);
}
if (cookies.Count > 0)
{ // put WscContext cookie into dictionary
Dictionary<XmlQualifiedName, string> newContext = new Dictionary<XmlQualifiedName, string>();
foreach (Cookie cookie in cookies.GetCookies(ccUri))
{
if (cookie.Name.Equals(WscContextKey))
{
newContext.Add(new XmlQualifiedName(WscContextKey), cookie.Value);
break;
}
}
return newContext;
}
}
return null;
}
相似的,ApplyContextToChannel操作添加特定状态到那个操作上下文。
static public bool ApplyContextToChannel(IDictionary<XmlQualifiedName, string> context, IClientChannel channel)
{
if (context != null)
{
IContextManager cm = channel.GetProperty<IContextManager>();
if (cm != null && cm.GetContext() == null)
{ // apply context to ContextChannel
cm.SetContext(context);
return true;
}
else if (OperationContext.Current != null)
{ // apply context as HttpCookie
CookieContainer cookies = new CookieContainer();
foreach (KeyValuePair<XmlQualifiedName, string> item in context)
{
cookies.Add(ccUri, new Cookie(item.Key.ToString(), item.Value));
}
if (cookies.Count > 0)
{
HttpRequestMessageProperty httpRequest = new HttpRequestMessageProperty();
OperationContext.Current.OutgoingMessageProperties.Add(HttpRequestMessageProperty.Name, httpRequest);
httpRequest.Headers.Add(HttpRequestHeader.Cookie, cookies.GetCookieHeader(ccUri));
return true;
}
}
}
return false;
}

简单的总结:WCF服务示例状态能够持久的存贮在存储介质譬如Sql Server并且提供了一定的机制使得在发生譬如断电等意外情况下能够恰当的恢复。这是一个伟大的特性,它能够与一些本身具有持久机制的channels(such as MsMQ or Service Broker with durable messaging)相结合,这在企业级应用中将发挥极大的作用。
本文思想及示例来自 Jesus Rodriguez's Weblog

