Context Bindings

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.

Client-Side Context Binding Interaction

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

Service-Side Context Binding Interaction

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

Streamlining the Client

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 : ContextClientBase<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.

Streamlining the Service

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.

Creating a Custom Context Binding

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>

Implementing NetNamedPipeContextBinding

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.

Tip

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.

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

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