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.
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.
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.
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.
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.Remove
Instance(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.
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 :Header
ClientBase< 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 }
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 }
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 }
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.
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.
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.
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
.
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.
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"];
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.