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, even though 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.

Table 4-1. Instance mode as a product of the binding, contract configuration, and service behavior

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.

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.