É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

Introduction

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:

  1. Les handlers HTTP
  2. Les modules HTTP

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

Ecriture d'un handler HTTP

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.

Créer un handler synchrone

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. Tout d'abord des problèmes de sécurité car elle vous oblige entre autres à révéler des informations sur la hiérarchie des répertoires de votre serveur. Et puis que penser d'une application qui autorise un téléchargement depuis un serveur sans contrôle ?

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. Un handler HTTP synchrone est une classe qui implémente l'interface IHttpHandler.

[C#]
public interface IHttpHandler
{
      bool IsReusable {get;}  
      void ProcessRequest(HttpContext context);
}

 

[VB]
Public Interface IHttpHandler
Sub ProcessRequest(ByVal context As HttpContext)
  ReadOnly Property IsReusable() As Boolean

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>  

Lorsqu'une telle URL est rencontrée le fichier dont le nom est fourni est aussitôt téléchargé.

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"];

                  if (Fichier == null || ! File.Exists(RepertoireDownload + Fichier))
                  {
                        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
      }
}

 

[VB]
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.

Créer un handler asynchrone

Introduction sur le bon usage du multi-threading

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.

Implémentation du handler

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
{
IAsyncResult BeginProcessRequest(HttpContext context,AsyncCallback cb,object extraData);
 
void EndProcessRequest(IAsyncResult result);
}

 

[VB]
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
      }

}

 

 

[VB]
#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!

 

Créer une fabrique de handler 

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

[C#]
public interface IHttpHandlerFactory
{
      IHttpHandler GetHandler(HttpContext context,string requestType,string url,string pathTranslated);
 
      void ReleaseHandler(IHttpHandler handler);

}

 

[VB]
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

 
Cette interface expose donc deux méthodes:

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. 

[C#]
#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
      }
}

 

[VB]
#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.

Utilisation des handlers natifs

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.

 

[C#]
#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
      }
}

 

[VB]
#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.

 

Activer l'état de session

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".

 

 

Ecriture d'un module HTTP

 

Implémentation d'un module


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);
}

 

[VB]
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);

 

[VB]
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");
}

}

 

[VB]
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: 

  1. BeginRequest
  2. AuthenticateRequest
  3. AuthorizeRequest
  4. ResolveRequestCache
  5. AcquireRequestState
  6. PreRequestHandlerExecute
  7. PostRequestHandlerExecute
  8. ReleaseRequestState
  9. UpdateRequestCache
  10. EndRequest

 

Les événements Error, PreSendRequestHeaders et PreSendRequestContent peuvent arriver à tout moment. Pour finir, notez que .NET fournit une collection typée de IHttpModule: HttpModuleCollection.

Le pipe de module

 

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 : 

[C#]
#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>");
            }
      }
}

 

[VB]
#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:

 

  Comme on le voit, Module1 est exécuté avant Module2 à l'échelle de chaque événement. Supposons qu'une erreur se produise dans Module1 au point d'interrompre le traitement de ce module. Nous modifions le code de context_AuthorizeRequest dans Module1 ainsi :

 

[C#]
private void context_AuthorizeRequest(object sender, EventArgs e)
{
      HttpApplication application = (HttpApplication) sender;
      application.CompleteRequest();
 
      application.Response.Write("Module2: AuthorizeRequest <br>");
}

 

[VB]
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: 

 

  Ce qui est cohérent avec l'ordre des événements. 

Notez tout de même que EndRequest reste levé quoi qu'il arrive, ainsi que les événements qui suivent.

 

Handler ou module?

 

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.

 

Les fichiers de configuration

 

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 " />

 

La section httpHandlers

 

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.  PageHandlerFactory est un handler spécialisé dans le rendu des pages ASP tandis que WebServiceHandlerFactory prend en charge les Web services. Il est possible d'avoir un aperçu de la façon dont en interne PageHandlerFactory agit en montant par exemple la page ASP contenant le code inline suivant:

<%
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.

 

La section httpModules

 

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>
     
<add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
     
<add name="Session" type="System.Web.SessionState.SessionStateModule" />
     
<add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
     
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
     
<add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" />
     
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
     
<add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" />

     
<add name="ErrorHandlerModule" type="System.Web.Mobile.ErrorHandlerModule, System.Web.Mobile, Version=1.0.5000.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a" />
</httpModules>

 

Un petit bug

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.