This sample was provided as part of a blog series on WCF extensibility. The entry for this sample can be found at http://blogs.msdn.com/b/carlosfigueira/archive/2011/04/05/wcf-extensibility-iendpointbehavior.aspx.

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:

C#
Edit|Remove
public class HelpPageEndpointBehavior : IEndpointBehavior 
{ 
    string companyName; 
    public HelpPageEndpointBehavior(string companyName) 
    { 
        this.companyName = companyName; 
    } 
  
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) 
    { 
    } 
  
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) 
    { 
    } 
  
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) 
    { 
        Uri endpointAddress = endpoint.Address.Uri; 
        if (endpointAddress.Scheme == Uri.UriSchemeHttp || endpointAddress.Scheme == Uri.UriSchemeHttps) 
        { 
            string address = endpointAddress.ToString(); 
            if (!address.EndsWith("/")) 
            { 
                address = address + "/"; 
            } 
  
            Uri helpPageUri = new Uri(address + "help"); 
            ServiceHostBase host = endpointDispatcher.ChannelDispatcher.Host; 
            ChannelDispatcher helpDispatcher = this.CreateChannelDispatcher(host, endpoint, helpPageUri); 
            host.ChannelDispatchers.Add(helpDispatcher); 
        } 
    } 
  
    public void Validate(ServiceEndpoint endpoint) 
    { 
    } 
} 
 
 

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.

C#
Edit|Remove
private ChannelDispatcher CreateChannelDispatcher(ServiceHostBase host, ServiceEndpoint endpoint, Uri helpPageUri) 
{ 
    Binding binding = new WebHttpBinding(); 
    EndpointAddress address = new EndpointAddress(helpPageUri); 
    BindingParameterCollection bindingParameters = new BindingParameterCollection(); 
    IChannelListener channelListener = binding.BuildChannelListener<IReplyChannel>(helpPageUri, bindingParameters); 
    ChannelDispatcher channelDispatcher = new ChannelDispatcher(channelListener, "HelpPageBinding", binding); 
    channelDispatcher.MessageVersion = MessageVersion.None; 
  
    HelpPageService service = new HelpPageService(endpoint, this.companyName); 
  
    EndpointDispatcher endpointDispatcher = new EndpointDispatcher(address, "HelpPageContract"""true); 
    DispatchOperation operationDispatcher = new DispatchOperation(endpointDispatcher.DispatchRuntime, "GetHelpPage""*""*"); 
    operationDispatcher.Formatter = new PassthroughMessageFormatter(); 
    operationDispatcher.Invoker = new HelpPageInvoker(service); 
    operationDispatcher.SerializeReply = false; 
    operationDispatcher.DeserializeRequest = false; 
  
    endpointDispatcher.DispatchRuntime.InstanceProvider = new SingletonInstanceProvider(service); 
    endpointDispatcher.DispatchRuntime.Operations.Add(operationDispatcher); 
    endpointDispatcher.DispatchRuntime.InstanceContextProvider = new SimpleInstanceContextProvider(); 
    endpointDispatcher.DispatchRuntime.SingletonInstanceContext = new InstanceContext(host, service); 
    endpointDispatcher.ContractFilter = new MatchAllMessageFilter(); 
    endpointDispatcher.FilterPriority = 0; 
  
    channelDispatcher.Endpoints.Add(endpointDispatcher); 
    return channelDispatcher; 
} 
 
 

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.

C#
Edit|Remove
class HelpPageService 
{ 
    ServiceEndpoint endpoint; 
    string companyName; 
    public HelpPageService(ServiceEndpoint endpoint, string companyName) 
    { 
        this.endpoint = endpoint; 
        this.companyName = companyName; 
    } 
    public Message Process(Message input) 
    { 
        return new HelpPageMessage(this.endpoint, this.companyName); 
    } 
} 
 
 

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.

C#
Edit|Remove
class HelpPageMessage : Message 
{ 
    MessageHeaders headers; 
    MessageProperties properties; 
    ServiceEndpoint endpoint; 
    string companyName; 
  
    public HelpPageMessage(ServiceEndpoint endpoint, string companyName) 
    { 
        this.headers = new MessageHeaders(MessageVersion.None); 
        this.properties = InitializeMessageProperties(); 
        this.endpoint = endpoint; 
        this.companyName = companyName; 
    } 
  
    public override MessageHeaders Headers 
    { 
        get { return this.headers; } 
    } 
  
    public override MessageProperties Properties 
    { 
        get { return this.properties; } 
    } 
  
    public override MessageVersion Version 
    { 
        get { return MessageVersion.None; } 
    } 
  
    protected override void OnWriteBodyContents(XmlDictionaryWriter writer) 
    { 
        writer.WriteStartElement("html"); 
        writer.WriteStartElement("head"); 
        writer.WriteElementString("title""Better help page, presented by " + this.companyName); 
        writer.WriteElementString("style"this.CreateStyleValue()); 
        writer.WriteEndElement(); 
        writer.WriteStartElement("body"); 
        writer.WriteElementString("h1""Endpoint help page, by " + this.companyName); 
  
        this.WriteEndpointDetails(writer); 
        this.WriteEndpointBehaviors(writer); 
        this.WriteContractOperations(writer); 
  
        writer.WriteEndElement(); // body 
        writer.WriteEndElement(); // html 
    } 
  
    private string CreateStyleValue() 
    { 
        return "table { border-collapse: collapse; border-spacing: 0px; font-family: Verdana;} " + 
            "table th { border-right: 2px white solid; border-bottom: 1.5px white solid; font-weight: bold; background-color: #cecf9c;} " + 
            "table td { border-right: 2px white solid; border-bottom: 1.5px white solid; background-color: #e5e5cc;} " + 
            "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;} " + 
            "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;} " + 
            ".signature { font-family: Consolas; font-size: 12px; }"; 
    } 
  
    private void WriteEndpointDetails(XmlDictionaryWriter writer) 
    { 
        writer.WriteElementString("h2""Endpoint details"); 
  
        writer.WriteStartElement("table"); 
  
        writer.WriteStartElement("tr"); 
        writer.WriteElementString("th""Element"); 
        writer.WriteElementString("th""Value"); 
        writer.WriteEndElement(); 
  
        writer.WriteStartElement("tr"); 
        writer.WriteElementString("th""Address"); 
        writer.WriteElementString("td"this.endpoint.Address.ToString()); 
        writer.WriteEndElement(); 
  
        writer.WriteStartElement("tr"); 
        writer.WriteElementString("th""Binding"); 
        writer.WriteElementString("td"this.endpoint.Binding.ToString()); 
        writer.WriteEndElement(); 
  
        writer.WriteStartElement("tr"); 
        writer.WriteElementString("th""Contract Type"); 
        writer.WriteElementString("td"this.endpoint.Contract.ContractType.FullName); 
        writer.WriteEndElement(); 
  
        writer.WriteEndElement(); // </table> 
    } 
  
    private void WriteEndpointBehaviors(XmlDictionaryWriter writer) 
    { 
        writer.WriteElementString("h2""Endpoint behaviors"); 
  
        writer.WriteStartElement("table"); 
  
        writer.WriteStartElement("tr"); 
        writer.WriteElementString("th""Index"); 
        writer.WriteElementString("th""Type"); 
        writer.WriteEndElement(); 
  
        for (int i = 0; i < this.endpoint.Behaviors.Count; i++) 
        { 
            IEndpointBehavior behavior = this.endpoint.Behaviors[i]; 
            writer.WriteStartElement("tr"); 
            writer.WriteElementString("td", i.ToString(CultureInfo.InvariantCulture)); 
            writer.WriteElementString("td", behavior.GetType().FullName); 
            writer.WriteEndElement(); 
        } 
  
        writer.WriteEndElement(); // </table> 
    } 
  
    private void WriteContractOperations(XmlDictionaryWriter writer) 
    { 
        writer.WriteElementString("h2""Operations"); 
  
        writer.WriteStartElement("table"); 
  
        writer.WriteStartElement("tr"); 
        writer.WriteElementString("th""Name"); 
        writer.WriteElementString("th""Signature"); 
        writer.WriteElementString("th""Description"); 
        writer.WriteEndElement(); 
  
        foreach (OperationDescription operation in this.endpoint.Contract.Operations) 
        { 
            writer.WriteStartElement("tr"); 
            writer.WriteElementString("td", operation.Name); 
            writer.WriteStartElement("td"); 
            writer.WriteAttributeString("class""signature"); 
            writer.WriteString(this.GetOperationSignature(operation)); 
            writer.WriteEndElement(); 
            writer.WriteElementString("td"this.GetOperationDescription(operation)); 
            writer.WriteEndElement(); 
        } 
  
        writer.WriteEndElement(); // </table> 
    } 
  
    private string GetOperationDescription(OperationDescription operation) 
    { 
        DescriptionAttribute[] description = (DescriptionAttribute[])operation.SyncMethod.GetCustomAttributes(typeof(DescriptionAttribute), false); 
        if (description == null || description.Length == 0 || string.IsNullOrEmpty(description[0].Description)) 
        { 
            return "Operation " + operation.SyncMethod.Name; 
        } 
        else 
        { 
            return description[0].Description; 
        } 
    } 
  
    private string GetOperationSignature(OperationDescription operation) 
    { 
        StringBuilder sb = new StringBuilder(); 
        if (operation.SyncMethod != null) 
        { 
            MethodInfo method = operation.SyncMethod; 
            if (method.ReturnType == null || method.ReturnType == typeof(void)) 
            { 
                sb.Append("void"); 
            } 
            else 
            { 
                sb.Append(method.ReturnType.Name); 
            } 
  
            sb.Append(' '); 
            sb.Append(method.Name); 
            sb.Append('('); 
            ParameterInfo[] parameters = method.GetParameters(); 
            for (int i = 0; i < parameters.Length; i++) 
            { 
                if (i > 0) 
                { 
                    sb.Append(", "); 
                } 
  
                sb.AppendFormat("{0} {1}", parameters[i].ParameterType.Name, parameters[i].Name); 
            } 
  
            sb.Append(')'); 
        } 
        else 
        { 
            throw new NotImplementedException("Behavior not yet implemented for async operations"); 
        } 
  
        return sb.ToString(); 
    } 
  
    private MessageProperties InitializeMessageProperties() 
    { 
        MessageProperties result = new MessageProperties(); 
        HttpResponseMessageProperty httpResponse = new HttpResponseMessageProperty(); 
        httpResponse.StatusCode = HttpStatusCode.OK; 
        httpResponse.Headers[HttpResponseHeader.ContentType] = "text/html"; 
        result.Add(HttpResponseMessageProperty.Name, httpResponse); 
        return result; 
    } 
} 
 
 

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.

C#
Edit|Remove
class SingletonInstanceProvider : IInstanceProvider 
{ 
    object instance; 
    public SingletonInstanceProvider(object instance) 
    { 
        this.instance = instance; 
    } 
    public object GetInstance(InstanceContext instanceContext, Message message) 
    { 
        return instance; 
    } 
  
    public object GetInstance(InstanceContext instanceContext) 
    { 
        return instance; 
    } 
  
    public void ReleaseInstance(InstanceContext instanceContext, object instance) 
    { 
    } 
} 
  
class HelpPageInvoker : IOperationInvoker 
{ 
    HelpPageService service; 
    public HelpPageInvoker(HelpPageService service) 
    { 
        this.service = service; 
    } 
  
    public object[] AllocateInputs() 
    { 
        return new object[1]; 
    } 
  
    public object Invoke(object instance, object[] inputs, out object[] outputs) 
    { 
        outputs = new object[0]; 
        return this.service.Process((Message)inputs[0]); 
    } 
  
    public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state) 
    { 
        throw new NotSupportedException(); 
    } 
  
    public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result) 
    { 
        throw new NotSupportedException(); 
    } 
  
    public bool IsSynchronous 
    { 
        get { return true; } 
    } 
} 
 
 

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.

C#
Edit|Remove
class SimpleInstanceContextProvider : IInstanceContextProvider 
{ 
    public InstanceContext GetExistingInstanceContext(Message message, IContextChannel channel) 
    { 
        return null; 
    } 
  
    public void InitializeInstanceContext(InstanceContext instanceContext, Message message, IContextChannel channel) 
    { 
    } 
  
    public bool IsIdle(InstanceContext instanceContext) 
    { 
        return false; 
    } 
  
    public void NotifyIdle(InstanceContextIdleCallback callback, InstanceContext instanceContext) 
    { 
    } 
} 
  
class PassthroughMessageFormatter : IDispatchMessageFormatter 
{ 
    public void DeserializeRequest(Message message, object[] parameters) 
    { 
        parameters[0] = message; 
    } 
  
    public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result) 
    { 
        return (Message)result; 
    } 
} 
 
 

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.