API discovery with OWIN self-host

If you write APIs with ASP.NET Web API then you probably keep the API code in a separate project in Visual Studio. If this is the case, then your startup project is likely to be in a different ASP.NET Web API project that, when run, is hosted by IIS or IISExpress. This works well enough, but the startup time is slow which can make iterating on an idea rather frustrating.

Use OWIN self-host for a faster startup

OWIN self-host has a much faster startup, which makes it ideal for this scenario. The startup for an OWIN self-host project looks remarkably similar to its IIS-hosted equivalent, except that you start the server yourself.

Here’s some typical startup code. Nothing unusual here – it configures the routing and makes sure that Web API is in the pipeline.

    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
                );
            app.UseWebApi(config);
        }
    }

And here’s the program that starts the server. Again, nothing unusual. It starts a self-hosted server and runs it until you press ENTER.

    class Program
    {
        static void Main(string[] args)
        {
            string baseAddress = "http://localhost:9000/";

            using (WebApp.Start<Startup>(url: baseAddress))
            {
                Console.WriteLine("Server listening on {0}", baseAddress);
                Console.WriteLine("Press [ENTER] to end");
                Console.ReadLine();
            }
        }
    }

Problem: The API controllers can’t be found

However, if your API is in a separate project then you might see this message when you invoke it (here I’m invoking it with cURL):

C:\Users\Rod>curl --silent http://localhost:9000/api/fortunes
{"Message":"No HTTP resource was found that matches the request URI 'http://loca
lhost:9000/api/fortunes'.","MessageDetail":"No type was found that matches the c
ontroller named 'fortunes'."}

Your first inclination might be to think that you’ve forgotten to add a reference to your API project, but even with a reference in place you will still see this message.

The reason for the message is that your OWIN startup project doesn’t automatically discover and load the API code’s assembly, whereas the IIS-hosted equivalent does. So, when Web API tries to find your controller, as described in Routing and Action Selection in Web API, it will never find it because the controller’s assembly was never loaded.

Solution: Load the assembly

The solution is simple. Load the assembly. There are at least a couple of ways of doing this. One is to use an IAssembliesResolver, and the other is to ensure the assembly is loaded by having the startup project use it directly.

Using an IAssembliesResolver

You could solve the problem by overriding the default assemblies resolver, as detailed in Customizing controller discovery in ASP.NET Web API. This works very well. Here’s an example:

    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
                );
            config.Services.Replace(typeof(IAssembliesResolver), new AssembliesResolver());
            app.UseWebApi(config);
        }
    }

    class AssembliesResolver : DefaultAssembliesResolver
    {
        public override ICollection<Assembly> GetAssemblies()
        {
            ICollection<Assembly> assemblies = base.GetAssemblies();
            var apiAssembly = Assembly.LoadFrom(@"API.dll");
            assemblies.Add(apiAssembly);
            return assemblies;
        }
    }

Using a dependency

There’s an even simpler option, which is to have the startup project make use of something in the API project. For example, here’s an ApiInfo class:

    public static class ApiInfo
    {
        public static readonly string Help = "Use GET /api/fortunes to access this API";
    }

Incidentally, if you’re wondering why the string is static readonly rather than const then here’s why.

And here’s a modified version of the program that starts the server. Now it displays some help text from the ApiInfo class:

    class Program
    {
        static void Main(string[] args)
        {
            string baseAddress = "http://localhost:9000/";

            using (WebApp.Start<Startup>(url: baseAddress))
            {
                Console.WriteLine("Server listening on {0}", baseAddress);
                Console.WriteLine(ApiInfo.Help);
                Console.WriteLine("Press [ENTER] to end");
                Console.ReadLine();
            }
        }
    }

Now, when the self-hosted project starts, it displays information about the API, using information from the API project. And because it does this, the API assembly is guaranteed to be loaded therefore the controllers will be discovered.

So here’s the output window. The server is running, and it has displayed the simple help text from the ApiInfo class.

Server listening on http://localhost:9000/
Use GET /api/fortunes to access this API
Press [ENTER] to end

So, does it work?

C:\Users\Rod>curl --silent http://localhost:9000/api/fortunes
"A language that doesn't affect the way you think about programming is not worth
 knowing."

True enough!

Code

The code for this post can be found on GitHub.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s