June 2010 Archives

ServiceMetadataExtension refactoring story

| No Comments | No TrackBacks

I had been fixing several bugs in our WCF code these months. Yeah, I kept silent just because it is too boring topic to blog :p

One of the most problematic one was ServiceMetadataExtension support, which is for WSDL output on the service site (the ones you get when you access to http://yourwebsite/yourservice.svc?wsdl). Yes, surprisingly. You would not understand why that is so problematic.

(0) (re)introduction

For public API wise, I once wrote an entry-level entry for entrypoint to service metadata support last year. I didn't give any explanation on its internals at all. Now it's time.

A simple code for hosting a WCF service with metadata support would look like:

var host = new ServiceHost (typeof (FooService), new Uri (uri));
host.AddServiceEndpoint (typeof (ITestService),
    new BasicHttpBinding (),
    new Uri ("", UriKind.Relative));
host.Description.Behaviors.Add (
    new ServiceMetadataBehavior () { HttpGetEnabled = true });
host.Open ();

Today, in the example above, what actually creates a working service listener is only the last line: host.Open (); only the last line matters. I explain what it's doing there, step by step.

(1) ServiceHostBase and Binding

First, I'll explain what ServiceHostBase supports and what you can do only with Binding.

Basically, ServiceHost(Base) manages more than one service "endpoint". You can call "AddServiceEndpoint" on the same host multiple times. A service endpoint is a set of an "endpoint address", a "binding" and a "service contract". You can implement multiple service contract interface within a service type (FooService) and serve requests to different interfaces at different endpoint URIs (addresses), using the same or different binding. A binding can be things like BasicHttpBinding, NetTcpBinding, WebHttpBinding, WSHttpBinding (not supported in mono yet) or anything that derives from Binding.

You might know that Binding class has a method that creates an IChannelListener object that is to actually listen for requests. Actually, to receive requests in Message form, you don't have to even use ServiceHost. Instead, you can do this:

var binding = new BasicHttpBinding ();
IChannelListener<IReplyChannel> listener =
   binding.BuildChannelListener<IReplyChannel> (
      new Uri ("http://localhost:8080/"));
listener.Open ();
IReplyChannel ch = listener.AcceptChannel ();
ch.Open ();
RequestContext ctx = ch.ReceiveRequest ();

RequestContext has a request Message that is created from the HTTP SOAP request it received. You can use Reply(Message) method to return a message in whatever form you want.

You wouldn't find it very useful. You'd rather use strongly-typed services instead so that you don't have to get messes by Message object. Also you wouldn't like to call ReceiveRequest() every time explicitly. Hence, there is ServiceHost(Base). It handles those tasks.

(2) ChannelDispatcher and EndpointDispatcher basics

When ServiceHostBase.Open() is called, the host creates a set of "ChannelDispatchers" in the host. A channel dispatcher is created for each Binding used in the ServiceHostBase (actually for each of its service endpoints).

A ChannelDispatcher manages an IChannelListener created from Binding.BuildChannelListener<TChannel> method. TChannel can be something other than IReplyChannel, but I don't explain it here (not primary topic today).

A ChannelDispatcher holds one or more "endpoint". In ServiceHostBase, there could be multiple ServiceEndpoints and they have a Binding and an EndpointAddress. Actually the Binding instance can be shared, and in such case, those ServiceEndpoints that shares the same Binding also shares the same ChannelDispatcher.

In ChannelDispatcher, the set of endpoints is represented as Endpoints property and an endpoint becomes an EndpointDispatcher. EndpointDispatcher is hence bound to a contract. When a request to the channel listener arrives (e.g. HTTP request), the ChannelDispatcher has to determine which "endpoint" (EndpointDispatcher) should process the request. Usually it is determined by its ContractFilter and AddressFilter properties, and since usually service endpoint URIs differ, it does not matter much (Use "/foo" for IFooService and "/bar" for IBarService.

... well, I wrote "basics". Yes, it is basic part. The core part starts from here.

(3) IMetadataExchange, ServiceMetadataBehavior and ServiceMetadataExtension

With BasicHttpBinding, an IChannelListener is created for HTTP scheme, and its listening URI is typically a local HTTP URI. In mono, it is either done by ASP.NET (xsp) or HttpListener (non-ASP.NET). In both cases, the services blocks the listening URI, and if we simply try to serve WSDLs using another HttpListener, there is no more room. Since WSDL requests could be sent to the same URI of the service itself, typically only differentiating the query parameter, there sould be some trick. (Do you understand it's getting messier?)

Another complication factory is that there is ServiceMetadataBehavior and ServiceMetadataExtension that have some Binding and Uri properties and show capability of handling "metadata exchange" requests. So it's not only about WSDL. Its endpoints must be exposed at users' will.

Interestingly, MetadataExchangeBindings class exposes a couple of static methods that creates a Binding for mex endpoints. And they are actually used by ServiceMetadataExtension, that is an IExtension for ServiceHostBase and realizes ServiceMetadataBehavior's requirements. Since Binding instance are different, it can create another IChannelListener, which typically has the identical listen URI as the service endpoint itself has.

The different Binding instance results in different ChannelDispatcher in the ServiceHostBase. I was originally aware of the fact that when the ServiceHostBase is opened there are two ChannelDispatchers (the reason came later). Different channel disptatchers have (again) different IChannelListener instances. And they still point to the same listen URI.

That's problematic. It makes request dispatching difficult. To make it worse, ChannelDispatcher has MessageVersion property. If they resided in the same ChannelDispatcher, it would have been easier since we could use message filters to select appropriate EndpointDispatcher. HTTP request interpretation varies dependin on the target MessageVersion. So the target ChannelDispatcher and EndpointDispatcher must be selected before IChannelListener.ReceiveContext() is done.

(4) HTTP listener manager

Hence we have to create another management layer for HTTP channel listener to reuse the same HttpListener (and some equivalent management layer for ASP.NET). Actually when Mainsoft hackers were working on it, they were aware of this issue, and created a management layer. It mostly worked until we reached the point that we have to handle more strict differentiation.

Basically, we determine if the request is GET and if the target URL matches the wsdl GET URL, then it is for WSDL. Actually there is ServiceDebugBehavior so we also handle it (if you omit "?wsdl" query parameter in your HTTP request, you'll see some "help page" for the service. It is what ServiceDebugBehavior is for).

BUT, we can't bindly determine such requests as for WSDLs. Do you remember there is WebHttpBinding? With this binding, you can access services by UriTemplate, and it is todally done by GET request (with no request stream). Whenever applicable, we should handle requests to the RESTful services within the binding. Interestingly, WebHttpBinding uses WebHttpBehavior to adjust endpoint behaviors, and it actually raises its EndointDispatcher's FilterPriority(!). So IF the endpoint dispatcher is in the same ChannelDispatcher as ServiceMetadataBehavior yields, there was no problem. Sadly, WebHttpBinding is, again, a different binding that ServiceMetadataBehavior gives, so it creates different ChannelDispatcher (as BasicHttpBinding does).

To get the higher priority settings working, we have to "hack" the order of searching appropriate ChannelDispatcher to dispatch an HTTP request, by its Endpoints (EndpointDispatchers).

All of those behaviors cannot be completely done within WCF public API. So, ServiceMetadataBehavior and ServiceMetadataExtension are special. You cannot create functionally-equivalent one without giving up some aspects (e.g. it would be able to be done if you don't allow hosting WSDL on the same URI as the service is listening).

(5) done

It had been a longstanding issue that attacked me over and over again (fix->regress->fix->regress...), and history may repeat, but I rewrote the HTTP listener stack and relevant stuff last week, it should work today, hopefully in more reliable state. And since that fixes I feel much comfortable than those annoyed days in my hacking life :)