Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Ecriture d’un middleware Katana/Owin

Poster un commentaire

Nous avons compris ce qu’est Owin, nous avons vu comment fonctionne le pipeline il nous reste à régler un point important: comment faire pour créer des middlewares plus sophistiqués que HelloWorld.

Ecrire un middleware est une étape importante dans un projet OWIN. Souvent vous allez en trouver des prêts à l’emploi dans Nuget, mais pas toujours.

Nous avons vu dans le blog qui précède comment Katana définit un middleware:

 

 Pour Katana, un middleware est une instance du délégué 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.

Techniquement cette définition nous fournit un moyen de répondre à la question initiale. Mais ce n’est pas la seule méthode et ce n’est pas la plus pratique.

Pour chaîner des middleware dans un pipeline, on invoque la méthode IAppBuilder.Use(object middleware).

Vous constatez que le paramètre est un Object, pas un Func<AppFunc,AppFunc>. C’est parce que l’on a plusieurs cas de figure:

 

  • un délégué (inline)
  • un type
  • une instance

 

Nous allons examiner chacun de ces cas en s’inspirant d’un article du blog de  Ben Foster.


Un délégué

C’est le cas que nous connaissons déjà, on implémente directement Func<AppFunc, AppFunc>.


using AppFunc = Func<IDictionary<string, object>, Task>;

public sealed class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use(new Func<AppFunc, AppFunc>(next => (async env =>
        {
            string message = "<h1>Hello world</h1>";
            Byte[] bytes = Encoding.UTF8.GetBytes(message);

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

           Stream response = (Stream) env["owin.ResponseBody"];
           await response.WriteAsync(bytes, 0, bytes.Length);

           await next(env);
      })));
    }
}

Ce code peut être réécrit ainsi, c’est peut être plus clair:

public void Configuration(IAppBuilder app)
{
    // on créée une instance du délégué
    var middleware = new Func<AppFunc, AppFunc>(next => env => MonMiddleware(next, env));
    app.Use(middleware);
}

async Task MonMiddleware(AppFunc next, IDictionary<string, object> env)
{
    string message = "<h1>Hello world</h1>";
    Byte[] bytes = Encoding.UTF8.GetBytes(message);

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

    Stream response = (Stream)env["owin.ResponseBody"];
    await response.WriteAsync(bytes, 0, bytes.Length);

    await next(env);
}

C’est le même code que celui vu ici:

https://amethyste16.wordpress.com/2014/04/10/ma-premiere-application-owinkatana/

 

Cette méthode est correcte pour écrire un middleware simple, mais elle est lourde pour un composant plus complexe.

 

D’autres signatures sont possibles, par exemple on réécrit le middleware ainsi avec l’interface utilitaire IOwinContext que nous reverrons plus loin:


void MonMiddleware(IOwinContext env)
{
    string message = "<h1>Hello world</h1>";

    env.Response.ContentLength = message.Length;
    env.Response.ContentType = "text/html";
    env.Response.WriteAsync(message);
}

 

Le placement dans le pipeline s’effectue avec un délégué:

public void Configuration(IAppBuilder app)
{
    app.Use(async (env, next) => {
        MonMiddleware(env);

        await next.Invoke();
    });
}

 

Un type

Pour simplifier les développement Katana propose la classe abstraite Microsoft.Owin.OwinMiddleware. Réécrivons notre composant Hello world!


class HelloMiddleware : OwinMiddleware
{
    public HelloMiddleware(OwinMiddleware next)
    : base(next)
    {

    }

    public async override Task Invoke(IOwinContext context)
    {
        string message = "<h1>Hello le monde!</h1>";
        context.Response.ContentType = "text/html";
        context.Response.ContentLength = message.Length;
        await context.Response.WriteAsync(message);

        await Next.Invoke(context);
    }
}

On complète Startup par passage du type (on ne peut pas passer d’instances):

app.Use<HelloMiddleware>();

Ou bien:

app.Use(typeof(HelloMiddleware));

Je vous laisse vérifier que l’affichage est bien conforme à ce qui était attendu.

Il n’est pas même besoin d’utiliser la classe OwinMiddleware, il suffit de fournir une classe avec la bonne signature:


public class MyMiddleware
{
    public MyMiddleware(AppFunc next)
    {

    }

    public async Task Invoke(IDictionary<string, object> env)
    {

    }
}

 

Et donc concrètement:

 

using AppFunc = Func<IDictionary<string, object>, Task>;

public class HelloMiddleware
{
     private AppFunc next;

     public HelloMiddleware(AppFunc next)
     {
         this.next = next;
     }

     public async Task Invoke(IDictionary<string, object> env)
     {
         string message = "<h1>Hello le monde!</h1>";
         Byte[] bytes = Encoding.UTF8.GetBytes(message);

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

         Stream response = (Stream)env["owin.<span style="font: 13px/normal 'Courier New'; color: #000000; text-transform: none; text-indent: 0px; letter-spacing: normal; word-spacing: 0px; float: none; display: inline !important; white-space: normal; -webkit-text-stroke-width: 0px;">ResponseBody</span>"];
         await response.WriteAsync(bytes, 0, bytes.Length);

         await this.next(env);
    }
}

Il n’y a plus qu’à tester.

Note: lors de mes premiers tests j’ai remplacé accidentellement ResponseBody par RequestBody. Cela ne marche pas, mais ne provoque pas d’exceptions. Donc faites attention.

On voit sur cet exemple l’intérêt de créer un middleware à partir de OwinMiddleware qui offre un certain nombre de services pour simplifier le développement. Mais au prix d’un inconvénient: votre composant est très étroitement lié à Katana, il ne sera pas facile de l’étendre à d’autres implémentations d’Owin.

A voir en fonction de vos objectifs, OWIN ne spécifie qu’une chose: Func<IDictionary<string,object>, Task>, tout le reste n’existe pas.

 Une instance

On peut aussi travailler directement avec une instance. Le problème est qu’au moment où on l’instancie on ne connait pas forcément la valeur de AppFunc next comme dans le cas précédent. Il est possible de différer la fourniture de ce paramètre au moment où le middleware sera effectivement chaîné dans le pipeline par Startup en créant une méthode Initialize avec la signature ci-dessous. Celle-ci sera automatiquement appelée par IAppBuilder.

 


public class MonMiddleware
{
    private AppFunc next;

    public void Initialize(AppFunc next)
    {
        this.next = next;
    }

    public async Task Invoke(IDictionary<string, object> env)
    {
        string message = "<h1>Hello world</h1>";
        Byte[] bytes = Encoding.UTF8.GetBytes(message);

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

       Stream response = (Stream) env["owin.ResponseBody"];
       await response.WriteAsync(bytes, 0, bytes.Length);

       await next(env);
    }
}

Le code n’est pas différent de précédemment si ce n’est que l’on créée l’instance d’un objet:


var middleware = new MonMiddleware();
app.Use(middleware);

 

Donc voilà, nous avons vu plusieurs techniques possibles pour créer des middleware.

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