Consider the case of a long-running business process or workflow, comprised of multiple execution sequences, that lasts many days or even weeks.
Note
I use the term workflow to denote a business workflow in general, not one that is necessarily supported by or related to the product called Windows Workflow.
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 states 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 of 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 File
Instance
Store
<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. File
Instance
Store<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. FileInstance
Store<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 IInstance
Store<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: static 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 static 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 Completes
Instance
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 Can
Create
Instance
property to false
. Example 4-18 demonstrates
the use of the Completes
Instance
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 Persistence
Provider
Factory
, and it creates a subclass of the abstract class
Persistence
Provider
:
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 File
PersistenceProviderFactory
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
in
C:\Windows\Microsoft.NET\Framework\v4.0.30316\SQL\EN.
Note that with the WCF-provided SQL persistence provider you can
only use SQL Server 2005, SQL Server 2008, or later for state
storage. The SQL provider comes in the form of Sql
Persistence
ProviderFactory
and SqlPersistenceProvider
, found in
the System.
Workflow
Services
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=4.0.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=4.0.0.0,Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
connectionStringName = "DurableServices"
serializeAsText = "true"
/>
Get Programming WCF Services, 3rd 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.