The contract session mode and the service instance mode have a paramount effect on the
behavior of the queued calls, the way the calls are played back to the service, and the
overall program workflow and allowed assumptions. The MSMQ binding cannot maintain a
transport session in the connected sense, since the client is inherently disconnected.
Instead, the equivalent MSMQ concept is called a sessiongram. If the
contract is configured with SessionMode.Allowed
(the
default) or SessionMode.NotAllowed
, there will be no
sessiongram. Every call the client makes on the proxy will be converted to a single WCF
message, and those WCF messages will be placed in individual MSMQ messages and posted to the
queue. A client making two calls on the proxy will result in two MSMQ messages. If the
contract is configured with SessionMode.Required
, all the
calls made by the client against the same proxy will be packaged in a single MSMQ message,
in the order in which they were made and posted to the queue. On the service side, WCF will
play the calls from the MSMQ message in the order they were made (like a recording) to the
same service instance. This mode is therefore analogous to a transport session and a
sessionful service.
In the case of a per-call service, the client has no way of knowing whether its calls
will eventually end up being played to a queued per-call service. All the client sees is
the session mode of the contract. If the session mode is either SessionMode.Allowed
or SessionMode.NotAllowed
, there will be no sessiongram. In this case,
regardless of whether the service is configured as per-call or sessionful it will amount
to the same result: per-call processing and instantiation.
When a client without an ambient transaction calls a sessiongram-less queued endpoint (as in Example 9-11), the MSMQ messages generated for each call are posted to the queue immediately after each call. If the client has an exception, the messages posted up to that point are not rejected and are delivered to the service.
Example 9-11. Nontransactional client of a sessionless queued endpoint
[ServiceContract]
interface IMyContract
{
[OperationContract(IsOneWay = true)]
void MyMethod( );
}
//Client code
using(TransactionScope scope =
new TransactionScope(TransactionScopeOption.Suppress
))
{
MyContractClient proxy = new MyContractClient( );
proxy.MyMethod( ); //Message posts to queue here
proxy.MyMethod( ); //Message posts to queue here
proxy.Close( );
}
With a transactional client (that is, client code with an ambient transaction) of a sessiongram-less queued endpoint (as in Example 9-12), the messages corresponding to each call are posted to the queue only when the client's transaction commits. If the client transaction aborts, all of those messages are rejected from the queue and all calls are canceled.
Example 9-12. Transactional client of a sessionless queued endpoint
[ServiceContract] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethod( ); } //Client code using(TransactionScope scope = new TransactionScope( )) { MyContractClient proxy = new MyContractClient( ); proxy.MyMethod( ); //Message written to queue proxy.MyMethod( ); //Message written to queue proxy.Close( ); scope.Complete( ); } //Messages committed to queue here
There is no relationship between the proxy and the ambient transaction. If the
client uses a transaction scope (as in Example 9-12), the client can close the proxy
inside or outside the scope and may continue to use the proxy even after the transaction
ends, or in a new transaction. The client may also close the proxy before or after the
call to Complete( )
.
On the host side, the queued calls are dispatched separately to the service, and each call is played to a separate service instance. This is the case even if the service instance mode is per-session. I therefore recommend that when using a sessiongram-less queued contract, you should always explicitly configure the service as per-call and configure the contract for disallowing sessions, to increase the readability of the code and clearly convey your design decision:
[ServiceContract(SessionMode = SessionMode.NotAllowed)] interface IMyContract {...} [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract {...}
After each call the service instance is disposed of, just as with a connected per-call service. The per-call service may or may not be transactional. If it is transactional and the playback transaction is aborted, only that particular call is rolled back to the queue for a retry. As you will see later, due to concurrent playback and WCF's failure-handling behavior, calls to a per-call queued service can execute and complete in any order, and the client cannot make any assumptions about call ordering. Note that even calls dispatched by a transactional client may fail or succeed independently. Never assume order of calls with a sessiongram-less queued service.
For sessionful queued services, the service contract must be configured with SessionMode.Required
:
[ServiceContract(SessionMode = SessionMode.Required
)]
interface IMyContract
{...}
class MyService : IMyContract
{...}
As mentioned previously, when the client queues up calls against a sessionful queued endpoint, all calls made throughout the session are grouped into a single MSMQ message. Once that single message is dispatched and played to the service, WCF creates a new dedicated service instance to handle all the calls in the message. All calls in the message are played back to that instance in their original order. After the last call, the instance is disposed of automatically.
WCF will provide both the client and the service with a unique session ID. However, the client session ID will be uncorrelated to that of the service. To approximate the session semantic, all calls on the same instance on the host side will share the same session ID.
In the case of a sessionful queued endpoint, the client must have an ambient
transaction in order to call the proxy. Nontransactional clients are disallowed and will
result in an InvalidOperationException
:
[ServiceContract(SessionMode = SessionMode.Required
)] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethod( ); } using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Suppress
)) { MyContractClient proxy = new MyContractClient( ); proxy.MyMethod( ); //Throws InvalidOperationException proxy.MyMethod( ); proxy.Close( ); }
For a transactional client, WCF posts a single message to the queue when the transaction commits, and that single message is rejected from the queue if the transaction aborts:
using(TransactionScope scope = new TransactionScope( )) { MyContractClient proxy = new MyContractClient( ); proxy.MyMethod( ); proxy.MyMethod( ); proxy.Close( ); //Finish composing message, writes to queue scope.Complete( ); } //Single message committed to queue here
It is important to note that the single message prepared by the proxy must be posted to the queue within the same client transaction—that is, the client must end the session inside the transaction. If the client does not close the proxy before the transaction is complete, the transaction will always abort:
MyContractClient proxy = new MyContractClient( );
using(TransactionScope scope = new TransactionScope( ))
{
proxy.MyMethod( );
proxy.MyMethod( );
scope.Complete( );
} //Transaction aborts
proxy.Close( );
This is required to enforce the atomicity of the sessiongram. All the calls in the session should either be posted to or rejected from the queue. If the client were to use the proxy in a second transaction that could commit or abort independently of the first, the results could be ambiguous or even dangerous.
An interesting side effect of this edict is that there is no point in storing a proxy to a queued sessionful endpoint in a member variable, because that proxy can only be used once in a single transaction and cannot be reused across client transactions.
Not only does the client have to close the proxy before the transaction ends, but
when using a transaction scope, the client must close the proxy before completing the
transaction. The reason is that closing the proxy to a queue's sessionful endpoint
requires accessing the current ambient transaction, which is not possible after calling
Complete( )
. Trying to do so results with InvalidOperationException
:
MyContractClient proxy = new MyContractClient( );
using(TransactionScope scope = new TransactionScope( ))
{
proxy.MyMethod( );
proxy.MyMethod( );
scope.Complete( );
proxy.Close( ); //Transaction aborts
}
A corollary of this requirement is that you cannot stack using
statements in any order, because doing so may result in calling
Dispose( )
in the wrong order (first on the scope,
and then on the proxy):
using
(MyContractClient proxy = new MyContractClient( ))
using(TransactionScope scope = new TransactionScope( ))
{
proxy.MyMethod( );
proxy.MyMethod( );
scope.Complete( );
} //Transaction aborts
A sessionful queued service must be configured to use transactions in all operations
by setting TransactionScopeRequired
to true
. Failing to do so will abort all playback transactions.
The service is required to have a transaction in every operation so that all the calls
in the session fail or succeed as one atomic operation (i.e., so that a failure in one
of the operations causes the entire queued session to fail). In addition, the
transaction must be the same transaction for all operations in the session. Partial
success is impossible here, because WCF cannot return only a portion of the MSMQ message
back to the queue after a failure of one of the operations but not the others. To ensure
that it is indeed the same transaction in all operations, the service must provide for
transactional affinity to the instance by setting TransactionAutoComplete
to false
in all
but the last operation in the session.
Only a sessionful service can support a sessiongram contract, since only a service
configured with InstanceContextMode.PerSession
can
set TransactionAutoComplete
to false
.
Due to a design flaw of WCF, the service cannot rely on setting TransactionAutoCompleteOnSessionClose
to true
to complete the transaction at the end of the session.
Instead, it must have the last method call in the session complete the transaction,
either automatically or manually. Failing to do so will cause all queued sessions to
always abort (as all uncompleted transactions always do).
This design flaw is scheduled to be fixed in the next release of WCF and the .NET Framework.
Example 9-13 is a template for
implementing a queued sessionful service, assuming MyMethod3(
)
is the last operation call in the session.
Example 9-13. Implementing a sessionful queued service
[ServiceContract(SessionMode = SessionMode.Required
)] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethod1( ); [OperationContract(IsOneWay = true)] void MyMethod2( ); [OperationContract(IsOneWay = true)] void MyMethod3( ); } class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired =true
, TransactionAutoComplete =false
)] public void MyMethod1( ) {...} [OperationBehavior(TransactionScopeRequired =true
, TransactionAutoComplete =false
)] public void MyMethod2( ) {...} [OperationBehavior(TransactionScopeRequired =true
)] public void MyMethod3( ) {...} }
Obviously, baking into your service code the assumption that a particular method
call will be the last in the session is often impractical. The best solution is to add
an explicit CompleteTransaction( )
operation to the
contract whose sole purpose is to complete the transaction and end the session. You
should explicitly document the need to call this method at the end of the
session:
[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{
[OperationContract(IsOneWay = true)]
void MyMethod1( );
[OperationContract(IsOneWay = true)]
void MyMethod2( );
[OperationContract(IsOneWay = true)]
void CompleteTransaction( );
}
class MyService : IMyContract
{
[OperationBehavior(TransactionScopeRequired = true,
TransactionAutoComplete = false)]
public void MyMethod1( )
{...}
[OperationBehavior(TransactionScopeRequired = true,
TransactionAutoComplete = false)]
public void MyMethod2( )
{...}
[OperationBehavior(TransactionScopeRequired = true)]
public void CompleteTransaction( )
{} //No code
}
A queued singleton service can never have a session and can only implement sessionless
contracts. Configuring the SessionMode
as either
SessionMode.Allowed
or SessionMode.NotAllowed
has the same result: a sessiongram-less interaction.
Consequently, I recommend always explicitly configuring the contracts of a queued
singleton service as sessionless:
[ServiceContract(SessionMode = SessionMode.NotAllowed
)] interface IMyContract {...} [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single
)] class MyService : IMyContract {...}
A nontransactional queued singleton service behaves like a regular WCF singleton as far as instancing. Regardless of the way the clients use their proxies, individual calls on the proxies are packaged into separate MSMQ messages and dispatched separately to the singleton, as with a per-call service. However, unlike with a per-call service, all these calls will be played back to the same single instance.
A transactional queued singleton, on the other hand, behaves by default like a
per-call service, because after every call that completes the transaction WCF will release
the singleton instance. The only difference between a true per-call service and a
singleton is that WCF will allow at most a single instance of the singleton, regardless of
the number of queued messages. While you could apply the techniques described in Chapter 7 to create a state-aware transactional singleton, you can also
restore the singleton semantic by setting the ReleaseServiceInstanceOnTransactionComplete
property to false
and use volatile resource managers.
Example 9-14 shows a template for implementing a transactional queued singleton.
Example 9-14. Transactional queued singleton
[ServiceContract(SessionMode = SessionMode.NotAllowed
)] interface IMyContract { [OperationContract(IsOneWay = true)] void MyMethod( ); } [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single
,ReleaseServiceInstanceOnTransactionComplete = false
)] class MySingleton : IMyContract,IDisposable { [OperationBehavior(TransactionScopeRequired = true)] public void MyMethod( ) {...} //More members }
Because the calls are packaged into individual MSMQ messages, they may be played to the singleton in any order (due to retries and transactions). In addition, calls may complete in any order, and even calls dispatched by a transactional client may fail or succeed independently. Never assume order of calls with a singleton.
18.188.218.226