Durable Services

Consider the case of a long-running business process or workflow, comprised of multiple execution sequences, that lasts many days or even weeks.

Tip

I use the term workflow to denote a business workflow in general, not one that is necessarily supported by or related to the Windows Workflow Foundation.

Such long-running processes may involve clients (or even end users) that connect to the application, perform a finite amount of work, transition the workflow to a new state, and then disconnect for an indeterminate amount of time before connecting again and continuing to execute the workflow. The clients may at any point also decide to terminate the workflow and start a new one, or the backend service supporting the workflow may end it. Obviously, there is little point in keeping proxies and services in memory waiting for the clients to call. Such an approach will not robustly withstand the test of time; at the very least, timeout issues will inevitably terminate the connection, and there is no easy way to allow machines on both sides to reboot or log off. The need to allow the clients and the services to have independent lifecycles is an important one in a long-running business process, because without it there is no way to enable the clients to connect, perform some work against the workflow, and disconnect. On the host side, over time you may even want to redirect calls between machines.

The solution for long-running services 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 then save the state back to the durable storage at the end of the call. Services that follow this model are called durable services. Since the durable storage can be shared across machines, using durable services also gives you the ability to route calls to different machines at different times, be it for scalability, redundancy, or maintenance purposes.

Durable Services and Instance Management Modes

This approach to state management for durable services is very much like the one proposed previously for per-call services, which proactively manage their state. Using per-call services makes additional sense because there is no point in keeping the instance around between calls if its state is coming from durable storage. The only distinguishing aspect of a durable service compared with a classic per-call service is that the state repository needs to be durable.

While in theory nothing prevents you from basing a durable service on a sessionful or even a singleton service and having that service manage its state in and out of the durable storage, in practice this would be counterproductive. In the case of a sessionful service, you would have to keep the proxy open on the client side for long periods of time, thus excluding clients that terminate their connections and then reconnect. In the case of a singleton service, the very notion of a singleton suggests an infinite lifetime with clients that come and go, so there is no need for durability. Consequently, the per-call instantiation mode offers the best choice all around. Note that with durable per-call services, because the primary concern is long-running workflows rather than scalability or resource management, supporting IDisposable is optional. It is also worth pointing out that the presence of a transport session is optional for a durable service, since there is no need to maintain a logical session between the client and the service. The transport session will be a facet of the transport channel used and will not be used to dictate the lifetime of the instance.

Initiating and terminating

When the long-running workflow starts, the service must first write its state to the durable storage, so that subsequent operations will find the state in the storage. When the workflow ends, the service must remove its state from the storage; otherwise, over time, the storage will become bloated with instance state not required by anyone.

Instance IDs and Durable Storage

Since a new service instance is created for every operation, an 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 connect to the service only occasionally, and client applications or even machines that recycle between calls, as long as the workflow is in progress the client will typically save the instance ID in some durable storage on the client side (such as a file) and provide that ID for every call. When the workflow ends, the client can discard that ID. For an instance ID, it is important to select a type that is serializable and equatable. Having a serializable ID is important because the service will need to save the ID along with its state into the durable storage. Having an equatable ID is required in order to allow the service to obtain the state from the storage. All the .NET primitives (such as int, string, and Guid) qualify as instance IDs.

The durable storage is usually some kind of dictionary that pairs the instance ID with the instance state. The service typically will use a single ID to represent all its state, although more complex relationships involving multiple keys and even hierarchies of keys are possible. For simplicity's sake, I will limit the discussion here to a single ID. In addition, the service often uses a dedicated helper class or a structure to aggregate all its member variables, and stores that type in and retrieves it from the durable storage. Finally, access to the durable storage itself must be thread-safe and synchronized. This is required 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:

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>
{
   protected readonly string Filename;

   public FileInstanceStore(string fileName);

   //Rest of the implementation
}

FileInstanceStore<ID,T> is a general-purpose file-based instance store. FileInstanceStore<ID,T> takes two type parameters: the ID type parameter is constrained to be an equatable type, and the T type parameter represents the instance state. FileInstanceStore<ID,T> verifies at runtime in a static constructor that both T and ID are serializable types.

FileInstanceStore<ID,T> provides a simple indexer allowing you to read and write the instance state to the file. You can also remove an instance state from the file, and check whether the file contains the instance state. These operations are defined in the IInstanceStore<ID,T> interface. The implementation of FileInstanceStore<ID,T> encapsulates a dictionary, and on every access it serializes and deserializes the dictionary to and from the file. When FileInstanceStore<ID,T> is used for the first time, if the file is empty FileInstanceStore<ID,T> will initialize it with an empty dictionary.

Explicit Instance IDs

The simplest way a client can provide the instance ID to the service is as an explicit parameter for every operation designed to access the state. Example 4-9 demonstrates such a client and service, along with the supporting type definitions.

Example 4-9. Passing explicit instance IDs

[DataContract]
class SomeKey : IEquatable<SomeKey>
{...}

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod(SomeKey instanceId);
}

//Helper type used by the service to capture its state
[Serializable]
struct MyState
{...}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   public void MyMethod(SomeKey instanceId)
   {
      GetState(instanceId);
      DoWork(  );
      SaveState(instanceId);
   }
   void DoWork(  )
   {...}

   //Get and set MyState from durable storage
   void GetState(SomeKey instanceId)
   {...}

   void SaveState(SomeKey instanceId)
   {...}
}

To make Example 4-9 more concrete, consider Example 4-10, which supports a pocket calculator with durable memory stored in a file.

Example 4-10. 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
}

In Example 4-10, the filename is available in the properties of the project in the Settings class. 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 locking on the service type. Clearing the memory signals to the calculator the end of the workflow, so it purges its state from the storage.

Instance IDs in Headers

Instead of explicitly passing the instance ID, the client can provide the instance ID in the message headers. Using message headers as a technique for passing out-of-band parameters used for custom contexts is described in detail in Appendix B. In this case, the client can use my HeaderClientBase<T,H> proxy class, and the service can read the ID in the relevant operations using my GenericContext<H> helper class. The service can use GenericContext<H> as-is or wrap it in a dedicated context.

The general pattern for this technique is shown in Example 4-11.

Example 4-11. Passing instance IDs in message headers

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod(  );
}
//Client-side
class MyContractClient : HeaderClientBase<
IMyContract,SomeKey>,IMyContract
{
   public MyContractClient(SomeKey instanceId)
   {}
   public MyContractClient(SomeKey instanceId,string endpointName) :
                                                      base(instanceId,endpointName)
   {}

   //More constructors

   public void MyMethod(  )
   {
      Channel.MyMethod();
   }
}
//Service-side
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   public void MyMethod(  )
   {
      SomeKey instanceId = GenericContext<SomeKey>.Current.Value;
      ...
   }
   //Rest same as Example 4-9
}

Again, to make Example 4-11 less abstract, Example 4-12 shows the calculator using the message headers technique.

Example 4-12. Calculator with instance ID in headers

[ServiceContract]
interface ICalculator
{
   [OperationContract]
   double Add(double number1,double number2);

   /* More arithmetic operations */

   //Memory management operations

   [OperationContract]
   void MemoryStore(double number);

   [OperationContract]
   void MemoryClear(  );
}
//Client-side
class MyCalculatorClient : HeaderClientBase
<ICalculator,string>,ICalculator
{
   public MyCalculatorClient(string instanceId)
   {}

   public MyCalculatorClient(string instanceId,string endpointName) :
                                                      base(instanceId,endpointName)
   {}

   //More constructors

   public double Add(double number1,double number2)
   {
      return Channel.Add(number1,number2);
   }

   public void MemoryStore(double number)
   {
      Channel.MemoryStore(number);
   }

   //Rest of the implementation
}
//Service-side
//If using GenericContext<T> is too raw, can encapsulate:
class CalculatorContext
{
   public static string Id
   {
      get
      {
         return GenericContext<string>.Current.Value ?? 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
}

Context Bindings for Instance IDs

WCF provides dedicated bindings for passing custom context parameters. These bindings, called context bindings, are also explained in Appendix B. Clients can use my ContextClientBase<T> class to pass the instance ID over the context binding protocol. Since the context bindings require a key and a value for every contextual parameter, the clients will need to provide both to the proxy. Using the same IMyContract as in Example 4-11, such a proxy will look like this:

class MyContractClient : Context
ClientBase<IMyContract>,IMyContract
{
   public MyContractClient(string key,string instanceId) : base(key,instanceId)
   {}
   public MyContractClient(string key,string instanceId,string endpointName) :
                                                  base(key,instanceId,endpointName)
   {}

   //More constructors

   public void MyMethod(  )
   {
      Channel.MyMethod(  );
   }
}

Note that the context protocol only supports strings for keys and values. Because the value of the key must be known to the service in advance, the client might as well hardcode the same key in the proxy itself. The service can then retrieve the instance ID using my ContextManager helper class (described in Appendix B). As with message headers, the service can also encapsulate the interaction with ContextManager in a dedicated context class.

Example 4-13 shows the general pattern for passing an instance ID over the context bindings. Note that the proxy hardcodes the key for the instance ID, and that the same ID is known to the service.

Example 4-13. Passing the instance ID over a context binding

//Client-side
class MyContractClient : ContextClientBase<IMyContract>,IMyContract
{
   public MyContractClient(string instanceId) : base("MyKey",instanceId)
   {}

   public MyContractClient(string instanceId,string endpointName) :
                                              base("MyKey",instanceId,endpointName)
   {}

   //More constructors

   public void MyMethod(  )
   {
      Channel.MyMethod(  );
   }
}
//Service-side
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   public void MyMethod(  )
   {
      string instanceId = ContextManager.GetContext("MyKey");

      GetState(instanceId);
      DoWork(  );
      SaveState(instanceId);
   }
   void DoWork(  )
   {...}

   //Get and set state from durable storage
   void GetState(string instanceId)
   {...}

   void SaveState(string instanceId)
   {...}
}

Example 4-14 shows the matching concrete calculator example.

Example 4-14. Calculator with instance ID over context binding

//Client-side
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 void MemoryStore(double number)
   {
      Channel.MemoryStore(number);
   }

   //Rest of the implementation
}
//Service-side
class CalculatorContext
{
   public static string Id
   {
      get
      {
         return ContextManager.GetContext("CalculatorId") ?? String.Empty;
      }
   }
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   //Same as Example 4-12
}

Using the standard ID for context binding

The need to hardcode and know in advance the key used for the instance ID is a liability. The context bindings were designed with durable services in mind, so every context binding always contains an autogenerated instance ID in the form of a Guid (in string format), accessible via the reserved key of instanceId. The client and the service will see the same value for the instance ID. The value 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 interacting with the standard instance ID, I extended ContextManager with ID management methods, properties, and proxy extension methods, as shown in Example 4-15.

Example 4-15. Standard instance ID management with ContextManager

public static class ContextManager
{
   public const string InstanceIdKey = "instanceId";

   public static Guid InstanceId
   {
      get
      {
         string id = GetContext(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(  ));
   }
   public static void SaveInstanceId(Guid instanceId,string fileName)
   {
      using(Stream stream =
                   new FileStream(fileName,FileMode.OpenOrCreate,FileAccess.Write))
      {
         IFormatter formatter = new BinaryFormatter(  );
         formatter.Serialize(stream,instanceId);
      }
   }

   public static Guid LoadInstanceId(string fileName)
   {
      try
      {
         using(Stream stream = new FileStream(fileName,FileMode.Open,
                                              FileAccess.Read))
         {
            IFormatter formatter = new BinaryFormatter(  );
            return (Guid)formatter.Deserialize(stream);
         }
      }
      catch
      {
         return Guid.Empty;
      }
   }
   //More members
}

ContextManager offers the GetInstanceId( ) and SetInstanceId( ) methods to enable the client to read an instance ID from and write it to the context. The service uses the InstanceId read-only property to obtain the ID. ContextManager adds type safety by treating the instance ID as a Guid and not as a string. It also adds error handling.

Finally, ContextManager provides the LoadInstanceId( ) and SaveInstanceId( ) methods to read the instance ID from and write it to a file. These methods are handy on the client side to store the ID between client application sessions against the service.

While the client can use ContextClientBase<T> (as in Example 4-13) to pass the standard ID, it is better to tighten it and provide built-in support for the standard instance ID, as shown in Example 4-16.

Example 4-16. Extending ContextClientBase<T> to support standard IDs

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
}

Example 4-17 shows the calculator client and service using the standard ID.

Example 4-17. Calculator using standard ID

//Client-side
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
   public MyCalculatorClient(  )
   {}
   public MyCalculatorClient(Guid instanceId) : base(instanceId)
   {}
   public MyCalculatorClient(Guid instanceId,string endpointName) :
                                                      base(instanceId,endpointName)
   {}

   //Rest same as Example 4-14
}
//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 nontrivial amount of work by the service—in particular, providing a durable state storage and explicitly managing the instance state against it in every operation. Given the repetitive nature of this work, WCF can automate it for you, and serialize and deserialize the service state on every operation from an indicated state store, using the standard instance ID.

When you let WCF manage your instance state, it follows these rules:

  • If the client does not provide an ID, WCF will create a new service instance by exercising its constructor. After the call, WCF will serialize the instance to the state store.

  • If the client provides an ID to the proxy and the store already contains state matching that ID, WCF will not call the instance constructor. Instead, the call will be serviced on a new instance deserialized out of the state store.

  • 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 provides an ID not found in the state store, WCF will throw an exception.

The durable service behavior attribute

To enable this automatic durable behavior, WCF provides the DurableService behavior attribute, defined as:

public sealed class DurableServiceAttribute : Attribute,IServiceBehavior,...
{...}

You apply this attribute directly on the service class. Most importantly, the service class must be marked either as serializable or as a data contract with the DataMember attribute on all members requiring durable state management:

[Serializable][DurableService]
class MyService : IMyContract
{
   /* Serializable member variables only  */

   public void MyMethod(  )
   {
      //Do work
   }
}

The instance can now manage its state in member variables, just as if it were a regular instance, trusting WCF to manage those members for it. If the service is not marked as serializable (or a data contract), the first call to it will fail once WCF tries to serialize it to the store. Any service relying on automatic durable state management must be configured as per-session, yet it will always behave as a per-call service (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 transport session, but cannot disallow it. These two constraints are verified at service load time.

The durable operation behavior attribute

A service can optionally use the DurableOperation 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;}
}

Setting the CompletesInstance property to true instructs WCF to 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. Example 4-18 demonstrates the use of the CompletesInstance property on the MemoryClear( ) operation of the calculator.

Example 4-18. Using CompletesInstance to remove the state

[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
}

The problem with relying on CompletesInstance is that the context ID is immutable. This means that if the client tries to make additional calls on the proxy after calling an operation for which CompletesInstance is set to true, all of those calls will fail, since the store will no longer contain the instance ID. The client must be aware, therefore, that it cannot continue to use the same 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. One way of enforcing this is to simply close the client program after completing the workflow (or create a new proxy reference). Using the proxy definition of Example 4-17, Example 4-19 shows how to manage the calculator proxy after clearing the memory while seamlessly continuing to use the proxy.

Example 4-19. Resetting the proxy after completing a workflow

class CalculatorProgram
{
   MyCalculatorClient m_Proxy;

   public CalculatorProgram(  )
   {
      Guid calculatorId =
              ContextManager.LoadInstanceId(Settings.Default.CalculatorIdFileName);

      m_Proxy = new MyCalculatorClient(calculatorId);
   }
   public void Add(  )
   {
      m_Proxy.Add(2,3);
   }
   public void MemoryClear(  )
   {
      m_Proxy.MemoryClear(  );

      ResetDurableSession(ref m_Proxy);
   }
   public void Close(  )
   {
      ContextManager.SaveInstanceId(m_Proxy.InstanceId,
                                    Settings.Default.CalculatorIdFileName);
      m_Proxy.Close(  );
   }
   void ResetDurableSession(ref MyCalculatorClient proxy)
   {
      ContextManager.SaveInstanceId(Guid.Empty,
                                    Settings.Default.CalculatorIdFileName);
      Binding binding = proxy.Endpoint.Binding;
      EndpointAddress address = proxy.Endpoint.Address;

      proxy.Close(  );

      proxy = new MyCalculatorClient(binding,address);
   }
}

Example 4-19 uses my ContextManager helper class to load an instance ID and save it to a file. The constructor of the client program creates a new proxy using the ID found in the file. As shown in Example 4-15, if the file does not contain an instance ID, LoadInstanceId( ) returns Guid.Empty. My ContextClientBase<T> is designed to expect an empty GUID for the context ID: if an empty GUID is provided, ContextClientBase<T> constructs itself without an instance ID, thus ensuring a new workflow. After clearing the memory of the calculator, the client calls the ResetDurableSession( ) helper method. ResetDurableSession( ) first saves an empty GUID to the file, and then duplicates the existing proxy. It copies the old proxy's address and binding, closes the old proxy, and sets the proxy reference to a new proxy constructed using the same address and binding as the old one and with an implicit empty GUID for the instance ID.

Programmatic instance management

WCF offers a simple helper class for durable services called DurableOperationContext:

public static class DurableOperationContext
{
   public static void AbortInstance(  );
   public static void CompleteInstance(  );
   public static Guid InstanceId
   {get;}
}

The CompleteInstance( ) method lets the service programmatically (instead of declaratively via the DurableOperation attribute) complete the instance and remove the state from the store once the call returns. AbortInstance( ), on the other hand, cancels any changes made to the store during the call, as if the operation was never called. The InstanceId property is similar to ContextManager.InstanceId.

Persistence providers

While the DurableService attribute instructs WCF when to serialize and deserialize the instance, it does not say anything about where to do so, or, for that matter, provide any information about the state storage. WCF actually uses a bridge pattern in the form of a provider model, which lets you specify the state store separately from the attribute. The attribute is thus decoupled from the store, allowing you to rely on the automatic durable behavior for any compatible storage.

If a service is configured with the DurableService attribute, you must configure its host with a persistence provider factory. The factory derives from the abstract class PersistenceProviderFactory, and it creates a subclass of the abstract class PersistenceProvider:

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 of specifying the persistence provider factory is to include it in the host config file as a service behavior, and to reference that behavior in the service definition:

<behaviors>
   <serviceBehaviors>
      <behavior name = "DurableService">
         <persistenceProvider
            type = "...type...,...assembly ..."
            <!—  Provider-specific parameters  —>
         />
      </behavior>
   </serviceBehaviors>
</behaviors>

Once the host is configured with the persistence provider factory, WCF uses the created PersistenceProvider for every call to serialize and deserialize the instance. If no persistence provider factory is specified, WCF aborts creating the service host.

Custom persistence providers

A nice way to demonstrate how to write a simple custom persistence provider is my FilePersistenceProviderFactory, defined as:

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> class. The constructor of FilePersistenceProviderFactory requires you to specify the desired filename. If no filename is specified, FilePersistenceProviderFactory defaults the filename to Instances.bin.

The key for 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 specified in the provider factory behavior section in the config file. Virtually any free-formed keys and values will work. For example, here's how to specify the filename:

<behaviors>
   <serviceBehaviors>
      <behavior name = "Durable">
         <persistenceProvider
            type = "FilePersistenceProviderFactory,ServiceModelEx"
            fileName = "MyService.bin"
         />
      </behavior>
   </serviceBehaviors>
</behaviors>

The constructor can then use the parameters collection to access these parameters:

string fileName = parameters["fileName"];

The SQL Server persistence provider

WCF ships with a persistence provider, which stores the instance state in a dedicated SQL Server table. After a default installation, the installation scripts for the database are found under C:\Windows\Microsoft.NET\Framework\v3.5\SQL\EN. Note that with the WCF-provided SQL persistence provider 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:

<connectionStrings>
   <add name = "DurableServices"
      connectionString = "..."
      providerName = "System.Data.SqlClient"
   />
</connectionStrings>

<behaviors>
   <serviceBehaviors>
      <behavior name = "Durable">
         <persistenceProvider
            type = "System.ServiceModel.Persistence.SqlPersistenceProviderFactory,
                    System.WorkflowServices,Version=3.5.0.0,Culture=neutral,
                    PublicKeyToken=31bf3856ad364e35"
            connectionStringName = "DurableServices"
         />
      </behavior>
   </serviceBehaviors>
</behaviors>

You can also instruct WCF to serialize the instances as text (instead of the default binary serialization), perhaps for diagnostics or analysis purposes:

<persistenceProvider
   type = "System.ServiceModel.Persistence.SqlPersistenceProviderFactory,
           System.WorkflowServices,Version=3.5.0.0,Culture=neutral,
           PublicKeyToken=31bf3856ad364e35"
   connectionStringName = "DurableServices"
   serializeAsText = "true"
/>

Get Programming WCF Services, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.