Monday, February 25, 2013

How to modify WCF message using routing service

Introduction


One of the amazing new feature of .NET 4.0 is addition of System.ServiceModel.Routing namespace (aka: routing service). Out of the box the routing service allows us to route messages from one endpoint to other endpoint(s). This is really nice in creating optimized architecture implementation. A good example of such implementation is a load-balancing application.

If you need to just do simple message routing then what you are provide out of the box is enough. It would serve you well. But if you have a need where you have to modify the content of messages, then you have to do some customization and enhancements that would do the job.

The purpose of this blog post is to introduce you to a method where you can not only route messages using the System.ServiceModel.Routing namespace but also modify its contents.


Detail


Let us start by taking a quick look at an implementation of System.ServiceModel.Routing,where the implementation is only routing the messages.

1. Create a WCF web service application.
2. Modify the. svc file to have following markup:
<%@ ServiceHost
Language="C#"
Debug="true"
Service="System.ServiceModel.Routing.RoutingService" %>


3. Remove any code behind file or interface files.
4. Make changes to web.config as following:


 version="1.0"?>
<configuration>
 <system.web>
  <compilation debug="true"
      targetFramework="4.0">
   <assemblies>
    <add assembly="System.ServiceModel.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
   </assemblies>
  </compilation>
 </system.web>
 <system.serviceModel>
  <behaviors>
   <serviceBehaviors>
    <behavior name="routingBehavior">
     <routing
      routeOnHeadersOnly="true"
      filterTableName="filters"/>

     <serviceDebug includeExceptionDetailInFaults="true"/>
     <serviceMetadata httpGetEnabled="true"/>
    </behavior>
   </serviceBehaviors>
  </behaviors>
  <bindings>;
   <basicHttpBinding>
    <binding
     name="basicHttp"
     maxReceivedMessageSize="2147483647"
     maxBufferPoolSize="2147483647"
     maxBufferSize="2147483647"
     openTimeout="00:02:10"
     sendTimeout="00:10:30"
     receiveTimeout="00:10:30">
     <readerQuotas maxStringContentLength="2147483647"
          maxArrayLength="67108664"/>
    </binding>
   </basicHttpBinding>
   <wsHttpBinding>
    <binding name="wsHttp"
       maxReceivedMessageSize="2147483647"
       openTimeout="00:02:10"
       sendTimeout="00:10:30"
       receiveTimeout="00:10:30">
     <readerQuotas maxStringContentLength="2147483647" />
    </binding?
   </wsHttpBinding>
  </bindings>
  <routing>
   <filters>
    
    <filter
     name="TargetServiceFilter"
     filterType="Action"
     filterData="http://tempuri.org/ITargetService/GetServiceData"/>
 
   </filters>
   <filterTables>
    
    <filterTable name="filters">
     <add filterName="TargetServiceFilter"
       endpointName="TargetService" />
    </filterTable>
   </filterTables>
  </routing>
  <services>
   
   <service name="System.ServiceModel.Routing.RoutingService"
      behaviorConfiguration="routingBehavior">
    <endpoint
     address="/identity"
     binding="wsHttpBinding"
     contract="System.ServiceModel.Routing.IRequestReplyRouter"
     name="TargetService"/>
    <host>
     <baseAddresses>
      <add baseAddress="http://localhost:44531/Router.svc"/>
     </baseAddresses>
    </host>
   </service>
  </services>
  
  <client>
   <endpoint
    address="http://localhost:2932/TargetService.svc/basic"
    binding="basicHttpBinding"
    bindingConfiguration="basicHttp"
    contract="*"
    name="TargetService"/>
  </client>
  <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
 </system.serviceModel>
 <system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
 </system.webServer>
 
</configuration>



The above implementation is for simple routing where any messages that are pointing to the router would be routed to TargetService (definition details are mentioned above).

In the following section we would take the above implementation and change it to allow us have the ability to modify the message that is reaching the router before it is forwarded to the target service.

Steps to enable message modifications for router service


In order for us to accomplish that we would use custom filter. A custom filter would allow us to plug in our class. This class would be invoked when ever a message is received by the router. There few conditions that must be met for that class to work. The most important condition is that this class should inherit from MessageFilter  abstract class. This class is part of System.ServiceModel.Dispatcher
 namespace.
Step 1: Create a new class called RegularMessageFilter. This class would be our custom message filter.


 public class RegularMessageFilter : MessageFilter
 {
  #region Private member
 
  private RegularMessageFilterGroup group;
  private string groupName;
 
  #endregion Private member
 
  #region Constructor(s)
 
  public RegularMessageFilter()
  {
  }
 
  public RegularMessageFilter(string groupName)
  {
   if (string.IsNullOrEmpty(groupName)) { throw new ArgumentNullException("groupName"); }
 
   this.groupName = groupName;
  }
 
  #endregion Constructor(s)
 
  #region Private methods
 
  private void SetGroup(RegularMessageFilterGroup group)
  {
   if (group == null) { throw new ArgumentNullException("group"); }
 
   this.group = group;
  }
 
  #endregion Private methods
 
  #region Overrides
 
  public override bool Match(System.ServiceModel.Channels.Message message)
  {
   throw new NotSupportedException();
  }
 
  public override bool Match(System.ServiceModel.Channels.MessageBuffer buffer)
  {
   throw new NotSupportedException();
  }
 
  protected override IMessageFilterTable CreateFilterTable()
  {
   return new RegularMessageFilterTable();
  }
 
  #endregion Overrides
 
  #region Private helper classes
 
  private class RegularMessageFilterGroup
  {
   #region Private members
 
   private string name;
   private List<RegularMessageFilter> filters = new List<RegularMessageFilter>();
   private IEnumerator<RegularMessageFilter> currentPosition;
 
   #endregion Private members
 
   #region Constructor(s)
 
   public RegularMessageFilterGroup(string name)
   {
    this.name = name;
   }
 
   #endregion Constructor(s)
 
   #region Private methods
 
   private void EnsureEnumerator()
   {
    if (this.currentPosition == null)
    {
     this.currentPosition = filters.GetEnumerator();
     this.currentPosition.MoveNext();
    }
   }
 
   private void AdvanceEnumerator()
   {
    if (!this.currentPosition.MoveNext())
    {
     //Reached the end, clear the enumerator
     this.currentPosition.Dispose();
     this.currentPosition = null;
    }
   }
 
   #endregion Private methods
 
   #region Public methods
 
   public RegularMessageFilter GetNext()
   {
    this.EnsureEnumerator();
    RegularMessageFilter next = (RegularMessageFilter)this.currentPosition.Current;
    this.AdvanceEnumerator();
    return next;
   }
 
   public bool Match(RegularMessageFilter filter)
   {
    this.EnsureEnumerator();
    RegularMessageFilter currentFilter = (RegularMessageFilter)this.currentPosition.Current;
    bool matched = Object.ReferenceEquals(currentFilter, filter);
    if (matched)
    {
     this.AdvanceEnumerator();
    }
    return matched;
   }
 
   #endregion Public methods
 
   #region Internal methods
 
   internal void AddFilter(RegularMessageFilter filter)
   {
    if (this.currentPosition != null)
    {
     throw new InvalidOperationException("Cannot add while enumerating");
    }
    this.filters.Add(filter);
    filter.SetGroup(this);
   }
 
   #endregion Internal methods
  }
 
  private class RegularMessageFilterTable : IMessageFilterTable
  {
   #region Constants
 
   private const string YOUR_NAMESPACE = "http://yournamespace/";
   private const string ENDPOINT_MESSAGE_FILTER_KEY_NAME = "System.ServiceModel.Routing.EndpointNameMessageFilter.Name";
 
   #endregion Constants
 
   #region Private members
 
   private Dictionary<MessageFilter, TFilterData> filters = new Dictionary<MessageFilter, TFilterData>();
   private Dictionary<stringRegularMessageFilterGroup> groups = new Dictionary<stringRegularMessageFilterGroup>();
 
   #endregion Private members
 
   #region Contructor(s)
 
   public RegularMessageFilterTable()
   {
   }
 
   #endregion Contructor(s)
 
   #region Public methods
 
   public bool GetMatchingFilter(MessageBuffer messageBuffer, out MessageFilter filter)
   {
    throw new NotImplementedException();
   }
 
   public bool GetMatchingFilter(Message message, out MessageFilter filter)
   {
    throw new NotImplementedException();
   }
 
   public bool GetMatchingFilters(MessageBuffer messageBuffer, ICollection<MessageFilter> results)
   {
    bool foundSome = false;
    foreach (RegularMessageFilterGroup group in this.groups.Values)
    {
     RegularMessageFilter matchingFilter = group.GetNext();
     results.Add(matchingFilter);
     foundSome = true;
    }
 
    return foundSome;
   }
 
   public bool GetMatchingFilters(Message message, ICollection<MessageFilter> results)
   {
    bool foundSome = false;
    foreach (RegularMessageFilterGroup group in this.groups.Values)
    {
     RegularMessageFilter matchingFilter = group.GetNext();
     results.Add(matchingFilter);
     foundSome = true;
    }
 
    return foundSome;
   }
 
   public bool GetMatchingValue(MessageBuffer messageBuffer, out TFilterData value)
   {
    value = default(TFilterData);
    List results = new List();
    bool outcome = this.GetMatchingValues(messageBuffer, results);
    if (results.Count > 1)
    {
     throw new MultipleFilterMatchesException();
    }
    else if (results.Count == 1)
    {
     value = results[0];
    }
    return outcome;
   }
 
   public bool GetMatchingValue(Message message, out TFilterData value)
   {
    SetupMessage(message, "TestValue");
    value = default(TFilterData);
    List results = new List();
    bool outcome = this.GetMatchingValues(message, results);
    if (results.Count > 1)
    {
     throw new MultipleFilterMatchesException();
    }
    else if (results.Count == 1)
    {
     value = results[0];
    }
    return outcome;
   }
 
   public bool GetMatchingValues(MessageBuffer messageBuffer, ICollection results)
   {
    bool foundSome = false;
    foreach (RegularMessageFilterGroup group in this.groups.Values)
    {
     RegularMessageFilter matchingFilter = group.GetNext();
     results.Add(this.filters[matchingFilter]);
     foundSome = true;
    }
 
    return foundSome;
   }
 
   public bool GetMatchingValues(Message message, ICollection results)
   {
    bool foundSome = false;
    object httpRequestMessageObject;
    if (message.Properties.TryGetValue(ENDPOINT_MESSAGE_FILTER_KEY_NAME, out httpRequestMessageObject))
    {
     foreach (RegularMessageFilterGroup group in this.groups.Values)
     {
      RegularMessageFilter matchingFilter = group.GetNext();
      if (matchingFilter.groupName == httpRequestMessageObject.ToString())
      {
       results.Add(this.filters[matchingFilter]);
       foundSome = true;
      }
     }
    }
    return foundSome;
   }
 
   public void SetupMessage(Message requestMessage, string customMessageHeaderValue)
   {
    if (!string.IsNullOrEmpty(customMessageHeaderValue))
    {
     MessageHeader messageHeaderForCookie = MessageHeader.CreateHeader("CustomHeader", YOUR_NAMESPACE, customMessageHeaderValue);
     requestMessage.Headers.Add(messageHeaderForCookie);
    }
   }
 
   public void Add(MessageFilter key, TFilterData value)
   {
    RegularMessageFilter filter = (RegularMessageFilter)key;
    RegularMessageFilterGroup group;
    if (!this.groups.TryGetValue(filter.groupName, out group))
    {
     group = new RegularMessageFilterGroup(filter.groupName);
     this.groups.Add(filter.groupName, group);
    }
    group.AddFilter(filter);
    this.filters.Add(key, value);
   }
 
   public bool ContainsKey(MessageFilter key)
   {
    throw new NotImplementedException();
   }
 
   public bool Remove(MessageFilter key)
   {
    throw new NotImplementedException();
   }
 
   #endregion Public methods
 
   #region Public members
 
   public ICollection<MessageFilter> Keys
   {
    get { throw new NotImplementedException(); }
   }
 
   public bool TryGetValue(MessageFilter key, out TFilterData value)
   {
    throw new NotImplementedException();
   }
 
   public ICollection Values
   {
    get { throw new NotImplementedException(); }
   }
 
   public TFilterData this[MessageFilter key]
   {
    get
    {
     throw new NotImplementedException();
    }
    set
    {
     throw new NotImplementedException();
    }
   }
 
   public void Add(KeyValuePair<MessageFilter, TFilterData> item)
   {
    throw new NotImplementedException();
   }
 
   public void Clear()
   {
    throw new NotImplementedException();
   }
 
   public bool Contains(KeyValuePair<MessageFilter, TFilterData> item)
   {
    throw new NotImplementedException();
   }
 
   public void CopyTo(KeyValuePair<MessageFilter, TFilterData>[] array, int arrayIndex)
   {
    throw new NotImplementedException();
   }
 
   public int Count
   {
    get { throw new NotImplementedException(); }
   }
 
   public bool IsReadOnly
   {
    get { throw new NotImplementedException(); }
   }
 
   public bool Remove(KeyValuePair<MessageFilter, TFilterData> item)
   {
    throw new NotImplementedException();
   }
 
   public IEnumerator<KeyValuePair<MessageFilter, TFilterData>> GetEnumerator()
   {
    throw new NotImplementedException();
   }
 
   IEnumerator IEnumerable.GetEnumerator()
   {
    throw new NotImplementedException();
   }
 
   #endregion Public members
  }
 
  #endregion Private helper classes
 }


If you notice the above code snippet, there is a function called 'SetupMessage'. This function is at the heart of this blog. In this function we have modified the incoming message before it is being forwarded to the target service.

Step 2: Modify the web.config file to incorporate the custom message filter that we created in the previous step. This will be done simply by commenting out the already created filter and adding the custom filter. Here is how it would be implemented.

    
 
    <filter
     name="TargetServiceFilter"
     filterType="Custom"
     customType="RoutingWithMessageModification.Service.RegularMessageFilter, RoutingWithMessageModification.Service"
     filterData="TargetService"/>


Conclusion

We have seen that using the out of the box router service, we can add our custom filter that can be used to modify message contents. The above example only adds a message header to the out going messages. But using the custom filter we can add any logic to manipulate messages.