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.
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, only 1 percent of all clients make concurrent calls (in a high-load Enterprise system, that figure rises to 3 percent). 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.
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()
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()
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 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.
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.
While the programming model of per-call services may look somewhat alien to client/server 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.
Get Programming WCF Services, 3rd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.