(Ab)using WCF for Serving HTTP GET Requests

20 February 2007

There's an interesting sample in the .NET 3.0 SDK docu called REST and POX that demonstrates how WCF can be used to send and receive “plain old XML” (POX) messages over HTTP GET.

I stumbled across this sample the other day because I was looking for some information on how I could serve simple HTTP GET requests from WCF. The motivation behind all this was that I wanted to implement HTTP GET meta-data retrieval for an SSDL contract using the ?ssdl convention, i.e. you deploy an SSDL service in Soya, fire up your browser, add ?ssdl to the end of the base address of the service, et voilà you'll get the SSDL contract XML flickering across your screen.

Although I'll use the SSDL meta-data as an example in this post, the techniques I'm describing here should apply to serving HTTP GET requests in general and could also be used for e.g. serving custom help pages and so on.

Unlike in the REST and POX example I had to configure the WCF runtime manually, because I wanted to serve HTTP GET requests just concurrently of a running service, which was using SOAP for communicating. In the following steps I'll thus explain, how the WCF runtime can be manually configured for that purpose.

First, I created an IServiceBehavior so that the meta functionality can easily be added to or removed from services. In the Behaviors' ApplyDispatchBehavior() method I create a new ChannelDispatcher and added it to the service host.

ChannelDispatcher cd = CreateMetaDispatcher(host, baseUri);
host.ChannelDispatchers.Add(cd);


Now to the more interesting part, i.e. the CreateMetaDispatcher(). I created the EndpointAddress where I wanted the service to be listening:

EndpointAddress address = new EndpointAddress(listenUri);

Then, I created a new custom binding by composing a HTTP and Text encoding binding elements. Note that I set MessageVersion to None, because HTTP GET requests have (obviously) no message version information in that sense.

HttpTransportBindingElement transport = new HttpTransportBindingElement();
TextMessageEncodingBindingElement text = new TextMessageEncodingBindingElement();
text.MessageVersion = MessageVersion.None;
Binding binding = new CustomBinding(text, transport);


Next, I created a ChannelListener using an IReplyChannel and the ChannelDispatcher with the above binding.

IChannelListener listener = binding.BuildChannelListener(listenUri);
ChannelDispatcher cd = new ChannelDispatcher(listener, "SoyaHttpBinding", binding);
cd.MessageVersion = MessageVersion.None;


Following that, I created an instance of the service implementation (I'll come to that later), built the EndpointDispatcher and configured it to be a bit promiscuous (i.e. set a MatchAllMessageFilter). I also had to write a custom IInstanceProvider and IInstanceContextProvider, because even though implementations exist in WCF, they are accessible only intra-assembly, which is a bit *#@!...

IHttpService service = new HttpGetMetadata();
EndpointDispatcher ed = new EndpointDispatcher(address, "IHttpGetMetadata", "urn:soya:runtime");
ed.ContractFilter = new MatchAllMessageFilter();
ed.DispatchRuntime.InstanceProvider = new SimpleInstanceProvider(new HttpGetMetadata());
ed.DispatchRuntime.InstanceProvider = new SimpleInstanceProvider(service);
ed.DispatchRuntime.InstanceContextProvider = new SingletonInstanceContextProvider();
ed.DispatchRuntime.SingletonInstanceContext = new InstanceContext(host, service);


Further, I created the DispatchOperation that will be invoked for serving the HTTP requests. Same story here: I had to write a custom IOperationInvoker because none of the WCF implementations can be accessed publicly. Luckily, by setting DeserializeRequest and SerializeReply to false, I didn't have to provide a custom IOperationFormatter, because the incoming and outgoing Message instances will be delivered straight to the service implementation.

DispatchOperation operation = new DispatchOperation(ed.DispatchRuntime, "Process", "*", "*");
MethodInfo method = service.GetType().GetMethod("Process");
operation.Invoker = new MessageOperationInvoker(method);
operation.DeserializeRequest = false;
operation.SerializeReply = false;


Finally, I made the above operation responsible for processing all requests and assigned the belonging EndpointDispatcher to the previously created ChannelDispatcher.

ed.DispatchRuntime.UnhandledDispatchOperation = operation;
cd.Endpoints.Add(ed);


The service contract and implementation are pretty straight-forward and basically identical to the ones in the REST and POX example.

[ServiceContract]
public interface IHttpService {
[OperationContract(Action = "*", ReplyAction = "*")]
Message Process(Message msg);
}

public Message Process(Message request) {
// do some HTTP request processing here and send a HTTP response
}


In the above Process() method I check the query string for the ssdl parameter and return (after some error checking etc) a HTTP response Message that has the HTTP StatusCode set to 200 and ContentType to "text/xml".

private Message CreateSsdlResponse() {
SsdlDescription ssdl = SoyaServiceHost.Current.Runtime.Ssdl;
Message reply = new SsdlMessage(ssdl);
HttpResponseMessageProperty property = new HttpResponseMessageProperty();
property.StatusCode = HttpStatusCode.OK;
property.Headers.Add(HttpResponseHeader.ContentType, xmlContentType);
reply.Properties.Add(HttpResponseMessageProperty.Name, property);

return reply;
}


The SsdlMessage class is nothing else but a subclass of Message that has no headers, no version and overwrites the OnWriteBodyContents() method to output the SSDL contract.

protected override void OnWriteBodyContents(XmlDictionaryWriter writer) {
ssdl.GetInfoSet().WriteContentTo(writer);
}


If you're trying to install the ChannelListener on the base address of your service, you should also make sure that there are no other ChannelListeners configured in the runtime that interfere with your ChannelListener (e.g. disable WCF's ServiceMetadataBehavior, disable HttpHelpPageEnabled in ServiceDebugBehavior, etc.).

The complete source of the discussed meta-data example can be accessed via the Soya SVN repository. For those who don't like browsing, here are direct links to the main classes:

MetaBehavior.cs
IHttpService.cs
HttpGetMetadata.cs