Per-Session Services



Per-Session Services

WCF can maintain a 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 remain in service usually until the client no longer needs it. This activation mode is very much like the classic client-server model. This mode is also sometimes referred to as private sessions. Each private session uniquely binds a proxy and its set of client- and service-side channels to a particular service instance (actually to its context, as discussed later on).

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. Consequently, it also 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 a hundred or two) outstanding clients due to the cost associated with each such dedicated service instance.

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

Supporting a session has three elements to itbehavior, binding, and contract. The behavior part is required so that WCF will keep the service instance 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. This causes the proxy to notify the service that the session has ended. If the service supports IDisposable, then 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. One way of doing that is to rely on a transport-level session; that is, a continuous connection at the transport level, such as the one maintained by the TCP and IPC protocols. As a result, when using the NetTcpBinding or the NetNamedPipeBinding, WCF associates that connection with the client. The situation is more complex when it comes to the connectionless nature of the HTTP protocol. Conceptually, each message over HTTP reaches the services on a new connection. Consequently, you cannot maintain a transport-level session over the BasicHttpBinding. The WS binding, on the other hand, is capable of emulating a transport-level session by including a logical session ID in the message headers that uniquely identifies the client. In fact, the WSHttpBinding will emulate a transport session whenever security or reliable messaging is enabled.

The contractual element is required across the service boundary because the client-side WCF runtime needs to know it should use a session. 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.

SessionMode.Allowed

SessionMode.Allowed is the default value of the 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. The SessionMode property does not refer to the instancing mode, but rather to the presence of a transport-level session (or its emulation in the case of the WS bindings). As its name implies, when the SessionMode property is configured with SessionMode.Allowed, it merely allows transport sessions, but does not enforce it. The exact resulting behavior is a product of the service configuration and the binding used. If the service is configured for per-call, it still behaves as per-call service, as is the case in Figure. When the service is configured for a per-session service, 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 security and without reliable messaging will also not maintain a transport-level session. In both of these cases, even though the service is configured with InstanceContextMode.PerSession and the contract with SessionMode.Allowed, the service will behave as a per-call service, and the calls to Dispose() are asynchronous; that is, the client is not blocked after the call while WCF disposes of the instance.

However, if you use the WSHttpBinding with security (its default configuration) or with reliable messaging, or the NetTcpBinding, or the NetNamedPipeBinding, then 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's 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
{...}

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.


Figure lists the same service and client as in Figure, 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.

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, it always behaves as a per-call service. If the service implements IDisposable, then Dispose( ) will be called asynchronously toward the client; that is, control will return to the client, and in the background (on the incoming call thread) WCF will call Dispose( ). Since both the TCP and IPC protocols maintain a session at the transport level, you cannot configure a service endpoint to expose a contract marked with SessionMode.NotAllowed that uses the NetTcpBinding or the NetNamedPipeBinding, 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, always configure the service as per-call also:

[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, always yielding an asynchronous Dispose( ).

Binding, contract, and service behavior

Figure 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 BasicHttpBinding.

Instance mode as a product of the binding, contract configuration, and service behavior

Binding

Session mode

Context mode

Async Dispose()

Instance mode

Basic

Allowed/NotAllowed

PerCall/PerSession

Yes

PerCall

TCP, IPC

Allowed/Required

PerCall

No

PerCall

TCP, IPC

Allowed/Required

PerSession

Yes

PerSession

WS (no security, no reliability)

NotAllowed/Allowed

PerCall/PerSession

Yes

PerCall

WS (with security or reliability)

Allowed/Required

PerSession

Yes

PerSession

WS (with security or reliability)

NotAllowed

PerCall/PerSession

Yes

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 it is allowed by WCF:

[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, it introduces cumbersome implementation for the underlying service class.

Sessions and Reliability

This 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 the endpoints that expose that contract use bindings that support reliable transport session. Make sure to always use a binding that supports reliability and that you explicitly enable it at both the client and the service either programmatically or administratively, as shown in Figure.

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 named pipes 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. Obviously, by the very nature of an application session, a client that interacts with a sessionful service expects that all messages are delivered in the order they are sent. Luckily, ordered delivery is enabled by default when reliable transport session is enabled, so no additional setting is required.

Session ID

Every session has a unique ID that both the client and the service can obtain. The session ID is 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. Every service operation has an operation call contexta set of properties that are used in addition to the session ID for callbacks, transaction management, security, host access, and access to the object representing the execution context itself. The class OperationContext provides access to all those, and the service obtains 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 a GUID in the form of a string:

string sessionID = OperationContext.Current.SessionId;
Trace.WriteLine(sessionID);
//Traces:
//urn:uuid:c8141f66-51a6-4c66-9e03-927d5ca97153

If a per-call service without a transport session (such as with the BasicHttpBinding or with SessionMode.NotAllowed) accesses the SessionId property, it will return null, since there is no session and therefore no ID.

In the IDisposable.Dispose( ) method, the service has no operation context and subsequently cannot access the session 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 generated by either Visual Studio 2005 or SvcUtil. ClientBase<T> provides the read-only property InnerChannel of the type IClientChannel. IClientChannel derives from the interface IContextChannel, which provides the 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 of Figure, obtaining the session ID by the client might look like this:

MyContractClient proxy = new MyContractClient( );
proxy.MyMethod( );

string sessionID = proxy.InnerChannel.SessionId;
Trace.WriteLine(sessionID);
//Traces:
//urn:uuid:c8141f66-51a6-4c66-9e03-927d5ca97153

However, to what degree the client-side session ID matches that of the service, and when the client is allowed to even access the SessionId property is a product of the binding used and its configuration. What correlates the client-side and service-side session ID 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 only obtain a valid session ID after issuing the first method call to the service to establish the session (or after explicitly opening the proxy). If it is accessed before the first call, the SessionId property will be set to null. The session ID obtained by the client will match that of the service. 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 any one of the WS bindings, with reliable messaging, the session ID will be null until after the first call (or after opening the proxy), but after that the client and the service will always have the same session ID. Without reliable messaging, you 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 namedpipe 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 the named-pipe 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 terminates ungracefully or in case of a communication problem, each session also has an idle-time timeout that defaults to 10 minutes. The session will automatically terminate after 10 minutes of inactivity from the client, even if the client still intends to use the session. Once the session is terminated due to the idle-timeout, if the client tries to use its proxy, the client will get a CommunicationObjectFaultedException. Both the client and the service can configure a different timeout by setting a different value in the binding. The bindings that support a reliable transport-level session provide the ReliableSession property of the type ReliableSession or OptionalReliableSession. The ReliableSession class offers the TimeSpan InactivityTimeout property that you can use to configure a new idle-time timeout:

public class ReliableSession
{
   public TimeSpan InactivityTimeout
   {get;set;}
   //More members
}
public class OptionalReliableSession : ReliableSession
{
   public bool Enabled
   {get;set;}
   //More members
}
public class NetTcpBinding : Binding,...
{
   public OptionalReliableSession ReliableSession
   {get;}
   //More members
}
public abstract class WSHttpBindingBase : ...
{
   public OptionalReliableSession ReliableSession
   {get;}
   //More members
}
public class WSHttpBinding : WSHttpBindingBase,...
{...}
public class WSDualHttpBinding : Binding,...
{
   public ReliableSession ReliableSession
   {get;}
   //More members
}

For example, here is the code required to programmatically configure an idle timeout of 25 minutes for the TCP binding:

NetTcpBinding tcpSessionBinding = new NetTcpBinding( );
tcpSessionBinding.ReliableSession.Enabled = true;
tcpSessionBinding.ReliableSession.InactivityTimeout = TimeSpan.FromMinutes(25);

Here is the equivalent configuration setting using a config file:

<netTcpBinding>
   <binding name = "TCPSession">
      <reliableSession enabled = "true" inactivityTimeout = "00:25:00"/>
   </binding>
</netTcpBinding>

If both the client and the service configure a timeout, then the shorter timeout prevails.

There is another esoteric service-side configuration for session termination. The ServiceBehavior attribute offers an advanced option for managing the session shutdown via the AutomaticSessionShutdown property. This property is intended for optimizing certain callback scenarios, and can be safely ignored in most cases. In a nutshell, AutomaticSessionShutdown defaults to TRue so that when the client closes the proxy, the session is terminated. Setting it to false causes the session to continue until the service explicitly closes its sending channel. When set to false, the client of a duplex session (discussed in Chapter 5) must manually close the output session on the duplex client channel, otherwise the client will hang waiting for the session to terminate.