The programming model of queued calls described so far was one-sided: the client posted a one-way message to a queue, and the service processed that message. This model is sufficient when the queued operations are one-way calls by nature. However, the queued service may need to report back to its client on the result of the invocation, or return results or even errors. By default, this is not possible: WCF equates queued calls with one-way calls, which inherently forbids any such response. In addition, queued services (and their clients) are potentially disconnected. If a client posts a queued call to a disconnected service, by the time the service finally gets the message and processes it, there may no longer be a client to return the values to. The solution is to have the service report back to a client-provided queued service. I call such a service a response service.[7] Figure 9-12 shows the architecture of such a solution.
The response service is just another queued service in the system. The response service may be disconnected toward the client as well, or it may share the client's process, or be hosted in a separate process or even on a separate machine. If the response service shares the client's process, when the client is launched the response service will start processing the queued responses. Having the response service in a separate process (or even on a separate machine) from the client's helps to further decouple lifeline-wise the response service from the client or clients that use it.
Not all queued services require a response service. Be pragmatic, and use a response service only where appropriate; that is, where it adds the most value.
As with any WCF service, the client and the service need to agree beforehand on the
response contract and what it will be used for; that is, whether it will be used for
returned values and error information, or just returned values. Note that you can also
split the response service into two services, and have one response service for results
and another for faults and errors. As an example, consider the ICalculator
contract implemented by the queued MyCalculator
service:
[ServiceContract] interface ICalculator { [OperationContract(IsOneWay = true)] void Add(int number1,int number2); //More operations } class MyCalculator : ICalculator {...}
The MyCalculator
service is required to respond to
its client with the result of the calculation and report on any errors. The result of the
calculation is an integer, and the error is in the form of the ExceptionDetail
data contract presented in Chapter 6. The
ICalculatorResponse
contract could be defined
as:
[ServiceContract] interface ICalculatorResponse { [OperationContract(IsOneWay = true)] void OnAddCompleted(int result,ExceptionDetail error); //More operations }
The response service supporting ICalculatorResponse
needs to examine the returned error information; notify the client application, the user,
or the application administrator on the method completion; and make the results available
to the interested parties. Example 9-21 shows a simple
response service that supports ICalculatorResponse
.
Example 9-21. A simple response service
class MyCalculatorResponse : ICalculatorResponse { public void OnAddCompleted(int result,ExceptionDetail error) { if(error != null) { //Handle error } else { MessageBox.Show("result = " + result,"MyCalculatorResponse"); } } //More operations }
As demonstrated by Example 9-21, the response service is just that—a simple service. There is nothing special about it other than its designation as a response service.
There are two immediate problems with the implementation of both MyCalculator
and MyCalculatorResponse
. The first is that the same response service could be
used to handle the response (or completion) of multiple calls on multiple queued
services, and yet, as listed in Example 9-21, MyCalculatorResponse
(and more importantly, the clients it
serves) has no way of distinguishing between responses. The solution for that is to have
the client that issued the original queued call tag the call by associating it with some
unique ID, or at least an ID that is unique enough across that client's application. The
queued service MyCalculator
needs to pass that ID to
the response service MyCalculatorResponse
, so that it
can apply its custom logic regarding that ID. Note that the service typically has no
direct use for the ID; all it needs to do is pass it along.
The second problem is how to enable the queued service to discover the address of
the response service. Unlike with duplex callbacks, there is no built-in support in WCF
for passing the response service's reference to the queued service, so the queued
service needs to manually construct a proxy to the response service and invoke the
operations of the response contract. While the response contract is decided upon at
design time, and the binding is always NetMsmqBinding
, the queued service lacks the address of the response service
to be able to respond. You could place that address in the service host config file (in
a client
section) but such a course of action is to
be avoided. The main reason is that the same queued service could be called by multiple
clients, each with its own dedicated response service and address.
One possible solution is to explicitly pass both the client-managed ID and the desired response service address as parameters to every operation on the queued service contract:
[ServiceContract] interface ICalculator { [OperationContract(IsOneWay = true)] void Add(int number1,int number2,string responseAddress
,string methodId
); }
Much the same way, the queued service could explicitly pass the method ID to the response service as a parameter to every operation on the queued response contract:
[ServiceContract]
interface ICalculatorResponse
{
[OperationContract(IsOneWay = true)]
void OnAddCompleted(int result,ExceptionDetail error,string methodId
);
}
While passing the address and the ID as explicit parameters would work, it does distort the original contract, and it introduces plumbing-level parameters alongside business-level parameters in the same operation. A better solution is to have the client store the response address and operation ID in the outgoing message headers of the call. Using the message headers this way is a general-purpose technique for passing out-of-band information to the service (information that is otherwise not present in the service contract). Appendix B explains in detail the use of the incoming and outgoing headers, including the related techniques and supporting classes in ServiceModelEx.
Since the client needs to pass both the address and the method ID in the message
headers, a single primitive type parameter will not do. Instead, use my ResponseContext
class, defined in Example 9-22.
Example 9-22. The ResponseContext class
[DataContract] public class ResponseContext { [DataMember] public readonly string ResponseAddress; [DataMember] public readonly string FaultAddress; [DataMember] public readonly string MethodId; public ResponseContext(string responseAddress,string methodId) : this(responseAddress,methodId,null) {} public ResponseContext(string responseAddress) : this(responseAddress, Guid.NewGuid( ). ToString( )) {} public ResponseContext(string responseAddress,string methodId, string faultAddress) { ResponseAddress = responseAddress; MethodId = methodId; FaultAddress = faultAddress; } public static ResponseContext Current { get { return GenericContext<ResponseContext
>.Current.Value; } set { GenericContext<ResponseContext
>.Current = new GenericContext<ResponseContext>(value); } } //More members }
ResponseContext
provides a place to store both
the response address and the ID. In addition, if the client wants to use a separate
response service for faults, ResponseContext
provides
a field for the fault response service address. (This chapter makes no use of that
feature.) The client is responsible for constructing an instance of ResponseContext
with a unique ID. While the client can
supply that ID as a construction parameter, the client can also use the constructor of
ResponseContext
, which takes just the response
address, and have that constructor generate a GUID for the ID. To streamline the act of
storing a ResponseContext
instance in and retrieving
it from the headers, ResponseContext
provides the
Current
property, which merely encapsulates my
GenericContext<T>
. The client can provide an
ID for each method call (even when dealing with a sessionful queued service) by using a
different instance of ResponseContext
for each
call.
My HeaderClientBase<T,H>
proxy base class
(defined in Appendix B) is designed to automate passing
information in the headers from the client to the service:
public abstract partial class HeaderClientBase<T,H> : ClientBase<T> where T : class
{
protected H
Header
{get;set;}
public HeaderClientBase(H header);
public HeaderClientBase(H header,string endpointName);
//More members
}
However, when it comes to the response context, there are differences compared with
the raw headers management discussed in Appendix B:
specifically, changing the context (that is, the headers) on each call as opposed to
setting it only at construction time, generating method IDs and providing them to the
client, and enqueuing rather than merely invoking the service call. While the client can
easily use my HeaderClientBase<T,H>
to do all
that, all clients will have to repeat such code for every contract and proxy they have. It
is better to automate and encapsulate these steps in a dedicated proxy base class such as
my ResponseClientBase<T>
, shown in Example 9-23.
Example 9-23. The ResponseClientBase<T> class
public abstract class ResponseClientBase<T> :HeaderClientBase
<T,ResponseContext
> where T : class { public readonly string ResponseAddress; public ResponseClientBase(string responseAddress) { ResponseAddress = responseAddress; Endpoint.VerifyQueue( ); } public ResponseClientBase(string responseAddress,string endpointName) {...} public ResponseClientBase(string responseAddress, NetMsmqBinding binding,EndpointAddress address) {...} /* More constructors */ protected string Enqueue(Action action) { string methodId = GenerateMethodId( ); Header = new ResponseContext(ResponseAddress,methodId); action( ); return Header.MethodId; } protected virtual string GenerateMethodId( ) { return Guid.NewGuid( ).ToString( ); } }
The constructors of ResponseClientBase<T>
accept the response address and the regular proxy parameters, such as the endpoint name,
address, and binding. The constructors store the response address in the read-only public
field ResponseAddress
. In addition, the constructors
use the VerifyQueue( )
endpoint extension method to
verify that the service queue (and the DLQ) exists and to create it if necessary.
ResponseClientBase<T>
provides the virtual
GenerateMethodId( )
method, which by default uses a
GUID for the method ID. However, your subclasses of ResponseClientBase<T>
can override it and provide their own unique
strings, such as an incremented integer.
The heart of ResponseClientBase<T>
is the
Enqueue( )
method. Enqueue(
)
accepts a delegate wrapping the operation to invoke. For every operation
invoked by the client, Enqueue( )
generates a new
method ID, provides it to a new ResponseContext
object
(along with the response address supplied to the constructor), and assigns the new
ResponseContext
object to the Header
property of HeaderClientBase<T,H>
. Thanks to generics, Header
is of the type ResponseContext
.
After invoking the delegate, Enqueue( )
returns the
method ID used for that call.
When using a ResponseClientBase<T>
-derived
proxy, I wanted to have all operations of the proxy return the method ID used to
dispatch them (something that is very handy when managing the responses on the client
side).
To that end, unlike with a normal proxy, when deriving from ResponseClientBase<T>
, do not have the subclass also derive from the
contract. Instead, you must provide a similar set of methods that all return a string
for the method ID, not void
(this is why you cannot derive from the contract—the operations on the
contract are all one-way and do not return anything).
You need merely provide the contract type as a type parameter to ResponseClientBase<T>
and have your proxy's
implementation of the operations return the method ID returned to them by Enqueue( )
. For example, for this calculator
contract:
[ServiceContract] interface ICalculator { [OperationContract(IsOneWay = true)] void Add(int number1,int number2); //More operations }
Example 9-24 shows the matching service proxy.
Example 9-24. Deriving from ResponseClientBase<T>
class CalculatorClient : ResponseClientBase<ICalculator>
{
public CalculatorClient(string responseAddress) : base(responseAddress)
{}
public CalculatorClient(string responseAddress,string endpointName) :
base(responseAddress,endpointName)
{}
public CalculatorClient(string responseAddress,
NetMsmqBinding binding,EndpointAddress address) :
base(responseAddress,binding,address)
{}
//More constructors
public string
Add(int number1,int number2)
{
return Enqueue(( )=>Channel.Add(number1,number2));
}
//More operations
}
Using the proxy in Example 9-24 yields this straightforward client code:
string responseAddress = "net.msmq://localhost/private/MyCalculatorResponseQueue"; CalculatorClient proxy = new CalculatorClient(responseAddress); string methodId = proxy.Add(2,3); proxy.Close( );
Note how closely the client that provides the response address to the proxy corresponds to a client that provides a duplex callback object to a proxy (as demonstrated in Chapter 5). In the queued services world, the response service address is the equivalent callback reference.
A queued response service is not limited to being used only with a queued service.
You can use the same technique to pass the address and method ID to a connected
service and have that service respond to a client-provided queued response service.
You will need to rework ResponseClientBase<T>
so that it only uses Binding
, and rename Enqueue( )
to InvokeService(
)
.
Since duplex callback was my reference programming model in designing the support for
a response service, I wanted to provide an easy way for the service to obtain a proxy for
the response service (on par with the duplex case, which simply obtains the callback
reference from the operation context). My solution was ResponseScope<T>
, defined as:
public class ResponseScope<T> : IDisposable where T : class { public readonly T Response; public ResponseScope( ); public ResponseScope(string bindingConfiguration); public ResponseScope(NetMsmqBinding binding); public void Dispose( ); }
ResponseScope<T>
automates reading the
response context from the message headers, writing the response itself to the outgoing
headers, and creating (and closing) a proxy to the response service queue. All you need to
do is instantiate a ResponseScope<T>
object and
optionally provide it with the binding or binding configuration. The Response
read-only field will contain a reference to a proxy
to the response service.
Example 9-25 demonstrates using ResponseScope<T>
.
Example 9-25. Using ResponseScope<T>
class MyCalculator : ICalculator { [OperationBehavior(TransactionScopeRequired = true)] public void Add(int number1,int number2) { int result = 0; ExceptionDetail error = null; try { result = number1 + number2; } //Don't rethrow catch(Exception exception) { error = new ExceptionDetail(exception); } finally { using(ResponseScope<ICalculatorResponse> scope = new ResponseScope<ICalculatorResponse>( )) { scope.Response.OnAddCompleted(result,error); } } } }
In Example 9-25, the MyCalculator
service catches any exception thrown by the
business logic operation and wraps that exception with an ExceptionDetail
object. The service does not rethrow the exception. As you
will see later, in the context of transactions and response services, rethrowing the
exception would also cancel the response. Moreover, when using a response service, being
able to respond in case of an error is a much better strategy than relying on WCF's
playback error handling.
In the finally
statement, regardless of exceptions,
the service responds. It creates a new ResponseScope<T>
object and uses the Response
field to call the response service proxy, in effect enqueuing the
response. The service then disposes of the response scope.
Example 9-26 shows the implementation of
ResponseScope<T>
.
Example 9-26. Implementing ResponseScope<T>
public class ResponseScope<T> : IDisposable where T : class { OperationContextScope m_Scope; public readonly T Response; public ResponseScope( ) : this(new NetMsmqBinding( )) {} public ResponseScope(string bindingConfiguration) : this(new NetMsmqBinding(bindingConfiguration)) {} public ResponseScope(NetMsmqBinding binding) { ResponseContext responseContext = ResponseContext.Current; EndpointAddress address = new EndpointAddress(responseContext.ResponseAddress); ChannelFactory<T> factory = new ChannelFactory<T>(binding,address); factory.Endpoint.VerifyQueue( ); Response = factory.CreateChannel( ); //Switching context now m_Scope = new OperationContextScope(Response as IContextChannel); ResponseContext.Current = responseContext; } public void Dispose( ) { using(Response as IDisposable) using(m_Scope) {} } }
This terse example contains some of the most advanced WCF code in this book. ResponseScope<T>
is a disposable object—it installs a
new operation context (as explained in Appendix B), and when
ResponseScope<T>
is disposed of, the scope
restores the old operation context. The trick in implementing ResponseScope<T>
is not to use an OperationContextScope
instance in a using
statement to scope a piece of code with a new operation context, but rather to create it
in one place, store it in the m_Scope
member, and
dispose of m_Scope
in another place. The constructor of
OperationContextScope
is what swaps the contexts, so
merely constructing the scope swaps contexts. The Dispose(
)
method of ResponseScope<T>
restores
the original operation context by disposing of m_Scope
.
To automate that behavior even in the face of exceptions, ResponseScope<T>
should be used in a using
statement. ResponseScope<T>
takes a generic type parameter representing the response contract, and Response
is of that type. Response
is a proxy to the response service, and the clients of ResponseScope<T>
use Response
to call operations on the response service. There is no need to
dispose of Response
, because ResponseScope<T>
does that in Dispose(
)
.
The constructor of ResponseScope<T>
first
uses ResponseContext.Current
to extract the incoming
response context. Since ResponseScope<T>
reads
the response address from the message headers of the service operation context at runtime,
it must programmatically construct a proxy to the response service, using a channel
factory. Note that the service can still read the exact settings to use for the NetMsmqBinding
from the config file, by specifying the binding
configuration name to the binding constructor. After instantiating Response
, ResponseScope<T>
swaps operation contexts; it then uses ResponseContext.Current
to add to the new operation context's
outgoing headers the response context and returns.
Note that ResponseScope<T>
sends the response
service the entire response context (not just the ID). This is done both for simplicity's
sake and because it may be beneficial for the response service to have access to the fault
and response address used.
The response service accesses its response context, reads from it the method ID, and responds accordingly. Example 9-27 demonstrates a possible implementation of such a response service.
Example 9-27. Implementing a response service
class MyCalculatorResponse : ICalculatorResponse
{
[OperationBehavior(TransactionScopeRequired = true)]
public void OnAddCompleted(int result,ExceptionDetail error)
{
string methodId = ResponseContext.Current.MethodId
;
...
}
}
It is common for the response service to update the application's user interfaces
with the queued results (or errors). Chapter 8 introduced
my FormHost<F>
class, which you can certainly
leverage to support the queued response contract. For example:
class CalculatorResponse : FormHost<CalculatorResponse>, ICalculatorResponse { [OperationBehavior(TransactionScopeRequired=true)] public void OnAddCompleted(int result, ExceptionDetail error) { Text = "Add returned: " + result; ... } }
In fact, nothing prevents you from having the client itself be the response service as well.
A queued service typically queues up the response as part of the incoming playback transaction. Given the queued service definition of Example 9-28, Figure 9-13 depicts the resulting transaction and the participating actions.
Example 9-28. Queuing up a response as part of the playback transaction
class MyCalculator : ICalculator
{
[OperationBehavior(TransactionScopeRequired = true
)]
public void Add(int number1,int number2)
{
...
try
{
...
}
catch //Do not rethrow
{
...
}
finally
{
using(ResponseScope<ICalculatorResponse> scope =
new ResponseScope<ICalculatorResponse>( ))
{
scope.Response.OnAddCompleted(...);
}
}
}
}
Design-wise, the nice thing about having the queued call playback and the queued response in the same transaction is that if the playback transaction is aborted for whatever reason (including due to other services in the transaction aborting), the response is canceled automatically. This is by far the most common choice for most applications.
Note in Example 9-28 that the service catches all exceptions and does not rethrow them. This is important, because any unhandled exception (or rethrown exception) will abort the response, so there won't be any point in the service bothering to respond. Using a response service intrinsically means that the service does not rely on the automatic retry mechanism of WCF, and it handles its own business logic failures because the clients expect it to respond in a prescribed manner.
As an alternative to always having the response be part of the playback transaction, the service can respond in a new transaction by encasing the response in a new transaction scope, as shown in Example 9-29 and illustrated in Figure 9-14.
Example 9-29. Responding in a new transaction
class MyCalculator : ICalculator { [OperationBehavior(TransactionScopeRequired = true)] public void Add(int number1,int number2) { ... finally { using(TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.RequiresNew
)) using(ResponseScope<ICalculatorResponse> responseScope = new ResponseScope<ICalculatorResponse>( )){
responseScope.Response.OnAddCompleted(...);}
} } }
Responding in a new transaction is required in two cases. The first is when the service wants to respond regardless of the outcome of the playback transaction (which could be aborted by other downstream services). The second case is when the response is nice to have, and the service does not mind if the playback transaction commits but the response aborts.
Since a response service is just another queued service, the mechanics of managing and participating in a transaction are just like those of any other queued service. However, there are a few points worth mentioning in this particular context. The response service can process the response as part of the incoming response playback transaction:
class MyCalculatorResponse : ICalculatorResponse { [OperationBehavior(TransactionScopeRequired = true)] public void OnAddCompleted(...) {...} }
This is by far the most common option, because it allows for retries. That said, the response service should avoid lengthy processing of the queued response, because it may risk aborting the playback transaction. The response service can process the response in a separate transaction if the response is nice to have (as far as the provider of the response service is concerned):
class MyCalculatorResponse : ICalculatorResponse { public void OnAddCompleted(int result,ExceptionDetail error) { using(TransactionScope scope = new TransactionScope( )) {...} } }
When the response is processed in a new transaction, if that transaction aborts, WCF will not retry the response out of the response service's queue. Finally, for response processing of long duration, you can configure the response service not to use a transaction at all (including the playback transaction):
class MyCalculatorResponse : ICalculatorResponse { public void OnAddCompleted(...) {...} }
[7] I first published my initial technique for a response service in the February 2007 issue of MSDN Magazine. In the next version of WCF and the .NET Framework, Microsoft intends to incorporate a similar solution to the one presented here.
3.22.74.160