WCF Extensibility – IEndpointBehavior

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 .

The next behavior in the list is the IEndpointBehavior. Like the other ones, it can be used to inspect and change the endpoint description (which includes the contract description) and its runtime. Unlike the other behaviors, Endpoint behaviors cannot be applied to endpoints as attributes (unless the attribute is applied to an implementation of a callback contract in a duplex pattern – see more details below). Endpoint behaviors are typically used to change the behavior of endpoints in a way which doesn’t interfere with other endpoints (which can potentially be using the same contract) in a service. For example, the WebHttpBehavior changes the runtime to understand the WCF REST attributes (WebGet/WebInvoke).

Public implementations in WCF

  • CallbackBehaviorAttribute: A client callback (for duplex scenarios) equivalent to the [ServiceBehavior]. On a duplex service. the client side acts both as a “normal” client and as a service, receiving callbacks initiated at the server side. This behavior allows the definition of parameters such as concurrency mode, transaction isolation level, among others.
  • CallbackDebugBehavior: The client callback equivalent to the ServiceDebugBehavior, defining whether the client should respond with unknown exceptions as faults.
  • ClientCredentials: The client equivalent to the ServiceCredentials, where the user can configure the client credentials as well as service authentication settings.
  • ClientViaBehavior: Defines the address used by the transport when sending a message, which may be different than the endpoint address (a.k.a. the final destination). Useful in proxying / multi-hop conversations.
  • DispatcherSynchronizationBehavior (new in 4.0): Defines the ability of the endpoint to send asynchronous replies. Sajay has a great post about when this can be useful.
  • MustUnderstandBehavior: Defines whether the endpoint must obey the mustUnderstand attribute in message headers. Normally, if an endpoint receives a message with a header marked with mustUnderstand=”1” (or “true”) then it must process the header, or generate a fault. For routing scenarios, however, we can disable this validation and relay the message to the entity which is supposed to process the headers.
  • SynchronousReceiveBehavior: Defines that the channel listener in the endpoint must use a synchronous receive call, instead of the default asynchronous mode.
  • TransactedBatchingBehavior: Instructs the endpoint to optimize transfer operations for transports which supports transacted receives. Essentially, it enables the batching of messages for MSMQ services. This port from Nick Allen has a good description of the scenario.
  • WebHttpBehavior (new in 3.5): Enables the WCF web programming model for this endpoint.
  • WebScriptEnablingBehavior (new in 3.5): Enables the endpoint to receive requests from an ASP.NET AJAX client. It provides a JavaScript proxy for the client to call the service in a strongly-typed way.
  • EndpointDiscoveryBehavior (new in 4.0): Controls the content of the discovery metadata returned by a discovery endpoint.
  • SoapProcessingBehavior (new in 4.0): Defines rules for translating messages between different SOAP versions in a routing scenario.

 

Interface declaration

  1. public interface IEndpointBehavior
  2. {
  3.     void Validate(ServiceEndpoint endpoint);
  4.     void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters);
  5.     void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher);
  6.     void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime);
  7. }

The pattern continues like the previous posts: Validate is called first, letting the behavior throw if something does not comply with the behavior rules. For example, the WebHttpBehavior (and the WebScriptEnablingBehavior) both throw during validation if the binding does not use HTTP (or HTTPS), or if it is a SOAP binding (i.e., if its message version is not None). AddBuindingParameters is the second one to be called, and it’s not commonly used.

ApplyDispatchBehavior and ApplyClientBehavior are the last ones to be called, after the runtime has been initialized. At that point, the behaviors can update the runtime, updating listeners / dispatchers and other runtime objects according to its logic. The example below will use an endpoint behavior to add a new listener for the endpoint to create a “help page” similar to the WCF Web HTTP Service Help Page.

How to add an endpoint behavior

Via service / endpoint description (server) : On the server side, the list of the behaviors for the endpoint can be accessed via the Behaviors property of the endpoint description, which can be obtained once you add an endpoint to the service.

  1. string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  2. ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
  3. ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
  4. endpoint.Behaviors.Add(new MyEndpointBehavior());

Via the channel factory / generated proxy (client) : The ChannelFactory class has an Endpoint property which is of the same type as the one on the server. Likewise, generated proxies (using svcutil / add service reference) are subclasses of ClientBase<T>, which also has an Endpoint property which contains a reference to the list of the endpoint behaviors. Like for contract behaviors, endpoint behaviors need to be added prior to opening the client, otherwise they will not be used.

  1. TestClient client = new TestClient();
  2. client.Endpoint.Behaviors.Add(new MyEndpointBehavior());
  3. int result = client.Add(4, 5);

Using attributes: Endpoint behaviors for “normal” clients / server cannot be applied as attributes – if you define an attribute class and apply it to either the service class, the contract interface or any operations, it will simply be ignored. For duplex endpoints, however, attribute-based endpoint behaviors can be applied to the class which implements the callback contract.

  1. public class MyEndpointBehaviorAttribute : Attribute, IEndpointBehavior
  2. {
  3.     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  4.     {
  5.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  6.     }
  7.  
  8.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  9.     {
  10.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  11.     }
  12.  
  13.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  14.     {
  15.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  16.     }
  17.  
  18.     public void Validate(ServiceEndpoint endpoint)
  19.     {
  20.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  21.     }
  22. }
  23.  
  24. [ServiceContract(CallbackContract = typeof(ITestCallback))]
  25. public interface ITest
  26. {
  27.     [OperationContract]
  28.     void Ping(string text);
  29. }
  30.  
  31. [ServiceContract]
  32. public interface ITestCallback
  33. {
  34.     [OperationContract(IsOneWay = true)]
  35.     void Pong(string text);
  36. }
  37.  
  38. public class Service : ITest
  39. {
  40.     public void Ping(string text)
  41.     {
  42.         OperationContext.Current.GetCallbackChannel<ITestCallback>().Pong(text);
  43.     }
  44. }
  45.  
  46. [MyEndpointBehavior]
  47. public class MyCallback : global::ITestCallback
  48. {
  49.     public void Pong(string text)
  50.     {
  51.         Console.WriteLine("Received on client: {0}", text);
  52.     }
  53. }
  54.  
  55. public class Program
  56. {
  57.     public static void Main(string[] args)
  58.     {
  59.         string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  60.         WSDualHttpBinding dualBinding = new WSDualHttpBinding(WSDualHttpSecurityMode.None);
  61.         ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
  62.         host.AddServiceEndpoint(typeof(ITest), dualBinding, "");
  63.         host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
  64.         Console.WriteLine("Opening the host");
  65.         host.Open();
  66.  
  67.         InstanceContext callbackInstance = new InstanceContext(new MyCallback());
  68.         TestClient client = new TestClient(callbackInstance, dualBinding, new EndpointAddress(baseAddress));
  69.         client.Ping("hello");
  70.  
  71.         Console.WriteLine("Press ENTER to close");
  72.         Console.ReadLine();
  73.         host.Close();
  74.         Console.WriteLine("Host closed");
  75.     }
  76. }

Using configuration: If the endpoint behavior has a configuration extension, then it can be applied directly in the configuration. This works for both service endpoints, and for client endpoints:

  1. <system.serviceModel>
  2.   <extensions>
  3.     <behaviorExtensions>
  4.       <add name="myEndpointBehavior"
  5.            type="assembly-qualified-name-of-extension-class"/>
  6.     </behaviorExtensions>
  7.   </extensions>
  8.   <behaviors>
  9.     <endpointBehaviors>
  10.       <behavior name="UsingMyEndpointBehavior">
  11.         <myEndpointBehavior/>
  12.       </behavior>
  13.     </endpointBehaviors>
  14.   </behaviors>
  15.   <services>
  16.     <service name="MyNamespace.MyService">
  17.       <endpoint address=""
  18.                 behaviorConfiguration="UsingMyEndpointBehavior"
  19.                 binding="basicHttpBinding"
  20.                 contract="MyNamespace.IMyContract" />
  21.     </service>
  22.   </services>
  23.   <client>
  24.     <endpoint address="https://contoso.com/service"
  25.               behaviorConfiguration="UsingMyBehavior"
  26.               binding="basicHttpBinding"
  27.               contract="MyNamespace.IMyClientContract" />
  28.   </client>
  29. </system.serviceModel>

 

Real-world scenario: an endpoint help page

One of the nice features added in .NET Framework 4 was a help page for REST endpoints. It described all the operations on the endpoint, as well as the schema for parameters. A few posts on the forums asked about how to customize that page (or the “service” help page), so this is a generic help page implementation (not only for REST endpoints, but for SOAP ones as well). The implementation idea is similar to the one for the POCO Service Host (described in the post about service behaviors): during the call to ApplyDispatchBehavior, the behavior will add a new dispatcher which can handle requests for the help page. Browser clients will then be able to point to that help page, and get a human-readable description of the endpoint.

And before I go any further, here goes 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, and doesn’t support all contract types (asynchronous operations, for example). Finally, this sample (as well as most other samples in this series) uses extensibility points other than the one for this post (e.g., operation invoker, instance providers, etc.) which are necessary to get a realistic scenario going. I’ll briefly describe what they do, and leave to their specific entries a more detailed description of their behavior.

This behavior will also have a customized part of the help page. For this sample, it’s a simple string (the “name of the company” which provides the services), but it can be easily extended to more complex information. Here’s the implementation of IEndpointBehavior. Only ApplyDispatchBehavior will be used here, where we’ll add the new dispatcher to the service. And only if the endpoint address is HTTP – it doesn’t make sense to add a help page for browsers for TCP or named pipes:

  1. public class HelpPageEndpointBehavior : IEndpointBehavior
  2. {
  3.     string companyName;
  4.     public HelpPageEndpointBehavior(string companyName)
  5.     {
  6.         this.companyName = companyName;
  7.     }
  8.  
  9.     public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  10.     {
  11.     }
  12.  
  13.     public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  14.     {
  15.     }
  16.  
  17.     public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
  18.     {
  19.         Uri endpointAddress = endpoint.Address.Uri;
  20.         if (endpointAddress.Scheme == Uri.UriSchemeHttp || endpointAddress.Scheme == Uri.UriSchemeHttps)
  21.         {
  22.             string address = endpointAddress.ToString();
  23.             if (!address.EndsWith("/"))
  24.             {
  25.                 address = address + "/";
  26.             }
  27.  
  28.             Uri helpPageUri = new Uri(address + "help");
  29.             ServiceHostBase host = endpointDispatcher.ChannelDispatcher.Host;
  30.             ChannelDispatcher helpDispatcher = this.CreateChannelDispatcher(host, endpoint, helpPageUri);
  31.             host.ChannelDispatchers.Add(helpDispatcher);
  32.         }
  33.     }
  34.  
  35.     public void Validate(ServiceEndpoint endpoint)
  36.     {
  37.     }
  38. }

CreateChannelDispatcher will create the listener (based on WebHttpBinding, which can handle GET requests natively). The help page will be created by an internal class (HelpPageService) which can handle requests for the help page, and an object of that class will be used as a singleton instance for this dispatcher (since the class is thread-safe, we can share the same instance). Unlike in the service behavior, in which we used the operation behaviors to set up the operation dispatcher properties, this time we need to set them ourselves. So we need to define our invoker and message formatter as well.

  1. private ChannelDispatcher CreateChannelDispatcher(ServiceHostBase host, ServiceEndpoint endpoint, Uri helpPageUri)
  2. {
  3.     Binding binding = new WebHttpBinding();
  4.     EndpointAddress address = new EndpointAddress(helpPageUri);
  5.     BindingParameterCollection bindingParameters = new BindingParameterCollection();
  6.     IChannelListener channelListener = binding.BuildChannelListener<IReplyChannel>(helpPageUri, bindingParameters);
  7.     ChannelDispatcher channelDispatcher = new ChannelDispatcher(channelListener, "HelpPageBinding", binding);
  8.     channelDispatcher.MessageVersion = MessageVersion.None;
  9.  
  10.     HelpPageService service = new HelpPageService(endpoint, this.companyName);
  11.  
  12.     EndpointDispatcher endpointDispatcher = new EndpointDispatcher(address, "HelpPageContract", "", true);
  13.     DispatchOperation operationDispatcher = new DispatchOperation(endpointDispatcher.DispatchRuntime, "GetHelpPage", "*", "*");
  14.     operationDispatcher.Formatter = new PassthroughMessageFormatter();
  15.     operationDispatcher.Invoker = new HelpPageInvoker(service);
  16.     operationDispatcher.SerializeReply = false;
  17.     operationDispatcher.DeserializeRequest = false;
  18.  
  19.     endpointDispatcher.DispatchRuntime.InstanceProvider = new SingletonInstanceProvider(service);
  20.     endpointDispatcher.DispatchRuntime.Operations.Add(operationDispatcher);
  21.     endpointDispatcher.DispatchRuntime.InstanceContextProvider = new SimpleInstanceContextProvider();
  22.     endpointDispatcher.DispatchRuntime.SingletonInstanceContext = new InstanceContext(host, service);
  23.     endpointDispatcher.ContractFilter = new MatchAllMessageFilter();
  24.     endpointDispatcher.FilterPriority = 0;
  25.  
  26.     channelDispatcher.Endpoints.Add(endpointDispatcher);
  27.     return channelDispatcher;
  28. }

Now for the implementation of the helper classes. First the HelpPageService. It’s a very, very simple class which only receives requests for the help page, then returns an instance of a custom Message class.

  1. class HelpPageService
  2. {
  3.     ServiceEndpoint endpoint;
  4.     string companyName;
  5.     public HelpPageService(ServiceEndpoint endpoint, string companyName)
  6.     {
  7.         this.endpoint = endpoint;
  8.         this.companyName = companyName;
  9.     }
  10.     public Message Process(Message input)
  11.     {
  12.         return new HelpPageMessage(this.endpoint, this.companyName);
  13.     }
  14. }

The HelpPageMessage implements the abstract Message class by overriding the OnWriteBodyContents method. Since WCF messages are XML-based, we’ll use the XML writer passed to the method to write the HTML (as well-formatted XML). The HTML page generated contains a few tables with information about the endpoint obtained from the description, but it’s plain HTML, not too interesting – although it’s useful to notice that the logic to print the operation signature is too simple, as it doesn’t handle out/ref/array/generic parameters correctly (to handle them all this sample would get too big). The interesting item in the message class is the InitializeMessageProperties method. By default, WCF will use the encoder’s ContentType property to determine the Content-Type header of the response, which is usually some XML (or SOAP) type. By adding a HttpResponseMessageProperty to the message, we change the response content type to text/html.

  1. class HelpPageMessage : Message
  2. {
  3.     MessageHeaders headers;
  4.     MessageProperties properties;
  5.     ServiceEndpoint endpoint;
  6.     string companyName;
  7.  
  8.     public HelpPageMessage(ServiceEndpoint endpoint, string companyName)
  9.     {
  10.         this.headers = new MessageHeaders(MessageVersion.None);
  11.         this.properties = InitializeMessageProperties();
  12.         this.endpoint = endpoint;
  13.         this.companyName = companyName;
  14.     }
  15.  
  16.     public override MessageHeaders Headers
  17.     {
  18.         get { return this.headers; }
  19.     }
  20.  
  21.     public override MessageProperties Properties
  22.     {
  23.         get { return this.properties; }
  24.     }
  25.  
  26.     public override MessageVersion Version
  27.     {
  28.         get { return MessageVersion.None; }
  29.     }
  30.  
  31.     protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
  32.     {
  33.         writer.WriteStartElement("html");
  34.         writer.WriteStartElement("head");
  35.         writer.WriteElementString("title", "Better help page, presented by " + this.companyName);
  36.         writer.WriteElementString("style", this.CreateStyleValue());
  37.         writer.WriteEndElement();
  38.         writer.WriteStartElement("body");
  39.         writer.WriteElementString("h1", "Endpoint help page, by " + this.companyName);
  40.  
  41.         this.WriteEndpointDetails(writer);
  42.         this.WriteEndpointBehaviors(writer);
  43.         this.WriteContractOperations(writer);
  44.  
  45.         writer.WriteEndElement(); // body
  46.         writer.WriteEndElement(); // html
  47.     }
  48.  
  49.     private string CreateStyleValue()
  50.     {
  51.         return "table { border-collapse: collapse; border-spacing: 0px; font-family: Verdana;} " +
  52.             "table th { border-right: 2px white solid; border-bottom: 1.5px white solid; font-weight: bold; background-color: #cecf9c;} " +
  53.             "table td { border-right: 2px white solid; border-bottom: 1.5px white solid; background-color: #e5e5cc;} " +
  54.             "h1 { background-color: #003366; border-bottom: #336699 6px solid; color: #ffffff; font-family: Tahoma; font-size: 26px; font-weight: normal;margin: 0em 0em 10px -20px; padding-bottom: 8px; padding-left: 30px;padding-top: 16px;} " +
  55.             "h2 { background-color: #003366; border-bottom: #336699 6px solid; color: #ffffff; font-family: Tahoma; font-size: 20px; font-weight: normal;margin: 0em 0em 10px -20px; padding-bottom: 8px; padding-left: 30px;padding-top: 16px;} " +
  56.             ".signature { font-family: Consolas; font-size: 12px; }";
  57.     }
  58.  
  59.     private void WriteEndpointDetails(XmlDictionaryWriter writer)
  60.     {
  61.         writer.WriteElementString("h2", "Endpoint details");
  62.  
  63.         writer.WriteStartElement("table");
  64.  
  65.         writer.WriteStartElement("tr");
  66.         writer.WriteElementString("th", "Element");
  67.         writer.WriteElementString("th", "Value");
  68.         writer.WriteEndElement();
  69.  
  70.         writer.WriteStartElement("tr");
  71.         writer.WriteElementString("th", "Address");
  72.         writer.WriteElementString("td", this.endpoint.Address.ToString());
  73.         writer.WriteEndElement();
  74.  
  75.         writer.WriteStartElement("tr");
  76.         writer.WriteElementString("th", "Binding");
  77.         writer.WriteElementString("td", this.endpoint.Binding.ToString());
  78.         writer.WriteEndElement();
  79.  
  80.         writer.WriteStartElement("tr");
  81.         writer.WriteElementString("th", "Contract Type");
  82.         writer.WriteElementString("td", this.endpoint.Contract.ContractType.FullName);
  83.         writer.WriteEndElement();
  84.  
  85.         writer.WriteEndElement(); // </table>
  86.     }
  87.  
  88.     private void WriteEndpointBehaviors(XmlDictionaryWriter writer)
  89.     {
  90.         writer.WriteElementString("h2", "Endpoint behaviors");
  91.  
  92.         writer.WriteStartElement("table");
  93.  
  94.         writer.WriteStartElement("tr");
  95.         writer.WriteElementString("th", "Index");
  96.         writer.WriteElementString("th", "Type");
  97.         writer.WriteEndElement();
  98.  
  99.         for (int i = 0; i < this.endpoint.Behaviors.Count; i++)
  100.         {
  101.             IEndpointBehavior behavior = this.endpoint.Behaviors[i];
  102.             writer.WriteStartElement("tr");
  103.             writer.WriteElementString("td", i.ToString(CultureInfo.InvariantCulture));
  104.             writer.WriteElementString("td", behavior.GetType().FullName);
  105.             writer.WriteEndElement();
  106.         }
  107.  
  108.         writer.WriteEndElement(); // </table>
  109.     }
  110.  
  111.     private void WriteContractOperations(XmlDictionaryWriter writer)
  112.     {
  113.         writer.WriteElementString("h2", "Operations");
  114.  
  115.         writer.WriteStartElement("table");
  116.  
  117.         writer.WriteStartElement("tr");
  118.         writer.WriteElementString("th", "Name");
  119.         writer.WriteElementString("th", "Signature");
  120.         writer.WriteElementString("th", "Description");
  121.         writer.WriteEndElement();
  122.  
  123.         foreach (OperationDescription operation in this.endpoint.Contract.Operations)
  124.         {
  125.             writer.WriteStartElement("tr");
  126.             writer.WriteElementString("td", operation.Name);
  127.             writer.WriteStartElement("td");
  128.             writer.WriteAttributeString("class", "signature");
  129.             writer.WriteString(this.GetOperationSignature(operation));
  130.             writer.WriteEndElement();
  131.             writer.WriteElementString("td", this.GetOperationDescription(operation));
  132.             writer.WriteEndElement();
  133.         }
  134.  
  135.         writer.WriteEndElement(); // </table>
  136.     }
  137.  
  138.     private string GetOperationDescription(OperationDescription operation)
  139.     {
  140.         DescriptionAttribute[] description = (DescriptionAttribute[])operation.SyncMethod.GetCustomAttributes(typeof(DescriptionAttribute), false);
  141.         if (description == null || description.Length == 0 || string.IsNullOrEmpty(description[0].Description))
  142.         {
  143.             return "Operation " + operation.SyncMethod.Name;
  144.         }
  145.         else
  146.         {
  147.             return description[0].Description;
  148.         }
  149.     }
  150.  
  151.     private string GetOperationSignature(OperationDescription operation)
  152.     {
  153.         StringBuilder sb = new StringBuilder();
  154.         if (operation.SyncMethod != null)
  155.         {
  156.             MethodInfo method = operation.SyncMethod;
  157.             if (method.ReturnType == null || method.ReturnType == typeof(void))
  158.             {
  159.                 sb.Append("void");
  160.             }
  161.             else
  162.             {
  163.                 sb.Append(method.ReturnType.Name);
  164.             }
  165.  
  166.             sb.Append(' ');
  167.             sb.Append(method.Name);
  168.             sb.Append('(');
  169.             ParameterInfo[] parameters = method.GetParameters();
  170.             for (int i = 0; i < parameters.Length; i++)
  171.             {
  172.                 if (i > 0)
  173.                 {
  174.                     sb.Append(", ");
  175.                 }
  176.  
  177.                 sb.AppendFormat("{0} {1}", parameters[i].ParameterType.Name, parameters[i].Name);
  178.             }
  179.  
  180.             sb.Append(')');
  181.         }
  182.         else
  183.         {
  184.             throw new NotImplementedException("Behavior not yet implemented for async operations");
  185.         }
  186.  
  187.         return sb.ToString();
  188.     }
  189.  
  190.     private MessageProperties InitializeMessageProperties()
  191.     {
  192.         MessageProperties result = new MessageProperties();
  193.         HttpResponseMessageProperty httpResponse = new HttpResponseMessageProperty();
  194.         httpResponse.StatusCode = HttpStatusCode.OK;
  195.         httpResponse.Headers[HttpResponseHeader.ContentType] = "text/html";
  196.         result.Add(HttpResponseMessageProperty.Name, httpResponse);
  197.         return result;
  198.     }
  199. }

The instance provider will always return the singleton instance of the HelpPageService class – nothing too fancy here. The operation invoker implementation is simple – it will always deliver the incoming Message object to the singleton HelpPageService instance.

  1. class SingletonInstanceProvider : IInstanceProvider
  2. {
  3.     object instance;
  4.     public SingletonInstanceProvider(object instance)
  5.     {
  6.         this.instance = instance;
  7.     }
  8.     public object GetInstance(InstanceContext instanceContext, Message message)
  9.     {
  10.         return instance;
  11.     }
  12.  
  13.     public object GetInstance(InstanceContext instanceContext)
  14.     {
  15.         return instance;
  16.     }
  17.  
  18.     public void ReleaseInstance(InstanceContext instanceContext, object instance)
  19.     {
  20.     }
  21. }
  22.  
  23. class HelpPageInvoker : IOperationInvoker
  24. {
  25.     HelpPageService service;
  26.     public HelpPageInvoker(HelpPageService service)
  27.     {
  28.         this.service = service;
  29.     }
  30.  
  31.     public object[] AllocateInputs()
  32.     {
  33.         return new object[1];
  34.     }
  35.  
  36.     public object Invoke(object instance, object[] inputs, out object[] outputs)
  37.     {
  38.         outputs = new object[0];
  39.         return this.service.Process((Message)inputs[0]);
  40.     }
  41.  
  42.     public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
  43.     {
  44.         throw new NotSupportedException();
  45.     }
  46.  
  47.     public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
  48.     {
  49.         throw new NotSupportedException();
  50.     }
  51.  
  52.     public bool IsSynchronous
  53.     {
  54.         get { return true; }
  55.     }
  56. }

Finally, the instance context provider is the same as the one used in the service behavior sample. And the message formatter (converting between Message objects and operation parameters) is trivial, since the operation in the service takes a parameter of that type and returns a value of the same type, so it simply passes them through.

  1. class SimpleInstanceContextProvider : IInstanceContextProvider
  2. {
  3.     public InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel)
  4.     {
  5.         return null;
  6.     }
  7.  
  8.     public void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel)
  9.     {
  10.     }
  11.  
  12.     public bool IsIdle(InstanceContext instanceContext)
  13.     {
  14.         return false;
  15.     }
  16.  
  17.     public void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext)
  18.     {
  19.     }
  20. }
  21.  
  22. class PassthroughMessageFormatter : IDispatchMessageFormatter
  23. {
  24.     public void DeserializeRequest(Message message, object[] parameters)
  25.     {
  26.         parameters[0] = message;
  27.     }
  28.  
  29.     public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
  30.     {
  31.         return (Message)result;
  32.     }
  33. }

And that’s it. The help page for REST endpoints also contains links to help pages for specific operations and operations schema. To to the same with this example one would need to simply create (one for each new page to be retrieved), or return a different message depending on the request URI.

Coming up

The last behavior interface (IOperationBehavior).

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    April 13, 2011
    Great series of articles, Carlos, really very informative....One thing though, if you an IIS hosted REST service, you're already able to write something like:http//localhost:port/service/helphow do you replace the default help page with your own one?
  • Anonymous
    April 14, 2011
    Hi Søren, if you want to do that for IIS-hosted WCF REST services you'd need first to disable the generation of the help page for the endpoint (set HelpEnabled to false on the WebHttpBehavior), and then add your help endpoint behavior to the REST endpoint. To do that you can either define a behavior extension element to add it via config (an upcoming post), or use a service host factory to hook to the service host initialization and add the behavior via code (also an upcoming post, but you can find some information at blogs.msdn.com/.../modifying-code-only-settings-on-webhosted-services.aspx)
  • Anonymous
    November 21, 2013
    Very good Carlos, you wouldn't have an example that also includes the documentation of each operation, would you?I mean the detail page.