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.
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).
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.
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); } }
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 Generic
Context<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 Generic
Context<T>
, the NumberContext
of Example B-3 is reduced to the code
shown in Example B-7.
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 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 Generic
Context<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.
3.16.130.201