Monday, August 18, 2014

Dynamic WCF routing and easy upgrading from ASMX to WCF

I’ve previously blogged on how to create WCF hosts programmatically (so rather than dozen configuration parameters you configure services in code) and how to manage the lifetime of WCF services by using custom instance providers.

This time we are going to discuss WCF routing, which is – how specific uris can be dynamically mapped to WCF service hosts. With this mechanism, a WCF service can be configured 100% programmatically, you won’t even need *.svc files.

Why would that matter? What’s wrong in having an auxiliary *.svc file with a factory behind? Well, using the dynamic routing approach you will be able to map any uri to a WCF service handler factory! This is great for migrating all legacy *.asmx services to WCF – you could rewrite all your legacy services with WCF and basic http binding and expose these services on their original uris. A client would still go to http://your.domain.com/service.asmx but the server would activate your WCF service instead.

Sounds great? Well, it should. There are numerous advantages of WCF over ASMX (logging, auditing, instance management, IoC integration, etc.) which means that you could upgrade your server environment without breaking client compatilibity.

The idea comes from the Maarten Balliauw’s blog entry and he gets the credit for figuring out the difficult part. It involves a custom route handler which is a part of the pipeline that is responsible for mapping uris to request handlers. Since ASP.NET 4 has been released, custom route handlers can be implemented so that the only issue is to create a route handler that maps an uri template to a WCF-compatible service host.

And this is where the problem is. You can’t easily write your own WCF request handler, it would involve a deep knowledge of WCF internals. Instead, you want your route handler to pick up an existing request handler and when it comes to WCF, this role is played by the ServiceRoute class.

But there is the caveat. The ServiceRoute class doesn’t support dynamic segments in the service uri! You just get an exception.

Maarten’s idea is to have a composite route handler, it uses two route handlers internally. On one hand, if an incoming uri is parsed and segments should be extracted, the built-in Route class is used (and it handles dynamic segments easily). On the other hand, when it comes to creating a handler, the built-in ServiceRoute class is used on a virtual, non-segmented uri and uses the internal router to provide a handler for free.

Setting up the Environment

We start with the definition of the service interface:

[ServiceContract(Namespace="http://www.foo.bar.qux/customservice")]
public interface ICustomService
{
    [OperationContract( Action = "http://www.foo.bar.qux/customservice/HelloWorld" )]
    string HelloWorld( string Message ); 
}

There is a caveat here. In an ideal world where we upgrade existing ASMX services to WCF, we would expect a cross-system compatilibity. This means that an existing ASMX client (proxy that inherits from the SoapHttpClientProtocol) should be able to call the new WCF service and the other way around – a new client (proxy that inherits from ClientBase<T> or a proxy created with the ChannelFactory) should be able to call the legacy ASMX service.

After all, it is all about a simple XML over the wire, isn’t it?

Well, it is not that simple. When a service request is serialized and sent over the wire to the server, request body carries the SOAP message and the SOAPAction header that points to an action to invoke at the server. The problem is that WCF and ASMX have different idea on how the SOAPAction should look like.

ASMX by default expects http://service.namespace/action. WCF by default expects http://service.namespace/interfacename/action.

This could be confusing and would prevent such cross-system calls. We need a fix and that fix is to override the SOAPAction either at the ASMX service side or the WCF side. I take the latter approach since I assume ASMX services are already implemented and working for years and thus I cannot change the SOAPAction there (it would break the compability of existing clients).

Then I need an example implementation of the WCF service:

[AspNetCompatibilityRequirements( 
   RequirementsMode = AspNetCompatibilityRequirementsMode.Required )]
public class CustomServiceImpl : ICustomService
{
    #region ICustomService Members
 
    public string HelloWorld( string Message )
    {
        return string.Format( "{0} from {1}", 
            Message, 
            DynamicServiceRoute.GetCurrentRouteData().Values["tenant"] );
    }
 
    #endregion
}

As you can see, I can access route data and pick a segment value from there – in this example a multitenant service will be exposed with {tenant} as an only dynamic segment.

For the sake of completeness, I also need a WCF service host class and this is where I could borrow some advanced ideas from my previous post on WCF instance providers and have my WCF service created by an IoC container of my choice and then disposed at the end of the call. In this example, however, a simplest service host will do:

public class CustomServiceHostFactory : ServiceHostFactory
{
    public override ServiceHostBase CreateServiceHost(
        string constructorString,
        Uri[] baseAddresses )
    {
        // Host our WCF Service
        ServiceHost serviceHost = 
            new ServiceHost( typeof( CustomServiceImpl ), baseAddresses );
 
        foreach ( var address in baseAddresses )
        {
            serviceHost.AddServiceEndpoint( 
               typeof ( ICustomService ), 
               new BasicHttpBinding(), address.ToString() );
        }
 
        return serviceHost;
    }
}
The dynamic route handler

And this is where Maarten’s route handler kicks in:

/// <remarks>
/// http://blog.maartenballiauw.be/post/2011/05/09/Using-dynamic-WCF-service-routes.aspx
/// </remarks>
public class DynamicServiceRoute : RouteBase, IRouteHandler
{
    private string virtualPath = null;
    private ServiceRoute innerServiceRoute = null;
    private Route innerRoute = null;
 
    public static RouteData GetCurrentRouteData()
    {
        if ( HttpContext.Current != null )
        {
            var wrapper = new HttpContextWrapper( HttpContext.Current );
            return wrapper.Request.RequestContext.RouteData;
        }
        return null;
    }
 
    public DynamicServiceRoute(
        string routePath,
        object defaults,
        ServiceHostFactoryBase serviceHostFactory,
        Type serviceType )
    {
        virtualPath = serviceType.FullName + "-" + Guid.NewGuid().ToString() + "/";
        innerServiceRoute = new ServiceRoute( virtualPath, serviceHostFactory, serviceType );
        innerRoute = new Route( routePath, new RouteValueDictionary( defaults ), this );
    }
 
    public override RouteData GetRouteData( HttpContextBase httpContext )
    {
        return innerRoute.GetRouteData( httpContext );
    }
 
    public override VirtualPathData 
       GetVirtualPath( RequestContext requestContext, RouteValueDictionary values )
    {
        return null;
    }
 
    public System.Web.IHttpHandler GetHttpHandler( RequestContext requestContext )
    {
        requestContext.HttpContext.RewritePath( "~/" + virtualPath, true );
        var handler = innerServiceRoute.RouteHandler.GetHttpHandler( requestContext );
        return handler;
    }
}
 
public static class DynamicServiceRouteExtensions
{
    public static void MapDynamicServiceRoute(
        this RouteCollection routes,
        string routePath, object defaults,
        ServiceHostFactoryBase serviceHostFactory,
        Type serviceType
        )
    {
        routes.Add( 
           new DynamicServiceRoute( routePath, defaults, serviceHostFactory, serviceType ) );
    }
}

And this is it! With all these in my hand I can finally set up a dynamic WCF service:

public static void RegisterRoutes( RouteCollection routes )
{
    routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" );
 
    routes.MapDynamicServiceRoute(
        "{tenant}/CustomService.asmx",
        new { tenant = "default" },
        new CustomServiceHostFactory(),
        typeof( ICustomService ) );
 
    // ... other routes follow
}
As said before, the route will expose a dynamic WCF service under {tenant}/CustomService.asmx. Any request that matches this template will involve the simple WCF host factory which in turn will activate the service, create the response and send it back.