The Response Service

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.

A response service

Figure 9-12. A response service

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 it may 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.

Note

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.

Designing a Response Service Contract

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.

Response address and method ID

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);
}

The ResponseContext class

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.

Client-Side Programming

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 class InterceptorClientBase<T> : ClientBase<T> where T : class
{
   protected virtual void PreInvoke(ref Message request)
   {}
   //More members
}
public abstract partial class HeaderClientBase<T,H> : InterceptorClientBase<T>
                                                      where T : class
{
   public H Header
   {get;protected 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 ClientResponseBase<T>, shown in Example 9-23.

Example 9-23. The ClientResponseBase<T> class

public abstract class ClientResponseBase<T> :
                               HeaderClientBase<T,ResponseContext> where T : class
{
   protected readonly string ResponseAddress;

   public ClientResponseBase(string responseAddress)
   {
      ResponseAddress = responseAddress;
      Endpoint.VerifyQueue();
   }
   public ClientResponseBase(string responseAddress,string endpointName)
   {...}
   public ClientResponseBase(string responseAddress,
                             NetMsmqBinding binding,EndpointAddress address)
   {...}

   /* More constructors */

   protected override void PreInvoke(ref Message request)
   {
      string methodId = GenerateMethodId();
      Header = new ResponseContext(ResponseAddress,methodId);
      base.PreInvoke(ref request);
   }
   protected virtual string GenerateMethodId()
   {
      return Guid.NewGuid().ToString();
   }
}

The constructors of ClientResponseBase<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.

ClientResponseBase<T> provides the virtual GenerateMethodId() method, which by default uses a GUID for the method ID. However, your subclasses of ClientResponseBase<T> can override it and provide their own unique strings, such as an incremented integer.

The heart of ClientResponseBase<T> is the overridden PreInvoke() method. PreInvoke() is defined as virtual in the InterceptorClientBase<T> base class of HeaderClientBase<T,H>. InterceptorClientBase<T> is part of a generic interception framework I wrote (defined in Appendix E) that enables you to perform custom pre-call and post-call interception steps. For every operation invoked by the client, PreInvoke() 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.

Using ClientResponseBase<T>

You use ClientResponseBase<T> like a regular proxy; for example, given 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 ClientResponseBase<T>

class CalculatorClient : ClientResponseBase<ICalculator>,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 void Add(int number1,int number2)
   {
      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);
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.

Note

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 ClientResponseBase<T> so that it uses only Binding.

When managing the responses on the client side using a ClientResponseBase<T>-derived proxy, it is often very handy to have the invoking client obtain the method ID used to dispatch the call. You can do this easily with the Header property:

CalculatorClient proxy = new CalculatorClient(responseAddress);
proxy.Add(2,3);
string methodId = proxy.Header.MethodId;
proxy.Close();

Queued Service-Side Programming

The service needs to construct a proxy that will dispatch messages to the client-side response service. To simplify this, use my ServiceResponseBase<T>, defined as:

public abstract class ServiceResponseBase<T> : HeaderClientBase<T,ResponseContext>
                                                                   where T : class
{
   public ServiceResponseBase();
   public ServiceResponseBase(string bindingName);
   public ServiceResponseBase(NetMsmqBinding binding);
}

ServiceResponseBase<T> automates reading the response context from the message headers and writing the response itself to the outgoing headers. Other than that, you can use it like any other proxy base class. Example 9-25 demonstrates the usage of ServiceResponseBase<T>.

Example 9-25. Using ServiceResponseBase<T>

class CalculatorResponseClient : ServiceResponseBase<ICalculatorResponse>,
                                                               ICalculatorResponse
{
   public void OnAddCompleted(int result,ExceptionDetail error)
   {
      Channel.OnAddCompleted(result,error);
   }
}
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
      {
         CalculatorResponseClient proxy = new CalculatorResponseClient();

         proxy.OnAddCompleted(result,error);

         proxy.Close();
      }
   }
}

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 proxy to the response service to enqueue the response. The proxy in Example 9-25 will default to using the same MSMQ binding as the host.

Example 9-26 shows the implementation of ServiceResponseBase<T>.

Example 9-26. Implementing ServiceResponseBase<T>

public abstract class ServiceResponseBase<T> : HeaderClientBase<T,ResponseContext>
                                                                   where T : class
{
   public ServiceResponseBase() : this(OperationContext.Current.Host.
                               Description.Endpoints[0].Binding as NetMsmqBinding)
   {}
   public ServiceResponseBase(string bindingName) :
                                              this(new NetMsmqBinding(bindingName))
   {}

   public ServiceResponseBase(NetMsmqBinding binding) :
                                              base(ResponseContext.Current,binding,
                      new EndpointAddress(ResponseContext.Current.ResponseAddress))
   {
      Endpoint.VerifyQueue();
   }
}

The default constructor of ServiceResponseBase<T> uses the same queued binding the host was using to dequeue the client’s call. You can also specify an MSMQ binding section in the config file or provide the constructor with the binding instance to use. Both of these constructors delegate the work to the third constructor, which accepts the MSMQ binding to use. That constructor reads the response address out of the response context and provides those two along with the response context to the base constructor of HeaderClientBase<T,H>. It also verifies the presence of the response queue.

Note that ServiceResponseBase<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.

Response Service-Side Programming

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;
      ...
   }
}

Note

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.

Transactions

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
      {
         CalculatorResponseClient proxy = new CalculatorResponseClient();

         proxy.OnAddCompleted(result,error);

         proxy.Close();
      }
   }
}
Queuing up in the playback transaction

Figure 9-13. Queuing up in the playback transaction

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.

Using a new transaction

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))
         {
            CalculatorResponseClient proxy = new CalculatorResponseClient();

            proxy.OnAddCompleted(result,error);

            proxy.Close();
         }
      }
   }
}
Responding in a new transaction

Figure 9-14. Responding in a new transaction

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.

Response service and transactions

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.39.142