Message Headers

Every WCF message contains a collection of outgoing and incoming message headers. When the client wishes to send out-of-band parameters to the service, it does so by adding those parameters to the outgoing headers. The service then reads those parameters from the incoming headers.

The operation context offers collections of incoming and outgoing headers, available via the IncomingMessageHeaders and OutgoingMessageHeaders properties:

public sealed class OperationContext : ...
{
   public MessageHeaders IncomingMessageHeaders
   {get;}

   public MessageHeaders OutgoingMessageHeaders
   {get;}

   //More members
}

Each collection is of the type MessageHeaders (that is, a collection of MessageHeader objects):

public sealed class MessageHeaders : ...
{
   public void Add(MessageHeader header);
   public T GetHeader<T>(int index);
   public T GetHeader<T>(string name,string ns);
   //More members
}

The class MessageHeader is not intended for application developers to interact with directly. Instead, use the MessageHeader<T> class, which provides for type-safe and easy conversion from a CLR type parameter to a message header:

public abstract class MessageHeader : ...
{...}

public class MessageHeader<T>
{
   public MessageHeader();
   public MessageHeader(T content);
   public T Content
   {get;set;}
   public MessageHeader GetUntypedHeader(string name,string ns);
   //More members
}

You can use any serializable or data contract type as the type parameter for MessageHeader<T>. You construct a MessageHeader<T> around a CLR type, and then use the GetUntypedHeader() method to convert it to a MessageHeader and store it in the outgoing headers. GetUntypedHeader() requires you to provide it with the generic type parameter name and namespace, which will be used later to look up the header from the headers collection. (Actually, using the name and namespace is just a suggestion; any unique value will do for this purpose. Since the type name and namespace combination tends to be unique, it is commonly used.) You perform the lookup via the GetHeader<T>() method of MessageHeaders. Calling GetHeader<T>() obtains the value of the type parameter of the MessageHeader<T> used.

Client-Side Header Interaction

As mentioned previously, the client needs to add the parameter to the outgoing headers collection. However, what if the client is not a WCF service, so it does not have an operation context? As it turns out, this doesn’t matter if the client is a service, since once the call enters a service, the operation context becomes immutable, so the client cannot write to its outgoing headers even if it has an operation context. The solution for all clients (services and non-services alike) is to create a new operation context and write to its outgoing headers. WCF enables a client to adopt a new operation context via the OperationContextScope class, defined as:

public sealed class OperationContextScope : IDisposable
{
   public OperationContextScope(IContextChannel channel);
   public OperationContextScope(OperationContext context);
   public void Dispose();
}

Using OperationContextScope is a general technique for spinning a new context when the one you have is inadequate. The constructor of OperationContextScope replaces the current thread’s operation context with the new operation context. Calling Dispose() on the OperationContextScope instance restores the old context (even if it was null). If you do not call Dispose(), that may damage other objects on the same thread that expect the previous context. As a result, OperationContextScope is designed to be used inside a using statement and provide only a scope of code with a new operation context, even in the face of exceptions (hence its name):

using(OperationContextScope scope = new OperationContextScope(...))
{
   //Do work with new context
   ...
}//Restores previous context here

When constructing a new OperationContextScope instance, you provide its constructor with the inner channel of the proxy used for the call (and thus affect the message). Example B-1 shows the steps required to send an integer to a service in the message headers.

Example B-1. Passing integer in headers by the client

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
class MyContractClient : ClientBase<IMyContract>,IMyContract
{...}

//Client code:
MessageHeader<int> numberHeader = new MessageHeader<int>(123);

MyContractClient proxy = new MyContractClient();
using(OperationContextScope contextScope =
                                     new OperationContextScope(proxy.InnerChannel))
{
   OperationContext.Current.OutgoingMessageHeaders.Add(
                                  numberHeader.GetUntypedHeader("Int32","System"));

   proxy.MyMethod();
}
proxy.Close();

The client first constructs an instance of MessageHeader<int>, initializing it with the value 123. The client then uses the GetUntypedHeader() method to convert the type-safe integer header to a non-type-safe representation, using the integer name and namespace as keys, and add that to the outgoing headers inside a new operation context scope. The call to the service is also inside the scope. After exiting the operation context scope, the client closes the proxy in the original operation context scope (if any).

Service-Side Header Interaction

Example B-2 shows the matching service code required to read the integer from the incoming message headers. Note that the service must know in advance the keys required to look up the number from the headers.

Example B-2. Reading integer from headers by the service

class MyService : IMyContract
{
   public void MyMethod()
   {
      int number = OperationContext.Current.IncomingMessageHeaders.
                                                  GetHeader<int>("Int32","System");
      Debug.Assert(number == 123);
   }
}

Logically, the service treats the out-of-band integer passed to it as a number context. Any party down the call chain from the service can also read the number context from the operation context.

Encapsulating the Headers

Both the client and the service will benefit greatly from encapsulating the interaction with the message headers by defining a NumberContext helper class, as shown in Example B-3.

Example B-3. The NumberContext helper class

class NumberContext
{
   public static int Current
   {
      get
      {
         OperationContext context = OperationContext.Current;
         if(context == null)
         {
            return 0;
         }
         return context.IncomingMessageHeaders.GetHeader<int>("Int32","System");
      }
      set
      {
         OperationContext context = OperationContext.Current;
         MessageHeader<int> numberHeader = new MessageHeader<int>(value);
         context.OutgoingMessageHeaders.Add(
                                  numberHeader.GetUntypedHeader("Int32","System"));
      }
   }
}

Using NumberContext mimics the use of any built-in .NET context, since it offers the Current static property, which gets and sets the appropriate headers collection. Using NumberContext, Example B-1 is reduced to the code shown in Example B-4.

Example B-4. Using NumberContext by the client

MyContractClient proxy = new MyContractClient();
using(OperationContextScope contextScope =
                                     new OperationContextScope(proxy.InnerChannel))
{
   NumberContext.Current = 123;

   proxy.MyMethod();
}
proxy.Close();

Likewise, Example B-2 is reduced to Example B-5.

Example B-5. Using NumberContext by the service

class MyService : IMyContract
{
   public void MyMethod()
   {
      int number = NumberContext.Current;
      Debug.Assert(number == 123);
   }
}

The GenericContext<T> helper class

While both Example B-4 and Example B-5 are a marked improvement over direct interaction with the headers, such use of the headers is still problematic, since it involves defining helper classes with repeated, complicated code for every use of message headers as a logical context. The solution is to generalize the technique shown in Example B-3 using a generic type parameter. This is exactly what my GenericContext<T> class, shown in Example B-6, does. GenericContext<T> and the rest of the helper classes in this chapter are available with ServiceModelEx.

Example B-6. The GenericContext<T> class

[DataContract]
public class GenericContext<T>
{
   [DataMember]
   public readonly T Value;

   internal static string TypeName;
   internal static string TypeNamespace;

   static GenericContext()
   {
      //Verify [DataContract] or [Serializable] on T
      Debug.Assert(IsDataContract(typeof(T)) || typeof(T).IsSerializable);

      TypeNamespace = "net.clr:" + typeof(T).FullName;
      TypeName = "GenericContext";
   }
   static bool IsDataContract(Type type)
   {
      object[] attributes =
                     type.GetCustomAttributes(typeof(DataContractAttribute),false);
      return attributes.Length == 1;
   }

   public GenericContext(T value)
   {
      Value = value;
   }

   public GenericContext() : this(default(T))
   {}
   public static GenericContext<T> Current
   {
      get
      {
         OperationContext context = OperationContext.Current;
         if(context == null)
         {
            return null;
         }
         try
         {
            return context.IncomingMessageHeaders.
                              GetHeader<GenericContext<T>>(TypeName,TypeNamespace);
         }
         catch
         {
            return null;
         }
      }
      set
      {
         OperationContext context = OperationContext.Current;
         Debug.Assert(context != null);

         //Having multiple GenericContext<T> headers is an error
         bool headerExists = false;
         try
         {
            context.OutgoingMessageHeaders.
                              GetHeader<GenericContext<T>>(TypeName,TypeNamespace);
            headerExists = true;
         }
         catch(MessageHeaderException exception)
         {
            Debug.Assert(exception.Message == "There is not a header with name " +
                                              TypeName + " and namespace " +
                                              TypeNamespace + " in the message.");
         }
         if(headerExists)
         {
            throw new InvalidOperationException("A header with name " + TypeName +
                                                " and namespace " + TypeNamespace +
                                                " already exists in the message.");
         }
         MessageHeader<GenericContext<T>> genericHeader =
                                       new MessageHeader<GenericContext<T>>(value);
         context.OutgoingMessageHeaders.Add(
                           genericHeader.GetUntypedHeader(TypeName,TypeNamespace));
      }
   }
}

GenericContext<T> lets you treat any serializable or data contract type parameter as a logical context, and its static constructor validates that. The type parameter used is a generic yet type-safe and application-specific custom context. GenericContext<T> uses “GenericContext” for the type name and the full name of T for the namespace to reduce the chance of a conflict. GenericContext<T> also validates that the outgoing headers do not already contain such a type parameter. Both the client and the service can use GenericContext<T> as-is. All a client has to do to pass some custom context to the service is set the static Current property inside a new OperationContextScope:

GenericContext<int>.Current = new GenericContext<int>(123);

On the service side, to read the value out of the headers, any downstream party can write:

int number = GenericContext<int>.Current.Value;

Alternatively, you can wrap GenericContext<T> with a dedicated context. Using GenericContext<T>, the NumberContext of Example B-3 is reduced to the code shown in Example B-7.

Example B-7. NumberContext using GenericContext<T>

class NumberContext
{
   public static int Current
   {
      get
      {
         return GenericContext<int>.Current.Value;
      }
      set
      {
         GenericContext<int>.Current = new GenericContext<int>(value);
      }
   }
}

Streamlining the Client

Even when using GenericContext<T>, the client code (as in Example B-4) is far too raw and exposed, requiring every invocation of the proxy to use an operation context scope. It is better to encapsulate these steps in the proxy itself. The constructors of the proxy should all take additional parameters for the value to pass in the headers. Inside every method, the proxy will create a new operation context and add the value to the outgoing headers collection. This will avoid on every invocation polluting the client code with the interaction with the logical context and the operation context.

Using the same contract definition as in Example B-1, Example B-8 shows such a proxy used to pass an out-of-band number in the message headers.

Example B-8. Encapsulating the headers and the operation context scope

class MyContractClient : ClientBase<IMyContract>,IMyContract
{
   readonly int Number;

   public MyContractClient(int number)
   {
      Number = number;
   }
   public MyContractClient(int number,string endpointName) : base(endpointName)
   {
      Number = number;
   }

   //More constructors

   public void MyMethod()
   {
      using(OperationContextScope contextScope =
                                           new OperationContextScope(InnerChannel))
      {
         NumberContext.Current = Number;

         Channel.MyMethod();
      }
   }
}

All the constructors of the proxy in Example B-8 accept the number to pass to the service and save it in a read-only variable. The proxy uses the NumberContext class of Example B-7 to encapsulate the interaction with the headers.

Using the proxy from Example B-8, the client code from Example B-4 is reduced to:

MyContractClient proxy = new MyContractClient(123);
proxy.MyMethod();
proxy.Close();

The HeaderClientBase<T,H> proxy class

The problem with the technique demonstrated in Example B-8 is that you would have to repeat such code in every method in the proxy, and for every other proxy that wishes to pass out-of-band parameters. It is therefore preferable to encapsulate these steps further in a dedicated proxy class, and even avoid the interaction with the operation context altogether using message interception. To that end, I wrote HeaderClientBase<T,H>, defined in Example B-9.

Example B-9. The HeaderClientBase<T,H> proxy base class

public abstract partial class HeaderClientBase<T,H> : InterceptorClientBase<T>
                                                                   where T : class
{
   public H Header
   {get;protected set;}

   public HeaderClientBase() : this(default(H))
   {}
   public HeaderClientBase(string endpointName) : this(default(H),endpointName)
   {}

   public HeaderClientBase(H header)
   {
      Header = header;
   }
   public HeaderClientBase(H header,string endpointName) : base(endpointName)
   {
      Header = header;
   }

   //More constructors

   protected override void PreInvoke(ref Message request)
   {
      GenericContext<H> context = new GenericContext<H>(Header);
      MessageHeader<GenericContext<H>> genericHeader =
                                    new MessageHeader<GenericContext<H>>(context);
      request.Headers.Add(genericHeader.GetUntypedHeader(
                     GenericContext<H>.TypeName,GenericContext<H>.TypeNamespace));
   }
}

The type parameter H can be any serializable or data contract type. In order for you to use it with or without header information, HeaderClientBase<T,H> offers two sets of constructors—one set that accepts a header and one set that does not. The constructors that accept the header store it in the protected Header property. HeaderClientBase<T,H> derives from the InterceptorClientBase<T> class defined in Appendix E as part of a generic interception framework. InterceptorClientBase<T> provides the PreInvoke() virtual method where its subclasses can hook the outgoing message and interact with it. The overridden version of PreInvoke() creates a new instance of GenericContext<H>, and manually adds it to the request message headers. Note that the value of the header is read every time by accessing the Header property.

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

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