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.
1. Per-Call Queued Services
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.
1.1. Nontransactional clients
When a client without an ambient transaction calls a sessiongram-less queued endpoint (as
in Example 1), 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 1. 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(); }
|
1.2. Transactional clients
With a transactional client (that is, client code with an
ambient transaction) of a sessiongram-less queued endpoint (as in
Example 2), 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 2. 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().
1.3. Per-call processing
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.
2. Sessionful Queued Services
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.
2.1. Clients and transactions
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
in an 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
2.2. Services and transactions
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. The service must equate the session boundary with the
transaction boundary. Do this by setting TransactionAutoComplete to false on all operations and relaying on
TransactionAutoCompleteOnSessionClose to
true. This will also have the
added benefit of creating the affinity to the same transaction in
all operations.
Note:
Only a sessionful service can support a sessiongram
contract, since only a service configured with InstanceContextMode.PerSession can set
TransactionAutoComplete to false.
To further enforce this
constraint, the service cannot rely on setting ReleaseServiceInstanceOnTransactionComplete to false in order to restore the instance
semantics while completing in each operation. Trying to do so will
cause all queued sessions to always abort.
Example 3 is a template for implementing a queued sessionful
service.
Example 3. 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(); }
[ServiceBehavior(TransactionAutoCompleteOnSessionClose = true)] class MyService : IMyContract { [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)] public void MyMethod1() {...}
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)] public void MyMethod2() {...}
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)] public void MyMethod3() {...} }
|
3. Singleton Service
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.
Example 4 shows a
template for implementing a transactional queued singleton.
Example 4. 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 }
|
3.1. Calls and order
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.