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 channel. 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.
Tip
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.
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
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 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
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.
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
{...}
Tip
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
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.
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 |
|
|
|
TCP, IPC |
|
|
|
TCP, IPC |
|
|
|
|
|
| |
WS (with security or reliability) |
|
|
|
WS (with security or reliability) |
|
|
|
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.
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.
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, 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);
//Traces:
//urn:uuid:c8141f66-51a6-4c66-9e03-927d5ca97153
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 either of the WS bindings, 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.
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, 2nd 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.