Self Host Web API 2

En cherchant des exemples d’application self host Web API avec OWIN (spécification implémentée par Katana), les seuls (mais nombreux) que j’ai trouvé mélangeaient tous les frameworks en une seule application, console typiquement.
Cet article présente l’exemple que j’aurai aimé trouver pour démarrer ma première application avec OWIN.


Mon objectif initial était de concevoir un service Windows servant de self host qui ne soit que cela (host), et une librairie tierce contenant mes Web API.

Cet exemple s’intéresse spécifiquement à l’implémentation d’une application basée sur Web API 2, en mode Self Host. Bien que l’application hôte soit généralement un service Windows, l’exemple présenté ici est une application console. Ma première impression de Katana est: c’est super!

Un rapide rappel sur la terminlogie d’OWIN (ubiquitous language)

La spécification d’OWIN débute par 4 notions clés dans la structure d’une application utilisant OWIN:

  • Host: process hôte de l’application (dans cet exemple, c’est une application Console). Ce process contient le Server.
  • Server: Katana est l’implémentation de notre serveur OWIN.
  • Web Framework: dans cet exemple, Web API est un Web Framework. Il s’appuie sur OWIN pour traiter les requêtes web.
  • Web Application: dans cet exemple, le projet contenant le code métier représente l’application web (le domaine dans la terminologie DDD).
  • Middleware : représente tout composant inscrit dans le pipeline d’OWIN pour accéder ou agir sur la requête ou sa réponse. Dans notre exemple, on utilise CacheCow qui serait un composant Middleware s’il était inscrit dans OWIN. En fait, il est inscrit dans le pipeline de Web API, ce n’est donc pas exactement un Middleware OWIN.

Disclaimer

L’exemple ci-dessous est basé sur ASP.NET Web API 2.1 (MVC 5). Dans la prochaine version MVC 6, il est prévu que soient uniformisés les deux frameworks MVC et Web API en un seul et même framework.

Place au code…

Le code source est disponible sur GitHub.

L’application est décomposée en deux projets :

  • Host: projet console contenant le serveur OWIN (Katana)
  • WebApiLib: librairie représentant véritablement notre application (au sens du domaine). Ce projet n’a aucune dépendance à OWIN (ni IIS évidemment).

Notre application (WebApiLib)…

using System;
using System.Threading.Tasks;
using System.Web.Http;

namespace KatanaWebApiSample.WebApiLib
{
    public interface IMyService
    {
        string SayHello(string name);
    }

    public class SayHelloController : ApiController
    {
        private readonly IMyService _myService;

        public SayHelloController(IMyService myService)
        {
            _myService = myService;
        }

        // GET /api/SayHello/Smith
        public string GetWithId(string id)
        {
            // some very useful app domain logic...
            return _myService.SayHello(id);
        }

        // GET /api/SayHello
        public string Get()
        {
            // some very useful app domain logic...
            return _myService.SayHello("World !");
        }
    }
}

C’est tout : il n’y a qu’un contrôleur Web API qui utilise une interface pour sa logique métier.

Ce projet dépend du package Nuget Microsoft.AspNet.WebApi.Core.

Notre serveur (Host)…

Pré-requis (Nuget) :

Commençons par la fin : le point d’entrée de l’application. Dans le code ci-dessous, tout est mis en place à la ligne suivante: WebApp.Start<OwinAppStartup>(baseAddress)

using System;
using Microsoft.Owin.Hosting;

namespace KatanaWebApiSample.Host
{
    internal static class Program
    {
        private static void Main(string[] args)
        {
            const string baseAddress = "http://localhost:9000/";
            using (WebApp.Start<Infrastructure.AppStartup.OwinAppStartup>(baseAddress))
            {
                var client = new System.Net.Http.HttpClient();
                var requestUri = string.Format("{0}api/SayHello/Smith", baseAddress);
                Console.WriteLine("GET {0} ...", requestUri);
                var response = client.GetAsync(requestUri).Result;
                Console.WriteLine("{0}\r\n{1}", response, response.Content.ReadAsStringAsync().Result);
                Console.ReadKey(true);
            }
        }
    }
}

La classe OwinAppStartup contient la configuration du serveur OWIN. Celle-ci doit respecter quelques conventions que je ne détaille pas ici (pour l’essentiel, une méthode publique void Configuration(IAppBuilder)). C’est dans cette classe que le Web Framework Web API est mis en place:

 internal class OwinAppStartup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            #region WebApi (Web Framework in OWIN terminology)
            
            HttpConfiguration webApiConfig = new HttpConfiguration();
            webApiConfig.DependencyResolver = IocInitializer.SetUp();
            WebApiConfig.Register(webApiConfig);
            appBuilder.UseWebApi(webApiConfig);
            
            #endregion
        }
    }

Ce devrait être la seule nouveauté, le reste constitue la configuration Web API avec un conteneur IoC (Simple Injector dans cet exemple), semblable à toute application basée sur ce framework:

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Default route
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional });

            // Replaces IAssembliesResolver to allow resolution 
            // of controllers in other assemblies (not necessarily 
            // already loaded into the current AppDomain).
            config.Services.Replace(typeof(IAssembliesResolver), new CustomAssembliesResolver());

            // Cache (depends on CacheCow package)
            config.MessageHandlers.Add(new CachingHandler(config));
        }
    }

    internal static class IocInitializer
    {
        public static IDependencyResolver SetUp()
        {
            var container = new Container();
            Configure(container);
            return new SimpleInjectorWebApiDependencyResolver(container);
        }

        private static void Configure(Container container)
        {
            container.RegisterSingle<IMyService, MyService>();
        }
    }

Le remplacement du service IAssembliesResolver sert à prendre en compte un assemblage tiers pour la résolution des contrôleurs Web API (par défaut, Web API ne tient compte que des contrôleurs déjà chargés dans le domaine d’application courant). L’implémentation n’est pas incluse ici, mais vous la trouverez sur GitHub.

La classe CachingHandler correspond au Middleware CacheCow pour gérer le cache des contrôleurs Web API (serveur et client).

Voilà, c’est fini.

Pour conclure sur cette approche…

Le fait d’avoir de petites applications self host est une bonne chose. Cela encourage à découpler les fonctionnalités de l’application. On peut rapidement imaginer avoir une multitude de petites applications plutôt qu’un gros monolithe.

Une limitation que je vois actuellement: l’intégration de vues (MVC) et donc de OAuth2 ne se prête sans doute pas à une intégration dans la version actuelle de Katana (bien que possible techniquement).