I. Qu'est-ce que Katana ?▲
Si vous avez manqué mon précédent billet, il est disponible ici.Ce billet traitait exclusivement d'OWIN, une spécification sur un nivesau d'abstraction représentant le dialogue entre un serveur et une application. Katana est l'implémentation d'OWIN qui est proposée par Microsoft.
Katana a une approche plus avancée qu'OWIN et propose une architecture sur quatre couches, de la plus haute vers la plus basse :
- Application ;
- Middleware ;
- Server ;
- Host.
Détaillons à présent ces différentes couches.
I-A. Host▲
C'est la couche qui a en charge la construction du tunnel OWIN et la création d'un serveur (la couche supérieure donc). Techniquement parlant, c'est le processus qui est chargé d'accueillir et d'héberger le serveur. On peut distinguer plusieurs types d'hôtes avec Katana :
- IIS, c'est le mode de fonctionnement historique que vous connaissez déjà et qui se base sur le pipeline HTTP standard (celui de System.Web). Il est disponible au travers du paquet Nuget ;
- Self Host, c'est un mode assez similaire au Self Host que vous pouviez déjà utiliser avec ASP.NET Web API (et dont je parlais dans le tutoriel intitulé : Héberger des WEB API hors d'un site ASP.NET MVC). Libre à vous donc d'utiliser une application console ou un service Windows ;
- Owin Host, c'est une variante du Self Host dans laquelle vous n'avez pas à vous soucier de créer une application console ou un service Windows, votre application sera exécutée au sein du processus OwinHost.exe. Ce dernier est disponible dans le répertoire d'installation de Katana. Il est très souple dans ses paramètres (serveur à utiliser, URL, port, etc.) et son lancement est facilement scriptable ou configurable depuis Visual Studio.
I-B. Server▲
Le serveur a pour but d'ouvrir et de maintenir la connexion réseau (le socket). Il va attendre l'arrivée de nouveaux messages, les requêtes des clients, puis les relayer vers les couches supérieures au travers du tunnel de traitement OWIN.
Katana possède deux types de serveur :
-
Microsoft.Owin.Host.SystemWeb. Dans ce mode, le serveur s'appuie sur le tunnel de traitement historique d'ASP.NET, le pipeline HTTP utilisé par System.Web. Il s'attache au cycle de traitement des requêtes via un HTTP Handler (ou module) et relaye ensuite le message au tunnel de traitement OWIN. Attention, si l'hôte utilisé est IIS, alors l'implémentation de serveur sera automatiquement Microsoft.Owin.Host.SystemWeb et ce choix n'est pas altérable. Les deux vont de pair, c'est le fonctionnement historique et le plus proche de ce que nous autres développeurs avions l'habitude d'utiliser jusqu'à maintenant. Il est néanmoins possible d'utiliser Microsoft.Owin.Host.SystemWeb avec les deux autres types d'hôtes ;
- Microsoft.Owin.Host.HttpListener. Dans ce mode, c'est la classe HttpListener (présente dans System.Net, il n'y a donc pas de référence vers System.Web) qui est utilisée pour l'écoute réseau. Dès lors qu'une requête arrive, elle est relayée au tunnel de traitement OWIN. C'est le mode utilisé par défaut si l'hôte choisi est de type Self Host ou Owin Host.
I-C. Middleware▲
Un middleware est une fonction, une tâche qui a pour but de traiter une requête et éventuellement d'altérer l'état de la réponse. Typiquement, là où vous avez l'habitude d'utiliser un module HTTP pour gérer l'authentification dans une application ASP.NET, c'est une tâche qui est réalisable au travers d'un middleware. De la même manière, les traitements réalisés par le gestionnaire HTTP MvcHandler (sélection du contrôleur, invocation de l'action et génération de la réponse) sont également des traitements qui peuvent être portés sous la forme d'un middleware.
Lorsque l'on parle du tunnel de traitement OWIN, on parle en fait d'un tunnel composé d'un nombre n de middlewares. Ces derniers étant enregistrés au lancement de l'application. En quelque sorte, les middlewares sont assez semblables à ce que l'on connaissait jusqu'à maintenant avec les modules et gestionnaires HTTP du pipeline HTTP. La différence principale étant que le pipeline HTTP est fortement couplé à IIS alors que le tunnel de traitement OWIN est théoriquement compatible avec tous les serveurs qui possèdent une implémentation d'OWIN.
Dans un précédent billet, je parlais du délégué applicatif d'OWIN, une fonction prenant en paramètre le dictionnaire applicatif et pouvant effectuer un traitement. Avec Katana, un middleware est donc tout simplement l'implémentation de ce délégué applicatif.
- Un middleware, le délégué applicatif donc, peut prendre différentes formes parmi lesquelles, de la plus simple à la plus complexe.
- Générer automatiquement une valeur de retour en écrivant systématiquement dans le flux de la réponse HTTP.
- Analyser les requêtes entrantes et ajouter des informations dans un fichier/une base de données de journalisation.
- Être la passerelle vers un Framework de développement complet et évolué, tel que Web API ou SignalR par exemple.
I-D. Application▲
La dernière couche est tout simplement l'application en elle-même. On considère que les Frameworks, qu'il s'agisse de Web API, SignalR ou encore ASP.NET MVC, font partie de cette couche. Le développement d'un logiciel qui utilise l'un de ces Frameworks doit se faire de manière transparente par rapport à la présence de Katana et l'implémentation d'OWIN. Seuls les middlewares correspondant aux Frameworks référencés doivent être spécifiques à OWIN/Katana.
II. Katana en pratique▲
Dans la seconde partie de ce billet, je vous propose de mettre en pratique les différents éléments théoriques propres à Katana que nous venons de découvrir. Le but sera de mettre en place une Web API from scratch et totalement indépendante de IIS. Nous verrons également comment ajouter des Middlewares supplémentaires, afin par exemple d'implémenter une gestion de la journalisation.
Commençons par créer un nouveau projet de type application console. À ce nouveau projet, nous allons ajouter plusieurs références vers les paquets Nuget suivants et leurs assemblies respectives :
- OWIN ; elle expose simplement une interface comprenant le dictionnaire d'environnement et le délégué application dont j'ai pu longuement parler dans le précédent billet ;
- Microsoft.Owin ; elle contient l'implémentation de l'interface citée précédemment ainsi que tout un tas de classes utilitaires et classes encapsulant des éléments du dictionnaire d'environnement (requête, réponse, en-têtes, etc.). On y trouve aussi l'implémentation du tunnel de traitement OWIN, et donc la gestion de middlewares et leur enchaînement dans le traitement d'une requête ;
- Microsoft.Owin.Hosting ; elle contient toutes les abstractions nécessaires pour la construction du tunnel et le démarrage d'un serveur. Elle ne contient néanmoins pas de spécificité liée à un type de serveur plutôt qu'un autre. Le lanceur d'hôte utilisera les options qu'il reçoit en paramètre pour exécuter le code du serveur spécifié (le tout est basé sur un respect de conventions de nommage de classes et de méthodes). Par défaut, si rien n'est spécifié, le type de serveur utilisé et un HttpListener ;
- Microsoft.Owin.Host.HttpListener ; elle contient l'implémentation d'un serveur HTTP basique. Elle est utilisable au travers d'un hôte créé à partir du modèle exposé par Microsoft.Owin.Hosting et notamment l'implémentation d'un OwinServerFactory. En pratique, les assemblies Microsoft.Owin.Hosting et Microsoft.Owin.Host.HttpListener n'ont aucune connaissance l'une de l'autre et ont donc un couplage très faible. Il est donc possible d'utiliser l'assembly Microsoft.Owin.Host.HttpListener dans un contexte tout autre n'ayant rien à voir avec Owin.
L'ajout des deux derniers paquets uniquement suffira. En effet, le paquet Microsoft.Owin.Hosting contient des dépendances vers OWIN et vers Microsoft.Owin.
Il existe plusieurs moyens de créer un lanceur OWIN dans une application : déclaration d'un attribut, convention de code, ou encore paramètres passés à l'hôte. Puisque l'on va hoster nous-mêmes le serveur dans une application console, c'est cette dernière méthode que nous allons utiliser dans l'exemple qui suit. Cependant, même dans ce cas précis, il est tout de même nécessaire d'appliquer quelques conventions dans le code pour que celui-ci puisse être découvert et exécutable.
Généralement, les classes de démarrage OWIN sont donc baptisées Startup. Dans notre exemple c'est facultatif, mais si nous avions décidé d'opter pour un autre hôte, celui basé sur le processus OwinHost.exe par exemple, cela aurait été une solution envisageable pour rendre le démarrage du serveur possible.
Cette classe doit contenir une méthode appelée Configuration, et cette méthode doit prendre pour seul paramètre une implémentation de l'interface b. Souvenez-vous, cette interface a été abordée un peu plus tôt, elle est définie dans l'assembly OWIN et expose le dictionnaire d'environnement et le délégué applicatif.
Attention ! Si votre classe de démarrage ne contient pas de méthode Configuration ou si cette méthode ne contient pas exactement un paramètre de type IAppBuilder, elle ne pourra pas être utilisée et le serveur ne pourra pas démarrer. Une exception vous indiquant la nature du problème sera tout de même levée.
public
class
Startup
{
public
void
Configuration
(
IAppBuilder appBuilder)
{
}
}
Manipuler directement l'interface IappBuilder est un peu complexe, car très abstrait. En pratique, nous allons généralement utiliser les différentes méthodes d'extensions proposées au-dessus de cette interface et exposées par l'assembly Microsoft.Owin.
Commençons par la méthode Run. Elle permet d'enregistrer un middleware sans que celui-ci fasse partie d'une chaîne d'exécution. Cela signifie qu'il ne peut y avoir qu'un seul middleware enregistré de cette manière. Si plus d'un middleware est enregistré, seul le premier pourra interagir avec la requête entrante, la chaîne d'exécution sera brisée et les autres middlewares ne seront pas exécutés.
La méthode prend en paramètre un délégué de type Func<IOwinContext, Task>. L'interface IowinContext est tout simplement une surcouche au dictionnaire d'environnement qui nous permet d'accéder facilement à la requête et à la réponse et de les manipuler.
L'exemple suivant est donc une implémentation d'un middleware, le fameux délégué applicatif, qui va simplement écrire dans la réponse salut ! pour toutes les requêtes entrantes.
public
class
Startup
{
public
void
Configuration
(
IAppBuilder appBuilder)
{
appBuilder.
Run
(
owinContext =>
owinContext.
Response.
WriteAsync
(
"salut !"
));
}
}
Il nous reste encore à rendre note hôte fonctionnel pour pouvoir tester notre application. Dans le point d'entrée de l'application, nous allons utiliser simplement la méthode Startup de la classe WebApp. Celle-ci appartient à l'assembly Microsoft.Howin.Hosting. Nous lui passons en paramètre le point d'écoute de notre hôte. C'est à ce stade que le HttpListener sera déduit comme choix de serveur.
class
Program
{
static
void
Main
(
string
[]
args)
{
using
(
WebApp.
Start<
Startup>(
"http://localhost:2627"
))
{
Console.
ReadLine
(
);
}
}
}
Vous pouvez maintenant exécuter l'application et envoyer une requête sur l'URL déclarée dans le point d'entrée.
L'écriture d'un middleware respectant le tunnel d'exécution d'OWIN n'est que très peu différente de celle que nous venons de voir. Plutôt que d'utiliser la méthode d'extension Run, nous utiliserons la méthode d'extension Use. Le délégué attendu par celle-ci a la particularité de prendre un paramètre supplémentaire, à savoir, un délégué retournant une tâche. Vous l'aurez compris, ce délégué retournant une tâche représente simplement l'enchaînement avec le prochain middleware du tunnel.
Exécutez l'exemple suivant pour bien visualiser l'enchaînement de traitement des différents middlewares enregistrés.
public
class
Startup
{
public
void
Configuration
(
IAppBuilder appBuilder)
{
appBuilder.
Use
((
owinContext,
next) =>
{
owinContext.
Response.
WriteAsync
(
"Une premier message"
);
return
next
(
).
ContinueWith
(
task =>
owinContext.
Response.
WriteAsync
(
"Où suis-je ?"
));
}
);
appBuilder.
Run
(
owinContext =>
owinContext.
Response.
WriteAsync
(
"Un second message !"
));
}
}
En pratique, écrire un middlware inline de cette manière se révèle trop fastidieux et trop peu réutilisable. Les concepteurs de Katana nous offrent donc la possibilité d'écrire nos middlewares dans des classes distribuables sous la forme de composants qui sont ensuite enfichables facilement dans le tunnel d'exécution d'OWIN.
Encore une fois, il existe deux possibilités pour écrire un composant middleware :
- l'approche la moins couplée, qui va donc fonctionner par convention ;
- l'approche la plus couplée, où le composant doit hériter d'une classe provenant de l'assembly Microsoft.Owin. C'est cette seconde approche que l'on rencontre le plus souvent.
L'inconvénient de la première méthode est également son avantage ; en réduisant l'adhérence avec Microsoft.Owin, le composant écrit ne peut pas s'appuyer sur les surcouches que cette assembly apporte. Ainsi, un composant s'écrirait de la manière suivante. Notez les deux points importants : un constructeur qui reçoit un délégué applicatif en paramètre et une méthode Invoke qui reçoit un dictionnaire d'environnement en paramètre et qui reçoit une tâche (notez que l'on retrouve là le même comportement que le délégué passé à la méthode Use présentée plus tôt).
public
class
LogMiddleware
{
private
readonly
Func<
IDictionary<
string
,
object
>,
Task>
_next;
public
LogMiddleware
(
Func<
IDictionary<
string
,
object
>,
Task>
next)
{
_next =
next;
}
public
Task Invoke
(
IDictionary<
string
,
object
>
context)
{
var
remoteIpAddress =
context[
"server.RemoteIpAddress"
];
var
requestMethod =
context[
"owin.RequestMethod"
];
var
requestPath =
context[
"owin.RequestPath"
];
Console.
WriteLine
(
"{0}
\t
{1}
\t
{2}"
,
remoteIpAddress,
requestMethod,
requestPath);
return
_next
(
context);
}
}
Le composant ci-dessus a pour but d'historiser les requêtes arrivant jusqu'à notre host. On y lit l'adresse IP du client, le verbe et le chemin de la requête.
Ce même composant pourrait s'écrire de la manière suivante. Notez la simplicité d'écriture accrue apportée par l'interface IOwinContext.
public
class
LogMiddleware :
OwinMiddleware
{
public
LogMiddleware
(
OwinMiddleware next)
:
base
(
next)
{
}
public
override
Task Invoke
(
IOwinContext context)
{
var
remoteIpAddress =
context.
Request.
RemoteIpAddress;
var
requestMethod =
context.
Request.
Method;
var
requestPath =
context.
Request.
Path;
Console.
WriteLine
(
"{0}
\t
{1}
\t
{2}"
,
remoteIpAddress,
requestMethod,
requestPath);
return
Next.
Invoke
(
context);
}
}
Dans les deux cas, l'enregistrement du Middleware se fait de la manière suivante
public
class
Startup
{
public
void
Configuration
(
IAppBuilder appBuilder)
{
appBuilder.
Use<
LogMiddleware>(
);
}
}
Exécutez l'application, faites une requête sur l'URL déclarée plus tôt et observez la trace qui apparaît dans la console.
Nous allons maintenant utiliser un middleware plus complexe et qui rentre dans la couche Application décrite en préambule de ce billet. Il va nous permettre de créer une API REST facilement.
Ajoutez simplement une référence vers le paquet NuGet Microsoft.AspNet.WebApi.Owin. Celui-ci va tirer les DLL nécessaires au fonctionnement cœur de Web API et à l'implémentation de son middleware OWIN (System.Web.Http.Owin). Je vous invite d'ailleurs à explorer cette dernière, qui, bien que légère, représente une parfait exemple d'à quel point le couple OWIN/Katana est générique et extensible.
L'assembly System.Web.Http.Owin apporte une nouvelle méthode d'extension sur l'interface IAppBuilder, appelée UseWebApi. L'exemple de code ci-dessous enregistre le middleware de Web API et configure ce dernier en activant notamment la découverte du routage par attribut.
public
class
Startup
{
public
void
Configuration
(
IAppBuilder appBuilder)
{
appBuilder.
Use<
LogMiddleware>(
);
var
httpConfiguration =
new
HttpConfiguration
(
);
httpConfiguration.
MapHttpAttributeRoutes
(
);
appBuilder.
UseWebApi
(
httpConfiguration);
}
}
En complément, ajoutez le contrôleur Web API suivant, avant de tester votre application en exécutant une requête GET sur le chemin /api/v1/companies/search/g par exemple.
public
class
Company
{
public
int
Id {
get
;
set
;
}
public
string
Name {
get
;
set
;
}
}
[RoutePrefix(
"api/v1/companies"
)]
public
class
CompanyController :
ApiController
{
[Route(
"search/{term}"
), HttpGet]
public
IEnumerable<
Company>
Search
(
string
term)
{
var
companies =
new
Company[]
{
new
Company
(
) {
Id =
1
,
Name =
"Microsoft"
},
new
Company
(
) {
Id =
2
,
Name =
"Burger King"
},
new
Company
(
) {
Id =
3
,
Name =
"Google"
},
};
return
companies.
Where
(
c =>
c.
Name.
ToLower
(
).
Contains
(
term.
ToLower
(
)));
}
}
Voilà ! Nous avons développé une application utilisant un tunnel de traitement OWIN qui contient deux middlewares : un de notre cru pour la journalisation des requêtes et un qui fait le pont avec Web API. Notre livrable est finalement découplé de IIS et de System.Web. Aussi, nous avons le choix de l'hoster facilement dans une application console, dans l'hôte standard OWIN ou même de revenir vers IIS.
III. Conclusion▲
Pour terminer, si vous développez actuellement sur un projet MVC 5, vous aurez certainement remarqué la présence d'un fichier Startup.cs à la racine de votre projet. En fait, le template de projet MVC 5 utilise déjà Katana pour définir les différentes options pour la gestion de l'identité des utilisateurs de votre application. Avec ASP.NET vNext, vous verrez que le bootstrapper de l'application est entièrement basé sur une fonction de démarrage similaire à Katana.
À bientôt.
IV. Remerciements▲
L'équipe de la rédaction .NET remercie sincèrement Soat de nous avoir autorisés à publier cet article sur Developpez.com. Soat est une société de conseil spécialisée dans l'accompagnement de ses clients, tout au long du cycle de vie de leurs projets et le développement de technologies Java et Web.
Nous remercions également Malick SECK pour la mise au gabarit et Claude LELOUP pour sa relecture orthographique.
N'hésitez pas donner vos points de vue sur cet article : Commentez