Running multiple ASP.NET Web API pipelines side by side

Β· 784 words Β· 4 minutes to read

Over the past 4 years or so, I have worked on many Web API projects, for a lot of different clients, and I thought I have seen almost everything.

Last week I came across an interesting new (well, at least to me) scenario though - with the requirement to run two Web API pipelines side by side, in the same process. Imagine having /api as one Web API “instance”, and then having /dashboard as completely separate one, with it’s own completely custom configuration (such as formatter settings, authentication or exception handling). And all of that running in the same process.

More after the jump.

The problem πŸ”—

On IIS, normally you would solve it having to separate web application assemblies and deploying them into different virtual directories. It gets more interesting, if you try to do this in the same process though.

Sure, this is what OWIN allows you to do - run multiple frameworks or “branches”, side by side. But turns out this is not that easy with the Web API OWIN integration.

Normally you’d go about doing this the following, right?

public class Startup

{

    public void Configuration(IAppBuilder app)

    {

        app.Map("/api", map =>

        {

            var config = new HttpConfiguration();

            config.MapHttpAttributeRoutes();





            // configure the API 1 here



            map.UseWebApi(config);

        });



        app.Map("/dashboard", map =>

        {

            var config = new HttpConfiguration();

            config.MapHttpAttributeRoutes();





            // configure the API 2 here



            map.UseWebApi(config);

        });

    }

}

Seems reasonable, typical usage of the OWIN middleware architecture.

The problem with this set up is that Web API doesn’t necessarily lend itself very well to such set up. This is mainly related to the way Web API discovers controllers and routes.

Since you want to have two pipelines side by side, in the same assembly, you want to have only specific controllers visible to specific pipeline.

By default Web API will look throughout the entire AppDomain for controllers and attribute routes, meaning that both of your pipelines will discover everything and you will end up with a huge mess.

The solution πŸ”—

In order to address that you can define the convention to divide your controllers into specific “areas” or, as we called it from the beginning, pipelines.

Then you can tell Web API configuration object, to only deal with this specific subset of types when discovering controllers and mapping attribute routes. This can be achieved in several ways - namespaces, attributes and so on. The simplest of which is probably using a base marker type though.

Let’s introduce two base controller types representing two different Web API pipelines that we defined earlier - /api and /dashboard.

public abstract class MyApiController : ApiController { }



public abstract class DashboardApiController : ApiController { }

Now we need to be able to set up HttpConfiguration, that’s used to define our Web API server, to respect these base controllers when looking for types that should be used as controllers and when discovering attribute routes.

This can be done by introducing a custom IHttpControllerTypeResolver and IDirectRouteProvider, that would only work with controllers that subclass a specific base class. These implementations are shown below.

public class TypedHttpControllerTypeResolver<TBaseController> : DefaultHttpControllerTypeResolver where TBaseController : IHttpController

{

    public TypedHttpControllerTypeResolver()

        : base(IsDashboardController)

    { }



    internal static bool IsDashboardController(Type t)

    {

        if (t == null) throw new ArgumentNullException("t");



        return

            t.IsClass &&

            t.IsVisible &&

            !t.IsAbstract &&

            typeof (TBaseController).IsAssignableFrom(t);

    }

}



 public class TypedDirectRouteProvider<TBaseController> : DefaultDirectRouteProvider where TBaseController : IHttpController

{

    public override IReadOnlyList<RouteEntry> GetDirectRoutes(HttpControllerDescriptor controllerDescriptor, IReadOnlyList<HttpActionDescriptor> actionDescriptors,

        IInlineConstraintResolver constraintResolver)

    {

        if (typeof(TBaseController).IsAssignableFrom(controllerDescriptor.ControllerType))

        {

            var routes = base.GetDirectRoutes(controllerDescriptor, actionDescriptors, constraintResolver);

            return routes;

        }



        return new RouteEntry[0];

    }

} 

In both cases, we are extending the default implementations - DefaultHttpControllerTypeResolver and DefaultDirectRouteProvider.

While Web API will still treat all AppDomain types as potential “controller” candidates, in TypedHttpControllerTypeResolver we use the same default constraint to discover controllers (must be class, must be public, must not be abstract) that Web API uses, but additionally we throw in the requirement to subclass our predefined base controller.

Similarly, for TypedDirectRouteProvider, Web API will inspect all controllers in the AppDomain, but we can define that routes should be picked up only from those controllers that extend our predefined base.

The final step is just to wire this in, which is very easy - the revised Startup class is shown below.

public class Startup

{

    public void Configuration(IAppBuilder app)

    {

        app.Map("/api", map =>

        {

            var config = CreateTypedConfiguration<MyApiController>();

            config.MapHttpAttributeRoutes();





            // configure the API 1 here



            map.UseWebApi(config);

        });



        app.Map("/dashboard", map =>

        {

            var config = CreateTypedConfiguration<DashboardApiController>();

            config.MapHttpAttributeRoutes();





            // configure the API 2 here



            map.UseWebApi(config);

        });

    }



    private static HttpConfiguration CreateTypedConfiguration<TBaseController>() where TBaseController : IHttpController

    {

        var config = new HttpConfiguration();



        config.Services.Replace(typeof(IHttpControllerTypeResolver), new TypedHttpControllerTypeResolver<TBaseController>());

        config.MapHttpAttributeRoutes(new TypedDirectRouteProvider<TBaseController>());



        return config;

    }       



}

And that’s it! No more cross-pipeline conflicts, and two Web API instances running side by side.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP