| Étendre ASP.NET avec du code managé par Frédéric De Lène Mirouze (amethyste16@hotmail.com) | ||
1
Introduction
2
Ecriture d'un handler HTTP
2.1
Créer
un handler synchrone
2.2
Créer un handler asynchrone
2.2.1
Introduction sur le bon usage du
multi-threading
2.2.2
Implémentation du handler
2.3
Créer une fabrique de handlers
2.4
Utilisation des handlers natifs
2.5
Activer l'état de session
3
Ecriture d'un module HTTP
3.1
Implémentation d'un module
3.2
Le pipe de modules
4
Handler ou module?
5
les fichiers de configuration
5.1
la section httpHandlers
5.2
La section httpModules
5.3
Un petit bug
6
Les fichiers ASHX
7
Bibliographie
7.1
Tutoriels
7.2
Idées d'utilisation
IIS est un service Windows spécialisé dans le traitement des requêtes HTTP. Lorsqu'une requête est interceptée, IIS examine l'extension du fichier de requête à la recherche d'une valeur connue. Certains fichiers peuvent être traités nativement, html, image..., d'autres le sont par une application spécialisée appelée extension ISAPI (Internet Service Application Programming Interface). C'est par exemple le cas des fichiers aspx. Considérons par exemple l'URL suivante:
http://www.amethyste.com/login.aspx
L'URL se termine par l'extension .aspx. Cette extension est caractéristique des applications ASP .NET. IIS communique à .NET cette URL via l'extension ISAPI non managée aspnet_isapi.dll et le filtre aspnet_filter.dll. L'extension agit comme routeur d'URL tandis que le filtre prend en charge les sessions sans cookies d'ASP .NET.
Pour savoir quelles extensions sont prises en charge par un site, il suffit de lancer l'administrateur du service IIS, ouvrir les propriétés du site et d'afficher les configurations de répertoire.
Si l'on dispose d'un filtre ISAPI adéquat, on peut donc très facilement configurer IIS pour rediriger les requêtes correspondantes à une certaine extension vers ce filtre. Jusqu'à présent l'écriture de filtres ISAPI ne pouvait se faire qu'en C ou C++ et a toujours été une grosse affaire hors de portée du programmeur lambda. Avec .NET les choses changent, l'écriture d'un filtre ou d'une extension personnalisés devient pratiquement un travail de routine.
Deux outils sont à notre disposition:
Un handler HTTP est une classe capable de traiter un fichier avec une certaine extension. On retrouve quelque chose de très comparable à une extension ISAPI. Les modules HTTP sont similaires aux handlers HTTP. La principale différence est qu'ils ne sont pas spécialement dédiés à une type particulier de fichier. Les modules HTTP sont tout à fait similaires aux filtres ISAPI. Une autre différence est que l'on peut définir autant de module que l'on souhaite, par contre il n'y a qu'un seul handler actif à la fois.
Le schéma suivant résume la séquence de couches applicatives entre IIS et ASP.NET.
L'objet de ce document est de montrer de quelle façon implémenter ces classes et les configurer. Nous examinerons également l'écriture du très peu documenté fichier ASHX qui permettent de manipuler les handlers HTTP à la façon d'un composant Web.
Note:
L'article a été rédigé avec .NET 1.1 et Visual Studio 2003. Nous donnons des exemples de code en C# et VB.NET. Nous avons testé nos exemples sur Windows XP avec IIS 6
Il existe trois types de handler HTTP, chacun associé à une interface particulière:
| handler | Implémente |
| Handler synchrone | IHttpHandler |
| Handler asynchrone | IAsyncHttpHandler |
| Fabrique de handler | IHttpHandlerFactory |
IHttpHandler implémente les handlers synchrones, c'est à dire les handlers qui ne retournent au client qu'une fois la requête entièrement traitée.
System.Web.UI.Page est un exemple d'implémentation natif à ASP.NET. Tous les handlers implémentent au minimum cette interface.
IHttpAsyncHandler implémente les handlers asynchrones. Par exemple System.Web.HttpApplication.
Ces handlers servent au traitement de requêtes longues dans lesquelles les informations sont renvoyées au client à différentes étapes du traitement.
IHttpHandlerFactory est l'interface qu'implémentent les fabriques de handler. Il s'agit de handler dont le type est créé dynamiquement. Par exemple on peut avoir un handler différent selon que le verbe est GET ou POST.
L'exemple le plus habituel est les fichiers aspx qui peuvent correspondre à autant de handlers Page qu'il y a de WebForm dans le site.
| Notre exemple:
Offrir une fonctionnalité de téléchargement sur un site Web |
Une solution simple consiste à placer une URL pointant
directement sur le fichier dans son répertoire.
Cette approche pose divers problèmes.
Par ailleurs le caractère statique de ce lien rend
difficile toute réorganisation du site. Un autre problème survient si la ressource à télécharger
n'existe pas physiquement, mais doit être obtenue dans une base de donnée ou
créée dynamiquement: image miniaturisée, ajout d'un filigrane de copyright...
D'autres contraintes sont également difficilement compatibles avec la mise en place d'un lien direct :
Toutes ces opérations nécessitent du code, c'est
justement pour ces situations où l'implémentation d'un handler HTTP se
justifie.
|
public interface
IHttpHandler { bool IsReusable {get;} void ProcessRequest(HttpContext context); } |
[VB]
|
Public Interface
IHttpHandler Sub ProcessRequest(ByVal context As HttpContext) End Interface |
Elle expose les deux membres suivants:
| IsReusable |
Obtient si une autre requête Http peut utiliser la même instance du handler. L'instance sera alors mise en
cache. Cette propriété peut par exemple être mise à false si le traitement personnalisé n'est pas thread-safe ou bien si l'on ne souhaite pas mettre en place un pool. On met a true également pour signifier que le handler supporte un mode asynchrone et que l'on implémente un IAsyncHttpHandler. |
| ProcessRequest | Méthode dans laquelle a lieu le traitement personnalisé |
Nous allons donc écrire un Http handler qui s'active pour
les URL au format suivant:
.../DownloadFile.down?name=<nom
fichier>
Première étape, commençons par créer un projet ASP depuis Visual Studio. On y ajoute la classe suivante:
[C#]
|
#region using using System; using System.IO; using System.Web; #endregion namespace OutilsDownload { public class DownloadFile: IHttpHandler { // évidemment dans une appli réelle on ne codera pas cela en dur private string RepertoireDownload = @"c:\temp\"; #region ProcessRequest public void ProcessRequest(HttpContext context) { // obtient le nom du fichier à télécharger string Fichier = context.Request["name"]; { context.Response.Write("Le fichier demandé est introuvable"); return; } // on sait maintenant que le fichier existe FileStream fs = null; try { // charge le fichier dans un buffer fs = new FileStream(RepertoireDownload + Fichier,FileMode.Open); byte[] buffer = new byte[fs.Length]; int OctetsLus = fs.Read(buffer,0,buffer.Length); // Selon les RFC 2616 et RFC 1806 on doit préciser content-type et attachement switch (Path.GetExtension(RepertoireDownload + Fichier)) { case ".doc": context.Response.Write("Vous n'êtes pas autorisé à télécharger les fichiers word"); return; case ".pdf": context.Response.AddHeader("content-type","application/pdf"); break; default: context.Response.AddHeader("content-type","application/octet-stream"); break; } context.Response.AppendHeader("Content-Disposition", "attachment;filename=" + Fichier); // place le buffer dans l'entête Http context.Response.OutputStream.Write(buffer,0,OctetsLus); context.Response.End(); } finally { // ne pas oublier if (fs != null) { fs.Close(); } } } #endregion #region IsReusable public bool IsReusable { get { return false; } } #endregion } } |
|
Imports System.Web Imports System.IO Public Class DownloadFile Implements IHttpHandler ' évidemment dans une appli réelle on ne codera pas cela en dur Private RepertoireDownload As String = "C:\temp\" #Region "ProcessRequest" Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest ' obtient le nom du fichier à télécharger Dim Fichier As String = context.Request.Item("name") If (Fichier = Nothing Or Not File.Exists(RepertoireDownload + Fichier)) Then context.Response.Write("Le fichier demandé est introuvable") Return End If ' on sait maintenant que le fichier existe Dim fs As FileStream = Nothing Try ' charge le fichier dans un buffer fs = New FileStream(RepertoireDownload + Fichier, FileMode.Open) Dim buffer(fs.Length) As Byte Dim OctetsLus As Int32 = fs.Read(buffer, 0, buffer.Length) ' Selon les RFC 2616 et RFC 1806 on doit préciser content-type et attachement Select Case (Path.GetExtension(RepertoireDownload + Fichier)) Case ".doc" context.Response.Write("Vous n'êtes pas autorisé à télécharger les fichiers word") Return Case ".pdf" context.Response.AddHeader("content-type", "application/pdf") Case Else context.Response.AddHeader("content-type", "application/octet-stream") End Select context.Response.AppendHeader("Content-Disposition", "attachment;filename=" + Fichier) ' place le buffer dans l'entête Http context.Response.OutputStream.Write(buffer, 0, OctetsLus) context.Response.End() Finally ' ne pas oublier If Not (fs Is Nothing) Then fs.Close() End If End Try End Sub #End Region #Region "IsReusable" Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable Get Return False End Get End Property #End Region End Class |
Le fonctionnement de cette classe est évident :
On récupère dans un premier temps le nom du fichier à télécharger. Le nom se trouve dans l'attribut name de la QueryString. On teste ensuite l'existence du fichier. Si le test réussi on génère l'entête HTTP adéquat. Ensuite on lit le fichier dans un buffer, le contenu du buffer est alors copié dans l'entête de la réponse HTTP. On termine en émettant la réponse.
Notez plusieurs choses:
J'ai également codé en dur le répertoire de téléchargement. Cette pratique est évidemment proscrite dans une application réelle...
Avant de pouvoir tester notre exemple deux tâches supplémentaires nous attendent :
Commençons par IIS.

· On sélectionne Configuration pour ouvrir la fenêtre suivante:

· On sélectionne Ajouter afin d'ouvrir:

L'exécutable est le chemin complet vers aspnet_isapi.dll. Le plus simple est de faire un copier/coller depuis une extension existante. Sinon l'adresse officielle est:
<lecteur>:\%windows%\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll
Pour le framework 1.1 bien sûr. Dans la case extension on entre .down
Note:
L'URL ne contient pas le vrai nom du fichier, par conséquent on décoche l'option "Vérifier l'existence du fichier".
On complète ensuite le fichier de configuration web.config en ajoutant:
|
<httpHandlers> <add verb="*" path="DownloadFile.down" type="OutilsDownload.DownloadFile,WebApplication2" />
</httpHandlers> |
On donnera des détails sur ce fichier plus loin, mais notez déjà la syntaxe particulière de l'attribut type.
|
Note: J'ai vu sur de nombreux tutoriels des syntaxes simplifiées genre le nom de l'assemblage disparaît. J'ignore peut-être quelque chose, mais chez moi elles ne marchent pas. |
Notez également un point. L'attribut path aurai pu être simplement *.down. En le typant plus on s'ouvre la possibilité de créer plusieurs Http handler avec un comportement différent selon ce que l'on trouve dans l'URL. Pour l'instant nous n'avons que DownloadFile. Les fichiers de configuration font l'objet d'un chapitre entier, nous n'en dirons donc pas plus ici.
Il ne reste plus qu'à ajouter des fichiers dans le répertoire de téléchargement (c:\temp dans notre exemple) et de tester des URL similaires à celles-ci:
http://localhost/WebApplication2/DownloadFile.down?name=f2.txt
http://localhost/WebApplication2/DownloadFile.down?name=f1.doc
|
Note: Evidemment votre site peut porter un nom différent de WebApplication2 ! |
Dans le premier cas on obtient la fenêtre bien connue:

Dans le deuxième cas on obtient:

|
Note: Lorsque vous ferez un essai vous devriez obtenir une exception de sécurité. Le compte où tourne ASP doit disposer de droits en écriture sur le répertoire de téléchargement, ou bien sur les fichiers. Les détails dépendent de la version de IIS et de la configuration de votre site. Le mieux est de se référer aux instructions qui s'affichent sur la page d'erreur. |
Les principes de base sont tous dans cet exemple. Vous ne devriez pas rencontrer de difficultés pour créer vos propres extensions.
Les handlers asynchrones interviennent typiquement pour améliorer la montée en charge des applications. Une erreur fréquente est de penser: si c'est lent, alors multi-threading. Dans le cas où le facteur bloquant est la CPU, il est peut probable que les choses s'améliorent et la solution est plutôt dans le redimensionnement du matériel. Dans les applications ASP il est fréquent que le bouchon soit plutôt les accès IO. Par exemple l'accès à une base de donnée. Dans ce contexte le multi-threading peut parfaitement se justifier. J'appellerai ce scénario: une situation de blocage IO.
Cette situation se diagnostique typiquement lorsqu'une méthode synchrone est très lente, mais n'utilise que très peu la CPU à un instant donné. Les IHttpHandler sont peu efficaces dans ce genre de scénario. Voici pourquoi .NET instancie par défaut un pool de threads par Process. C'est dans ce pool qu'il puise pour obtenir un nouveau thread. Il est possible d'y accéder via la classe ThreadPool.
Imaginons qu'ASP serve une requête en associant son traitement au thread Thread1. Ce traitement nous place dans une situation de blocage IO.Windows place alors le thread dans un état d'attente jusqu'à ce que la requête IO soit satisfaite. Cet état d'attente permet au traitement re récupérer de façon particulièrement efficace son thread, en revanche le thread ne peut être recyclé car il n'est pas replacé dans le pool.
Un autre client se connecte. Thread1 est toujours dans un état d'attente, par conséquent la CLR lui affecte Thread2 dans le pool. Ce scénario peut se reproduire pour n'importe quel client. Dans le cas ou le serveur ASP doit faire face à une forte montée en charge, Thread3, Thread4... seront ainsi créés jusqu'à la limite par défaut de 25. Le pool sera alors saturé, bien qu'en moyenne les threads sont inactifs !
Il est clair que la montée en charge du site sera fortement compromise. C'est tout le problème des IHttpHandler.
Avec les handlers asynchrones nous pouvons améliorer cette situation. En moyenne l'accès aux threads sera moins efficace, par contre ils assureront globalement une meilleure montée en charge en consommant moins de threads.
L'implémentation des handlers asynchrones n'est pas très différente de celle des handlers synchrones. Ceci étant l'architecture du code est plus technique car on doit se frotter aux joies de la réentrance et le modèle de programmation asynchrone. Tous les appels IO que l'on fait lorsque l'on construit la réponse, doivent être asynchrone et donc utiliser les méthodes Begin()/End(). Les handlers asynchrones implémentent IHttpAsyncHandler qui est défini ainsi:
[C#]|
public interface
IHttpAsyncHandler |
|
Public Interface
IHttpAsyncHandler Function BeginProcessRequest(ByVal context As HttpContext, ByVal cb As AsyncCallback, ByVal extraData As Object) As IAsyncResult Sub EndProcessRequest(ByVal result As IAsyncResult) End Interface |
L'interface expose les deux méthodes suivantes:
| BeginProcessRequest | Lance un appel asynchrone au gestionnaire HTTP. |
| EndProcessRequest | Fournit une méthode End de processus asynchrone lorsque le processus se termine. |
IHttpAsyncHandler implémente aussi IHttpHandler.
Ces méthodes sont les versions Begin()/End() de la méthode ProcessRequest() des IHttpHandler. Attention, elles ne nous dispensent pas d'utiliser les version Begin()/End() des méthodes utilisées pour construire la réponse. Il faut les deux.
Voici un résumé des principes d'implémentation du code d'un IAsyncHttpHandler:
Le mieux est d'examiner un exemple.
| Notre exemple:
Un handler asynchrone minimaliste. Vous trouverez l'exemple complet dans la bibliographie. |
[C#]
|
#region using using System; using System.Web; using System.Threading; #endregion namespace TestHandler { public class AsyncHttpHandler : IHttpAsyncHandler { #region BeginProcessRequest public IAsyncResult BeginProcessRequest(HttpContext context,AsyncCallback cb,Object extraData) { Async async = new Async(cb, extraData); // sauvegarde le contexte async.context = context; // ici le code que vous souhaitez appeler de façon asynchrone // ce code doit être thread-safe et s'il contient d'autres appels à // du code IO, celui-ci doit également être asynchrone Thread.Sleep(3000); // juste histoire d'avoir quelque chose à tester async.SetCompleted(); return async; } #endregion #region EndProcessRequest public void EndProcessRequest(IAsyncResult result) { Async async = result as Async; async.context.Response.Write("<H1>Voici une réponse <i>Asynchrone</i>!!</H1>"); } #endregion #region ProcessRequest // jamais appelé dans un contexte asynchrone public void ProcessRequest(HttpContext context) { } #endregion #region IsReusable // Indique que le handler supporte un mode asynchone public bool IsReusable { get { return true; } } #endregion } // cet objet est nécessaire pour gardre la trace des états entre les appels Begin et End. // dans un contexte ASP on y garde essentiellement le contexte class Async:IAsyncResult { #region Constructeur internal Async(AsyncCallback cb, Object extraData) { this.cb = cb; asyncState = extraData; isCompleted = false; } #endregion private AsyncCallback cb = null; // Contexte ASP internal HttpContext context=null; #region AsyncState private Object asyncState; // Obtient un objet défini par l'utilisateur qui qualifie ou qui contient des informations sur une opération asynchrone. public object AsyncState { get { return asyncState; } } #endregion #region CompletedSynchronously public bool CompletedSynchronously { get { return false; } } #endregion #region AsyncWaitHandle // ASP n'utilise jamais cette propriété, il n'y a donc rien à implémenter public WaitHandle AsyncWaitHandle { get { return null; } } #endregion #region IsCompleted private Boolean isCompleted; // passe à true si l'opération asynchrone est terminée public bool IsCompleted { get { return isCompleted; } } #endregion #region SetCompleted internal void SetCompleted() { isCompleted = true; if(cb != null) { cb(this); } } #endregion } } |
|
#Region
"using" Imports System Imports System.Web Imports System.Threading #End Region Public Class AsyncHttpHandler Implements IHttpAsyncHandler #Region "BeginProcessRequest" Public Function BeginProcessRequest(ByVal context As HttpContext, ByVal cb As AsyncCallback, ByVal extraData As Object) As IAsyncResult Implements IHttpAsyncHandler.BeginProcessRequest Dim async As Async = New Async(cb, extraData) ' sauvegarde le contexte async.context = context ' ici le code que vous souhaitez appeler de façon asynchrone ' ce code doit gérer le multi-threading et s'il contient d'autres appels à ' du code IO, celui-ci doit également être asynchrone Thread.Sleep(3000) ' juste histoire d'avoir quelque chose à tester async.SetCompleted() Return async End Function #End Region #Region "EndProcessRequest" Public Sub EndProcessRequest(ByVal result As IAsyncResult) Implements IHttpAsyncHandler.EndProcessRequest Dim async As Async = CType(result, Async) async.context.Response.Write("<H1>Voici une réponse <i>Asynchrone</i>!!</H1>") End Sub #End Region #Region "ProcessRequest" ' jamais appelé dans un contexte asynchrone Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest End Sub #End Region #Region "IsReusable" ' Indique que le handler supporte un mode asynchone Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable Get Return True End Get End Property #End Region End Class ' cet objet est nécessaire pour gardre la trace des états entre les appels Begin et End. ' dans un contexte ASP on y garde essentiellement le contexte Class Async Implements IAsyncResult #Region "Constructeur" Friend Sub New(ByVal cb As AsyncCallback, ByVal extraData As Object) _cb = cb _asyncState = extraData _isCompleted = False End Sub #End Region Private _cb As AsyncCallback = Nothing ' Contexte ASP Friend context As HttpContext = Nothing #Region "AsyncState" Private _asyncState As Object ' Obtient un objet défini par l'utilisateur qui qualifie ou qui contient des informations sur une opération asynchrone. Public ReadOnly Property AsyncState() As Object Implements IAsyncResult.AsyncState Get Return AsyncState End Get End Property #End Region #Region "CompletedSynchronously" Public ReadOnly Property CompletedSynchronously() As Boolean Implements IAsyncResult.CompletedSynchronously Get Return False End Get End Property #End Region #Region "AsyncWaitHandle" ' ASP n'utilise jamais cette propriété, il n'y a donc rien à implémenter Public ReadOnly Property AsyncWaitHandle() As WaitHandle Implements IAsyncResult.AsyncWaitHandle Get Return Nothing End Get End Property #End Region #Region "IsCompleted" Private _isCompleted As Boolean ' passe à true si l'opération asynchrone est terminée Public ReadOnly Property IsCompleted() As Boolean Implements IAsyncResult.IsCompleted Get Return _isCompleted End Get End Property #End Region #Region "SetCompleted" Friend Sub SetCompleted() _isCompleted = True If Not (_cb Is Nothing) Then _cb(Me) End If End Sub #End Region End Class |
Notez que IAsyncHttpHandler implémente aussi IHttpHandler, on doit donc fournir une implémentation de cette interface. On rend éventuellement un handler "bilingue", ce que ne fait pas l'exemple toutefois.
Concrètement le traitement se limite à attendre (Thread.Sleep). Bien sûr vous implémenterez quelque chose de plus productif dans vos applications !
Ce code doit absolument être thread-safe. Dans le cas où il effectue un appel IO, il doit utiliser les version Begin/End, plutôt que les versions synchrones. Normalement cela devrait être indiqué dans la documentation de la classe.
Le fichier de configuration se paramètre exactement comme pour un handler normal:
|
<httpHandlers> <add verb="*" path="*.*" type="TestHandler.AsyncHttpHandler,WebApplication2" />
</httpHandlers> |
Cet exemple utilise les threads du pool par défaut. On pourrait également envisager de créer un nouveau thread hors du pool, puis de le lancer. Mais tout cela sort du cadre de cet article.
Dans tous les cas, doit s'afficher:
Voici une réponse asynchrone!
Comme l'indique le nom, les fabriques de handler implémentent essentiellement le pattern factory. En d'autres termes on va pouvoir ajouter une indirection entre la requête et l'appel effectif d'un handler HTTP.
Les applications sont nombreuses, on en trouvera quelques unes dans la bibliographie. Mais citons par exemple:
Dans tous les cas on souhaite que ces modifications soient
transparentes pour les clients.
|
Notre exemple: L'exemple que nous allons développer est très différent car il n'y a qu'un seul handler HTTP, par contre nous démontrons une méthode pour mettre en cache les handlers générés. Cet exemple est tiré du livre de Fritz Onion "Essential ASPNet". |
Les fabriques de handler sont des classes qui implémentent
l'interface IHttpHandlerFactory.
|
public interface
IHttpHandlerFactory { IHttpHandler GetHandler(HttpContext context,string requestType,string url,string pathTranslated); void ReleaseHandler(IHttpHandler handler); } |
|
Public Interface
IHttpHandlerFactory Function GetHandler(ByVal context As HttpContext, ByVal requestType As String, ByVal url As String, ByVal pathTranslated As String) As IHttpHandler Sub ReleaseHandler(ByVal handler As IHttpHandler) End
Interface |
|
GetHandler |
Retourne une instance d'une classe qui implémente
l'interface IHttpHandler. Permet à une fabrique de réutiliser une instance de gestionnaire existante. |
|
ReleaseHandler |
C'est dans cette méthode que l'on mettrai la gestion d'un cache ou bien que l'on appellerai la méthode Dispose du handler, si il en expose un, afin de le détruire. |
Première étape, comme toujours, la fabrique de handler.
|
#region using using System; using System.Collections; using System.Web; #endregion namespace HandlerFactoryDemo { public class Distributeur: IHttpHandlerFactory { private Stack Pile = new Stack(); // pool de handler private int MaxPool = 10; // taille limite du pool private readonly object LockObject = new object(); #region ReleaseHandler public void ReleaseHandler(IHttpHandler handler) { if (handler.IsReusable) { lock (LockObject) { if (Pile.Count < MaxPool) Pile.Push(handler); } } else { // le handler est à usage unique IDisposable DisposableHandler = handler as IDisposable; if (DisposableHandler != null) { DisposableHandler.Dispose(); } } } #endregion #region GetHandler public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated) { IHttpHandler handler = null; lock(LockObject) { if (Pile.Count > 0) { handler = (IHttpHandler) Pile.Pop(); } } if (handler == null) { handler = new Handler1(); } return handler; } #endregion } } |
|
#Region
"imports" Imports System Imports System.Collections Imports System.Web Imports System.Threading #End Region Public Class Distributeur Implements IHttpHandlerFactory Private Pile As Stack = New Stack ' pool de handler Private MaxPool As Int32 = 10 ' taille limite du pool Private ReadOnly LockObject As Object = New Object #Region "ReleaseHandler" Public Sub ReleaseHandler(ByVal handler As IHttpHandler) Implements IHttpHandlerFactory.ReleaseHandler If (handler.IsReusable) Then Monitor.Enter(LockObject) Try If (Pile.Count < MaxPool) Then Pile.Push(handler) End If Finally Monitor.Exit(LockObject) End Try Else ' le handler est à usage unique Dim DisposableHandler As IDisposable = CType(handler, IDisposable) If Not (DisposableHandler Is Nothing) Then DisposableHandler.Dispose() End If End If End Sub #End Region #Region "GetHandler" Public Function GetHandler(ByVal context As HttpContext, ByVal requestType As String, ByVal url As String, ByVal pathTranslated As String) As IHttpHandler Implements IHttpHandlerFactory.GetHandler Dim handler As IHttpHandler = Nothing Monitor.Enter(LockObject) Try If (Pile.Count > 0) Then handler = CType(Pile.Pop(), IHttpHandler) End If Finally Monitor.exit(LockObject) End Try If (handler Is Nothing) Then handler = New Handler1 End If Return handler End Function #End Region End Class |
Le cache n'est pas bien compliqué, il s'agit d'une instance de Stack. Le code est potentiellement accessible par plusieurs threads simultanément. De plus, ces threads accèdent à un objet commun (la pile). D'où l'utilisation de l'instruction lock() sur les parties sensibles aux accès concurrentiels du code. On trouvera en bibliographie l'adresse de mon premier blog dans lequel je démontrais une façon correcte d'implémenter le pattern lock. Notez également l'implémentation de ReleaseHandler(). Elle n'est pas tout à fait en rapport avec le contexte de cet exemple, mais le but est surtout de montrer des implémentations possibles. Notre code instancie un handler. Vous pourrez mettre le code que vous souhaitez. Par exemple un de ceux déjà développés. Comme pour tout handler, on doit ensuite configurer IIS et modifier le fichier web.config. Il n'y a aucune différence avec les autres types de handler.
Les handlers que l'on voit apparaître
dans le fichier machine.config sont des classes internal. On ne
peut donc les instancier directement et il ne reste pas d'autres choix que de
les déclarer dans les fichiers de configuration. On pourrait penser instancier le
code behind d'une page Web qui est après tout un IHttpHandler. Pour
diverses raisons cela aussi ne marche pas. On doit donc s'en sortir par des
moyens indirects.
Nous avons vu comment créer dynamiquement une page Web depuis un handler HTTP. Mais la plupart du temps ce n'est pas cela que l'on souhaite faire. On a déjà créé la page dans le site, et l'on souhaite se dérouter vers cette page.
|
Notre exemple: Nous surveillons les accès à un certain site. Tant qu'il n'est pas 18 heures, l'accès est autorisé, mais un message d'alerte est affiché sur chaque page. Passé cette heure, le site est fermé. |
Dans notre situation plusieurs handler HTTP vont intervenir, c'est du travail pour un IHttpHandlerFactory. Pour compiler cet exemple vous devrez ajouter une référence à System.Web.Services et System.Runtime.Remoting.
|
#region using using System; using System.Collections; using System.Web; using System.Web.Services.Protocols; using System.Runtime.Remoting.Channels.Http; using System.Web.UI; #endregion namespace HandlerFactoryDemo { public class Distributeur: IHttpHandlerFactory { #region ReleaseHandler public void ReleaseHandler(IHttpHandler handler) { } #endregion #region GetHandler public IHttpHandler GetHandler(HttpContext context,string requestType,string url,string pathTranslated) { IHttpHandler handler = null; if (DateTime.Now.Hour > 18) { return new Handler1(); } try { String NomFichier = url.Substring(url.LastIndexOf('/') + 1); String Extension = NomFichier.Substring(NomFichier.LastIndexOf('.') + 1); if (Extension == "aspx") { context.Response.Write("Le site ferme à 18 heures"); return PageParser.GetCompiledPageInstance(url,pathTranslated,context); } else if (Extension == "asmx") { WebServiceHandlerFactory fact = new WebServiceHandlerFactory(); handler = fact.GetHandler(context, context.Request.RequestType,url,pathTranslated); } else if (Extension == "rem" || Extension == "soap") { HttpRemotingHandlerFactory fact = new HttpRemotingHandlerFactory(); handler = fact.GetHandler(context, context.Request.RequestType,url,pathTranslated); } else { throw new HttpException("Impossible de traiter l'extension *." + Extension); } } catch (Exception e) { throw new HttpException("Une erreur absolument inattendue s'est produite", e); } return handler; } #endregion } public class Handler1:IHttpHandler { #region ProcessRequest public void ProcessRequest(HttpContext context) { context.Response.Write("Vous avez une vie à vivre"); } #endregion #region IsReusable public bool IsReusable { get { return true; } } #endregion } } |
|
#Region
"imports" Imports System Imports System.Collections Imports System.Web Imports System.Web.Services.Protocols Imports System.Runtime.Remoting.Channels.Http Imports System.Web.UI #End Region Public Class Distributeur Implements IHttpHandlerFactory #Region "ReleaseHandler" Public Sub ReleaseHandler(ByVal handler As IHttpHandler) Implements IHttpHandlerFactory.ReleaseHandler End Sub #End Region #Region "GetHandler" Public Function GetHandler(ByVal context As HttpContext, ByVal requestType As String, ByVal url As String, ByVal pathTranslated As String) As IHttpHandler Implements IHttpHandlerFactory.GetHandler Dim handler As IHttpHandler = Nothing If (DateTime.Now.Hour > 18) Then Return New Handler1 End If Try Dim NomFichier As String = url.Substring(url.LastIndexOf("/") + 1) Dim Extension As String = NomFichier.Substring(NomFichier.LastIndexOf(".") + 1) If (Extension = "aspx") Then context.Response.Write("Le site ferme à 18 heures") Return PageParser.GetCompiledPageInstance(url, pathTranslated, context) ElseIf (Extension = "asmx") Then Dim fact As WebServiceHandlerFactory = New WebServiceHandlerFactory handler = fact.GetHandler(context, context.Request.RequestType, url, pathTranslated) ElseIf (Extension = "rem" Or Extension = "soap") Then Dim fact As HttpRemotingHandlerFactory = New HttpRemotingHandlerFactory handler = fact.GetHandler(context, context.Request.RequestType, url, pathTranslated) Else Throw New HttpException("Impossible de traiter l'extension *." + Extension) End If Catch e As Exception Throw New HttpException("Une erreur absolument inattendue s'est produite", e) End Try Return handler End Function #End Region End Class Public Class Handler1 Implements IHttpHandler #Region "ProcessRequest" Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest context.Response.Write("Vous avez une vie à vivre") End Sub #End Region #Region "IsReusable" Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable Get Return False End Get End Property #End Region End Class |
Notez un point: ce code utilise des méthodes signalées comme:
Ce membre prend en charge
l'infrastructure .NET Framework et n'est pas destiné à une utilisation directe
à partir de votre code.
Concrètement vous n'avez pas de garantie de portabilité d'une version à l'autre de .NET. Pour l'essentiel le code distingue 3 cas de figure: aspx, asmx, rem, soap et active le handler associé.
Remoting utilise un handler HTTP lorsque le canal de remoting est HTTP.
Le fichier de configuration ne présente pas de différences, si ce n'est l'attribut path:
|
<httpHandlers>
<add verb="*" path="*.*" type="HandlerFactoryDemo.Distributeur,WebService2" />
</httpHandlers> |
Il n'y a rien de particulier à faire concernant IIS.
Tant qu'il n'est pas 18 heures, le site sera accessible de façon normale. Notez toutefois l'ajout du message d'alerte (Le site ferme à 18 heures) en haut de toutes les pages ASP. Passé 18 heures le message suivant s'affiche:

Cette technique peut parfaitement être adaptée pour ne permettre l'accès au site qu'à des utilisateurs particuliers, par exemple des administrateurs. C'est bien utile pour des travaux de maintenance.
Par défaut les états de session ne sont pas accessibles dans un handler HTTP. Pour les activer il suffit d'implémenter l'interface IRequiresSessionState. Il s'agit juste d'une interface marqueur. Aucun code supplémentaire n'est à ajouter, elle demande simplement à ASP d'instancier Session afin d'éviter une exception du genre "Référence d'Objet non initialisée à une instance d'un objet".
Dans la plupart des cas on aura guère l'utilité d'écrire
soi même un module puisque Visual Studio fournit un squelette tout fait sous la
forme du fichier global.asax. L'écriture d'un module supplémentaire se justifie
donc si l'on souhaite pouvoir activer/désactiver son traitement depuis le
fichier de configuration ou bien si l'on souhaite isoler le traitement effectué
du code présent dans global.asax.
|
Notre exemple: Mise en place de l'authentification d'un utilisateur du site |
Les modules sont des classes qui implémentent IHttpModule. Cette interface expose les méthodes suivantes.
[C#]|
public interface
IHttpModule { void Dispose(); void Init(HttpApplication context); } |
|
Public Interface
IHttpModule Sub Dispose() Sub Init(ByVal context As HttpApplication) End Interface |
Les méthodes suivantes sont exposées:
| Dispose | Supprime les ressources (autres que la mémoire) utilisées par le module qui implémente IHttpModule. |
|
Init |
Initialise un module et le prépare à gérer les demandes. |
Les modules utilisent les événements pour communiquer avec l'application et les réponses ou requêtes HTTP. Les modules peuvent soit écouter des événements levés par l'application, soit lever eux même des événements auxquels l'application s'est abonnée.
Le module est celui-ci :
[C#]|
#region using using System; using System.Web; using System.Web.Caching; using System.Web.Security; using System.Security.Principal; using System.Security; #endregion namespace DemoAuthentication { public class AuthenticationModule: IHttpModule { #region Init public void Init(HttpApplication context) { // enregistre le gestionnaire d'événement personnalisé context.AuthenticateRequest += new EventHandler(this.MonAuthentification); } #endregion #region Dispose public void Dispose() { // on a rien de spécial à nettoyer dans cet exemple } #endregion #region MonAuthentification (private) /// <summary> /// Gestionnaire d'événement personnalisé /// </summary> private void MonAuthentification(object sender,EventArgs e) { HttpApplication application = (HttpApplication) sender; HttpContext contexte = application.Context; if (contexte.Request.IsAuthenticated) { // obtient l'identité de l'utilisateur connecté FormsIdentity identity = (FormsIdentity) contexte.User.Identity; // Recherche le Principal dans le cache IPrincipal principal = (IPrincipal) contexte.Cache[identity.Name]; if (principal == null) { // il n'est pas dans le cache, il faut donc le construire principal = MonPrincipal(identity.Name); // il existe d'autres signatures permettant de gérer des stratégies d'expiration contexte.Cache.Insert(identity.Name,principal); } contexte.User = principal; } } #endregion #region MonPrincipal (private) /// <summary> /// Obtient un <see cref="IPrincipal"/> avec les rôles attribués à l'utilisateur /// </summary> /// <param name="Name">Nom de login</param> /// <returns></returns> private IPrincipal MonPrincipal(string Name) { // NOTE: dans la vie réelle ces informations sont obtenues directement depuis une base de données par exemple string ListeRoles = "role1;role2;role3"; // Création du Principal IIdentity identity = new GenericIdentity(Name); return new GenericPrincipal(identity,ListeRoles.Split(';')); } #endregion } } |
[VB]
|
#Region
"using" Imports System Imports System.Web Imports System.Web.Caching Imports System.Web.Security Imports System.Security.Principal Imports System.Security #End Region Public Class AuthenticationModule Implements IHttpModule #Region "Init" Public Sub Init(ByVal context As HttpApplication) Implements IHttpModule.Init ' enregistre le gestionnaire d'événement personnalisé AddHandler context.AuthenticateRequest, AddressOf Me.MonAuthentification End Sub #End Region #Region "Dispose" Public Sub Dispose() Implements IHttpModule.Dispose ' on a rien de spécial à nettoyer dans cet exemple End Sub #End Region #Region "MonAuthentification (private)" Private Sub MonAuthentification(ByVal sender As Object, ByVal e As EventArgs) Dim application As HttpApplication = CType(sender, HttpApplication) Dim contexte As HttpContext = Application.Context If (contexte.Request.IsAuthenticated) Then ' obtient l'identité de l'utilisateur connecté Dim identity As FormsIdentity = CType(contexte.User.Identity, FormsIdentity) ' Recherche le Principal dans le cache Dim principal As IPrincipal = CType(contexte.Cache.Item(identity.Name), IPrincipal) If (principal Is Nothing) Then ' il n'est pas dans le cache, il faut donc le construire principal = MonPrincipal(identity.Name) ' il existe d'autres signatures permettant de gérer des stratégies d'expiration contexte.Cache.Insert(identity.Name, principal) End If contexte.User = principal End If End Sub #End Region #Region "MonPrincipal (private)" Private Function MonPrincipal(ByVal Name As String) As IPrincipal ' NOTE: dans la vie réelle ces informations sont obtenues directement depuis une base de données par exemple Dim ListeRoles As String = "role1;role2;role3" ' Création du Principal Dim identity As IIdentity = New GenericIdentity(Name) Return New GenericPrincipal(identity, ListeRoles.Split(";")) End Function #End Region End Class |
On suppose que l'on s'authentifie par formulaire, mais on pourrai adapter facilement ce code à d'autres cas de figure. La méthode Init() permet au module de s'abonner à l'événement AuthenticateRequest de HttpApplication. Cet événement est levé lorsque le module de sécurité a établit l'identité de l'utilisateur. Un Principal est recherché dans le cache, s'il n'existe pas déjà il sera créé. La méthode MonPrincipal attribue les rôles à l'utilisateur connecté. Bien sûr, dans une application réelle on ne procèdera pas ainsi. Il s'agit juste d'une démo.
Il manque encore un formulaire d'authentification. Pour faire des tests on se contente de placer deux TextBox et un bouton Valider. Le code est le suivant :
[C#]|
FormsAuthentication.Authenticate(TextBox1.Text,TextBox2.Text); FormsAuthentication.RedirectFromLoginPage(TextBox1.Text,false); |
|
FormsAuthentication.Authenticate(TextBox1.Text,
TextBox2.Text) FormsAuthentication.RedirectFromLoginPage(TextBox1.Text,
False) |
Il n'y a aucune logique d'authentification dans notre exemple (tout le monde est authentifié!), dans une application réelle on ferai des tests avant d'invoquer la méthode Authenticate() comme vérifier que le compte existe dans une base de données. Enfin on créé une page principale portant le code suivant:
[C#]|
private void
Page_Load(object sender, System.EventArgs
e) { IPrincipal ip = System.Threading.Thread.CurrentPrincipal; if (ip.IsInRole("role2")) { Response.Write(ip.Identity.Name + " appartient au rôle role2"); } } |
|
Private Sub
Page_Load(ByVal sender As
System.Object, ByVal e As
System.EventArgs) Handles MyBase.Load Dim ip As IPrincipal = System.Threading.Thread.CurrentPrincipal If (ip.IsInRole("role2")) Then Response.Write(ip.Identity.Name + " appartient au rôle role2") End If End Sub |
On modifie également le fichier web.config de sorte à activer l'authentification par formulaire sans oublier d'interdire l'accès anonyme!
Il reste une dernière formalité, déclarer le module. C'est chose faite avec :
|
<httpModules> <add type="DemoAuthentication.AuthenticationModule,WebApplication2 " name="module1" /> </httpModules> |
Si tout va bien la page suivante devrait s'afficher:

Il existe une grande variété d'événements HttpApplication auquel on peut s'abonner. Voici une synthèse:
| AcquireRequestState | Lorsque ASP acquiert l'état courant de la requête
pour la première fois. Concrètement, c'est le premier événement à partir duquel on peut accéder à Session. |
|
AuthenticateRequest |
ASP vient d'établir l'identité de l'utilisateur qui génère la requête. |
|
AuthorizeRequest |
Lorsque ASP vient de vérifier les autorisations de l'utilisateur |
|
BeginRequest |
ASP démarre le traitement de la requête. Il s'agit du tout premier événement levé dans le traitement |
|
Disposed |
Lorsque l'application HTTP a été disposée |
|
EndRequest |
ASP termine le traitement de la requête. Il s'agit du tout dernier événement levé dans le traitement |
|
Error |
Lorsqu'une erreur non prise en charge est levée |
|
PostRequestHandlerExecute |
Lorsque le handler HTTP a terminé son exécution |
|
PreRequestHandlerExecute |
Juste avant qu'ASP passe la main au handler HTTP |
|
PreSendRequestContent |
Juste avant qu'ASP émet un contenu vers l'utilisateur |
|
PreSendRequestHeaders |
Juste avant qu'ASP émette l'entête HTTP vers l'utilisateur |
|
ReleaseRequestState |
Se produit après que ASP.NET a terminé d'exécuter
tous les gestionnaires de demandes. Cet événement entraîne un enregistrement des données sur l'état actuel par les modules d'état. Session n'est plus accessible après cet événement |
|
ResolveRequestCache |
Lorsque ASP a terminé l'événement AuthorizeRequest. Juste avant AcquireRequestState. |
|
UpdateRequestCache |
Après ReleaseRequestState et juste avant EndRequest. |
Il n'est pas inutile de rappeler que les événements HttpApplication
sont levés dans l'ordre suivant:
Les événements Error, PreSendRequestHeaders et PreSendRequestContent peuvent arriver à tout moment. Pour finir, notez que .NET fournit une collection typée de IHttpModule: HttpModuleCollection.
Les modules s'enchaîne les uns à la suite des autres dans l'ordre où ils sont déclarés dans les fichiers de configuration. Que se passe t'il si un module décide d'interrompre son traitement ?
L'exemple suivant utilise deux modules :
|
#region using using System; using System.Web; #endregion namespace DemoModules { public class Module2: IHttpModule { #region Init public void Init(HttpApplication context) { context.AuthorizeRequest += new EventHandler(context_AuthorizeRequest); context.ResolveRequestCache+=new EventHandler(context_ResolveRequestCache); } #endregion #region Dispose() public void Dispose() { } #endregion private void context_AuthorizeRequest(object sender, EventArgs e) { HttpApplication application = (HttpApplication) sender; application.Response.Write("Module2: AuthorizeRequest <br>"); } private void context_ResolveRequestCache(object sender, EventArgs e) { HttpApplication application = (HttpApplication) sender; application.Response.Write("Module2: ResolveRequestCache <br>"); } } public class Module1: IHttpModule { #region Init public void Init(HttpApplication context) { context.AuthorizeRequest += new EventHandler(context_AuthorizeRequest); context.ResolveRequestCache+=new EventHandler(context_ResolveRequestCache); } #endregion #region Dispose() public void Dispose() { } #endregion private void context_AuthorizeRequest(object sender, EventArgs e) { HttpApplication application = (HttpApplication) sender; application.Response.Write("Module1: AuthorizeRequest <br>"); } private void context_ResolveRequestCache(object sender, EventArgs e) { HttpApplication application = (HttpApplication) sender; application.Response.Write("Module1: ResolveRequestCache <br>"); } } } |
|
#Region "using" Imports System Imports System.Web #End Region Public Class Module2 Implements IHttpModule #Region "Init" Public Sub Init(ByVal context As HttpApplication) Implements IHttpModule.Init AddHandler context.AuthorizeRequest, AddressOf context_AuthorizeRequest AddHandler context.ResolveRequestCache, AddressOf context_ResolveRequestCache End Sub #End Region #Region "Dispose()" Public Sub Dispose() Implements IHttpModule.Dispose End Sub #End Region Private Sub context_AuthorizeRequest(ByVal sender As Object, ByVal e As EventArgs) Dim application As HttpApplication = CType(sender, HttpApplication) application.Response.Write("Module2: AuthorizeRequest <br>") End Sub Private Sub context_ResolveRequestCache(ByVal sender As Object, ByVal e As EventArgs) Dim application As HttpApplication = CType(sender, HttpApplication) application.Response.Write("Module2: ResolveRequestCache <br>") End Sub End Class Public Class Module1 Implements IHttpModule #Region "Init" Public Sub Init(ByVal context As HttpApplication) Implements IHttpModule.Init AddHandler context.AuthorizeRequest, AddressOf context_AuthorizeRequest AddHandler context.ResolveRequestCache, AddressOf context_ResolveRequestCache End Sub #End Region #Region "Dispose()" Public Sub Dispose() Implements IHttpModule.Dispose End Sub #End Region Private Sub context_AuthorizeRequest(ByVal sender As Object, ByVal e As EventArgs) Dim application As HttpApplication = CType(sender, HttpApplication) application.Response.Write("Module1: AuthorizeRequest <br>") End Sub Private Sub context_ResolveRequestCache(ByVal sender As Object, ByVal e As EventArgs) Dim application As HttpApplication = CType(sender, HttpApplication) application.Response.Write("Module1: ResolveRequestCache <br>") End Sub End Class |
L'exemple implémente 2 modules: Module1 et Module2. Ces modules affichent juste un message dans le flux de réponse indiquant leur nom et l'événement intercepté. Les modules Module1 et Module2 ne diffèrent que par leur nom. Nous utilisons le fichier de configuration suivant:
|
<httpModules> <add type="DemoModules.Module1,WebApplication2" name="module1" /> <add type="DemoModules.Module2, WebApplication2" name="module2" /> </httpModules> |
La séquence suivante devrait s'afficher:

[C#]
|
private void context_AuthorizeRequest(object sender, EventArgs e) { HttpApplication application = (HttpApplication) sender; application.CompleteRequest(); application.Response.Write("Module2: AuthorizeRequest <br>"); } |
|
Private Sub context_AuthorizeRequest(ByVal sender As Object, ByVal e As EventArgs) Dim application As HttpApplication = CType(sender, HttpApplication) application.CompleteRequest() application.Response.Write("Module1: AuthorizeRequest <br>") End Sub |
Cette fois la séquence qui s'affiche est la suivante:

L'interruption provoquée n'interrompt pas l'événement en court (le message continue à s'afficher). Par contre une fois sortis du gestionnaire l'événement Module1 et Module2 sont interrompus.
Si on place maintenant l'interruption dans Module2 on
obtient:

Notez tout de même que EndRequest reste levé quoi qu'il arrive, ainsi que les événements qui suivent.
Les handlers ont en commun avec les modules de pouvoir accéder à l'instance en cours de HttpApplication, HttpRequest, HttpResponse et HttpSessionState. Les handlers peuvent intercepter une requête et modifier la façon dont elle sera traitée. Un module ne peut que renvoyer une réponse personnalisée ou bloquer le traitement effectué par le handler.
L'architecture ASP.NET garantit que les différents objets sont créés dans l'ordre suivant :
Lorsque vous décidez d'implémenter un de ces objets il est bon d'avoir en tête qu'il s'agit d'une pile logicielle supplémentaire et que de plus cette pile reste malgré tout technique. Les personnes amenées à maintenir votre code auront t'elles une maîtrise suffisante de ces objets pour le faire dans de bonnes conditions?
Voici donc une bonne question: Ce niveau
d'indirection en vaut t'il la peine?
Dans bien des cas créer une classe dérivant de Page ou bien implémenter global.asax sera largement suffisant.
Un point important lorsque vous remplissez les attributs
des fichiers de configuration est de ne pas mettre d'espaces avant ou après la valeur
de l'attribut. Le résultat n'est pas garanti.
On peut mettre:
<add toto="KKKK" />
Mais pas:
<add
toto="KKK " />
Nous avons vu que la section <httpHandlers> permet de relier un type de fichier à une extension handler HTTP particulière. Cette section peut se trouver dans web.config ou bien dans machine.config si l'on souhaite que ses effets s'appliquent à tous les sites ASP d'un serveur.
Commençons par sa syntaxe:
|
<httpHandlers> <add verb="liste de verbes http" path="chemin d'une url" type="classe implémentant le httphandler" validate="true|false" /> </httpHandlers> |
Dans le détail nous avons ceci:
| verb |
Liste de verbes HTTP séparés par une virgule. On peut
utiliser * pour mapper le handler pour tous les verbes HTTP.
Par
exemple: GET,POST,HEAD |
|
path |
Format de l'URL à laquelle s'applique le mappage. On peut
utiliser le joker *. Par exemple: *.aspx DownloadFile.down |
| type |
Désigne la classe et l'assemblage qui implémentent le handler, tout deux séparés par une virgule. |
| validate |
Spécifie à quel moment le handler est chargé. False signifie que ASP.NET charge le handler uniquement au moment où c'est nécessaire (lazzy loading". True implique un chargement au moment où le fichier de configuration est traité pour la première fois. |
On peut avoir plusieurs sections <add>. Toutefois si deux handlers sont compatibles pour une requête donnée, seul le premier rencontré sera utilisé.
<httpHandlers> accepte également les deux sections suivantes:
|
<httpHandlers> <clear /> <remove verb="liste de verbes http" path="chemin d'une url" /> </httpHandlers> |
Elles sont principalement utilisées
dans web.config pour surclasser les paramètres de machine.config. <clear> supprime les IHttpHandler configurés ou hérités du fichier de
configuration. <remove> supprime un
mappage particulier.
Intéressons nous au fichier de configuration machine.config.
|
<httpHandlers> <add verb="*" path="*.vjsproj" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.java" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.jsl" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="trace.axd" type="System.Web.Handlers.TraceHandler" /> <add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory" /> <add verb="*" path="*.ashx" type="System.Web.UI.SimpleHandlerFactory" /> <add verb="*" path="*.asmx" type="System.Web.Services.Protocols.WebServiceHandlerFactory, System.Web.Services, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" validate="false" /> <add verb="*" path="*.rem" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="false" /> <add verb="*" path="*.soap" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="false" /> <add verb="*" path="*.asax" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.ascx" type="System.Web.HttpForbiddenHandler" /> <add verb="GET,HEAD" path="*.dll.config" type="System.Web.StaticFileHandler" /> <add verb="GET,HEAD" path="*.exe.config" type="System.Web.StaticFileHandler" /> <add verb="*" path="*.config" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.cs" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.csproj" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.vb" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.vbproj" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.webinfo" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.asp" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.licx" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.resx" type="System.Web.HttpForbiddenHandler" /> <add verb="*" path="*.resources" type="System.Web.HttpForbiddenHandler" /> <add verb="GET,HEAD" path="*" type="System.Web.StaticFileHandler" /> <add verb="*" path="*" type="System.Web.HttpMethodNotAllowedHandler" /> </httpHandlers> |
Le point intéressant est que .NET définit
un certain nombre de HttpHandler. Nous les avons mis en gras. Il s'agit de
classes internal qui ne sont pas documentées. Nous ne les détaillerons
pas tous, mais on peut déjà observer le fonctionnement de certains d'entre
eux.
Par exemple le mécanisme de trace
qui est associé au handler TraceHandler. Ce handler intervient dès que
la requête porte sur le fichier trace.axd et ajoute un rapport d'exécution
de trace à la page Web.
On voit également comment ASP.NET interdit le chargement de certains type de fichier en utilisant le handler HttpForbiddenHandler. Le traitement de ce handler consiste à lever l'exception HttpException avec le code d'erreur 403. Il incrémente aussi le compteur de performance REQUEST_NOT_FOUND.
Ce handler est utilisé pour protéger
les ressources sensibles comme le code-behind en empêchant leur chargement.
Vous pouvez parfaitement vous en servir dans vos applications.
|
<% int p; p=1/0; %> |
Bien sûr l'exécution de ce code génère une erreur.
Cliquez sur le lien Afficher la source de la compilation complète. Un
code similaire à celui-ci apparaît:
|
Ligne 1 : //------------------------------------------------------------------------------ Ligne 2 : // <autogenerated> Ligne 3 : // This code was generated by a tool. Ligne 4 : // Runtime Version: 1.1.4322.2032 Ligne 5 : // Ligne 6 : // Changes to this file may cause incorrect behavior and will be lost if Ligne 7 : // the code is regenerated. Ligne 8 : // </autogenerated> Ligne 9 : //------------------------------------------------------------------------------ Ligne 10 : Ligne 11 : namespace ASP { Ligne 12 : using System; Ligne 13 : using System.Collections; Ligne 14 : using System.Collections.Specialized; Ligne 15 : using System.Configuration; Ligne 16 : using System.Text; Ligne 17 : using System.Text.RegularExpressions; Ligne 18 : using System.Web; Ligne 19 : using System.Web.Caching; Ligne 20 : using System.Web.SessionState; Ligne 21 : using System.Web.Security; Ligne 22 : using System.Web.UI; Ligne 23 : using System.Web.UI.WebControls; Ligne 24 : using System.Web.UI.HtmlControls; Ligne 25 : using ASP; Ligne 26 : Ligne 27 : Ligne 28 : [System.Runtime.CompilerServices.CompilerGlobalScopeAttribute()] Ligne 29 : public class tutu_aspx : WebApplication2.tutu, System.Web.SessionState.IRequiresSessionState { Ligne 30 : Ligne 31 : private static bool __initialized = false; Ligne 32 : Ligne 33 : private static System.Collections.ArrayList __fileDependencies; Ligne 34 : Ligne 35 : public tutu_aspx() { Ligne 36 : System.Collections.ArrayList dependencies; Ligne 37 : if ((ASP.tutu_aspx.__initialized == false)) { Ligne 38 : dependencies = new System.Collections.ArrayList(); Ligne 39 : dependencies.Add("c:\\inetpub\\wwwroot\\WebApplication2\\tutu.aspx"); Ligne 40 : ASP.tutu_aspx.__fileDependencies = dependencies; Ligne 41 : ASP.tutu_aspx.__initialized = true; Ligne 42 : } Ligne 43 : this.Server.ScriptTimeout = 30000000; Ligne 44 : } Ligne 45 : Ligne 46 : protected override bool SupportAutoEvents { Ligne 47 : get { Ligne 48 : return false; Ligne 49 : } Ligne 50 : } Ligne 51 : Ligne 52 : protected ASP.Global_asax ApplicationInstance { Ligne 53 : get { Ligne 54 : return ((ASP.Global_asax)(this.Context.ApplicationInstance)); Ligne 55 : } Ligne 56 : } Ligne 57 : Ligne 58 : public override string TemplateSourceDirectory { Ligne 59 : get { Ligne 60 : return "/WebApplication2"; Ligne 61 : } Ligne 62 : } Ligne 63 : Ligne 64 : private void __BuildControlTree(System.Web.UI.Control __ctrl) { Ligne 65 : __ctrl.SetRenderMethodDelegate(new System.Web.UI.RenderMethod(this.__Render__control1)); Ligne 66 : } Ligne 67 : Ligne 68 : private void __Render__control1(System.Web.UI.HtmlTextWriter __output, System.Web.UI.Control parameterContainer) { Ligne 69 : __output.Write("\r\n<html>\r\n\t<body MS_POSITIONING=\"FlowLayout\">\r\n\t\t"); Ligne 70 : Ligne 71 : #line 4 "http://localhost/WebApplication2/tutu.aspx" Ligne 72 : Ligne 73 : int p; Ligne 74 : p=1/0; Ligne 75 : Ligne 76 : Ligne 77 : Ligne 78 : #line default Ligne 79 : #line hidden Ligne 80 : __output.Write("\r\n\t</body>\r\n</html>\r\n"); Ligne 81 : } Ligne 82 : Ligne 83 : protected override void FrameworkInitialize() { Ligne 84 : this.__BuildControlTree(this); Ligne 85 : this.FileDependencies = ASP.tutu_aspx.__fileDependencies; Ligne 86 : this.EnableViewStateMac = true; Ligne 87 : this.Request.ValidateInput(); Ligne 88 : } Ligne 89 : Ligne 90 : public override int GetTypeHashCode() { Ligne 91 : return 208178709; Ligne 92 : } Ligne 93 : } Ligne 94 : } |
Sans entrer dans les détails qui sortent du cadre de cet article on doit surtout repérer une structure similaire à celle des handlers déjà écrit dans nos exemples. Les balises HTML de la page ASP sont générées dans le flux de réponse à l'aide de méthodes Write. Tout comme nous l'avons fait. Le code généré est simplement plus sophistiqué.
StaticFileHandler traite les fichiers statiques (html, image...). On peut compléter la liste pour demander à ASP.NET de traiter des fichiers non traités à priori, par exemple les fichiers gif. Dans ce cas il faut savoir qu'ASP ajoute un header HTTP Expires, ce que ne fait pas IIS.
Nous avons vu que la section <httpModules> permet de relier un type de fichier à une extension HttpModule particulier. Cette section peut se trouver dans web.config ou bien dans machine.config si l'on souhaite que ses effets s'appliquent à tous les sites ASP d'un serveur. Commençons par sa syntaxe:
|
<httpModules> <add type="type" name="nom" /> </ httpModules > |
Dans le détail nous avons ceci:
| Type | Désigne la classe et l'assemblage qui implémentent le handler, tout deux séparés par une virgule. |
| name |
Nom donné au module. Ce nom peut servir pour supprimer le module |
La balise:
|
<remove name="name" /> |
Supprime un module nommé. Typiquement on va supprimer un
des modules standards comme le module d'authentification par formulaire pour le
substitué à un module personnalisé. Les modules standards sont déclarés dans machine.config :
|
<httpModules> |
Les modules et les handlers se paramètrent et s'activent dans le fichiers de configuration .NET. Ils bénéficient ainsi du mécanisme de configuration hiérarchique de ASP.NET. Si on déclare un handler dans le fichier web.config, toutes les sous-applications héritent de cette nouvelle configuration et s'attendent à trouver la dll du handler dans leur fichier bin. Dans le cas contraire une exception est levée. On peut s'attendre à résoudre le problème en ajoutant une balise <remove>. C'est ce que suggère la documentation.
Malheureusement cela ne marche pas en ASP NET 1.1.
L'exception continue à être levée. Le problème semble disparaître avec ASP NET 2.0
toutefois. Pour le contourner on dispose de plusieurs solutions. La
plus simple est de déployer la dll dans le GAC. <remove>
fonctionne alors comme prévu. Une autre possibilité est de placer plutôt
une balise <location> dans le fichier de configuration de
l'application principale.