Tout développeur ASP.NET sait que chaque requête aboutit à la création d’un nouveau thread pour traiter cette requête. Mais qui crée ce thread et comment? De même, que se passe-t-il avant le moment où la page affiche nos contrôles et traite nos évènements? Qui crée et gère les différents évènements du cycle de vie de la page ASP.NET? Force est de constater qu’ASP.NET donne l’impression d’un iceberg dont nous ne voyons qu’une toute petite partie, et que de nombreuses erreurs pourraient être évitées si nous avions une compréhension plus importante des mécanismes internes.
Pourtant, il est plutôt facile d’en savoir plus sur le fonctionnement interne d’ASP.NET, tout simplement parce que le processus de traitement des pages est connu et documenté, et parce que des outils tels que Visual Studio ou Reflector permettent de mettre les mains dans le cambouis pour la partie .NET.
Pour résumer, lorsque le serveur Web reçoit une requête, celui-ci analyse l’extension de la ressource demandée pour savoir comment traiter la requête. Dans le cas d’une page ASP.NET (extension .aspx par défaut), c’est l’assembly aspnet_isapi.dll (non managée) qui est sollicitée (exemple d’association de mappage avec IIS 7.5) :

Le serveur Web, après avoir chargé aspnet_isapi, inscrit la requête dans une queue par un appel asynchrone vers le Worker Process d’ASP.NET. Celui-ci traite successivement les requêtes de la queue. Lorsqu’un traitement est terminé, la réponse à la requête est envoyée du Worker Process vers aspnet_isapi.
Concrètement, voici ce qu’on obtient avec Visual Studio 2010 lorsqu’on crée une application Web ASP.NET et qu’on stoppe dans le Page_Load de la page Default.aspx :

Dans cette pile des appels, on voit clairement les étapes majeures :
- L’appel à des classes de threading .NET (namespace System.Threading de mscorlib) depuis du code non managé (certainement aspnet_isapi). Ici, c’est le ThreadPool qui est utilisé : c’est le mécanisme le plus logique pour inscrire un appel asynchrone dans une queue. A la fin de cette étape, WaitCallback_Context intervient ; on constate que l’appel asynchrone transmet le traitement au serveur Web, en lui fournissant un socket :

- L’intervention du serveur Web de développement ASP.NET WebDev.WebHost40.dll. Cette assembly contient des classes pour matérialiser les échanges classiques d’un serveur Web (Host, Request, Messages, Connection etc.). Ici, on voit clairement qu’elle est sollicitée pour que l’hôte traite la requête. Plus précisément, OnSocketAccept reconstruit une connexion Web à l’aide du socket et utilise l’hôte pour démarrer le traitement :

L’intervention de WebDev.WebHost40.dll aboutit à la méthode Process, où plusieurs vérifications, notamment sur la requête, sont déjà effectuées. Par exemple, TryParseRequest appelle IsBadPath, qui détecte entre autres les URLs mal formées et retourne le cas échéant un code d’erreur HTTP en stoppant le traitement. Ceci explique pourquoi il est parfois difficile de déboguer un problème d’URL mal formée, et pourquoi il est inutile de tester la validité de l’URL de la requête dans le code d’une page ASP.NET. Si les vérifications passent, PrepareResponse inscrit déjà le code HTTP 200 (succès) et Process transmet la requête à HttpRuntime :
- Le passage dans System.Web.dll marque l’entrée réelle dans les classes du Framework .NET. HttpRuntime est sollicitée en premier. La requête provenant du serveur Web est transmise jusqu’à ProcessRequestInternal, où elle sert à créer l’objet majeur d’ASP.NET HttpContext :

Dans cette même méthode, on trouve également l’utilisation des HttpHandlers, un autre concept majeur relatif à ASP.NET :

Le traitement de la requête est transmis de façon asynchrone au HttpHandler représentant l’application. Celui-ci est construit en récupérant le type déclaré dans le fichier Global.asax (héritant toujours d’HttpApplication). C’est ainsi qu’ASP.NET sait quelle est l’application Web concernée dans la suite des appels. HttpApplication traite la requête en parcourant un ensemble d’étapes (qui sont toutes des types imbriqués à HttpApplication implémentant HttpApplication.IExecutionStep). Ces étapes sont ajoutées lors de l’initialisation (méthode BuildSteps appelée depuis InitInternal) de l’HttpApplication de la façon suivante :
Les étapes sont de 2 types : des étapes d’exécution et des évènements. Une classe appelée ApplicationStepManager est utilisée par sa méthode ResumeSteps pour parcourir les différentes étapes, sachant que l’étape “principale” est CallHandlerExecutionStep :
L’intervention d’HttpHandler s’achève en délégant le traitement de la requête au Handler de la page déterminé à l’étape MapHandlerExecutionStep. Ceci mène directement à l’assembly (ici, App_Web_inl2fuua.dll) générée automatiquement pour l’application Web pour traiter la page demandée (ici, Default.aspx). Le traitement de la page y est simplement transmis à la classe Page :

Avant de lancer le traitement de la requête, Page affecte un certain nombre de variables de classe provenant du contexte HTTP dans la méthode SetIntrinsics (notamment Request, Response et Application). Puisque toutes les pages ASP.NET héritent de Page, ceci permettra par exemple d’accéder indifféremment à la requête HTTP depuis n’importe quelle page en récupérant “Context.Request”, ou directement “Request”. De plus, la méthode SetIntrinsics appelle TemplateControl.HookUpAutomaticHandlers qui utilise la méthode GetDelegateInformationWithNoAssert pour rechercher par réflexion les gestionnaires d’évènements déclarés dans la Page :
Chaque appel à GetDelegateInformationFromMethod recherche un EventHandler ayant le nom exact spécifié dans la Page, et l’inscrit dans un dictionnaire s’il le trouve (le dictionnaire est ensuite mis en cache). Ceci explique que les évènements du cycle de vie de la page ASP.NET doivent être déclarés avec la bonne syntaxe et la bonne signature, sans avoir besoin de les attacher manuellement à la Page. L’extrait de code précédent montre également les évènements spécifiques à la Page, les autres étant valides seulement pour les contrôles.
Avant d’effectuer le traitement de la Page caractérisé par la méthode ProcessRequestMain, Page récupère diverses informations supplémentaires tells que la culture courante, le StyleSheetTheme ou encore le type de PostBack. Par exemple, Page recherche le champ “__PreviousPage”. Ensuite, ProcessRequestMain parcours effectivement le cycle de vie de la Page en effectuant les étapes du cycle, en levant les évènements correspondants (ceux détectés et inscrits dans le dictionnaire en cache) au fur et à mesure et en inscrivant les messages dans la Trace ASP.NET. Lors du traitement effecif, Page commence par effectuer la pré-initialisation :
Ceci explique comment un évènement Page_PreInit dans une page permet effectivement de modifier le thème et la page maître juste avant leur application par le Framework ASP.NET. En allant un peu plus loin, voici un extrait de ProcessRequestMain autour du Load :
La méthode LoadRecursive est en fait définie dans la classe System.Web.UI.Control. Elle cherche simplement à lancer l’évènement Page_Load s’il existe et à charger ensuite les enfants du contrôle. En partant de la Page, ceci permet de parcourir toute l’arborescence des contrôles de la page. De plus, ceci explique que le Page_Load de la Page soit appelé avant le Page_Load des contrôles :
Lorsque LoadRecursive appelle la méthode OnLoad, le dictionnaire (en cache) des gestionnaires d’évènements est sollicité :

Ce qui appelle (enfin !) le gestionnaire Page_Load :
En explorant plus en profondeur HttpRuntime, HttpApplication et Page, toutes les fonctionnalités d’ASP.NET pourraient être expliquées : l’application des Master Pages, le fonctionnement du ViewState et du ControlState, l’application des thèmes, le databinding, etc.