Chapter 4. Instance Management
Instance management is my name for the set of techniques WCF uses to bind client requests to service instances, governing which service instance handles which client request, and when. Instance management is necessary because of the extent to which applications differ in their needs for scalability, performance, throughput, durability, transactions, and queued calls—there simply isn’t a one-size-fits-all solution. However, there are a few canonical instance management techniques that are applicable across the range of applications, thus enabling a wide variety of scenarios and programming models. These techniques are the subject of this chapter, and understanding them is essential to developing scalable and consistent applications. WCF supports three types of instance activation: per-call services allocate (and destroy) a new service instance for each client request; sessionful services allocate a service instance for each client connection; and singleton services share the same service instance for all clients, across all connections and activations. This chapter provides the rationale for each of these instance management modes, and offers guidelines on when and how to best use them. It also addresses some related topics, such as behaviors, contexts, demarcating operations, instance deactivation, durable services, and throttling.1
Behaviors
By and large, the service instance mode is strictly a service-side implementation detail that should not manifest itself on the client side in any way. To support that and a few other local service-side aspects, WCF defines the notion of behaviors. A behavior is a local attribute of the service or the client that does not affect its communication patterns. Clients should be unaware of service behaviors, and behaviors do not manifest themselves in the service’s binding or published metadata. You have already seen two service behaviors in the previous chapters: Chapter 1 uses the service metadata behavior to instruct the host to publish the service’s metadata over HTTP-GET or to implement the MEX endpoint, and Chapter 3 uses the service behavior to ignore the data object extension. No client can ever tell simply by examining the communication and the exchanged messages if the service is ignoring the data object extension or who published its metadata.
WCF defines two types of declarative service-side behaviors, governed by two corresponding attributes. The ServiceBehaviorAttribute
is used to configure service behaviors; that is, behaviors that affect all endpoints (all contracts and operations) of the service. Apply the ServiceBehavior
attribute directly on the service implementation class.
Use the OperationBehaviorAttribute
to configure operation behaviors; that is, behaviors that affect only the implementation of a particular operation. The OperationBehavior
attribute can be applied only on a method that implements a contract operation, never on the operation definition in the contract itself. You will see the use of OperationBehavior
attribute later in this chapter and in subsequent chapters as well.
In the context of this chapter, the ServiceBehavior
attribute is used to configure the service instance mode. As shown in Example 4-1, the attribute defines the InstanceContextMode
property of the enum type InstanceContextMode
. The value of the InstanceContextMode
enum controls which instance mode is used for the service.
Example 4-1. ServiceBehaviorAttribute used to configure the instance context mode
public enum InstanceContextMode { PerCall, PerSession, Single } [AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute,... { public InstanceContextMode InstanceContextMode {get;set;} //More members }
The enum is correctly called InstanceContextMode
rather than InstanceMode
because it actually controls the instantiation mode of the context hosting the instance, rather than that of the instance itself (recall from Chapter 1 that the instance context is the innermost execution scope of the service). By default, however, the instance and its context are treated as a single unit, so the enum does control the life of the instance as well. You will see later in this chapter and in subsequent chapters how (and when) you can disengage the two, and for what purposes.
Per-Call Services
When the service type is configured for per-call activation, a service instance (the CLR object) exists only while a client call is in progress. Every client request (that is, a method call on the WCF contract) gets a new dedicated service instance. The following list explains how per-call activation works, and the steps are illustrated in Figure 4-1:
The client calls the proxy and the proxy forwards the call to the service.
WCF creates a new context with a new service instance and calls the method on it.
When the method call returns, if the object implements
IDisposable
, WCF callsIDisposable.Dispose()
on it. WCF then destroys the context.The client calls the proxy and the proxy forwards the call to the service.
WCF creates an object and calls the method on it.
Disposing of the service instance is an interesting point. As I just mentioned, if the service supports the IDisposable
interface, WCF will automatically call the Dispose()
method, allowing the service to perform any required cleanup. Note that Dispose()
is called on the same thread that dispatched the original method call, and that Dispose()
has an operation context (presented later). Once Dispose()
is called, WCF disconnects the instance from the rest of the WCF infrastructure, making it a candidate for garbage collection.
Benefits of Per-Call Services
In the classic client/server programming model, using languages such as C++ or C#, every client gets its own dedicated server object. The fundamental problem with this approach is that it doesn’t scale well. Imagine an application that has to serve many clients. Typically, these clients create the objects they need when the client application starts and dispose of them when the client application shuts down. What impedes scalability with the client/server model is that the client applications can hold onto objects for long periods of time, while actually using them for only a fraction of that time. Those objects may hold expensive or scarce resources, such as database connections, communication ports, or files. If you allocate an object for each client, you will tie up such crucial and/or limited resources for long periods, and you will eventually run out of resources.
A better activation model is to allocate an object for a client only while a call is in progress from the client to the service. That way, you have to create and maintain in memory only as many objects as there are concurrent calls, not as many objects as there are outstanding clients. My personal experience indicates that in a typical Enterprise system, especially one that involves users, at most 1% of all clients make concurrent calls (in a high-load Enterprise system, that figure rises to 3%). Thus, if your system can concurrently sustain 100 expensive service instances, it can still typically serve as many as 10,000 outstanding clients. This is precisely the benefit the per-call instance activation mode offers. In between calls, the client holds a reference on a proxy that doesn’t have an actual object at the end of the wire. This means that you can dispose of the expensive resources the service instance occupies long before the client closes the proxy. By that same token, acquiring the resources is postponed until they are actually needed by a client.
Keep in mind that creating and destroying a service instance repeatedly on the service side without tearing down the connection to the client (with its client-side proxy) is a lot cheaper than repeatedly creating an instance and a connection. The second benefit is that forcing the service instance to reallocate or connect to its resources on every call caters very well to transactional resources and transactional programming (discussed in Chapter 7), because it eases the task of enforcing consistency with the instance state. The third benefit of per-call services is that they can be used in conjunction with queued disconnected calls (described in Chapter 9), because they allow easy mapping of service instances to discrete queued messages.
Configuring Per-Call Services
To configure a service type as a per-call service, you apply the ServiceBehavior
attribute with the InstanceContextMode
property set to InstanceContextMode.PerCall
:
[ServiceContract] interface IMyContract {...} [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract {...}
Example 4-2 lists a simple per-call service and its client. As you can see from the program output, for each client method call a new service instance is constructed.
Example 4-2. Per-call service and client
///////////////////////// Service Code ///////////////////// [ServiceContract] interface IMyContract { [OperationContract] void MyMethod(); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract,IDisposable { int m_Counter = 0; MyService() { Trace.WriteLine("MyService.MyService()"); } public void MyMethod() { m_Counter++; Trace.WriteLine("Counter = " + m_Counter); } public void Dispose() { Trace.WriteLine("MyService.Dispose()"); } } ///////////////////////// Client Code ///////////////////// MyContractClient proxy = new MyContractClient(); proxy.MyMethod(); proxy.MyMethod(); proxy.Close(); //Possible output MyService.MyService() Counter = 1 MyService.Dispose() MyService.MyService() Counter = 1 MyService.Dispose()
Per-Call Services and Transport Sessions
The use of a per-call service is independent from the presence of a transport session (described in Chapter 1). A transport session correlates all messages from a particular client to a particular channel. If the service is configured for per-call instantiation, there can still be a transport session, but for every call WCF will create a new context used just for that call. If transport-level sessions are not used, as you will see later, the service always behaves as a per-call service, regardless of its configuration.
If the per-call service has a transport session, communication from the client is subjected to the inactivity timeout of the transport session (which defaults to 10 minutes). Once the timeout has expired, the client can no longer use the proxy to invoke operations on the per-call service, since the transport session has ended.
The biggest effect transport sessions have on per-call services is that when the service is configured for single-threaded access (the WCF default, explained in Chapter 8), the transport session enforces a lock-step execution, where calls to the per-call service from the same proxy are serialized. That is, even if the client issues the calls concurrently, they are executed against different instances, one at a time, in order. This has particular implications for disposing of the instance. WCF does not block the client while it disposes of the service instance. However, if during the call to Dispose()
the client has issued a second call, that call will be allowed to access a new instance only after Dispose()
has returned. For example, the output at the end of Example 4-2 represents a case where there is a transport session, since the second call can only execute once Dispose()
has returned. If Example 4-2 had no transport session, you might end up with the same output but also an out-of-order invocation where Dispose()
is nonblocking, such as:
MyService.MyService() Counter = 1 MyService.MyService() Counter = 1 MyService.Dispose() MyService.Dispose()
Designing Per-Call Services
Although in theory you can use the per-call instance activation mode on any service type, in practice you need to design the service and its contracts to support this mode from the ground up. The main problem is that the client doesn’t know it’s getting a new instance each time it makes a call. Per-call services must be state-aware; that is, they must proactively manage their state, giving the client the illusion of a continuous session. A state-aware service isn’t the same as a stateless service. In fact, if the per-call service were truly stateless, there would be no need for per-call activation in the first place. It is precisely because it has state, and an expensive state at that, that you need the per-call mode. An instance of a per-call service is created just before every method call and is destroyed immediately after each call. Therefore, at the beginning of each call, the object should initialize its state from values saved in some storage, and at the end of the call it should return its state to the storage. Such storage is typically either a database or the file system, but volatile storage (e.g., static variables) may be used instead.
Not all of the object’s state can be saved as-is, however. For example, if the state contains a database connection, the object must reacquire the connection at construction or at the beginning of every call and dispose of the connection at the end of the call or in its implementation of IDisposable.Dispose()
.
Using the per-call instance mode has one important implication for operation design: every operation must include a parameter to identify the service instance whose state needs to be retrieved. The instance uses that parameter to get its state from the storage, and not the state of another instance of the same type. Consequently, state storage is typically keyed (for example, as a static dictionary in memory or a database table). Examples of such state parameters are the account number for a bank account service, the order number for an order-processing service, and so on.
Example 4-3 shows a template for implementing a per-call service.
Example 4-3. Implementing a per-call service
[DataContract] class Param {...} [ServiceContract] interface IMyContract { [OperationContract] void MyMethod(Param stateIdentifier); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyPerCallService : IMyContract,IDisposable { public void MyMethod(Param stateIdentifier) { GetState(stateIdentifier); DoWork(); SaveState(stateIdentifier); } void GetState(Param stateIdentifier) {...} void DoWork() {...} void SaveState(Param stateIdentifier) {...} public void Dispose() {...} }
The class implements the MyMethod()
operation, which accepts a parameter of type Param
(a pseudotype invented for this example) that identifies the instance:
public void MyMethod(Param stateIdentifier);
The instance then uses the identifier to retrieve its state and to save the state back at the end of the method call. Any piece of state that is common to all clients can be allocated at the constructor and disposed of in Dispose()
.
The per-call activation mode works best when the amount of work to be done in each method call is finite, and there are no more activities to complete in the background once a method returns. Because the object will be discarded once the method returns, you should not spin off background threads or dispatch asynchronous calls back into the instance.
Since the per-call service retrieves its state from some storage in every method call, per-call services work very well in conjunction with a load-balancing machine, as long as the state repository is some global resource accessible to all machines. The load balancer can redirect calls to different machines at will, knowing that each per-call service can execute the call after retrieving its state.
Per-call services and performance
Per-call services clearly offer a trade-off between performance (the overhead of retrieving and saving the instance state on each method call) and scalability (holding onto the state and the resources it ties in). There are no hard-and-fast rules as to when and to what extent you should trade some performance for a lot of scalability. You may need to profile your system and ultimately design some services to use per-call activation and others not to use it.
Cleanup operations
Whether or not the service type supports IDisposable
is an implementation detail and is of no relevance to the client. In fact, the client has no way of calling the Dispose()
method anyway. When you design a contract for a per-call service, avoid defining operations that are dedicated for state or resource cleanup, like this:
//Avoid [ServiceContract] interface IMyContract { void DoSomething(); void Cleanup(); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyPerCallService : IMyContract,IDisposable { public void DoSomething() {...} public void Cleanup() {...} public void Dispose() { Cleanup(); } }
The folly of such a design is obvious: if the client does call the cleanup method, it has the detrimental effect of creating an object just so the client can call Cleanup()
on it, followed by a call to IDisposable.Dispose()
(if present) by WCF to do the cleanup again.
Choosing Per-Call Services
While the programming model of per-call services may look somewhat alien to application developers transitioning to service developers, per-call services are actually the preferred instance management mode for many WCF services. This is simply because per-call services scale better, or at least are scale-invariant. When designing a service, my golden rule for scalability is 10X. That is, every service should be designed to handle a load at least an order of magnitude greater than what its requirements call for. In every other engineering discipline, engineers never design a system to handle its exact nominal specified load. You would not want to enter a building whose beams could support only the exact load they were required to handle, ride in an elevator whose cable could handle only the exact number of passengers it’s rated for, and so on. Software systems are no different—why design a system for the specific current load while every other person in the company is working to increase business and the implied load? You should design software systems to last years and to sustain current and future loads. As a result, when using the 10X golden rule, you very quickly end up needing the scalability of the per-call service.
Per-Session Services
WCF can maintain a logical session between a client and a particular service instance. When the client creates a new proxy to a service configured as a sessionful service, the client gets a new dedicated service instance that is independent of all other instances of the same service. That instance will typically remain in service until the client no longer needs it. This activation mode (sometimes also referred to as the private-session mode) is very much like the classic client/server model: each private session uniquely binds a proxy and its set of client- and service-side channels to a particular service instance, or more specifically, to its context. It follows that a transport session is required for the private-session instantiation mode, as discussed later in this section.
Because the service instance remains in memory throughout the session, it can maintain state in memory, and the programming model is very much like that of the classic client/server model. Consequently, it suffers from the same scalability and transaction issues as the classic client/server model. A service configured for private sessions cannot typically support more than a few dozen (or perhaps up to one or two hundred) outstanding clients, due to the cost associated with each such dedicated service instance.
Note
The client session is per service endpoint per proxy. If the client creates another proxy to the same or a different endpoint, that second proxy will be associated with a new instance and session.
Configuring Private Sessions
There are three elements to supporting a session: behavior, binding, and contract. The behavior part is required so that WCF will keep the service instance context alive throughout the session, and to direct the client messages to it. This local behavior facet is achieved by setting the InstanceContextMode
property of the ServiceBehavior
attribute to InstanceContextMode.PerSession
:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] class MyService : IMyContract {...}
Since InstanceContextMode.PerSession
is the default value of the InstanceContextMode
property, these definitions are equivalent:
class MyService : IMyContract {...} [ServiceBehavior] class MyService : IMyContract {...} [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] class MyService : IMyContract {...}
The session typically terminates when the client closes the proxy, which causes the proxy to notify the service that the session has ended. If the service supports IDisposable
, the Dispose()
method will be called asynchronously to the client. However, Disposed()
will be called on a worker thread without an operation context.
In order to correlate all messages from a particular client to a particular instance, WCF needs to be able to identify the client. As explained in Chapter 1, this is exactly what the transport session achieves. If your service is designed to be used as a sessionful service, there has to be some contract-level way for you to express that expectation. The contractual element is required across the service boundary, because the client-side WCF runtime needs to know it should use a session. To that end, the ServiceContract
attribute offers the property SessionMode
, of the enum type SessionMode
:
public enum SessionMode { Allowed, Required, NotAllowed } [AttributeUsage(AttributeTargets.Interface|AttributeTargets.Class, Inherited=false)] public sealed class ServiceContractAttribute : Attribute { public SessionMode SessionMode {get;set;} //More members }
SessionMode
defaults to SessionMode.Allowed
. The configured SessionMode
value is included in the service metadata and is reflected correctly when the client imports the contract metadata. The enum value of SessionMode
has nothing to do with the service session; in fact, its proper name should have been TransportSessionMode
since it pertains to the transport session, not to the logical session maintained between the client and the instance.
SessionMode.Allowed
SessionMode.Allowed
is the default value of the SessionMode
property, so these definitions are equivalent:
[ServiceContract] interface IMyContract {...} [ServiceContract(SessionMode = SessionMode.Allowed)] interface IMyContract {...}
All bindings support configuring the contract on the endpoint with SessionMode.Allowed
. When the SessionMode
property is configured with this value, transport sessions are allowed, but not enforced. The exact resulting behavior is a product of the service configuration and the binding used. If the service is configured for per-call activation, it still behaves as per-call service, as is the case in Example 4-2. When the service is configured for per-session activation, it will behave as a per-session service only if the binding used maintains a transport-level session. For example, the BasicHttpBinding
can never have a transport-level session, due to the connectionless nature of the HTTP protocol. The WSHttpBinding
without Message security and without reliable messaging will also not maintain a transport-level session. In both of these cases, although the service is configured with InstanceContextMode.PerSession
and the contract with SessionMode.Allowed
, the service will behave as a per-call service.
However, if you use the WSHttpBinding
with Message security (its default configuration) or with reliable messaging, or if you use the NetTcpBinding
or the NetNamedPipeBinding
, the service will behave as a per-session service. For example, assuming use of the NetTcpBinding
, this service behaves as sessionful:
[ServiceContract] interface IMyContract {...} class MyService : IMyContract {...}
Note that the previous code snippet simply takes the default of both the SessionMode
and the InstanceContextMode
properties.
SessionMode.Required
The SessionMode.Required
value mandates the use of a transport-level session, but not necessarily an application-level session. You cannot have a contract configured with SessionMode.Required
with a service endpoint whose binding does not maintain a transport-level session, and this constraint is verified at the service load time. However, you can still configure the service to be a per-call service, and the service instance will be created and destroyed on each client call. Only if the service is configured as a sessionful service will the service instance persist throughout the client’s session:
[ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract {...} class MyService : IMyContract {...}
Note
When designing a sessionful contract, I recommend explicitly using SessionMode.Required
and not relying on the default of SessionMode.Allowed
. The rest of the code samples in this book actively apply SessionMode.Required
when sessionful interaction is by design.
Example 4-4 lists the same service and client as in Example 4-2, except the contract and service are configured to require a private session. As you can see from the output, the client got a dedicated instance.
Example 4-4. Per-session service and client
///////////////////////// Service Code ///////////////////// [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContract] void MyMethod(); } class MyService : IMyContract,IDisposable { int m_Counter = 0; MyService() { Trace.WriteLine("MyService.MyService()"); } public void MyMethod() { m_Counter++; Trace.WriteLine("Counter = " + m_Counter); } public void Dispose() { Trace.WriteLine("MyService.Dispose()"); } } ///////////////////////// Client Code ///////////////////// MyContractClient proxy = new MyContractClient(); proxy.MyMethod(); proxy.MyMethod(); proxy.Close(); //Output MyService.MyService() Counter = 1 Counter = 2 MyService.Dispose()
SessionMode.NotAllowed
SessionMode.NotAllowed
disallows the use of a transport-level session, which precludes an application-level session. Regardless of the service configuration, when this value is used, the service will always behave as a per-call service.
Since both the TCP and IPC protocols maintain a session at the transport level, you cannot configure a service endpoint that uses the NetTcpBinding
or the NetNamedPipeBinding
to expose a contract marked with SessionMode.NotAllowed
, and this is verified at the service load time. However, the use of the WSHttpBinding
with an emulated transport session is still allowed. In the interest of readability, I recommend that when selecting SessionMode.NotAllowed
, you always also configure the service as per-call:
[ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract {...} [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract {...}
Since the BasicHttpBinding
cannot have a transport-level session, endpoints that use it behave as if the contract is always configured with SessionMode.NotAllowed
. I view SessionMode.NotAllowed
as a setting available for the sake of completeness more than anything else, and I would not explicitly choose it.
Bindings, contracts, and service behavior
Table 4-1 summarizes the resulting instance mode as a product of the binding being used, the session mode in the contract, and the configured instance context mode in the service behavior. The table does not list invalid configurations, such as SessionMode.Required
with the BasicHttpBinding
.
Binding | Session mode | Context mode | Instance mode |
---|---|---|---|
Basic | Allowed /NotAllowed |
PerCall /PerSession |
PerCall |
TCP, IPC | Allowed /Required |
PerCall |
PerCall |
TCP, IPC | Allowed /Required |
PerSession |
PerSession |
WS (no Message security, no reliability) | NotAllowed /Allowed |
PerCall /PerSession |
PerCall |
WS (with Message security or reliability) | Allowed /Required |
PerSession |
PerSession |
WS (with Message security or reliability) | NotAllowed |
PerCall /PerSession |
PerCall |
Consistent configuration
I strongly recommend that if one contract the service implements is a sessionful contract, then all contracts should be sessionful, and that you should avoid mixing per-call and sessionful contracts on the same per-session service type (even though WCF allows it):
[ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract {...} [ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyOtherContract {...} //Avoid class MyService : IMyContract,IMyOtherContract {...}
The reason is obvious: per-call services need to proactively manage their state, while per-session services do not. While the two contracts will be exposed on two different endpoints and can be consumed independently by two different clients, this duality requires cumbersome implementation for the underlying service class.
Sessions and Reliability
The session between the client and the service instance is only as reliable as the underlying transport session. Consequently, a service that implements a sessionful contract should have all of its endpoints that expose that contract use bindings that enable reliable transport sessions. Make sure to always use a binding that supports reliability and to explicitly enable it at both the client and the service, either programmatically or administratively, as shown in Example 4-5.
Example 4-5. Enabling reliability for per-session services
<!—Host configuration:—> <system.serviceModel> <services> <service name = "MyPerSessionService"> <endpoint address = "net.tcp://localhost:8000/MyPerSessionService" binding = "netTcpBinding" bindingConfiguration = "TCPSession" contract = "IMyContract" /> </service> </services> <bindings> <netTcpBinding> <binding name = "TCPSession"> <reliableSession enabled = "true"/> </binding> </netTcpBinding> </bindings> </system.serviceModel> <!—Client configuration:—> <system.serviceModel> <client> <endpoint address = "net.tcp://localhost:8000/MyPerSessionService/" binding = "netTcpBinding" bindingConfiguration = "TCPSession" contract = "IMyContract" /> </client> <bindings> <netTcpBinding> <binding name = "TCPSession"> <reliableSession enabled = "true"/> </binding> </netTcpBinding> </bindings> </system.serviceModel>
The one exception to this rule is the IPC binding. This binding has no need for the reliable messaging protocol (all calls will be on the same machine anyway), and it is considered an inherently reliable transport.
Just as a reliable transport session is optional, so is ordered delivery of messages, and WCF will provide for a session even when ordered delivery is disabled. However, by the very nature of an application session, a client that interacts with a sessionful service expects all messages to be delivered in the order they are sent. Luckily, ordered delivery is enabled by default when reliable transport sessions are enabled, so no additional setting is required.
The Session ID
Every session has a unique ID that both the client and the service can obtain. The session ID is largely in the form of a GUID, and it can be used for logging and diagnostics. The service can access the session ID via the operation call context, which is a set of properties (including the session ID) that are used for callbacks, message headers, transaction management, security, host access, and access to the object representing the execution context itself. Every service operation has an operation call context, accessible via the OperationContext
class. A service can obtain a reference to the operation context of the current method via the Current
static method of the OperationContext
class:
public sealed class OperationContext : ... { public static OperationContext Current {get;set;} public string SessionId {get;} }
To access the session ID, the service needs to read the value of the SessionId
property, which returns (almost) a GUID in the form of a string. In the case of the TCP binding without reliability, it will be followed by the ordinal number of the session from that host:
string sessionID = OperationContext.Current.SessionId; Trace.WriteLine(sessionID); //Traces: //uuid:8a0480da-7ac0-423e-9f3e-b2131bcbad8d;id=1
If a per-call service without a transport session accesses the SessionId
property, the session ID will be null
, since there is no session and therefore no ID.
The client can access the session ID via the proxy. As introduced in Chapter 1, the class ClientBase<T>
is the base class of the proxy. ClientBase<T>
provides the read-only property InnerChannel
of the type IClientChannel
. IClientChannel
derives from the interface IContextChannel
, which provides a SessionId
property that returns the session ID in the form of a string:
public interface IContextChannel : ... { string SessionId {get;} //More members } public interface IClientChannel : IContextChannel,... {...} public abstract class ClientBase<T> : ... { public IClientChannel InnerChannel {get;} //More members }
Given the definitions in Example 4-4, the client might obtain the session ID like this:
MyContractClient proxy = new MyContractClient(); proxy.MyMethod(); string sessionID = proxy.InnerChannel.SessionId; Trace.WriteLine(sessionID);
However, the degree to which the client-side session ID matches that of the service (and even when the client is allowed to access the SessionId
property) is a product of the binding used and its configuration. What correlates the client-side and service-side session IDs is the reliable session at the transport level. If the TCP binding is used, when a reliable session is enabled (as it should be) the client can obtain a valid session ID only after issuing the first method call to the service to establish the session, or after explicitly opening the proxy. In this case, the session ID obtained by the client will match that of the service. (If the client accesses the session ID before the first call, the SessionId
property will be set to null
.) If the TCP binding is used but reliable sessions are disabled, the client can access the session ID before making the first call, but the ID obtained will be different from that obtained by the service. With the WS binding, if reliable messaging is enabled, the session ID will be null
until after the first call (or after the client opens the proxy), but after that the client and the service will always have the same session ID. Without reliable messaging, the client must first use the proxy (or just open it) before accessing the session ID, or risk an InvalidOperationException
. After opening the proxy, the client and the service will have a correlated session ID. With the IPC binding, the client can access the SessionId
property before making the first call, but the client will always get a session ID different from that of the service. When using this binding, it is therefore better to ignore the session ID altogether.
Session Termination
Typically, the session will end once the client closes the proxy. However, in case the client neglects to close the proxy, or when the client terminates ungracefully or there is a communication problem, the session will also terminate once the inactivity timeout of the transport session is exceeded.
Singleton Service
The singleton service is the ultimate shareable service. When you configure a service as a singleton, all clients are independently connected to the same single well-known instance context and implicitly to the same instance inside, regardless of which endpoint of the service they connect to. The singleton is created exactly once, when the host is created, and lives forever: it is disposed of only when the host shuts down.
Note
IIS or the WAS only create a singleton when the first request to any service in that process is made. To maintain the semantic of the singleton, you must use self-hosting.
Using a singleton does not require clients to maintain a logical session with the singleton instance, or to use a binding that supports a transport-level session. If the contract the client consumes has a session, during the call the singleton will have the same session ID as the client (binding permitting), but closing the client proxy will terminate only the transport session, not the singleton context and the instance inside. If the singleton service supports contracts without a session, those contracts will not be per-call: they too will be connected to the same instance. By its very nature, the singleton is shared, and each client should simply create its own proxy or proxies to it.
You configure a singleton service by setting the InstanceContextMode
property to InstanceContextMode.Single
:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] class MySingleton : ... {...}
Example 4-6 demonstrates a singleton service with two contracts, one that requires a session and one that does not. As you can see from the client call, the calls on the two endpoints were routed to the same instance, and closing the proxies did not terminate the singleton.
Example 4-6. A singleton service and client
///////////////////////// Service Code ///////////////////// [ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContract] void MyMethod(); } [ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyOtherContract { [OperationContract] void MyOtherMethod(); } [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)] class MySingleton : IMyContract,IMyOtherContract,IDisposable { int m_Counter = 0; public MySingleton() { Trace.WriteLine("MySingleton.MySingleton()"); } public void MyMethod() { m_Counter++; Trace.WriteLine("Counter = " + m_Counter); } public void MyOtherMethod() { m_Counter++; Trace.WriteLine("Counter = " + m_Counter); } public void Dispose() { Trace.WriteLine("Singleton.Dispose()"); } } ///////////////////////// Client Code ///////////////////// MyContractClient proxy1 = new MyContractClient(); proxy1.MyMethod(); proxy1.Close(); MyOtherContractClient proxy2 = new MyOtherContractClient(); proxy2.MyOtherMethod(); proxy2.Close(); //Output MySingleton.MySingleton() Counter = 1 Counter = 2
Initializing a Singleton
Sometimes, you may not want to create and initialize the singleton using just the default constructor. Perhaps initializing the state requires some custom steps or specific knowledge that the clients should not be bothered with, or that is not available to the clients. Whatever the reason, you may want to create the singleton using some other mechanism besides the WCF service host. To support such scenarios, WCF allows you to directly create the singleton instance beforehand using normal CLR instantiation, initialize it, and then open the host with that instance in mind as the singleton service. The ServiceHost
class offers a dedicated constructor that accepts an object
:
public class ServiceHost : ServiceHostBase,... { public ServiceHost(object singletonInstance,params Uri[] baseAddresses); public object SingletonInstance {get;} //More members }
Note that the object
must be configured as a singleton. For instance, consider the code in Example 4-7. The class MySingleton
will be first initialized and then hosted as a singleton.
Example 4-7. Initializing and hosting a singleton
//Service code [ServiceContract] interface IMyContract { [OperationContract] void MyMethod(); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] class MySingleton : IMyContract { public int Counter {get;set;} public void MyMethod() { Counter++; Trace.WriteLine("Counter = " + Counter); } } //Host code MySingleton singleton = new MySingleton(); singleton.Counter = 287; ServiceHost host = new ServiceHost(singleton); host.Open(); //Client code MyContractClient proxy = new MyContractClient(); proxy.MyMethod(); proxy.Close(); //Output: Counter = 288
If you do initialize and host a singleton this way, you may also want to be able to access it directly on the host side. WCF enables downstream objects to reach back into the singleton directly using the SingletonInstance
property of ServiceHost
. Any party on the call chain leading down from an operation call on the singleton can always access the host via the operation context’s read-only Host
property:
public sealed class OperationContext : ... { public ServiceHostBase Host {get;} //More members }
Once you have the singleton reference, you can interact with it directly:
ServiceHost host = OperationContext.Current.Host as ServiceHost; Debug.Assert(host != null); MySingleton singleton = host.SingletonInstance as MySingleton; Debug.Assert(singleton != null); singleton.Counter = 388;
If no singleton instance was provided to the host, SingletonInstance
returns null
.
Streamlining with ServiceHost<T>
The ServiceHost<T>
class presented in Chapter 1 can be extended to offer type-safe singleton initialization and access:
public class ServiceHost<T> : ServiceHost { public ServiceHost(T singleton,params Uri[] baseAddresses) : base(singleton,baseAddresses) {} public virtual T Singleton { get { if(SingletonInstance == null) { return default(T); } return (T)SingletonInstance; } } //More members }
The type parameter provides type-safe binding for the object used for construction:
MySingleton singleton = new MySingleton(); singleton.Counter = 287; ServiceHost<MySingleton> host = new ServiceHost<MySingleton>(singleton); host.Open();
and the object returned from the Singleton
property:
ServiceHost<MySingleton> host = OperationContext.Current.Host as ServiceHost<MySingleton>; Debug.Assert(host != null); host.Singleton.Counter = 388;
Note
The InProcFactory<T>
(presented in Chapter 1) is similarly extended to initialize a singleton instance.
Choosing a Singleton
The singleton service is the sworn enemy of scalability. The reason has to do with singleton state synchronization, rather than the cost of that single instance. Having a singleton implies that the singleton has some valuable state that you wish to share across multiple clients. The problem is that if the singleton’s state is mutable and multiple clients connect to the singleton, they may all do so concurrently, and the incoming client calls will be on multiple worker threads. The singleton must therefore synchronize access to its state to avoid state corruption. This, in turn, means that only one client at a time can access the singleton. This constraint may degrade throughput, responsiveness, and availability to the point that the singleton is unusable in a decent-sized system. For example, if an operation on a singleton takes one-tenth of a second, the singleton can service only 10 clients per second. If there are many more clients (say 20 or 100), the system’s performance will be inadequate.
In general, you should use a singleton only if it maps well to a natural singleton in the application domain. A natural singleton is a resource that is, by its very nature, single and unique. Examples of natural singletons are a global logbook to which all services should log their activities, a single communication port, or a single mechanical motor. Avoid using a singleton if there is even the slightest chance that the business logic will allow more than one such service in the future (for example, adding another motor or a second communication port). The reason is clear: if your clients all depend on implicitly being connected to the well-known instance and more than one service instance is available, the clients will suddenly need to have a way to bind to the correct instance. This can have severe implications for the application’s programming model. Because of these limitations, I recommend that you avoid singletons in the general case and find ways to share the state of the singleton instead of the singleton instance itself. That said, there are cases when using a singleton is acceptable, as mentioned earlier.
Demarcating Operations
Sometimes, a sessionful contract has an implied order of operation invocations. Some operations cannot be called first, while other operations must be called last. For example, consider this contract, used to manage customer orders:
[ServiceContract(SessionMode = SessionMode.Required)] interface IOrderManager { [OperationContract] void SetCustomerId(int customerId); [OperationContract] void AddItem(int itemId); [OperationContract] decimal GetTotal(); [OperationContract] bool ProcessOrders(); }
The contract has the following constraints: the client must provide the customer ID as the first operation in the session, or else no other operations can take place; items may be added, and the total calculated, as often as the client wishes; processing the order terminates the session, and therefore must come last. In classic .NET, such requirements often forced the developers to support some state machine or state flags and to verify the state on every operation.
WCF, however, allows contract designers to designate contract operations as operations that can or cannot start or terminate the session, using the IsInitiating
and IsTerminating
properties of the OperationContract
attribute:
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationContractAttribute : Attribute { public bool IsInitiating {get;set;} public bool IsTerminating {get;set;} //More members }
These properties can be used to demarcate the boundary of the session; hence, I call this technique demarcating operations. At service load time (or during the proxy use time on the client side), if these properties are set to their nondefault values, WCF verifies that the demarcating operations are part of a contract that mandates sessions (i.e., that SessionMode
is set to SessionMode.Required
) and throws an InvalidOperationException
otherwise. Both a sessionful service and a singleton can implement contracts that use demarcating operations to manage their client sessions.
The default values of these properties are true
for IsInitiating
and false
for IsTerminating
. Consequently, these two definitions are equivalent:
[OperationContract] void MyMethod(); [OperationContract(IsInitiating = true,IsTerminating = false)] void MyMethod();
As you can see, you can set both properties on the same method. In addition, operations do not demarcate the session boundary by default—operations can be called first, last, or in between any other operations in the session. Using nondefault values enables you to dictate that a method is not called first, or that it is called last, or both:
[ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContract] void StartSession(); [OperationContract(IsInitiating = false)] void CannotStart(); [OperationContract(IsTerminating = true)] void EndSession(); [OperationContract(IsInitiating = false,IsTerminating = true)] void CannotStartCanEndSession(); }
Going back to the order-management contract, you can use demarcating operations to enforce the interaction constraints:
[ServiceContract(SessionMode = SessionMode.Required)] interface IOrderManager { [OperationContract] void SetCustomerId(int customerId); [OperationContract(IsInitiating = false)] void AddItem(int itemId); [OperationContract(IsInitiating = false)] decimal GetTotal(); [OperationContract(IsInitiating = false,IsTerminating = true)] bool ProcessOrders(); } //Client code OrderManagerClient proxy = new OrderManagerClient(); proxy.SetCustomerId(123); proxy.AddItem(4); proxy.AddItem(5); proxy.AddItem(6); proxy.ProcessOrders(); proxy.Close();
When IsInitiating
is set to true
(its default), it means the operation will start a new session if it is the first method the client calls but will be part of the ongoing session if another operation is called first. When IsInitiating
is set to false
, it means that a client can never call that operation as the first operation in a new session, and that the method can only be part of an ongoing session.
When IsTerminating
is set to false
(its default), it means the session continues after the operation returns. When IsTerminating
is set to true
, it means the session terminates once the method returns, and WCF disposes of the service instance asynchronously. The client will not be able to issue additional calls on the proxy. Note that the client should still close the proxy.
Instance Deactivation
Conceptually, the sessionful service instance management technique as described so far connects a client (or clients) to a service instance. Yet, the real picture is more complex. Recall from Chapter 1 that each service instance is hosted in a context, as shown in Figure 4-2.
What sessions actually do is correlate the client messages not to the instance, but to the context that hosts it. When the session starts, the host creates a new context. When the session ends, the context is terminated. By default, the lifetime of the context is the same as that of the instance it hosts. However, for optimization and extensibility purposes, WCF provides the service designer with the option of separating the two lifetimes and deactivating the instance separately from its context. In fact, WCF also allows a context to exist without an associated instance at all, as shown in Figure 4-2. I call this instance management technique context deactivation. The common way of controlling context deactivation is via the ReleaseInstanceMode
property of the OperationBehavior
attribute:
public enum ReleaseInstanceMode { None, BeforeCall, AfterCall, BeforeAndAfterCall, } [AttributeUsage(AttributeTargets.Method)] public sealed class OperationBehaviorAttribute : Attribute,... { public ReleaseInstanceMode ReleaseInstanceMode {get;set;} //More members }
ReleaseInstanceMode
is of the enum type ReleaseInstanceMode
. The various values of ReleaseInstanceMode
control when to release the instance in relation to the method call: before, after, before and after, or not at all. When releasing the instance, if the service supports IDisposable
, the Dispose()
method is called and Dispose()
has an operation context.
You typically apply instance deactivation on some but not all service methods, or with different values on different methods:
[ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContract] void MyMethod(); [OperationContract] void MyOtherMethod(); } class MyService : IMyContract,IDisposable { [OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.AfterCall)] public void MyMethod() {...} public void MyOtherMethod() {...} public void Dispose() {...} }
The reason you typically apply it sporadically is that if you were to apply it uniformly, you would end up with a per-call-like service, in which case you might as well have configured the service as per-call.
If relying on instance deactivation assumes a certain call order, you can try to enforce that order using demarcating operations.
Configuring with ReleaseInstanceMode.None
The default value for the ReleaseInstanceMode
property is ReleaseInstanceMode.None
, so these two definitions are equivalent:
[OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.None)] public void MyMethod() {...} public void MyMethod() {...}
ReleaseInstanceMode.None
means that the instance lifetime is not affected by the call, as shown in Figure 4-3.
Configuring with ReleaseInstanceMode.BeforeCall
When a method is configured with ReleaseInstanceMode.BeforeCall
, if there is already an instance in the session, before forwarding the call WCF will deactivate it, create a new instance in its place, and let that new instance service the call, as shown in Figure 4-4.
WCF deactivates the instance and calls Dispose()
before the call is done on the incoming call thread, while the client blocks. This ensures that the deactivation is indeed done before the call, not concurrently with it. ReleaseInstanceMode.BeforeCall
is designed to optimize methods such as Create()
that acquire some valuable resources, yet wish to release the previously allocated resources. Instead of acquiring the resources when the session starts, you wait until the call to the Create()
method and then both release the previously allocated resources and allocate new ones. After Create()
is called, you are ready to start calling other methods on the instance, which are typically configured with ReleaseInstanceMode.None
.
Configuring with ReleaseInstanceMode.AfterCall
When a method is configured with ReleaseInstanceMode.AfterCall
, WCF deactivates the instance after the call, as shown in Figure 4-5.
This is designed to optimize a method such as Cleanup()
that cleans up valuable resources held by the instance, without waiting for the session to terminate. ReleaseInstanceMode.AfterCall
is typically applied on methods called after methods configured with ReleaseInstanceMode.None
.
Configuring with ReleaseInstanceMode.BeforeAndAfterCall
As its name implies, configuring a method with ReleaseInstanceMode.BeforeAndAfterCall
has the combined effect of using ReleaseInstanceMode.BeforeCall
and ReleaseInstanceMode.AfterCall
. If the context has an instance before the call is made, just before the call WCF deactivates that instance and creates a new instance to service the call. It then deactivates the new instance after the call, as shown in Figure 4-6.
ReleaseInstanceMode.BeforeAndAfterCall
may look superfluous at first glance, but it actually complements the other values. It is designed to be applied on methods called after methods marked with ReleaseInstanceMode.BeforeCall
or None
, or before methods marked with ReleaseInstanceMode.AfterCall
or None
. Consider a situation where the sessionful service wants to benefit from state-aware behavior (like a per-call service), while holding onto resources only when needed to optimize resource allocation and security lookup. If ReleaseInstanceMode.BeforeCall
were the only available option, there would be a period of time after the call when the resources would still be allocated to the object, but would not be in use. A similar situation would occur if ReleaseInstanceMode.AfterCall
were the only available option, because there would be a period of time before the call when the resources would be wasted.
Explicit Deactivation
Instead of making a design-time decision on which methods to use to deactivate an instance, you can make a runtime decision to deactivate the instance after the method returns. You do that by calling the ReleaseServiceInstance()
method on the instance context. You obtain the instance context via the InstanceContext
property of the operation context:
public sealed class InstanceContext : ... { public void ReleaseServiceInstance(); //More members } public sealed class OperationContext : ... { public InstanceContext InstanceContext {get;} //More members }
Example 4-8 demonstrates using explicit deactivation to implement a custom instance management technique that is dependent on the value of a counter.
Example 4-8. Using ReleaseServiceInstance()
[ServiceContract(SessionMode = SessionMode.Required)] interface IMyContract { [OperationContract] void MyMethod(); } class MyService : IMyContract,IDisposable { int m_Counter = 0; public void MyMethod() { m_Counter++; if(m_Counter > 4) { OperationContext.Current.InstanceContext.ReleaseServiceInstance(); } } public void Dispose() {...} }
Calling ReleaseServiceInstance()
has a similar effect to using ReleaseInstanceMode.AfterCall
. When used in a method decorated with ReleaseInstance
Mode.BeforeCall
, it has a similar effect to using ReleaseInstanceMode
.BeforeAndAfterCall
.
Note
Instance deactivation affects a singleton as well, although combining the two makes little sense—by its very definition, it is permissible and even desirable to never deactivate the singleton.
Using Instance Deactivation
Instance deactivation is an optimization technique, and like all optimization techniques, you should avoid it in the general case. It adds complexity to the application and makes the code less approachable and maintainable to all but the WCF expert. Consider using instance deactivation only after failing to meet both your performance and scalability goals and when careful examination and profiling has proven beyond a doubt that using instance deactivation will improve the situation. If scalability and throughput are your concerns, you should take advantage of the simplicity of the per-call instancing mode and avoid instance deactivation. The main reason I share this technique with you is that WCF itself makes extensive use of instance deactivation; thus, knowledge of it is instrumental in demystifying other aspects of WCF, such as durable services and transactions.
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.
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.
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 states 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 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 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: 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 }
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 the 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 : ContextClientBase<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 }
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); } } protected 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 in C:\Windows\Microsoft.NET\Framework\<.NET version>\SQL\EN. Note that with the WCF-provided SQL persistence provider you can only use SQL Server 2005 or later 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=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" />
Warning
With .NET 4.5, Microsoft relocated the various Workflow Foundation V3.0 types formally maintained in the System.WorkflowServices
assembly. The move also included a deprecation warning stating that the Workflow Foundation System.Activities
assembly in .NET 4.5 should provide replacement types. However, the assembly does not contain replacement types.
Normally it is poor practice to use compiler directives to cover up compilation warnings in your code, unless you are compensating for an oversight such as in this case. You can clean up your build and remove the warning messages by applying the following compiler directive: [add the restore pragma after your code]
#pragma warning disable 618
Throttling
While it is not a direct instance management technique, throttling enables you to restrain client connections and the load they place on your service. You need throttling because software systems are not elastic, as shown in Figure 4-7.
In other words, you cannot keep increasing the load on the system and expect an infinite, gradual decline in its performance, as if stretching chewing gum. Most systems will initially handle the increase in load well, but then begin to yield and abruptly snap and break. All software systems behave this way, for reasons that are beyond the scope of this book and are related to queuing theory and the overhead inherent in managing resources. This snapping, inelastic behavior is of particular concern when there are spikes in load, as shown in Figure 4-8.
Even if a system is handling a nominal load well (the horizontal line in Figure 4-8), a spike may push it beyond its design limit, causing it to snap and resulting in the clients experiencing a significant degradation in their level of service. Spikes can also pose a challenge in terms of the rate at which the load grows, even if the absolute level reached would not otherwise cause the system problems.
Throttling enables you to avoid maxing out your service and the underlying resources it allocates and uses. When throttling is engaged, if the settings you configure are exceeded, WCF will automatically place the pending callers in a queue and serve them out of the queue in order. If a client’s call timeout expires while its call is pending in the queue, the client will get a TimeoutException
. Throttling is inherently an unfair technique, because those clients whose requests are buffered will see a degradation in their level of service. However, in this case, it is better to be smart than just: if all the callers in the spike are allowed in, that will be fair, but all callers will then see a significant drop in the level of service as the system snaps. Throttling therefore makes sense when the area under the spike is relatively small compared with the area under the entire load graph, implying that the probability of the same caller being queued successively is very low. Every once in a while, in response to a spike, some callers will get buffered, but the system as a whole will still function well. Throttling does not work well when the load increases to a new level and remains constant at that level for a long time (as shown in Figure 4-9). In that case, all it does is defer the problems a bit, eventually causing all callers to time out. Such a system should be designed from the ground up to handle the higher level of load.
Throttling is done per service type; that is, it affects all instances of the service and all its endpoints. This is done by associating the throttle with every channel dispatcher the service uses.
WCF lets you control some or all of the following service consumption parameters:
- Maximum number of concurrent sessions
- Indicates the overall number of outstanding clients that can have a transport session with the service. In plain terms, this represents the maximum overall number of outstanding clients using TCP, IPC, or either of the WS bindings (with reliability, security, or both). Because the connectionless nature of a basic HTTP connection implies a very short transport session that exists only for the duration of the call, this number usually has no effect on clients using the basic binding or a WS binding without a transport session; such clients are instead limited by the maximum allowed number of concurrent calls. The default value is 100 times the processor (or cores) count.
- Maximum number of concurrent calls
- Limits the total number of calls that can currently be in progress across all service instances. This number should usually be kept at 1%–3% of the maximum number of concurrent sessions. The default value is 16 times the processor (or cores) count.
- Maximum number of concurrent instances
- Controls the total number of concurrently alive contexts. Unless you set this value explicitly, it will implicitly equate to the sum of the maximum concurrent calls and maximum of the concurrent sessions (116 times the processor count). Once set, it will retain its value regardless of the other two properties. How instances map to contexts is a product of the instance context management mode, as well as context and instance deactivation. With a per-session service, the maximum number of instances is both the total number of concurrently active instances and the total number of concurrent sessions. When instance deactivation is employed, there may be far fewer instances than contexts, and yet clients will be blocked if the number of contexts has reached the maximum number of concurrent instances. With a per-call service, the number of instances is actually the same as the number of concurrent calls. Consequently, the maximum number of instances with a per-call service is the lesser of the configured maximum concurrent instances and maximum concurrent calls. The value of this parameter is ignored with a singleton service, since it can only have a single instance anyway.
Warning
Throttling is an aspect of hosting and deployment. When you design a service, you should make no assumptions about throttling configuration—always assume your service will bear the full brunt of the client’s load. This is why, although it is fairly easy to write a throttling behavior attribute, WCF does not offer one.
Configuring Throttling
Administrators typically configure throttling in the config file. This enables you to throttle the same service code differently over time or across deployment sites. The host can also programmatically configure throttling based on some runtime decisions.
Administrative throttling
Example 4-20 shows how to configure throttling in the host config file. Using the behaviorConfiguration
tag, you add to your service a custom behavior that sets throttled values.
Example 4-20. Administrative throttling
<system.serviceModel> <services> <service name = "MyService" behaviorConfiguration = "ThrottledBehavior"> ... </service> </services> <behaviors> <serviceBehaviors> <behavior name = "ThrottledBehavior"> <serviceThrottling maxConcurrentCalls = "500" maxConcurrentSessions = "10000" maxConcurrentInstances = "100" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel>
Programmatic throttling
The host process can programmatically throttle the service based on some runtime parameters. You can only configure the throttle programmatically before the host is opened. Although the host can override the throttling behavior found in the config file by removing it and adding its own, you typically should provide a programmatic throttling behavior only when there is no throttling behavior in the config file.
The ServiceHostBase
class offers the Description
property of the type ServiceDescription
:
public abstract class ServiceHostBase : ... { public ServiceDescription Description {get;} //More members }
The service description, as its name implies, is a description of the service, with all its aspects and behaviors. ServiceDescription
contains a property called Behaviors
of the type KeyedByTypeCollection<T>
, with IServiceBehavior
as the generic parameter.
Example 4-21 shows how to set the throttled behavior programmatically.
Example 4-21. Programmatic throttling
ServiceHost host = new ServiceHost(typeof(MyService)); ServiceThrottlingBehavior throttle; throttle = host.Description.Behaviors.Find<ServiceThrottlingBehavior>(); if(throttle == null) { throttle = new ServiceThrottlingBehavior(); throttle.MaxConcurrentCalls = 500; throttle.MaxConcurrentSessions = 10000; throttle.MaxConcurrentInstances = 100; host.Description.Behaviors.Add(throttle); } host.Open();
First, the hosting code verifies that no service throttling behavior was provided in the config file. This is done by calling the Find<T>()
method of KeyedByTypeCollection<T>
, using ServiceThrottlingBehavior
as the type parameter:
public class ServiceThrottlingBehavior : IServiceBehavior { public int MaxConcurrentCalls {get;set;} public int MaxConcurrentSessions {get;set;} public int MaxConcurrentInstances {get;set;} //More members }
If the returned throttle is null
, then the hosting code creates a new ServiceThrottlingBehavior
, sets its values, and adds it to the behaviors in the service description.
Streamlining with ServiceHost<T>
Using C# 3.0 extensions, you can extend ServiceHost
(or any subclass of it, such as ServiceHost<T>
) to automate the code in Example 4-21, as shown in Example 4-22.
Example 4-22. Extending ServiceHost to handle throttling
public static class ServiceThrottleHelper { public static void SetThrottle(this ServiceHost host, int maxCalls,int maxSessions,int maxInstances) { ServiceThrottlingBehavior throttle = new ServiceThrottlingBehavior(); throttle.MaxConcurrentCalls = maxCalls; throttle.MaxConcurrentSessions = maxSessions; throttle.MaxConcurrentInstances = maxInstances; host.SetThrottle(throttle); } public static void SetThrottle(this ServiceHost host, ServiceThrottlingBehavior serviceThrottle, bool overrideConfig) { if(host.State == CommunicationState.Opened) { throw new InvalidOperationException("Host is already opened"); } ServiceThrottlingBehavior throttle = host.Description.Behaviors.Find<ServiceThrottlingBehavior>(); if(throttle == null) { host.Description.Behaviors.Add(serviceThrottle); return; } if(overrideConfig == false) { return; } host.Description.Behaviors.Remove(throttle); host.Description.Behaviors.Add(serviceThrottle); } public static void SetThrottle(this ServiceHost host, ServiceThrottlingBehavior serviceThrottle) { host.SetThrottle(serviceThrottle,false); } }
ServiceThrottleHelper
offers the SetThrottle()
method, which accepts the throttle to use, and a Boolean flag indicating whether or not to override the configured values, if present. The default value (using an overloaded version of SetThrottle()
) is false
. SetThrottle()
verifies that the host hasn’t been opened yet using the State
property of the CommunicationObject
base class. If it is required to override the configured throttle, SetThrottle()
removes it from the description. The rest of Example 4-22 is similar to Example 4-21. Here is how to use ServiceHost<T>
to set a throttle programmatically:
ServiceHost<MyService> host = new ServiceHost<MyService>(); host.SetThrottle(12,34,56); host.Open();
Note
The InProcFactory<T>
class presented in Chapter 1 was similarly extended to streamline throttling.
Reading throttle values
Service developers can read the throttle values at runtime, for diagnostic and analytical purposes. For a service instance to access its throttle properties from its dispatcher at runtime, it must first obtain a reference to the host from the operation context.
The host base class ServiceHostBase
offers the read-only ChannelDispatchers
property:
public abstract class ServiceHostBase : CommunicationObject,... { public ChannelDispatcherCollection ChannelDispatchers {get;} //More members }
ChannelDispatchers
is a strongly typed collection of ChannelDispatcherBase
objects:
public class ChannelDispatcherCollection : SynchronizedCollection<ChannelDispatcherBase> {...}
Each item in the collection is of the type ChannelDispatcher
. ChannelDispatcher
offers the property ServiceThrottle
:
public class ChannelDispatcher : ChannelDispatcherBase { public ServiceThrottle ServiceThrottle {get;set;} //More members } public sealed class ServiceThrottle { public int MaxConcurrentCalls {get;set;} public int MaxConcurrentSessions {get;set;} public int MaxConcurrentInstances {get;set;} }
ServiceThrottle
contains the configured throttle values:
class MyService : ... { public void MyMethod() //Contract operation { ChannelDispatcher dispatcher = OperationContext.Current.Host. ChannelDispatchers[0] as ChannelDispatcher; ServiceThrottle serviceThrottle = dispatcher.ServiceThrottle; Trace.WriteLine("Max Calls = " + serviceThrottle.MaxConcurrentCalls); Trace.WriteLine("Max Sessions = " + serviceThrottle.MaxConcurrentSessions); Trace.WriteLine("Max Instances = " + serviceThrottle.MaxConcurrentInstances); } }
Note that the service can only read the throttle values and has no way of affecting them. If the service tries to set the throttle values, it will get an InvalidOperationException
.
Again, you can streamline the throttle lookup via ServiceHost<T>
. First, add a ServiceThrottle
property:
public class ServiceHost<T> : ServiceHost { public ServiceThrottle Throttle { get { if(State == CommunicationState.Created) { throw new InvalidOperationException("Host is not opened"); } ChannelDispatcher dispatcher = OperationContext.Current.Host. ChannelDispatchers[0] as ChannelDispatcher; return dispatcher.ServiceThrottle; } } //More members }
Then, use ServiceHost<T>
to host the service and use the ServiceThrottle
property to access the configured throttle:
//Hosting code ServiceHost<MyService> host = new ServiceHost<MyService>(); host.Open(); class MyService : ... { public void MyMethod() { ServiceHost<MyService> host = OperationContext.Current.Host as ServiceHost<MyService>; ServiceThrottle serviceThrottle = host.Throttle; ... } }
1 This chapter contains excerpts from my articles “WCF Essentials: Discover Mighty Instance Management Techniques for Developing WCF Apps” (MSDN Magazine, June 2006) and “Managing State with Durable Services” (MSDN Magazine, October 2008).
Get Programming WCF Services, 4th 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.