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.
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 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 NetNamed
Pipe
Binding
, 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
{...}
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
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 (no Message security, no reliability) |
|
|
|
WS (with Message security or reliability) |
|
|
|
WS (with Message 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. 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 Invalid
Operation
Exception
. 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, 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.