次の方法で共有


WCF Extensibility – Operation Selectors

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

We’re now entering the realm of the less used extensibility points for the WCF runtime. Operation selectors could well be left as an internal implementation detail for WCF, but for some reason the designers decided to make them public (I think the guideline back then was if anyone can come up with a scenario where a user would use it, then WCF would expose a hook for that). The server selector (IDispatchOperationSelector) is actually interesting to have as a public hook, as the whole UriTemplate model for REST services was built on top of it – basically, the selector knows about the HTTP method and URI templates for each service operation, and based on those properties of incoming messages it can call the appropriate operation. For “normal” (i.e., SOAP) endpoints, the operation selector implementation (which happens to be internal) is based on the addressing of the binding – in most of the cases it simply matches the Action header of the message with a map of action to the operation names.

The client operation selector (IClientOperationSelector) is another story. The client selector receives the method invoked by the client (the System.Reflection.MethodBase object) and it must return an operation name which will be used by the WCF runtime to retrieve the actual client operation from the client runtime. I’ve tried for a while to think of any scenario where one would actually want to call one method in the proxy and have another operation be executed (besides some practical joke scenario where one would swap Add for Subtract in a calculator), but I just can’t think of any. Even searching on Bing or Google doesn’t show any real scenarios where a client selector is used. But well, WCF is extensible, so let’s let people choose their client operations as well Smile.

Public implementations in WCF

There are no public implementations for IClientOperationSelector, only an internal one which simply returns the operation name of the method being called.

Interface declaration

  1. public interface IDispatchOperationSelector
  2. {
  3.     string SelectOperation(ref Message message);
  4. }
  5.  
  6. public interface IClientOperationSelector
  7. {
  8.     bool AreParametersRequiredForSelection { get; }
  9.     string SelectOperation(MethodBase method, object[] parameters);
  10. }

The dispatch operation selector is quite simple – on SelectOperation you get the message, you return the operation name. In most cases the operation can be selected based on information on the message header (e.g., the Action header for SOAP endpoints) or the message properties (e.g., the HTTP request property attached to the message). There are scenarios, however, where the routing to the operation needs to be done based on the message contents, so the message is passed by reference – if it needs to be consumed by the operation selector, it can recreated it to hand it back to the WCF pipeline.

On the client side the AreParametersRequiredForSelection is used to determine whether the response to SelectOperation can be cached or not – if the parameters are not required, the WCF runtime will only call SelectOperation once per method in the proxy, otherwise every time a proxy call is made the selector will be invoked again.

How to add operation selectors

At the server side: the operation selector is a property of the DispatchRuntime object. It’s typically accessed via the endpoint dispatcher in a call to IEndpointBehavior.ApplyDispatchBehavior.

  1. public class MyEndpointBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  4.     {
  5.         endpointDispatcher.DispatchRuntime.OperationSelector = new MyOperationSelector();
  6.     }
  7. }
  8.  
  9. public class MyOperationSelector : IDispatchOperationSelector
  10. {
  11.     public string SelectOperation(ref Message message)
  12.     {
  13.         string action = message.Headers.Action;
  14.         return action.Substring(action.LastIndexOf('/') + 1);
  15.     }
  16. }

At the client side: the operation selector is a property of the ClientRuntime object, and is typically accessed via the implementation of IEndpointBehavior.ApplyClientBehavior.

  1. public class MyEndpointBehavior : IEndpointBehavior
  2. {
  3.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  4.     {
  5.         clientRuntime.OperationSelector = new MyClientOperationSelector();
  6.     }
  7. }
  8.  
  9. public class MyClientOperationSelector : IClientOperationSelector
  10. {
  11.     public bool AreParametersRequiredForSelection
  12.     {
  13.         get { return false; }
  14.     }
  15.  
  16.     public string SelectOperation(MethodBase method, object[] parameters)
  17.     {
  18.         return method.Name;
  19.     }
  20. }

 

Real world scenario: Support for X-HTTP-Method-Override in REST endpoints

This issue came from the forum. REST services we use HTTP verbs (methods) to determine the action to be applied to the resource. For example, a GET request to /Contacts/1 would return the contact with id 1, while DELETE /Contacts/1 would remove that same contact from the repository. This is a great – and simple – model, but there are some browsers, proxies, firewalls or even some web service platforms which don’t support the full set of verbs (rejecting anything other than GET and POST). In order to overcome this problem, some platforms (such as ASP.NET MVC and Google Data) started supporting a new HTTP header or field, called X-HTTP-Method-Override, which contain the actual method which should be used by the service to access the resource. The WCF REST API doesn’t support this override natively, but that doesn’t preclude us from using an extensibility point to enable it.

Implementing the support for X-HTTP-Method-Override is nothing but a matter of replacing the operation selector (based on methods and URI templates) used by REST endpoints with our own, which is aware of this new header. When the IDispatchOperationSelector.SelectOperation method is called, we can check if the incoming request contained the override header, and if so, we can update the incoming message properties to let the original selector chose the appropriate operation.

For this example I’ll reuse the contact manager service from the message inspectors post – it contains the different operations which differ based on the HTTP verb of the incoming request, so it’s perfect for this issue. And before I start, the usual disclaimer – this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). Also, for simplicity sake it doesn’t have a lot of error handling which a production-level code would. Also, the contact manager is all stored in memory, a “real” one would have a backing database or something more “persistent”.

One small parenthesis: the link below for the code in this post contains the version shown here in C# and also a version in VB.NET which does the same thing; if you’re a VB developer, you can check the MSDN code samples out.

The contracts and service implementation are exactly the same as the one from before (I removed the version for Text, as it didn’t add any value to this topic):

  1. [DataContract]
  2. public class Contact
  3. {
  4.     [DataMember]
  5.     public string Id { get; set; }
  6.     [DataMember]
  7.     public string Name { get; set; }
  8.     [DataMember]
  9.     public string Email { get; set; }
  10.     [DataMember]
  11.     public string[] Telephones { get; set; }
  12. }
  13.  
  14. [ServiceContract]
  15. public interface IContactManager
  16. {
  17.     [WebInvoke(
  18.         Method = "POST",
  19.         UriTemplate = "/Contacts",
  20.         ResponseFormat = WebMessageFormat.Json)]
  21.     string AddContact(Contact contact);
  22.  
  23.     [WebInvoke(
  24.         Method = "PUT",
  25.         UriTemplate = "/Contacts/{id}",
  26.         ResponseFormat = WebMessageFormat.Json)]
  27.     void UpdateContact(string id, Contact contact);
  28.  
  29.     [WebInvoke(
  30.         Method = "DELETE",
  31.         UriTemplate = "/Contacts/{id}",
  32.         ResponseFormat = WebMessageFormat.Json)]
  33.     void DeleteContact(string id);
  34.  
  35.     [WebGet(UriTemplate = "/Contacts", ResponseFormat = WebMessageFormat.Json)]
  36.     List<Contact> GetAllContacts();
  37.  
  38.     [WebGet(UriTemplate = "/Contacts/{id}", ResponseFormat = WebMessageFormat.Json)]
  39.     Contact GetContact(string id);
  40. }
  41.  
  42. public class ContactManagerService : IContactManager
  43. {
  44.     static List<Contact> AllContacts = new List<Contact>();
  45.     static int currentId = 0;
  46.     static object syncRoot = new object();
  47.  
  48.     public string AddContact(Contact contact)
  49.     {
  50.         int contactId = Interlocked.Increment(ref currentId);
  51.         contact.Id = contactId.ToString(CultureInfo.InvariantCulture);
  52.         lock (syncRoot)
  53.         {
  54.             AllContacts.Add(contact);
  55.             WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.Created;
  56.         }
  57.  
  58.         return contact.Id;
  59.     }
  60.  
  61.     public void UpdateContact(string id, Contact contact)
  62.     {
  63.         contact.Id = id;
  64.         lock (syncRoot)
  65.         {
  66.             int index = this.FetchContact(id);
  67.             if (index >= 0)
  68.             {
  69.                 AllContacts[index] = contact;
  70.             }
  71.         }
  72.     }
  73.  
  74.     public void DeleteContact(string id)
  75.     {
  76.         lock (syncRoot)
  77.         {
  78.             int index = this.FetchContact(id);
  79.             if (index >= 0)
  80.             {
  81.                 AllContacts.RemoveAt(index);
  82.             }
  83.         }
  84.     }
  85.  
  86.     public List<Contact> GetAllContacts()
  87.     {
  88.         List<Contact> result;
  89.         lock (syncRoot)
  90.         {
  91.             result = AllContacts.ToList();
  92.         }
  93.  
  94.         return result;
  95.     }
  96.  
  97.     public Contact GetContact(string id)
  98.     {
  99.         Contact result;
  100.         lock (syncRoot)
  101.         {
  102.             int index = this.FetchContact(id);
  103.             result = index < 0 ? null : AllContacts[index];
  104.         }
  105.  
  106.         return result;
  107.     }
  108.  
  109.     private int FetchContact(string id)
  110.     {
  111.         int result = -1;
  112.         for (int i = 0; i < AllContacts.Count; i++)
  113.         {
  114.             if (AllContacts[i].Id == id)
  115.             {
  116.                 result = i;
  117.                 break;
  118.             }
  119.         }
  120.  
  121.         if (result < 0)
  122.         {
  123.             WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
  124.         }
  125.  
  126.         return result;
  127.     }
  128. }

Now for the behavior used to set up the operation selector. The behavior wraps the original operation selector from the endpoint, so it needs to be added after the WebHttpBehavior (which will add the first selector to the runtime) – see the discussion about wrapping internal objects at the post about formatters).

  1. public class HttpOverrideBehavior : IEndpointBehavior
  2. {
  3.     public const string HttpMethodOverrideHeaderName = "X-HTTP-Method-Override";
  4.     public const string OriginalHttpMethodPropertyName = "OriginalHttpMethod";
  5.  
  6.     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  7.     {
  8.     }
  9.  
  10.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  11.     {
  12.     }
  13.  
  14.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  15.     {
  16.         endpointDispatcher.DispatchRuntime.OperationSelector = new HttpOverrideOperationSelector(endpointDispatcher.DispatchRuntime.OperationSelector);
  17.     }
  18.  
  19.     public void Validate(ServiceEndpoint endpoint)
  20.     {
  21.     }
  22. }

And the selector itself. As I mentioned before, The first thing the selector will do is to try to fetch the HttpRequestMessageProperty from the message properties – if the message was received using HTTP, it will contain information about the HTTP verb and headers in the request. It will then try to find the X-HTTP-Method-Override header, and if it’s present, it will update the message property with the method override. For completeness sake, this selector also adds a new property to the message with the original HTTP verb from the request (if the service needs this information somehow). Finally, the selector delegates the selection to the original selector, which will then use that information to route the incoming request to the appropriate method.

  1. class HttpOverrideOperationSelector : IDispatchOperationSelector
  2. {
  3.     private IDispatchOperationSelector originalSelector;
  4.  
  5.     public HttpOverrideOperationSelector(IDispatchOperationSelector originalSelector)
  6.     {
  7.         this.originalSelector = originalSelector;
  8.     }
  9.  
  10.     public string SelectOperation(ref Message message)
  11.     {
  12.         if (message.Properties.ContainsKey(HttpRequestMessageProperty.Name))
  13.         {
  14.             HttpRequestMessageProperty reqProp;
  15.             reqProp = (HttpRequestMessageProperty)message.Properties[HttpRequestMessageProperty.Name];
  16.             string httpMethodOverride = reqProp.Headers[HttpOverrideBehavior.HttpMethodOverrideHeaderName];
  17.             if (!String.IsNullOrEmpty(httpMethodOverride))
  18.             {
  19.                 message.Properties[HttpOverrideBehavior.OriginalHttpMethodPropertyName] = reqProp.Method;
  20.                 reqProp.Method = httpMethodOverride;
  21.             }
  22.         }
  23.  
  24.         return this.originalSelector.SelectOperation(ref message);
  25.     }
  26. }

Finally, the test code to wrap it all together. The code is very similar to the one for the message inspectors post, with a few differences: the new behavior (HttpOverrideBehavior) is added after the REST ((WebHttp) behavior, and after sending requests with the “real” HTTP verb it will send some additional requests using POST, but using the HTTP override method to route it to different operations.

  1. static void Main(string[] args)
  2. {
  3.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  4.     ServiceHost host = new ServiceHost(typeof(ContactManagerService), new Uri(baseAddress));
  5.     ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(IContactManager), new WebHttpBinding(), "");
  6.     endpoint.Behaviors.Add(new WebHttpBehavior());
  7.     endpoint.Behaviors.Add(new HttpOverrideBehavior());
  8.     host.Open();
  9.     Console.WriteLine("Host opened");
  10.  
  11.     string johnId = SendRequest(
  12.         "POST",
  13.         baseAddress + "/Contacts",
  14.         "application/json",
  15.         CreateJsonContact(null, "John Doe", "john@doe.com", "206-555-3333"));
  16.     string janeId = SendRequest(
  17.         "POST",
  18.         baseAddress + "/Contacts",
  19.         "application/json",
  20.         CreateJsonContact(null, "Jane Roe", "jane@roe.com", "202-555-4444 202-555-8888"));
  21.  
  22.     Console.WriteLine("All contacts");
  23.     SendRequest("GET", baseAddress + "/Contacts", null, null);
  24.  
  25.     Console.WriteLine("Updating Jane");
  26.     SendRequest(
  27.         "PUT",
  28.         baseAddress + "/Contacts/" + janeId,
  29.         "application/json",
  30.         CreateJsonContact(janeId, "Jane Roe", "jane@roe.org", "202-555-4444 202-555-8888"));
  31.  
  32.     Console.WriteLine("All contacts");
  33.     SendRequest("GET", baseAddress + "/Contacts", null, null);
  34.  
  35.     Console.WriteLine("Deleting John");
  36.     SendRequest("DELETE", baseAddress + "/Contacts/" + johnId, null, null);
  37.  
  38.     Console.WriteLine("Is John still here?");
  39.     SendRequest("GET", baseAddress + "/Contacts/" + johnId, null, null);
  40.  
  41.     Console.WriteLine("Adding John again");
  42.     johnId = SendRequest(
  43.         "POST",
  44.         baseAddress + "/Contacts",
  45.         "application/json",
  46.         CreateJsonContact(null, "John Doe", "john@doe.com", "206-555-3333"));
  47.  
  48.     Console.WriteLine("Updating John, now using X-HTTP-Method-Override");
  49.     Dictionary<string, string> overrideWithPut = new Dictionary<string, string>();
  50.     overrideWithPut.Add("X-HTTP-Method-Override", "PUT");
  51.     SendRequest(
  52.         "POST",
  53.         baseAddress + "/Contacts/" + johnId,
  54.         "application/json",
  55.         CreateJsonContact(johnId, "John Doe Updated", "john@doe.com", "206-555-3333"),
  56.         overrideWithPut);
  57.  
  58.     Console.WriteLine("All contacts");
  59.     SendRequest("GET", baseAddress + "/Contacts", null, null);
  60.  
  61.     Console.WriteLine("Deleting Jane, using X-HTTP-Method-Override");
  62.     Dictionary<string, string> overrideWithDelete = new Dictionary<string, string>();
  63.     overrideWithDelete.Add("X-HTTP-Method-Override", "DELETE");
  64.     SendRequest("POST", baseAddress + "/Contacts/" + janeId, "application/json", "", overrideWithDelete);
  65.  
  66.     Console.WriteLine("All contacts");
  67.     SendRequest("GET", baseAddress + "/Contacts", null, null);
  68.  
  69.     Console.WriteLine("Press ENTER to close");
  70.     Console.ReadLine();
  71.     host.Close();
  72. }

Coming up

Operation invokers, for both synchronous and asynchronous operations.

[Code in this post]

[Back to the index]

Carlos Figueira
https://blogs.msdn.com/carlosfigueira
Twitter: @carlos_figueira https://twitter.com/carlos_figuei