Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Premiers pas avec Katana/Owin

Poster un commentaire

Maintenant que nous avons compris ce qu’est OWIN, essayons d’écrire du code.

La première chose à noter est qu’OWIN n’est rien d’autre qu’une spécification. Pour écrire du code nous avons besoin d’une implémentation d’OWIN.

Dans cet article on va se focaliser sur Katana qui est l’implémentation de Microsoft. On peut récupérer le code source ici:

http://katanaproject.codeplex.com/

Commençons par une application peu utile certes, mais qui va nous permettre de démontrer les principes de base de Katana et par là même d’écrire notre première application.

 

Première application

 

  • On commence par créer une application console. On va lui ajouter les dépendances suivantes:

OWIN/Katana

Install-Package Owin

Infrastructure pour héberger OWIN

Install-Package Microsoft.Owin.Hosting

Un écouteur Http

Install-Package Microsoft.Owin.Host.HttpListener

OWIN diagnostics

Install-Package Microsoft.Owin.Diagnostics

 

  • On ajoute la classe suivante:
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseWelcomePage();
    }
}

 

  • On complète par le code suivant:

class Program
{
    static void Main(string[] args)
    {
        using (WebApp.Start<Startup>("http://localhost:18000"))
        {
            Console.WriteLine("Je suis à votre écoute sur http://localhost:18000");
            Console.ReadLine();
        }
    }
}

 

Il ne reste plus qu’à lancer la console.

10-04-2014 08-32-17Puis lancer votre navigateur favori:

10-04-2014 08-34-09

Cette application est une juste une application de démo qui ne va rien faire de très extraordinaire. Mais c’est déjà plus jolie que ‘Hello world!’.

 

Commentons un peu ce qui vient d’être fait.

Nous avons réalisé une application OWIN en quelques lignes de code. On pourrait prover qu’il s’agit bien du pipeline OWIN en ajoutant ceci dans Configuration:


var assemblages = AppDomain.CurrentDomain.GetAssemblies();
foreach(var assemblage in  assemblages)
{
    Console.WriteLine(assemblage);
}

 

On vérifie qu’il n’existe aucune référence à System.Web.

 

Le code est certes très simple, mais il démarre par une partie laborieuse d’ajout de packages. Ben ça il va falloir s’y habituer. Contrairement au pipeline ASP.NET qui est (trop) remplit, le pipeline OWIN est vide. Mais en contrepartie on n’a ajouté QUE ce dont on a eu besoin. Je n’ai par exemple pas d’authentification, de gestion de cache ou de cookies…

Le package Microsoft.Owin.Diagnostics a été utilisé pour fournir une page de démo. Le rôle réel de ce middleware est ailleurs et sert plutôt comme outil de débogage afin de tester que le site fonctionne correctement.

Nous allons décrire la classe Startup dans le chapitre sur Katana, mais attardons nous un peu sur la méthode Main de l’application console.

La partie la plus notable est l’utilisation de la classe WebApp. C’est la classe qui est responsable de l’instanciation de l’hôte puis qui lance l’installation du pipeline.

Elle attend en paramètre le point de terminaison où l’application sera à l’écoute. D’autres surcharges permettent d’injecter un StartOptions.

Les principes de base de Katana

On pourrait également écrire Main de la façon suivante:


using (WebApp.Start("http://localhost:18000"))

 

Et ça marche quand même!
La raison est que Katana attend par défaut une classe définie de la façon suivante:


public sealed class Startup
{
    public void Configuration(IAppBuilder app)
    {
    }
}

Si elle se trouve dans votre application elle sera chargée, c’est le comportement standard.

On est pas obligé d’y adhérer et on peut en pratique renommer Startup comme on le souhaite, par exemple Startup1 et ajouter quelque part dans le projet:


using (WebApp.Start<Startup1>("http://localhost:18000"))

On dispose également d’un attribut:

[assembly: OwinStartup(typeof(Startup1))]

On peut même renommer Configuration:

[assembly: OwinStartup(typeof(Startup1), "Configuration1")]

Ce paramétrage peut éventuellement se faire dans le fichier de configuration, on peut ainsi disposer de deux pipeline, un pour la production et un pour le dév par exemple.

http://www.asp.net/aspnet/overview/owin-and-katana/owin-startup-class-detection

 

L’interface IAppBuilder est elle-aussi propre à Katana. La spécification n’en parle pas.
Lorsque vous écrivez des middleware il est important de veiller à ce qu’ils ne dépendent de IAppBuildder car vous ajoutez alors une dépendance à Katana.

Nous découvrirons les secrets de cette interface au fil des différents articles…

 

La spécification OWIN ne définit pas de façon claire comment on doit écrire un composant du middleware. Seul le rôle de IDictionary<string, object> est décrit avec précision, pour le reste:

OWIN is defined in terms of a delegate structure. There is no assembly called OWIN.dll or similar. Implementing either the host or application side the OWIN spec does not introduce a dependency to a project.

In this document, the C# Action/Func syntax is used to notate some delegate structures. However, the delegate structure could be equivalently represented with F# native functions, CLR interfaces, or named delegates. This is by design; when implementing OWIN, choose a delegate representation that works for you and your stack.

Il appartient donc à chaque implémentation d’OWIN de définir ses standards.

On se heurte très vite au problème qui plombe de nombreux projets Open Source: le manque de documentation.
Documenter n’est pas considéré comme une tâche très noble par nombre de développeurs. Je ne compte même plus les projets Codeplex ou Github où l’auteur n’écrit pas même une ligne pour expliquer ce que fabrique son code!

Attendez vous donc à expérimenter avec Katana.

Regardez cet exemple qui peut paraître raisonnable à la lecture de la spécification:

 


// définit AppFunc comme un alias
using AppFunc = Func<IDictionary<string, object>, System.Threading.Tasks.Task>;

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use(middleware);
    }

    AppFunc middleware = (IDictionary<string, object> env) =>
    {
        Stream response = (Stream)env["owin.ResponsetBody"];
        Byte[] bytes = Encoding.UTF8.GetBytes("<h1>Hello le monde!</h1>");

        var headers = (IDictionary<string, string[]>)env["owin.ResponseHeaders"];
        headers["Content-Type"] = new[] { "text/html" };
        headers["Content-Length"] = new[] { bytes.Length.ToString() };
        return response.WriteAsync(bytes, 0, bytes.Length);
    };
}

Le but de ce middleware n’est pas de faire des choses extraordinaires, mais de montrer comment utiliser le dictionnaire d’environnement.

Si vous lancez ce projet dans l’application précédente le message d’erreur suivant est levé.

System.ArgumentException was unhandled
HResult=-2147024809
Message=No conversion available between System.Func`2[System.Collections.Generic.IDictionary`2[System.String,System.Object],System.Threading.Tasks.Task] and System.Collections.Generic.IDictionary`2[System.String,System.Object].
Nom du paramètre : signature
Source=Microsoft.Owin
ParamName=signature

 

J’ai passé pas mal de temps la dessus et très péniblement remonté une explication et une solution.

IAppbuilder est une interface propre à Katana, elle ne fait pas partie d’OWIN, mais décrit comment est instancié le pipeline OWIN comme nous l’avons expliqué dans l’article précédent.
C’est cette interface qui est chargée d’implémenter l’algorithme en fournissant une instance de la classe Microsoft.Owin.BuilderAppBuilder.

Cette classe attend que les middlewares soient non pas des AppFunc, mais des Func<AppFunc, AppFunc>!

Oula!

Pour que ça marche il va falloir mouiller un peu la chemise.

 

A un moment donné on va devoir instancier quelque chose de ce genre:


var middleware = new Func<AppFunc, AppFunc>(????);

On a donc besoin de remplacer ??? par une fonction qui attend un type AppFunc en paramètre et retourne un autre type AppFunc.

On peut se dire que l’une de ces fonction implémente le nouveau composant du pipeline tandis que l’autre représente le reste du pipeline. Mais qui est qui?
La documentation de la méthode Use() indique ceci:

10-04-2014 22-45-11

Donc on attend en paramètre le pipeline et on renvoie le nouveau composant après l’avoir rattaché. Il ne reste plus qu’à construire notre fonction. D’où le code:

public AppFunc Middleware(AppFunc next)
{
    AppFunc appFunc = (IDictionary<string, object> env) =>
    {
        Stream response = (Stream)env["owin.RequestBody"];
        Byte[] bytes = Encoding.UTF8.GetBytes("<h1>Hello le monde!</h1>");

        var headers = (IDictionary<string, string[]>)env["owin.ResponseHeaders"];
        headers["Content-Type"] = new[] { "text/html" };
        headers["Content-Length"] = new[] { bytes.Length.ToString() };
        response.WriteAsync(bytes, 0, bytes.Length);

        return next(env);
    };

    return appFunc;
}

C’est bien ainsi que s’écrit notre middleware, on a une fonction qui attend un AppFunc et qui retourne un AppFunc.

Du côté de la classe Startup:

var middleware = new Func<AppFunc, AppFunc>(Middleware);

app.Use(middleware);

Et cette fois tout rentre dans l’ordre.

public async Task Middleware(AppFunc next, IDictionary<string, object> env)
{
        Stream response = (Stream)env["owin.RequestBody"];
        Byte[] bytes = Encoding.UTF8.GetBytes("<h1>Hello le monde!</h1>");

        var headers = (IDictionary<string, string[]>)env["owin.ResponseHeaders"];
        headers["Content-Type"] = new[] { "text/html" };
        headers["Content-Length"] = new[] { bytes.Length.ToString() };
        await response.WriteAsync(bytes, 0, bytes.Length);

        await next(env);
    };
}

et côté appel:


app.Use(new Func<AppFunc, AppFunc>(next => env => Middleware(next, env) );

 

 

Retenons donc ceci:

 Pour Katana, un middleware est une instance de Func<AppFunc, AppFun>. Le paramètre est le pipeline existant , c’est à dire la chaîne des middleware déjà injectés. Le retour est le nouveau middleware, prêt à être enchainé à un éventuel suivant.

 

 Deuxième application

Lorsque l’on voit du code comme celui qui précède, un développeur normalement constitué doit penser: il faut faire une abstraction. Cette couche existe déjà dans Katana.

  • IOwinContext.
    Cette interface est un wrapper pour exposer des propriétés fortement typées du dictionnaire d’environnement: IOwinRequest, IOwinResponse….
  • Run.
    Il faut bien une méthode capable de consommer IOwinContext, la voici! Run attend un Func<IOwinContext, Task> contrairement à Use qui attend un object uniquement.

Avec ces deux outils il devient nettement plus facile de manipuler du code sans trop se préoccuper du cast et avec une lisibilité bien meilleure.

 


public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Run(async context =>
        {
            context.Response.ContentType = "text/html";
            await context.Response.WriteAsync("<h1>Hello le monde!</h1>");
        });
    }
}

Le dictionnaire d’environnement est toujours là, c’est context.Environment. Mais des méthodes et propriétés utilitaires sont également apparues qui simplifient le code comme vous le contatez.

Bien sûr ce code est très bien pour des middleware relativement simples. Pour les cas plus complexes d’autres abstractions existent. Nous les verrons dans d’autres articles.

 

 Quelques autres options

Microsoft.Owin.Host.HttpListener est un serveur Http très léger. C’est en général celui recommandé par défaut. Les packages Nuget en fournissent d’autres comme Microsoft.Owin.Host.SystemWeb.

Install-Package Microsoft.Owin.Host.SystemWeb

Ce package permet d’ajouter à une application Owin un middleware qui implémente le pipeline ASP.NET. Dans ce cas IIS agira à la fois comme serveur et comme hôte.

Microsoft travaille sur un projet appelé Helios:

Install-Package Microsoft.Owin.Host.IIS

Ce package permettra de lancer une application OWIN dans IIS, mais sans passer par ASP.NET. Mais attention, pour l’instant ce projet est encore en version alpha et sans garanties qu’il sortira un jour.

 

L’article qui suite donne un panorama de diverses autres façons d’héberger un composant Owin, Web Api en l’occurrence:

https://amethyste16.wordpress.com/2014/03/30/web-api-deux-modes-dhebergement/

 

Et en particulier OwinHost.exe.

 

Nous allons en rester là dans cet article déjà relativement long. Mais nous sommes loin d’en avoir terminé avec le pipeline Owin.

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s