With .NET 3.5, WCF gained three additional bindings dedicated to managing custom
contexts. These bindings, found in the System.WorkflowServices.dll assembly, are the BasicHttpContextBinding
, the NetTcpContextBinding
, and the WSHttpContextBinding
. The context bindings all derive from their respective
regular bindings:
public class BasicHttpContextBinding : BasicHttpBinding { /* Same constructors as BasicHttpBinding */ } public class NetTcpContextBinding : NetTcpBinding { /* Same constructors as NetTcpBinding */ public ProtectionLevel ContextProtectionLevel {get;set;} } public class WSHttpContextBinding : WSHttpBinding { /* Same constructors as WSHttpBinding */ public ProtectionLevel ContextProtectionLevel {get;set;} }
In the case of the NetTcpContextBinding
and the
WSHttpContextBinding
, the ContextProtectionLevel
indicates how to protect the context while in transfer,
as discussed in Chapter 10.
The context bindings are used exactly the same way as their base bindings, yet they add support for a dedicated context management protocol. These bindings can be used with or without a context. The context protocol lets you pass as a custom context a collection of strings in the form of pairs of keys and values, stored implicitly in the message headers. There are several important differences between using a context binding and using the direct message headers for passing out-of-band parameters to a custom context:
With a context binding, you can only set the information to pass to the service once, before opening the proxy (or using it for the first time). After that, the custom context is cached, and any attempt to modify it results in an error. With the message headers, every call to the services on the same proxy may contain different headers.
With the context binding, you can only pass as parameters simple strings in the form of a keys/values dictionary. This is a liability when trying to pass composite types that go beyond simple values. With message headers, any serializable or data contract type will do.
The use of strings means there is inherently no type safety with the context
parameters. While this is also true with message headers, my GenericContext<T>
does restore the missing type safety.
Out of the box, only a limited set of bindings support the context protocol. Glaringly missing are the IPC and MSMQ bindings. The message headers technique works over any binding.
The client sets the context to send to the service using the IContextManager
interface:
public interface IContextManager { IDictionary<string,string> GetContext( ); void SetContext(IDictionary<string,string> context); bool Enabled {get;set;} }
The client obtains the reference to the IContextManager
interface by accessing the proxy's inner channel
properties:
public abstract class ClientBase<T> : ICommunicationObject where T : class
{
public IClientChannel InnerChannel
{get;}
//More members
}
public interface IClientChannel : IContextChannel,...
{...}
public interface IContextChannel : IChannel,...
{...}
public interface IChannel : ICommunicationObject
{
T GetProperty<T>( )
where T : class;
}
The InnerChannel
property supports the IChannel
interface, which offers the GetProperty<T>( )
method:
MyContractClient proxy = new MyContractClient( );
IContextManager contextManager = proxy.InnerChannel.GetProperty
<IContextManager
>( );
Once the client obtains IContextManager
, it can
copy the current context by calling the GetContext( )
method. The context is merely a dictionary of strings as keys and values. Since the
dictionary returned from GetContext( )
is a copy of the
actual context, the client cannot use it to change the context. Instead, the client needs
to call the SetContext( )
method, providing the new
context. The client can override the old context or just add values to the old context and
then set it back in, as shown in Example B-10.
Example B-10. Setting the context on the proxy
MyContractClient proxy = new MyContractClient( ); IContextManager contextManager = proxy.InnerChannel.GetProperty <IContextManager
>( ); //Just add in, not overwriting dictionary IDictionary<string,string> context = contextManager.GetContext
( ); context["NumberContext"] = "123"; contextManager.SetContext
(context); proxy.MyMethod( ); proxy.Close( );
The service reads the context values from the incoming message properties, accessed via the operation context:
public sealed class OperationContext : ... { public MessageProperties IncomingMessageProperties { get; } //More members }
MessageProperties
is a non-type-safe dictionary
that accepts a string key and returns the matching object value:
public sealed class MessageProperties : IDictionary<string,object> {...}
To obtain the context property, the service uses the static string ContextMessageProperty.Name
. This returns an object of the
type ContextMessageProperty
, defined as:
[Serializable]
public class ContextMessageProperty : IMessageProperty
{
public IDictionary<string,string> Context
{get;}
public static string Name
{get;}
//More members
}
The Context
property of ContextMessageProperty
is the same dictionary of parameters passed by the
client. Example B-11 shows the required
service-side steps to read the number context passed in Example B-10.
Example B-11. Reading the context by the service
class MyService : IMyContract { public void MyMethod( ) { ContextMessageProperty contextProperty = OperationContext.Current. IncomingMessageProperties[ContextMessageProperty.Name] as ContextMessageProperty; Debug.Assert(contextProperty.Context.ContainsKey("NumberContext")); string number = contextProperty.Context["NumberContext"]; Debug.Assert(number == "123"); } }
You can streamline the steps required of the client to read or write to the context
using my ContextManager
static helper class, shown in
Example B-12.
Example B-12. Client-side methods of ContextManager
public static class ContextManager { public static void SetContext(IClientChannel innerChannel, string key,string value) { SetContext(innerChannel,CreateContext(key,value)); } public static void SetContext(IClientChannel innerChannel, IDictionary<string,string> context) { IContextManager contextManager = innerChannel.GetProperty<IContextManager>( ); contextManager.SetContext(context); } public static IDictionary<string,string> CreateContext(string key,string value) { IDictionary<string,string> context = new Dictionary<string,string>( ); context[key] = value; return context; } public static IDictionary<string,string> UpdateContext( IClientChannel innerChannel, string key,string value) { IContextManager contextManager = innerChannel.GetProperty<IContextManager>( ); IDictionary<string,string> context = new Dictionary<string,string>(contextManager.GetContext( )); context[key] = value; return context; } //Proxy extensions public static void SetContext<T>(this ClientBase<T> proxy, string key,string value) where T : class { SetContext(proxy.InnerChannel,key,value); } public static void SetContext<T>(this ClientBase<T> proxy, IDictionary<string,string> context) where T : class { SetContext(proxy.InnerChannel,context); } public static IDictionary<string,string> UpdateContext<T>( this ClientBase<T> proxy,string key,string value) where T : class { return UpdateContext(proxy.InnerChannel,key,value); } }
ContextManager
offers overloaded versions of the
SetContext( )
method that allow the client to set a
new context on a proxy's inner channel, using a single key/value pair or a collection of
such pairs in a dictionary. These methods are useful both with a proxy class and with a
channel factory. ContextManager
also exposes setting
the context as an extension method on the proxy class. You can use the CreateContext( )
method to create a new dictionary or the
UpdateContext( )
method to add a key/value pair to an
existing context. Using ContextManager
, Example B-10 is reduced to:
MyContractClient proxy = new MyContractClient( );
proxy.SetContext
("NumberContext","123");
proxy.MyMethod( );
proxy.Close( );
However, relying on SetContext( )
this way requires
you to explicitly use it upon every instantiation of the proxy. It is better to
encapsulate ContextManager
in a dedicated proxy class,
such as my ContextClientBase<T>
:
public abstract class ContextClientBase<T> : ClientBase<T> where T : class { public ContextClientBase( ); public ContextClientBase(string endpointName); public ContextClientBase(string key,string value); public ContextClientBase(IDictionary<string,string> context); public ContextClientBase(string key,string value,string endpointName); public ContextClientBase(IDictionary<string,string> context, string endpointName); //More constructors }
The constructors of ContextClientBase<T>
accept the usual proxy parameters, such as the endpoint name or binding and address, as
well as the contextual parameters to send the service (either a single key/value pair, or
a collection of keys and values using a dictionary). Your proxy can derive directly from
ContextClientBase<T>
:
class MyContractClient : Context
ClientBase<IMyContract>,IMyContract
{
public MyContractClient(string key,string value) : base(key,value)
{}
/* More constructors */
public void MyMethod( )
{
Channel.MyMethod( );
}
}
Using ContextClientBase<T>
, Example B-10 is reduced to:
MyContractClient proxy = new MyContractClient("NumberContext","123"); proxy.MyMethod( ); proxy.Close( );
Example B-13 shows the implementation of ContextClientBase<T>
.
Example B-13. Implementing ContextClientBase<T>
public abstract class ContextClientBase<T> : ClientBase<T> where T : class { public ContextClientBase(string key,string value,string endpointName) : this(ContextManager.CreateContext(key,value),endpointName) {} public ContextClientBase(IDictionary<string,string> context,string endpointName) : base(endpointName) { SetContext(context); } /* More constructors */ void SetContext(IDictionary<string,string> context) { VerifyContextBinding( ); ContextManager.SetContext(InnerChannel,context); } void VerifyContextBinding( ) { BindingElementCollection elements = Endpoint.Binding.CreateBindingElements( ); if(elements.Contains(typeof(ContextBindingElement))) { return; } throw new InvalidOperationException("Can only use context binding"); } }
A few of the constructors of ContextClientBase<T>
use ContextManager
to create a new context and pass it to another constructor,
which calls the SetContext( )
helper method. SetContext( )
first verifies that the binding used is indeed a
context binding and then uses ContextManager
to set the
context. Verifying that the binding indeed supports the context protocol is done by
searching for the ContextBindingElement
in the
collection of binding elements. This way of verifying is better than looking at the
binding type, since it also works automatically with a custom context binding.
For the service, the ContextManager
helper class
encapsulates the interaction with operation context and message properties. ContextManager
provides the GetContext( )
method:
public static class ContextManager { public static string GetContext(string key); //More members }
Using GetContext( )
, the service code in Example B-11 is reduced to:
class MyService : IMyContract { public void MyMethod( ) { string number = ContextManager.GetContext("NumberContext"); Debug.Assert(number == "123"); } }
Example B-14 shows the implementation of
GetContext( )
.
Example B-14. Implementing GetContext( )
public static class ContextManager { public static string GetContext(string key) { if(OperationContext.Current == null) { return null; } if(OperationContext.Current.IncomingMessageProperties. ContainsKey(ContextMessageProperty.Name)) { ContextMessageProperty contextProperty = OperationContext.Current.IncomingMessageProperties[ContextMessageProperty.Name] as ContextMessageProperty; if(contextProperty.Context.ContainsKey(key) == false) { return null; } return contextProperty.Context[key]; } else { return null; } } }
GetContext( )
is similar to the explicit steps
taken in Example B-11, except it adds state and
error management. If the context does not contain the request key (or if no context was
found), GetContext( )
returns null
.
WCF provides context support for the basic, WS, and TCP bindings. Missing from that list is the IPC binding. It would be valuable to have that support for the IPC binding for custom context support on the same machine. Creating such a custom binding is a worthy exercise, and it serves as a good demonstration of how to write a custom binding.
ServiceModelEx contains the NetNamedPipeContextBinding
class, defined as:
public class NetNamedPipeContextBinding : NetNamedPipeBinding { /* Same constructors as NetNamedPipeBinding */ public ProtectionLevel ContextProtectionLevel {get;set;} }
NetNamedPipeContextBinding
is used exactly like its
base class, and you can use it with or without a context. Both the client and the host can
use this binding programmatically as-is, by instantiating it like any other built-in
binding. However, when using a custom binding in conjunction with a config file, you need
to inform WCF where the custom binding is defined.
To that end, ServiceModelEx also defines the NetNamedPipeContextBindingElement
and NetNamedPipeContextBindingCollectionElement
helper classes:
public class NetNamedPipeContextBindingElement : NetNamedPipeBindingElement { public NetNamedPipeContextBindingElement( ); public NetNamedPipeContextBindingElement(string name); public ProtectionLevel ContextProtectionLevel {get;set;} } public class NetNamedPipeContextBindingCollectionElement : StandardBindingCollectionElement<NetNamedPipeContextBinding, NetNamedPipeContextBindingElement> {}
You need to add the type of NetNamedPipeContextBindingCollectionElement
and its assembly to the list of
binding extensions, naming NetNamedPipeContextBinding
as a custom binding. You can do this on a per-application basis by adding it to the
application config file. Due to a deficiency in the WCF configuration system, when doing
this you must also add a dummy binding configuration section for NetNamedPipeContextBinding
, even if no endpoint (client or service) makes use
of it.
Example B-15 shows such an application-specific config file for the host side, but you have to enter the same directives in the client's config file as well.
Example B-15. Adding per-application administrative custom binding support
<system.serviceModel> <extensions> <bindingExtensions> <add name = "netNamedPipeContextBinding" type = "ServiceModelEx.NetNamedPipeContextBindingCollectionElement, ServiceModelEx" /> </bindingExtensions> </extensions> <services> <service name = "..."> <endpoint address = "net.pipe://..." binding = "netNamedPipeContextBinding" contract = "..." /> </service> </services> <bindings> <netNamedPipeContextBinding> <binding name = "ContextIPC"/> </netNamedPipeContextBinding> </bindings> </system.serviceModel>
Alternatively, you can add NetNamedPipeContextBindingCollectionElement
to machine.config to affect every application on the machine. In that case,
there is no need to list the binding extensions in the client or service config file, and
there is no need to add a dummy binding configuration section. Example B-16 shows such a configuration.
Example B-16. Adding machine-wide administrative custom binding support
<!--In machine.config--> <bindingExtensions> <add name = "wsHttpContextBinding" type = "..."/> <add name = "netTcpContextBinding" type = "..."/> <add name = "netNamedPipeContextBinding" type = "ServiceModelEx.NetNamedPipeContextBindingCollectionElement, ServiceModelEx" /> <!--Additional bindings--> </bindingExtensions> <!--In app.config--> <system.serviceModel> <services> <service name = "..."> <endpoint address = "net.pipe://..." binding = "netNamedPipeContextBinding" contract = "..." /> </service> </services> </system.serviceModel>
Of course, you can configure a binding
section to
customize any property of NetNamedPipeContextBinding
,
whether it comes from NetNamedPipeBinding
or from
NetNamedPipeContextBinding
:
<bindings> <netNamedPipeContextBinding> <binding name = "TransactionalContextIPC" contextProtectionLevel = "EncryptAndSign" transactionFlow = "True" /> </netNamedPipeContextBinding> </bindings>
Example B-17 lists the implementation of
NetNamedPipeContextBinding
and its supporting
classes.
Example B-17. Implementing NetNamedPipeContextBinding
public class NetNamedPipeContextBinding : NetNamedPipeBinding { internal const string SectionName = "netNamedPipeContextBinding"; public ProtectionLevel ContextProtectionLevel {get;set;} public NetNamedPipeContextBinding( ) { ContextProtectionLevel = ProtectionLevel.EncryptAndSign; } public NetNamedPipeContextBinding(NetNamedPipeSecurityMode securityMode) : base(securityMode) { ContextProtectionLevel = ProtectionLevel.EncryptAndSign; } public NetNamedPipeContextBinding(string configurationName) { ContextProtectionLevel = ProtectionLevel.EncryptAndSign; ApplyConfiguration(configurationName); } public override BindingElementCollection CreateBindingElements( ) { BindingElement element = new ContextBindingElement(ContextProtectionLevel, ContextExchangeMechanism.ContextSoapHeader); BindingElementCollection elements = base.CreateBindingElements( ); elements.Insert(0,element); return elements; } void ApplyConfiguration(string configurationName) { Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); ServiceModelSectionGroup sectionGroup = ServiceModelSectionGroup.GetSectionGroup(config); BindingsSection bindings = sectionGroup.Bindings; NetNamedPipeContextBindingCollectionElement section = (NetNamedPipeContextBindingCollectionElement)bindings[SectionName]; NetNamedPipeContextBindingElement element = section.Bindings[configurationName]; if(element == null) { throw new ConfigurationErrorsException( ); } else { element.ApplyConfiguration(this); } } } public class NetNamedPipeContextBindingElement : NetNamedPipeBindingElement { const string ContextProtectionLevelName = "contextProtectionLevel"; public NetNamedPipeContextBindingElement( ) { Initialize( ); } public NetNamedPipeContextBindingElement(string name) : base(name) { Initialize( ); } void Initialize( ) { ConfigurationProperty property = new ConfigurationProperty(ContextProtectionLevelName, typeof(ProtectionLevel), ProtectionLevel.EncryptAndSign); Properties.Add(property); ContextProtectionLevel = ProtectionLevel.EncryptAndSign; } protected override void OnApplyConfiguration(Binding binding) { base.OnApplyConfiguration(binding); NetNamedPipeContextBinding netNamedPipeContextBinding = binding as NetNamedPipeContextBinding; Debug.Assert(netNamedPipeContextBinding != null); netNamedPipeContextBinding.ContextProtectionLevel = ContextProtectionLevel; } protected override Type BindingElementType { get { return typeof(NetNamedPipeContextBinding); } } public ProtectionLevel ContextProtectionLevel { get { return (ProtectionLevel)base[ContextProtectionLevelName]; } set { base[ContextProtectionLevelName] = value; } } } public class NetNamedPipeContextBindingCollectionElement : StandardBindingCollectionElement <NetNamedPipeContextBinding,NetNamedPipeContextBindingElement> {}
The constructors of NetNamedPipeContextBinding
all delegate the actual construction to the base constructors of NetNamedPipeBinding
, and the only initialization they do is
setting the context protection level to default to ProtectionLevel.EncryptAndSign
.
The heart of any binding class is the CreateBindingElements( )
method. NetNamedPipeContextBinding
accesses its base binding collection of binding
elements and adds to it the ContextBindingElement
.
Inserting this element into the collection adds support for the context protocol. The
rest of Example B-17 is mere bookkeeping to
enable administrative configuration. The ApplyConfiguration(
)
method is called by the constructor, which takes the binding section
configuration name. ApplyConfiguration( )
uses the
ConfigurationManager
class (discussed in Chapter 9) to parse out of the config file the netNamedPipeContextBinding
section, and from it an instance
of NetNamedPipeContextBindingElement
. That binding
element is then used to configure the binding instance by calling its ApplyConfiguration( )
method. The constructors of NetNamedPipeContextBindingElement
add to its base class
Properties
collection of configuration properties a
single property for the context protection level. In OnApplyConfiguration( )
(which is called as a result of calling ApplyConfiguration( )
on NetNamedPipeBindingElement
by NetNamedPipeContextBinding.ApplyConfiguration( ))
), the method first
configures its base element and then sets the context protection level according to the
configured level.
The NetNamedPipeContextBindingCollectionElement
type is used to bind NetNamedPipeContextBinding
with
the NetNamedPipeContextBindingElement
. This way, when
adding NetNamedPipeContextBindingCollectionElement
as
a binding extension, the configuration manager knows which type to instantiate and
provide with the binding parameters.
Since you can use NetNamedPipeContextBinding
with or without a context, the InProcFactory
class
presented in Chapter 1 actually uses the NetNamedPipeContextBinding
to enable transparent support
for custom contexts if required.
3.17.157.6