Foundations
Managing State With Durable Services
Juval Lowy
Code download available at:MSDN Code Gallery(240 KB)
Contents
Instance ID and Durable Storage
Explicit Instance ID
Context Binding for Instance ID
Standard Key for Context ID
Automatic Durable Behavior
Persistence Providers
Long-running processes are common in distributed computing. Some business processes are made up of multiple execution sequences which may last many days or even weeks. They may involve clients (or even end users) that connect to the application, perform a small amount of work, transition the workflow to a new state, and then disconnect for an indeterminate amount of time before connecting again and continuing the workflow. The clients may at any point decide to terminate the workflow and start a new one, or the back-end workflow service may end it. With this intermittent flow, do you keep proxies and services in memory waiting for the clients to call?
In this column I will discuss several techniques for supporting such long-running services and answer some of these common questions. I will also present the accompanying Windows Communication Foundation (WCF) facilities and a few helper classes I have developed.
Instance ID and Durable Storage
With long-running services, there is obviously little point in keeping proxies and services in memory while waiting around. Such an approach will not withstand the test of time; at the very least, timeout issues will eventually cause the connection to terminate, and there is no easy way to allow machines on either side to reboot or log off. The solution is to avoid keeping the service state in memory, and to handle each call on a new instance with its own temporary in-memory state. For every operation, the service should retrieve its state from some durable storage (such as a file or a database), perform the requested unit of work for that operation, and save the state back to the durable storage at the end of the call. A service that operates in this manner is called a durable service. Because the storage can be shared, durable services can also scale across multiple machines, be it for scalability, redundancy or maintenance purposes.
Before the actual work of the long-running process begins, the service must write its state to the durable storage so that subsequent operations will be able to find it. When the workflow ends, the service must remove its state; otherwise, over time the store will become bloated with unnecessary state information.
Since a new service instance is created for every operation, the instance must have a way of looking up and loading its state from the durable storage. The client must therefore provide some state identifier for the instance. That identifier is called the instance ID. To support clients that occasionally connect to the service and client applications or even machines that recycle between calls, the client will typically save the instance ID in its own durable storage and provide that ID for every call. When the workflow ends, the client can discard that ID. It is important that your instance ID is serializable and equatable. The ID must be serializable because the service will need to save the ID along with its state into the durable storage. It must be equatable so the service can look up the state from the storage.
The durable storage is usually some kind of a dictionary that pairs the instance ID with the instance state. The service that uses it typically uses a single data structure to store all of its state rather than storing individual instance members with unique IDs. The service often then uses a dedicated helper class or a structure to aggregate all its member variables, and stores and retrieves that type from the durable storage in one operation. Access to the durable storage itself must be thread-safe and synchronized because multiple instances may try to access and modify the store concurrently.
To help you implement and support simple durable services, I wrote the FileInstanceStore<ID,T> class that you can see in Figure 1 . FileInstanceStore<ID,T> is a general-purpose, file-based instance store that takes two type parameters: the ID type parameter, which is constrained to be an equable type, and the T type parameter, which represents the instance state. Additionally both types must be serializable.
Figure 1 FileInstanceStore
public interface IInstanceStore<ID,T> where ID : IEquatable<ID>
{
void RemoveInstance(ID instanceId);
bool ContainsInstance(ID instanceId);
T this[ID instanceId]
{get;set;}
}
public class FileInstanceStore<ID,T> : IInstanceStore<ID,T>
where ID : IEquatable<ID>
{
public FileInstanceStore(string fileName);
//Rest of the implementation
}
FileInstanceStore<ID,T> provides a simple indexer, allowing you to read and write the instance state to the file. You can also check for the presence of the instance state in the file or remove it.
Explicit Instance ID
The simplest way a client can provide the instance ID to the service is as an explicit parameter for every operation that will access the state. Figure 2 demonstrates such a client and service. The service is a per-call pocket calculator with durable memory stored in a file.
Figure 2 Calculator with Explicit Instance ID
[ServiceContract]
interface ICalculator
{
[OperationContract]
double Add(double number1,double number2);
/* More arithmetic operations */
//Memory management operations
[OperationContract]
void MemoryStore(string instanceId,double number);
[OperationContract]
void MemoryClear(string instanceId);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
static IInstanceStore<string,double> Memory = new
FileInstanceStore<string,double> (Settings.Default.MemoryFileName);
public double Add(double number1,double number2)
{
return number1 + number2;
}
public void MemoryStore(string instanceId,double number)
{
lock(typeof(MyCalculator))
{
Memory[instanceId] = number;
}
}
public void MemoryClear(string instanceId)
{
lock(typeof(MyCalculator))
{
Memory.RemoveInstance(instanceId);
}
}
//Rest of the implementation
}
All instances of the calculator use the same static memory in the form of a FileInstanceStore<string,double>. The calculator synchronizes access to the memory in every operation across all instances by using a lock on the service type. Clearing the memory signals the end of the workflow, so the calculator purges its state from storage.
Context Binding for Instance ID
Instead of explicit parameters, you could use the dedicated context bindings introduced with the Microsoft .NET Framework 3.5. These bindings, BasicHttpContextBinding, NetTcpContextBinding, and WSHttpContextBinding, are to be found in the System.WorkflowServices.dll assembly. These context bindings each derive from their regular respective protocol-specific bindings, yet add support for a dedicated context management protocol. These bindings can be used with or without a context.
The context management protocol lets you pass (in the message headers) a dictionary of pairs of key/value strings, referred to as a context. This represents your application-specific execution context, which is analogous to the built-in security and operation contexts. With a context binding you can only set the context to pass to the service once, before opening the proxy (or using it for the first time). After that, the dictionary is cached, and any attempt to modify it will result in an error. The client sets the context to send to the service using the IContextManager interface, defined as:
public interface IContextManager
{
IDictionary<string,string> GetContext();
void SetContext(IDictionary<string,string> context);
//More members
}
The client obtains the reference to the IContextManager interface by accessing the proxy's InnerChannel property, which supports the IChannel interface with the GetProperty<T>() method:
[ServiceContract]
interface IMyContract
{
[OperationContract]
void MyMethod();
}
class MyContractClient : ClientBase<IMyContract>,IMyContract
{...}
MyContractClient proxy = new MyContractClient();
IContextManager contextManager =
proxy.InnerChannel.GetProperty<IContextManager>();
Once the client obtains IContextManager, it can clone the current context by calling the GetContext method. Since the dictionary returned from GetContext is a copy of the actual context, the client cannot use it to change the context. Instead, the client needs to call the SetContext method and provide the new context. The client can override the old context or just add values to it and then set the context again, as shown in Figure 3 .
Figure 3 Setting the Context on the Proxy
MyContractClient proxy = new MyContractClient();
IContextManager contextManager =
proxy.InnerChannel.GetProperty<IContextManager>();
IDictionary<string,string> context = contextManager.GetContext();
context["NumberContext"] = "123";
contextManager.SetContext(context);
proxy.MyMethod();
proxy.Close();
The service reads the context values from the incoming message properties, accessed via the operation context:
public sealed class OperationContext : ...
{
public MessageProperties IncomingMessageProperties
{
get;
}
//More members
}
MessageProperties is a non-type safe dictionary that accepts a string key and returns the matching object value:
public sealed class MessageProperties : IDictionary<string,object>
{...}
To obtain the context property, the service uses the static string ContextMessageProperty.Name. This then returns an object of the type ContextMessageProperty, which is defined like so:
[Serializable]
public class ContextMessageProperty : IMessageProperty
{
public IDictionary<string,string> Context
{get;}
public static string Name
{get;}
//More members
}
The Context property of ContextMessageProperty is the same dictionary of parameters passed by the client. Figure 4 shows the required service-side steps to read the number context passed in Figure 3 . You can streamline the steps required by the client to write to the context using my ContextManager static helper class, which is shown in Figure 5 .
Figure 4 Reading the Context by the Service
class MyService : IMyContract
{
public void MyMethod()
{
ContextMessageProperty contextProperty =
OperationContext.Current
.IncomingMessageProperties[ContextMessageProperty.Name]
as ContextMessageProperty;
string number = contextProperty.Context["NumberContext"];
Debug.Assert(number == 123);
}
}
Figure 5 Client-Side Methods of ContextManager
public static class ContextManager
{
public static void SetContext(IClientChannel innerChannel,
string key, string value) {
SetContext(innerChannel, UpdateContext(innerChannel, key, value));
}
public static void SetContext(IClientChannel innerChannel,
IDictionary<string, string> context) {
Debug.Assert((innerChannel as ICommunicationObject).State !=
CommunicationState.Opened);
IContextManager contextManager =
innerChannel.GetProperty<IContextManager>();
contextManager.SetContext(context);
}
public static IDictionary<string, string> UpdateContext(
IClientChannel innerChannel, string key, string value) {
IContextManager contextManager =
innerChannel.GetProperty<IContextManager>();
IDictionary<string, string> context =
new Dictionary<string, string>(contextManager.GetContext());
context[key] = value;
return context;
}
//Proxy extensions
public static void SetContext<T>(this ClientBase<T> proxy,
IDictionary<string, string> context) where T : class {
SetContext(proxy.InnerChannel, context);
}
public static void SetContext<T>(this ClientBase<T> proxy,
string key, string value) where T : class {
SetContext(proxy.InnerChannel, key, value);
}
//More helper methods
}
ContextManager offers overloaded versions of the SetContext methods, allowing the client to set a new context on a proxy's inner channel, using a single key/value pair or a collection of pairs using a dictionary. These methods are useful with both a proxy class and a channel factory. ContextManager also allows you to set the context as a proxy class extension. You can use the CreateContext method to create a new dictionary or add a key/value pair to an existing context. By using ContextManager, Figure 3 is reduced to:
MyContractClient proxy = new MyContractClient();
proxy.SetContext("NumberContext","123");
proxy.MyMethod();
proxy.Close();
However, relying on SetContext this way requires you to explicitly use it with every instantiation of the proxy. It is better to encapsulate ContextManager in a dedicated proxy class, such as my ContextClientBase<T> proxy class:
public abstract class ContextClientBase<T> : ClientBase<T>
where T : class
{
public ContextClientBase();
public ContextClientBase(string endpointName);
public ContextClientBase(string key,string value);
public ContextClientBase(IDictionary<string,string> context);
public ContextClientBase(string key,string value,
string endpointName);
public ContextClientBase(IDictionary<string,string> context,
string endpointName);
//More constructors
}
The constructors of ContextClientBase<T> accept the usual proxy parameters such as endpoint name or binding and address, but in addition accept contextual parameters to send to the service: either a single key/value pair or a collection of them using a dictionary. Your proxy can derive directly from ContextClientBase<T>:
class MyContractClient : ContextClientBase<IMyContract>,IMyContract
{
public MyContractClient(string key,string value) : base(key,value)
{}
/* More constructors */
public void MyMethod()
{
Channel.MyMethod();
}
}
Using ContextClientBase<T>, Figure 3 is reduced to:
MyContractClient proxy = new MyContractClient("NumberContext","123");
proxy.MyMethod();
proxy.Close();
Figure 6 shows the implementation of ContextClientBase<T>.
Figure 6 Implementing ContextClientBase<T>
public abstract class ContextClientBase<T> : ClientBase<T>
where T : class
{
public ContextClientBase(string key,string value,string endpointName):
this(ContextManager.CreateContext(key,value),endpointName){}
public ContextClientBase(IDictionary<string,string> context, string endpointName) : base(endpointName){
SetContext(context);
}
/* More constructors */
void SetContext(IDictionary<string,string> context){
VerifyContextBinding();
ContextManager.SetContext(InnerChannel,context);
}
void VerifyContextBinding(){
BindingElementCollection elements = Endpoint.Binding.CreateBindingElements();
if(elements.Contains(typeof(ContextBindingElement))){
return;
}
throw new InvalidOperationException("Can only use context binding");
}
}
A few of the constructors of ContextClientBase<T> use ContextManager to create a new context and pass it to another constructor, which calls the SetContext helper method. SetContext first verifies that the binding used is indeed a context binding, and then it uses ContextManager to set the context. Verifying that the binding supports the context protocol is done by searching for the ContextBindingElement in the collection of binding elements. This way of verifying is better than looking at the binding type, since it also works automatically with a custom context binding.
For the service, the ContextManager helper class can encapsulate the interaction between operation context and message properties. ContextManager provides the GetValue method:
public static class ContextManager
{
public static string GetValue(string key);
//More members
}
Using GetValue, the service code in Figure 4 is reduced to:
class MyService : IMyContract
{
public void MyMethod()
{
string number = ContextManager.GetValue("NumberContext");
Debug.Assert(number == "123");
}
}
Figure 7 shows the implementation of GetValue with some of the error handling code removed.
Figure 7 Implementing GetValue
public static class ContextManager
{
public static string GetValue(string key)
{
if(OperationContext.Current == null)
return null;
ContextMessageProperty contextProperty =
OperationContext.Current.IncomingMessageProperties
[ContextMessageProperty.Name]
as ContextMessageProperty;
if(contextProperty.Context.ContainsKey(key) == false)
return null;
return contextProperty.Context[key];
}
}
GetValue is similar to the explicit steps taken in Figure 4 , except it adds state and error management. If the context does not contain the request key (or no context at all was found) GetValue returns null.
Clients can use ContextClientBase<T> to pass the instance ID over the client context binding protocol. Since the context bindings require a key and a value for every contextual parameter, the client needs to provide both to the proxy. Because the value of the key must be known to the service in advance, the client might as well hard-code the key in the proxy itself. The service will retrieve the instance ID in the relevant operations using ContextManager.
Figure 8 shows the calculator service using the context binding. In the figure, the service encapsulates the interaction with ContextManager in a dedicated context class.
Figure 8 Calculator with Instance ID over Context Binding
//Client Side:
[ServiceContract]
interface ICalculator
{
[OperationContract]
double Add(double number1,double number2);
[OperationContract]
void MemoryStore(double number);
[OperationContract]
void MemoryClear();
}
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
public MyCalculatorClient(string instanceId) : base("CalculatorId",instanceId)
{}
public MyCalculatorClient(string instanceId,string endpointName):
base("CalculatorId",instanceId,endpointName)
{}
//More constructors
public double Add(double number1,double number2)
{
return Channel.Add(number1,number2);
}
public double MemoryStore (double number)
{
return Channel.MemoryStore (number);
}
//Rest of the implementation
}
//Service Side:
class CalculatorContext
{
public static string Id
{
get
{
return ContextManager.GetValue("CalculatorId") ?? String.Empty;
}
}
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
static IInstanceStore<string,double> Memory = new
FileInstanceStore<string,double>(Settings.Default.MemoryFileName);
public double Add(double number1,double number2)
{
return number1 + number2;
}
public void MemoryStore(double number)
{
lock(typeof(MyCalculator))
{
Memory[CalculatorContext.Id] = number;
}
}
public void MemoryClear()
{
lock(typeof(MyCalculator))
{
Memory.RemoveInstance(CalculatorContext.Id);
}
}
//Rest of the implementation
}
Standard Key for Context ID
The need to hard-code the key used for the instance ID and the need to know it in advance are liabilities. As such, the durable service dispatcher logic automatically creates an instance ID in the form of a Guid (in string format), accessible via the reserved key named instanceId. The client and the service will see the same value for the instance ID. The value of the instance ID is initialized once the first call on the proxy returns, after the binding has had the chance to correlate it between the client and the service. Like any other parameter passed over a context binding, the value of the instance ID is immutable throughout the life of the proxy. To streamline interaction with the standard instance ID, I extended ContextManager with ID management methods, properties, and proxy extensions, as you can see in Figure 9 .
Figure 9 Standard Instance ID Management
public static class ContextManager
{
public const string InstanceIdKey = "instanceId";
public static Guid InstanceId
{
get
{
string id = GetValue(InstanceIdKey) ?? Guid.Empty.ToString();
return new Guid(id);
}
}
public static Guid GetInstanceId(IClientChannel innerChannel)
{
try
{
string instanceId = innerChannel.GetProperty<IContextManager>()
.GetContext()[InstanceIdKey];
return new Guid(instanceId);
}
catch(KeyNotFoundException)
{
return Guid.Empty;
}
}
public static void SetInstanceId(IClientChannel innerChannel,
Guid instanceId)
{
SetContext(innerChannel,InstanceIdKey,instanceId.ToString());
}
//More members
}
ContextManager offers the GetInstanceId and SetInstanceId methods for the client to read or write an instance ID to the context. The InstanceId read-only property is used by the service to obtain the ID. ContextManager adds type safety by treating the instance ID as a Guid rather than a string, and adds error handling as well.
While the client can use ContextClientBase<T> as in Figure 8 to pass the standard ID, it is better to tighten it and provide built-in support on ContextClientBase<T> for the standard instance ID, as shown in Figure 10 . Figure 11 shows the durable calculator client and service using the standard ID.
Figure 10 ContextClientBase<T> Support for Standard ID
public abstract class ContextClientBase<T> : ClientBase<T>
where T : class
{
public Guid InstanceId
{
get
{
return ContextManager.GetInstanceId(InnerChannel);
}
}
public ContextClientBase(Guid instanceId) :
this(ContextManager.InstanceIdKey,instanceId.ToString())
{}
public ContextClientBase(Guid instanceId,string endpointName) :
this(ContextManager.InstanceIdKey,instanceId.
ToString(),endpointName)
{}
//More constructors
}
Figure 11 Calculator Using Standard ID
//Client Side:
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
public MyCalculatorClient()
{}
public MyCalculatorClient(Guid instanceIdId) : base(instanceId)
{}
public MyCalculatorClient(Guid instanceId,string endpointName) :
base(instanceId,endpointName)
{}
//Rest same as Figure 8
}
//Service Side:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
static IInstanceStore<Guid,double> Memory = new
FileInstanceStore<Guid,double>(Settings.Default.MemoryFileName);
public double Add(double number1,double number2)
{
return number1 + number2;
}
public void MemoryStore(double number)
{
lock(typeof(MyCalculator))
{
Memory[ContextManager.InstanceId] = number;
}
}
public void MemoryClear()
{
lock(typeof(MyCalculator))
{
Memory.RemoveInstance(ContextManager.InstanceId);
}
}
//Rest of the implementation
}
Automatic Durable Behavior
All the techniques shown so far for durable services require a non-trivial amount of work by the service—mostly to provide a durable state storage and explicitly manage the instance state against it in every operation. Given the repetitive nature of this work, WCF can automate it for you, serializing and deserializing the service state on every operation from an indicated state store, using the standard context ID. When the client provides a valid ID, for every operation WCF will deserialize an instance out of the store, call the operation, and serialize the new state modified by the operation back to the store. If the client does not provide an ID, WCF will create a new service instance by calling its constructor, but immediately following will serialize the instance to the state store.
WCF will never call the instance constructor if the store already contains state matching the ID provided to the proxy by the client. Instead, the call will be serviced on a new instance deserialized out of the state store. If the client provides an ID not found in the state store, WCF will throw an exception.
WCF provides the DurableService behavior attribute, which is defined as:
public sealed class DurableServiceAttribute :
Attribute,IServiceBehavior,...
{...}
You apply this attribute directly on the service class. Most importantly, the service class itself must be marked either as Serializable or as a data contract with respective DataMember attributes on all members requiring durable state management.
The instance can manage its state in member variables, just as if it were a regular instance, and it can trust WCF to manage its members for it, as shown in Figure 12 .
Figure 12 Using DurableService Behavior
[Serializable]
[DurableService]
class MyCalculator : ICalculator
{
double Memory
{get;set;}
public double Add(double number1,double number2)
{
return number1 + number2;
}
public void MemoryStore(double number)
{
Memory = number;
}
[DurableOperation(CompletesInstance = true)]
public void MemoryClear()
{
Memory = 0;
}
//Rest of the implementation
}
Any service relying on automatic durable state management must be configured as per-session, yet will always behave as per-call (WCF uses context deactivation after every call). In addition, the service must use one of the context bindings with every endpoint to enable the standard instance ID and the contract must allow or require a session, but cannot disallow it. These two constraints are verified at service load time.
The service can optionally use the DurableOperation operation behavior attribute to instruct WCF to purge its state from the store at the end of the workflow:
[AttributeUsage(AttributeTargets.Method)]
public sealed class DurableOperationAttribute : Attribute,...
{
public bool CanCreateInstance
{get;set;}
public bool CompletesInstance
{get;set;}
}
By setting the CompletesInstance property to true, WCF will remove the instance ID from the store once the operation call returns. The default value of the CompletesInstance property is false. In case the client does not provide an instance ID, you can also prevent an operation from creating a new instance by setting the CanCreateInstance property to false. Figure 12 demonstrates the use of the CompletesInstance property on the calculator's MemoryClear operation.
The problem with relying on CompletesInstance is that the context ID is immutable. If the client tries to make another call on the proxy after calling an operation marked with CompletesInstance set to true, then the call will fail, since the store no longer contains the instance ID.
The client must be aware that it cannot continue to use the proxy. If the client wants to make further calls against the service, it must do so on a new proxy that does not have an instance ID yet, and by doing so, the client will start a new workflow. To enforce this you can simply close the client program after completing the workflow or create a new proxy reference.
Persistence Providers
While the DurableService attribute instructs WCF when to serialize and deserialize the instance, it says nothing about where to do so. Rather, WCF uses a bridge pattern in the form of a provider model, which lets you specify the state store separately from the attribute. By doing so the attribute is decoupled from the store, allowing you to use automatic durable behavior with any compatible storage medium.
If the service is configured with the DurableService attribute, then you must configure its host with a persistence provider factory. The factory derives from the abstract class PersistenceProviderFactory and creates a subclass of the abstract class PersistenceProvider, as you see in Figure 13 .
Figure 13 PersistenceProviderFactory
public abstract class PersistenceProviderFactory : CommunicationObject
{
protected PersistenceProviderFactory();
public abstract PersistenceProvider CreateProvider(Guid id);
}
public abstract class PersistenceProvider : CommunicationObject
{
protected PersistenceProvider(Guid id);
public Guid Id
{get;}
public abstract object Create(object instance,TimeSpan timeout);
public abstract void Delete(object instance,TimeSpan timeout)
public abstract object Load(TimeSpan timeout);
public abstract object Update(object instance,TimeSpan timeout);
//Additional members
}
The most common way to specify the persistence provider factory is to indicate it in the host .config file (as a service behavior), and also to reference that behavior in the service definition, like the following:
<behaviors>
<serviceBehaviors>
<behavior name = "DurableService">
<persistenceProvider
type = "...type...,...assembly ..."
<!-- Provider-specific parameters -->
/>
</behavior>
</serviceBehaviors>
</behaviors>
If no persistence provider factory is specified, WCF aborts creation of the service host. Once the host is configured with the persistence provider factory, WCF uses the created PersistenceProvider for every call to serialize and deserialize the instance. A simple persistence provider factory is my FilePersistenceProviderFactory:
public class FilePersistenceProviderFactory : PersistenceProviderFactory
{
public FilePersistenceProviderFactory();
public FilePersistenceProviderFactory(string fileName);
public FilePersistenceProviderFactory(
NameValueCollection parameters);
}
public class FilePersistenceProvider : PersistenceProvider
{
public FilePersistenceProvider(Guid id,string fileName);
}
FilePersistenceProvider wraps my FileInstanceStore<ID,T>. The constructor of FilePersistenceProviderFactory requires you to specify the desired file name. If no file name is specified, FilePersistenceProviderFactory defaults to Instances.bin.
The key to using a custom persistence factory in a .config file is to define a constructor that takes a NameValueCollection of parameters. These parameters are simple text-formatted pairs of the keys and values that are specified in the provider factory behavior section in the .config file. Virtually any free-formed key and value will work. For example, in order to specify what the file name is, use the following:
<persistenceProvider type =
"FilePersistenceProviderFactory,ServiceModelEx"
fileName = "MyService.bin"
/>
The constructor can then use the parameters collection to access these parameters:
string fileName = parameters["fileName"];
WCF ships with a persistence provider that stores the instance state in a dedicated SQL Server table. After a default installation of Visual Studio 2008, the installation scripts for the database are found under C:\Windows\Microsoft.NET\Framework\v3.5\SQL\EN. Note that you can only use SQL Server 2005 or SQL Server 2008 for state storage.
The SQL provider comes in the form of SqlPersistenceProviderFactory and SqlPersistenceProvider found in the System.WorkflowServices assembly under the System.ServiceModel.Persistence namespace. All you need to do is specify the SQL provider factory and the connection string name from the application settings, as shown in Figure 14 .
Figure 14 Specifying SQL Provider and Connection String
<connectionStrings>
<add name = "DurableServices"
connectionString = "..."
providerName = "System.Data.SqlClient"
/>
</connectionStrings>
<persistenceProvider
type = "System.ServiceModel.Persistence.SqlPersistenceProviderFactory,
System.WorkflowServices,Version=3.5.0.0,Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
connectionStringName = "DurableServices"
/>
You can also instruct WCF to serialize the instances as text (instead of using the default binary serialization), perhaps for diagnostics or analysis purposes, by setting the serializeAsText parameter to true:
<persistenceProvider
serializeAsText = "True"
...
/>
Send your questions and comments to mmnet30@microsoft.com .
Juval Lowy is a software architect with IDesign, providing WCF training and architecture consulting. His recent book is Programming WCF Services, Second Edition . He is also the Microsoft Regional Director for the Silicon Valley. Contact Juval at www.idesign.net .