Préservez le bon fonctionnement de vos sites en évitant ces 10 pièges ASP.NET courants

Jeff Prosise

Consultez cet article en anglais


Cet article met en oeuvre les technologies suivantes :
.NET Framework, ASP.NET, Windows Server 2003


Sur cette page
LoadControl et mise en cache de la sortieLoadControl et mise en cache de la sortie
Sessions et mise en cache de la sortieSessions et mise en cache de la sortie
Durée de vie des tickets d'authentification FormsDurée de vie des tickets d'authentification Forms
État d'affichage : la dégradation silencieuse des performancesÉtat d'affichage : la dégradation silencieuse des performances
État de session SQL Server : autre type de dégradation des performancesÉtat de session SQL Server : autre type de dégradation des performances
Rôles non mis en cacheRôles non mis en cache
Sérialisation des propriétés des profilsSérialisation des propriétés des profils

        Saturation des pools de threads
      Saturation des pools de threads

        Usurpation d'identité et autorisation ACL
      Usurpation d'identité et autorisation ACL
N'ayez pas une confiance aveugle : profilez votre base de données !N'ayez pas une confiance aveugle : profilez votre base de données !
ConclusionConclusion

L'une des raisons du succès de ASP.NET est qu'il place la barre moins haut pour les développeurs Web. Vous n'avez pas besoin d'un doctorat en informatique pour écrire du code ASP.NET. Nombre des programmeurs ASP.NET que je rencontre dans mon travail sont des autodidactes qui écrivaient des feuilles de calcul Microsoft® Excel® avant d'écrire du code C# ou Visual Basic®. Ils écrivent à présent des applications Web et font généralement du bon travail.

Mais le pouvoir entraîne la responsabilité, et même les développeurs ASP.NET les plus aguerris ne sont pas à l'abri des erreurs. Des années de conseil sur des projets ASP.NET m'ont appris que certaines erreurs ont une prédisposition toute particulière pour engendrer des pièges. Certaines de ces erreurs affectent les performances. D'autres inhibent l'évolutivité. D'autres encore entraînent des pertes de temps pour les équipes de développement, à la recherche des bogues et des causes de comportements inattendus.

Voici 10 des pièges qui entravent la mise en production de vos applications ASP.NET, et ce que vous pouvez faire pour les éviter. Tous les exemples données sont issus de mon expérience avec des entreprises réelles, créant des applications Web réelles, et dans certains cas je décris des problèmes rencontrés par l'équipe de développement ASP.NET.

LoadControl et mise en cache de la sortie

Rares sont les applications ASP.NET qui n'emploient pas de contrôles utilisateur. Avant l'avénement des pages maître, les développeurs employaient des contrôles utilisateur pour factoriser le contenu commun, par exemple les en-têtes et les pieds de page. Même dans ASP.NET 2.0, les contrôles utilisateur offrent un moyen efficace d'encapsuler le contenu et le comportement, et de diviser les pages en régions dont la mise en cache peut être contrôlée indépendamment de la page dans son ensemble ; il s'agit d'une forme particulière de mise en cache de la sortie, appelée mise en cache de fragments

Les contrôles utilisateur peuvent être chargés de manière déclarative ou impérative. Le chargement impératif repose sur Page.LoadControl, qui instancie un contrôle utilisateur et renvoie une référence Control. Si le contrôle utilisateur contient des membres de types personnalisés (par exemple des propriétés publiques), vous pouvez convertir cette référence et accéder aux membres personnalisés à partir de votre code. Le contrôle utilisateur de la figure 1 implémente une propriété nommée BackColor. Le code suivant charge le contrôle utilisateur et affecte une valeur à BackColor :

        protected void Page_Load(object sender, EventArgs e)
        {
        // Load the user control and add it to the page
        Control control = LoadControl("~/MyUserControl.ascx");
        PlaceHolder1.Controls.Add(control);

        // Set its background color
        ((MyUserControl)control).BackColor = Color.Yellow;
        }
      

Ce code, aussi simple soit-il, est un piège qui attend la première occasion pour piéger le développeur peu méfiant. Pouvez-vous identifier la faille ?

Si vous pensez que le problème concerne la mise en cache de la sortie, vous avez raison. Ces exemples de code sont compilés et s'exécutent correctement, mais essayez d'ajouter l'instruction suivante (parfaitement correcte) à MyUserControl.ascx :

        <%@ OutputCache="" Duration="5" VaryByParam="None" %>
        

Lors de la prochaine exécution de la page, vous recevez une exception InvalidCastException, accompagnée du message d'erreur suivant :

        "Unable to cast object of type ‘System.Web.UI.PartialCachingControl’ to type ‘MyUserControl’."
      

Voici donc du code qui fonctionne bien sans directive OutputCache, mais qui pose problème lors de l'ajout de la directive OutputCache. ASP.NET n'est pas censé fonctionner de cette façon. Les pages (et contrôles) sont censés être agnostiques vis-à-vis de la mise en cache de la sortie. Que se passe-t-il donc ?

Le problème est que lorsque la mise en cache de la sortie est activée pour un contrôle utilisateur, LoadControl ne renvoie plus de référence à une instance du contrôle ; il renvoie une référence à une instance de PartialCachingControl qui peut ou non encapsuler une instance de contrôle, selon que la sortie du contrôle est mise en cache ou non. Par conséquent, les développeurs qui appellent LoadControl pour charger un contrôle utilisateur de manière dynamique et qui convertissent également la référence au contrôle afin d'accéder à des méthodes et propriétés propres au contrôle doivent être vigilants quant à la façon de procéder pour que le code fonctionne avec ou sans directive OutputCache.

La figure 2 illustre le moyen approprié de charger les contrôles utilisateur de manière dynamique et de convertir les références au contrôle renvoyées. Voici une vue d'ensemble du fonctionnement :

Si le fichier ASCX n'a pas de directive OutputCache, LoadControl renvoie une référence MyUserControl. Page_Load convertit la référence en MyUserControl et définit la propriété BackColor du contrôle.

Si le fichier ASCX inclut une directive OutputCache et que la sortie du contrôle n'est pas mise en cache, LoadControl renvoie une référence à un contrôle PartialCachingControl dont la propriété CachedControl contient une référence au contrôle MyUserControl sous-jacent. Page_Load convertit PartialCachingControl.CachedControl en MyUserControl et définit la propriété BackColor du contrôle.

Si le fichier ASCX inclut une directive OutputCache et que la sortie du contrôle est mise en cache, LoadControl renvoie une référence à un contrôle PartialCachingControl dont la propriété CachedControl est null. Voyant cela, Page_Load ne fait rien d'autre. Il ne sert à rien de définir la propriété BackColor du contrôle, car la sortie du contrôle est fournie à partir du cache de sortie. Autrement dit, il n'y a pas de contrôle MyUserControl sur lequel définir une propriété.

Le code de la figure" 2 fonctionne avec ou sans directive OutputCache dans le fichier .ascx. Ce n'est pas joli, mais cela permet d'éviter les mauvaises surprises. Plus simple ne signifie pas toujours plus facile à gérer.

Haut de pageHaut de page

Sessions et mise en cache de la sortie

S'agissant de la mise en cache de la sortie, il existe un problème potentiel concernant ASP.NET 1.1 et ASP.NET 2.0, qui affecte les pages dont la sortie est mise en cache sur les serveurs exécutant Windows Server™ 2003 et IIS 6.0. J'ai personnellement eu l'occasion de constater ce problème à deux reprises sur des serveurs ASP.NET en production, et dans les deux cas, le problème a été résolu via la désactivation de la mise en cache de la sortie. Par la suite, j'ai appris qu'il existait une meilleure solution, ne nécessitant pas la désactivation de la mise en cache de la sortie. Voici comment j'ai rencontré le problème la première fois.

L'histoire a commencé lorsqu'une startup (appelons-la Contoso.com) gérant une application publique de commerce électronique sur un petit groupe de serveurs Web ASP.NET a contacté mon équipe en se plaignant de rencontrer des erreurs de type « inter-threading ». De temps en temps, un client utilisant le site Web Contoso.com perdait subitement les données saisies et voyait les données correspondant à un autre utilisateur. Une petite investigation a montré que « inter-threading » n'était pas une description appropriée du problème ; il s'agissait davantage d'erreurs « inter-session ». Il semble que Contoso.com stockait les données dans l'état de session, et pour une raison donnée, les utilisateurs étaient de manière occasionnelle (et aléatoire) connectés aux sessions d'autres utilisateurs.

L'un des membres de mon équipe a écrit un outil de diagnostic afin de consigner les principaux éléments de chaque demande et réponse HTTP, y compris les en-têtes des cookies. Il l'a alors installé sur les serveurs Web de Contoso.com et l'a laissé fonctionner quelques jours. Les résultats étaient intéressants. Environ une fois toutes les 100 000 demandes, ASP.NET affectait correctement un ID de session à une nouvelle session et renvoyait l'ID de session dans un en-tête Set-Cookie. Il renvoyait ensuite le même ID de session (c'est-à-dire le même en-tête Set-Cookie) lors de la demande suivante, même si cette demande était déjà associée à une session valide et envoyait correctement l'ID de session dans un cookie. En effet, ASP.NET écartait de manière aléatoire les utilisateurs de leur propre session avant de les connecter à d'autres sessions.

Surpris, nous avons commencé à rechercher les causes. Nous avons d'abord examiné le code source de Contoso.com, pour constater que le problème se situait ailleurs. Ensuite, simplement pour vérifier que le problème n'était pas lié au fait que l'application était hébergée sur un groupe de serveurs Web, nous avons mis hors tension tous les serveurs, sauf un. Le problème persistait, ce qui n'était pas surprenant, puisque nos journaux montraient que les en-têtes Set-Cookie concernés ne provenaient jamais de deux serveurs différents. Il était incroyable que ASP.NET génère accidentellement des ID de session dupliqués, car il utilise la classe .NET Framework RNGCryptoServiceProvider pour générer ces ID, et leur longueur est suffisante pour garantir que le même ID ne sera jamais généré deux fois (disons, pas avant au moins quelques milliards d'années). En outre, même si RNGCryptoServiceProvider générait de manière erronée des nombres aléatoires dupliqués, cela n'expliquerait pas pourquoi ASP.NET remplace des ID de session valides par de nouveaux (non uniques).

Par intuition, nous avons décidé d'examiner la mise en cache de la sortie. Lorsque OutputCacheModule met en cache les réponses HTTP, il doit prendre soin de ne pas mettre en cache les en-têtes Set-Cookie ; sinon, une réponse en cache contenant un nouvel ID de session connecte tous les destinataires de la réponse en cache (ainsi que l'utilisateur dont la demande a généré cette réponse) à la même session. Nous avons examiné le code source ; la mise en cache de la sortie était activée sur deux pages de Contoso.com. Nous l'avons désactivée. L'application a alors fonctionné pendant plusieurs jours sans un seul incident inter-session. Depuis, elle fonctionne sans erreurs depuis plus de deux ans. Nous avons vu exactement le même cas dans une autre entreprise, avec une application différente et un groupe de serveurs Web différent. Chez Contoso.com, la suppression de la mise en cache de la sortie a fait disparaître le problème.

Microsoft a depuis confirmé que ce comportement vient d'un problème dans OutputCacheModule. Une mise à jour sera peut-être déjà disponible lorsque vous lirez ce document. Lorsque ASP.NET est associé à IIS 6.0 et que la mise en cache en mode noyau est activée, il arrive que OutputCacheModule ne parvienne pas à supprimer les en-têtes Set-Cookie des réponses en cache transmises à Http.sys. Voici la suite d'événements qui entraîne l'apparition de ce bogue :

1.

Un utilisateur qui n'a pas visité le site récemment (et qui n'a donc pas de session correspondante) demande une page pour laquelle la mise en cache de la sortie est activée, mais dont la sortie n'est actuellement pas disponible dans le cache.

2.

La demande exécute le code qui accède à la session nouvellement créée de l'utilisateur, ce qui entraîne le renvoi d'un cookie d'ID de session dans un en-tête Set-Cookie de la réponse.

3.

OutputCacheModule envoie la sortie à Http.sys, mais ne parvient pas à supprimer l'en-tête Set-Cookie de la réponse.

4.

Http.sys renvoie la réponse en cache lors des demandes suivantes, connectant ainsi d'autres utilisateurs à la session.

Quelle est la morale de l'histoire ? Il ne faut pas mélanger l'état de session et la mise en cache de la sortie en mode noyau. Si vous utilisez l'état de session dans une page dans laquelle la mise en cache de la sortie est activée, et que l'application s'exécute sur IIS 6.0, vous devez désactiver la mise en cache de la sortie en mode noyau. Vous avez toujours les avantages de la mise en cache de la sortie, mais étant donné que la mise en cache de la sortie en mode noyau est nettement plus rapide que la mise en cache traditionnelle, la mise en cache n'est pas aussi efficace. Pour plus d'informations sur ce problème, consultez l'article support.microsoft.com/kb/917072.

Vous pouvez désactiver la mise en cache de la sortie en mode noyau pour des pages individuelles en incluant des attributs VaryByParam="*" dans les directives OutputCache de la page, même si cela peut entraîner une forte augmentation des besoins en mémoire. L'alternative la plus sûre consiste à désactiver la mise en cache en mode noyau pour l'application entière, en incluant l'élément suivant dans web.config :

        <httpRuntime enableKernelOutputCache="false" />
      

Vous pouvez également désactiver globalement la mise en cache de la sortie en mode noyau (c'est-à-dire pour des serveurs entiers) avec un paramètre du registre. Pour plus d'informations, reportez-vous à l'article support.microsoft.com/kb/820129.

Chaque fois qu'un client me parle de comportements inexplicables concernant les sessions, je lui demande s'il utilise la mise en cache de la sortie dans une ou plusieurs pages. Si la réponse est oui et que le système d'exploitation hôte est Windows Server 2003, je lui conseille de désactiver la mise en cache de la sortie en mode noyau. Généralement, le problème disparaît. Dans le cas contraire, le bogue se trouve dans le code. Vous êtes averti !

Haut de pageHaut de page

Durée de vie des tickets d'authentification Forms

Pouvez-vous identifier le problème concernant le code suivant :

        FormsAuthentication.RedirectFromLoginPage(username, true);
      

Aussi inoffensif qu'il puisse paraître, ce code ne doit jamais être utilisé dans une application ASP.NET 1.x, à moins que l'application comporte du code permettant de contrecarrer les effets de cette instruction. Si vous n'êtes pas certain de la raison, continuez de lire.

FormsAuthentication.RedirectFromLoginPage effectue deux opérations. Tout d'abord, il redirige un utilisateur vers la page demandée à l'origine lors de la redirection vers la page d'ouverture de session par FormsAuthenticationModule. Ensuite, il émet un ticket d'authentification (généralement transporté dans un cookie, et même toujours dans ASP.NET 1.x) qui permet à l'utilisateur de rester authentifié pour une période prédéterminée.

Le problème est la période. Dans ASP.NET 1.x, le fait de transmettre à RedirectFromLoginPage un deuxième paramètre égal à false provoque l'expiration d'un ticket d'authentification temporaire après 30 minutes par défaut. Vous pouvez changer la période d'expiration en utilisant un attribut d'expiration dans l'élément <forms> de web.config. La transmission d'un deuxième paramètre égal à true entraîne en revanche l'émission d'un ticket d'authentification persistant qui est valide pendant, tenez-vous bien, 50 ans ! C'est une bombe à retardement, car si quelqu'un vole ce ticket d'authentification, il peut accéder au site Web en usurpant l'identité des victimes pendant toute la durée de vie du ticket. Les moyens de voler des tickets d'authentification ne manquent pas : espionner le trafic réseau sur les points d'accès sans fil publics, écrire des scripts inter-site, accéder physiquement au PC d'une victime, etc. Il est donc préférable de transmettre true à RedirectFromLoginPage plutôt que de désactiver la sécurité sur votre site Web. Heureusement, ce problème a été résolu dans ASP.NET 2.0. La page RedirectFromLoginPage actuelle honore l'expiration spécifiée dans web.config pour les tickets d'authentification temporaires et persistants

Une solution consiste à ne jamais transmettre true dans le deuxième paramètre de RedirectFromLoginPage dans les applications ASP.NET 1.x. Mais ce n'est pas très pratique, car les pages d'ouverture de session offrent généralement une case « Conserver ma connexion », permettant aux utilisateurs de recevoir des cookies d'authentification persistants et non temporaires. Une solution alternative est un extrait de code dans Global.asax (ou, si vous préférez, un module HTTP) qui modifie les cookies contenant des tickets d'authentification persistants avant qu'ils ne soient renvoyés au navigateur.

La figure 3 contient un tel extrait. S'il est présent dans Global.asax, ce code modifie la propriété Expires des cookies d'authentification de formulaires persistants, de sorte que les cookies expirent après 24 heures. En modifiant la ligne avec le commentaire « New expiration date », vous pouvez définir l'expiration à la valeur souhaitée.

Vous pouvez trouver étrange que la méthode Application_EndRequest appelle une méthode d'assistance locale (GetCookieFromResponse) pour vérifier les réponses sortantes pour les cookies d'authentification. La méthode d'assistance permet de contourner un autre bogue de ASP.NET 1.1, par lequel un cookie erroné est ajouté à la réponse si vous recherchez un cookie inexistant avec l'indexeur de chaîne de HttpCookieCollection. L'utilisation de l'indexeur entier en tant que GetCookieFromResponse contourne le problème.

Haut de pageHaut de page

État d'affichage : la dégradation silencieuse des performances

D'une certaine façon, l'état d'affichage est la meilleure invention depuis le pain tranché. Après tout, c'est l'état d'affichage qui autorise la persistance des pages et des contrôles entre les postbacks. C'est pour cette raison que vous n'avez pas besoin d'écrire de code pour empêcher le texte d'un contrôle TextBox de disparaître lorsque l'utilisateur clique sur un bouton, comme c'était le cas en ASP classique, ou à réinterroger une base de données et à relier un contrôle DataGrid après un postback.

Mais l'état d'affichage a un inconvénient : lorsqu'il devient trop volumineux, il dégrade les performances. Pour certains contrôles, tels que les TextBoxes, l'état d'affichage est judicieux. D'autres, en particulier DataGrids et GridViews, émettent l'état d'affichage proportionnellement à la quantité d'informations affichées. Je tremble quand je vois un GridView affichant 200 ou 300 lignes de données. Même si l'état d'affichage ASP.NET 2.0 est environ deux fois moins volumineux que dans ASP.NET 1.x, au mauvais contrôle GridView peut facilement réduire de 50 % ou plus la bande passante effective d'une connexion entre un navigateur et un serveur Web.

Vous pouvez désactiver l'état d'affichage pour des contrôles individuels en configurant EnableViewState sur false, mais certains contrôles, en particulier les DataGrids, perdent des fonctionnalités lorsqu'ils ne peuvent pas utiliser l'état d'affichage. Une bien meilleure solution pour dompter l'état d'affichage consiste à le conserver sur le serveur. Dans ASP.NET 1.x, vous pouvez remplacer les méthodes LoadPageStateFromPersistenceMedium et SavePageStateToPersistenceMedium d'une page et gérer l'état d'affichage comme vous le souhaitez. Les remplacements illustrés dans le code de la figure 4 empêchent la persistance de l'état d'affichage dans un champ masqué et permettent sa persistance dans l'état de session. Le stockage de l'état d'affichage dans l'état de session est particulièrement efficace lorsqu'il est associé au modèle de processus d'état de session par défaut, c'est-à-dire lorsque l'état de session est stocké en mémoire dans le processus de traitement ASP.NET. Si l'état de session est stocké dans une base de données, seul les tests permettent de déterminer si la conservation de l'état d'affichage dans l'état de session améliore ou dégrade les performances.

La même technique fonctionne dans ASP.NET 2.0, mais ASP.NET 2.0 offre un moyen plus simple de conserver l'état d'affichage dans l'état de session. Commencez par définir un adaptateur de page personnalisé dont la méthode GetStatePersister renvoie une instance de la classe .NET Framework SessionPageStatePersister :

        public class SessionPageStateAdapter :
        System.Web.UI.Adapters.PageAdapter
        {
        public override PageStatePersister GetStatePersister ()
        {
        return new SessionPageStatePersister(this.Page);
        }
        }
      

Enregistrez ensuite l'adaptateur de page personnalisé comme adaptateur de page par défaut en plaçant un fichier App.browsers tel que le suivant dans le dossier App_Browsers de l'application :

      <browsers>
      <browser refID="Default">
      <controlAdapters>
      <adapter controlType="System.Web.UI.Page" adapterType="SessionPageStateAdapter" />
      </controlAdapters>
      </browser>
      </browsers>
      

Vous pouvez nommer le fichier comme vous le souhaitez, dès lors qu'il comporte une extension .browsers. Ensuite, ASP.NET charge l'adaptateur de page et utilise le SessionPageStatePersister renvoyé pour conserver l'état complet de la page, y compris l'état d'affichage.

Un inconvénient de l'utilisation d'un adaptateur de page personnalisé est qu'il agit globalement pour chaque page de l'application. Si vous préférez conserver l'état d'affichage dans l'état de session pour certaines pages mais pas pour d'autres, utilisez la technique illustrée figure 4. En outre, vous pouvez rencontrer des problèmes concernant cette technique si un utilisateur crée plusieurs fenêtres de navigateur dans la même session.

Haut de pageHaut de page

État de session SQL Server : autre type de dégradation des performances

ASP.NET facilite le stockage de l'état de session dans des bases de données : intégrez simplement un commutateur dans web.config et l'état de session est placé comme par magie dans une base de données back-end. Il s'agit d'une fonctionnalité essentielle pour les applications qui s'exécutent sur des groupes de serveurs Web, car elle autorise chaque serveur du groupe à partager un référentiel commun pour l'état de session. L'activité de base de données ajoutée ralentit les demandes individuelles, mais la perte de performance est compensée par une meilleure évolutivité.

Tout se passe bien jusqu'à ce que vous preniez un moment pour méditer les points suivants :

Même dans une application qui utilise l'état de session, la plupart des pages n'utilisent pas l'état de session.

Par défaut, le gestionnaire d'état de session d'ASP.NET effectue deux accès (un en lecture et un en écriture) à la banque de données de session lors de chaque demande, que la page demandée utilise l'état de session ou non.

Autrement dit, lorsque vous utilisez l'option d'état de session SQL Server™, vous payez le prix (deux accès à la base de données) lors de chaque demande, même pour les pages qui ne font rien de l'état de session. Cela affecte directement le débit du site dans son ensemble.


figure 5. Suppression des accès inutiles à la base de données pour l'état de session

Comment faire ? C'est simple : désactivez l'état de session dans les pages qui ne l'utilisent pas. Il est toujours judicieux de faire cela, mais c'est particulièrement important lorsque l'état de session est stocké dans une base de données. La figure 5 illustre comment le désactiver. Si une page n'utilise pas du tout l'état de session, incluez EnableSessionState="false" dans sa directive Page, comme suit :

        <%@ Page="" EnableSessionState="false" ...="" %>
        

Cette directive empêche le gestionnaire d'état de session de lire et d'écrire dans la base de données d'état de session lors de chaque demande. Si une page lit les données à partir de l'état de session, mais ne les écrit pas (c'est-à-dire qu'elle ne modifie pas le contenu de la session de l'utilisateur), configurez EnableSessionState sur ReadOnly, comme illustré ici :

        <%@ Page="" EnableSessionState="ReadOnly" ...="" %>
      

Enfin, si une page nécessite un accès en lecture/écriture à un état de session, omettez l'attribut EnableSessionState ou configurez-le sur true :

        <%@ Page="" EnableSessionState="true" ...="" %>
      

En dominant l'état de session de cette façon, vous avez la certitude que ASP.NET n'accède à la base de données d'état de session que lorsque cela s'avère réellement nécessaire. La suppression des accès inutiles à la base de données est la première étape pour la création d'applications performantes.

Au passage, l'attribut EnableSessionState n'est pas un secret. Il est documenté depuis ASP.NET 1.0, même si je vois rarement les développeurs en tirer parti. Peut-être cela tient-il au fait que ce n'est pas très important avec le modèle par défaut d'état de session en mémoire. Mais cela devient essentiel avec le modèle SQL Server.

Haut de pageHaut de page

Rôles non mis en cache

L'instruction suivante est fréquemment présente dans les fichiers web.config des applications ASP.NET 2.0 et dans les exemples qui présentent le gestionnaire de rôles ASP.NET 2.0 :

        <roleManager enabled="true" />
      

Telle qu'elle est présentée, cette instruction peut cependant avoir un impact négatif sur les performances. Savez-vous pourquoi ?

Par défaut, le gestionnaire de rôles ASP.NET 2.0 ne met pas en cache les données de rôles. À l'inverse, il consulte la banque de données des rôles chaque fois qu'il doit déterminer à quels rôles un utilisateur appartient. Cela signifie qu'une fois qu'un utilisateur est authentifié, toute page qui utilise les données de rôle (par exemple les pages qui utilisent des plans de site avec sécurité limitée, et les pages auxquelles l'accès est restreint à l'aide de directives URL basées sur les rôles dans web.config) provoque l'interrogation de la banque de données des rôles par le gestionnaire de rôles. Si les rôles sont stockés dans une base de données, cela entraîne un accès supplémentaire à la base de données pour chaque demande, dont vous vous passeriez bien. La solution consiste à configurer le gestionnaire de rôles pour qu'il mette en cache les données de rôle dans des cookies :

        <roleManager enabled="true" cacheRolesInCookie="true" />
      

Vous pouvez utiliser d'autres attributs <roleManager> pour contrôler les caractéristiques des cookies de rôle, par exemple la durée pendant laquelle les cookies doivent rester valides (et par conséquent la fréquence selon laquelle le gestionnaire de rôles revient à la base de données des rôles). Les cookies de rôles sont signés et cryptés par défaut, de sorte que les risques en termes de sécurité, s'ils ne sont pas nuls, sont limités.

Haut de pageHaut de page

Sérialisation des propriétés des profils

Le service de profil ASP.NET 2.0 offre une solution toute prête au problème de persistance de l'état par utilisateur, par exemple les préférences de personnalisation et les préférences de langue. Pour utiliser le service de profil, vous définissez un profil XML contenant les propriétés que vous souhaitez conserver au nom des utilisateurs individuels. ASP.NET compile ensuite une classe contenant les mêmes propriétés et fournit un accès fortement typé aux instances de classe via l'ajout de la propriété de profil à la page.

Les profils sont suffisamment flexibles pour permettre l'utilisation de types de données personnalisés comme propriétés de profil. Mais c'est là que se pose le problème. La figure 6 contient une classe simple nommée Posts, ainsi qu'une définition de profil qui utilise Posts comme propriété de profil. Cependant, cette classe et ce profil entraînent un comportement inattendu lors de l'exécution. Pouvez-vous déterminer pourquoi ?

Le problème est que Posts contient un champ privé nommé _count, qui doit être sérialisé et désérialisé afin d'hydrater et de réhydrater totalement les instances de classe. Mais _count n'est pas sérialisé et désérialisé parce qu'il est privé et, par défaut, le gestionnaire de profils ASP.NET utilise la sérialisation XML pour sérialiser et désérialiser les types personnalisés. Le sérialiseur XML ignore les membres non publics. Par conséquent, les instances de Posts sont sérialisées et désérialisées, mais chaque fois qu'une instance de classe est désérialisée, _count est réinitialisé à 0.

Une solution consiste à rendre _count public plutôt que privé. Une autre consiste à encapsuler _count avec une propriété lecture/écriture publique. La meilleure solution, qui préserve la conception de la classe proprement dite, consiste à marquer Posts comme sérialisable (avec SerializableAttribute) et à configurer le gestionnaire de profils pour qu'il utilise le sérialiseur binaire .NET Framework pour sérialiser et désérialiser les instances de classe. Le sérialiseur binaire, contrairement au sérialiseur XML, sérialise les champs, indépendamment de l'accessibilité. La figure 7 illustre une version fixe de la classe Posts, ainsi que la définition de profil correspondante, avec les changements mis en évidence.

Retenez que si vous utilisez un type de données personnalisé comme propriété de profil, et que ce type de données comporte des membres de données non publics devant être sérialisés afin de sérialiser totalement les instances de type, vous devez utilisez un attribut serializeAs="Binary" dans la déclaration de propriété et vous assurer que le type proprement dit est sérialisable. Sinon, la sérialisation complète n'aura pas lieu et vous perdrez du temps à essayer de déterminer pourquoi le profil ne fonctionne pas.

Haut de pageHaut de page

Saturation des pools de threads

Je suis souvent surpris par le nombre de pages ASP.NET qui effectuent des requêtes de base de données et qui attendent 15 secondes ou plus pour recevoir la réponse$. J'ai aussi vu des requêtes qui prennent 15 minutes ! Ce délai est parfois la conséquence inévitable du volume de données renvoyé ; dans d'autres cas, c'est le résultat d'une mauvaise conception de la base de données. Mais quelle que soit la raison, les requêtes de base de données ou opérations d'E/S longues entraînent une réduction du débit des applications ASP.NET.

J'ai déjà décrit ce problème en détail précédemment, je n'y reviendrai donc pas. Je préciserai simplement que ASP.NET repose sur un pool de threads limité pour traiter les demandes, et que si tous les threads sont occupés à attendre l'exécution de requêtes de base de données, d'appels de services Web ou d'autres opérations d'E/S, les demandes complémentaires doivent être mises en file d'attente jusqu'à ce qu'une opération soit terminée et un thread libéré. Les performances chutent rapidement lorsque des demandes sont mises en file d'attente. Si la file d'attente est pleine, ASP.NET provoque l'échec des demandes suivantes avec des erreurs HTTP 503. Cette situation est inacceptable pour une application de production sur un serveur Web de production.

La solution réside bien entendu dans les pages asynchrones, l'une des fonctionnalités les plus intéressantes (mais les moins connues) d'ASP.NET 2.0. Une demande de page asynchrone commence sa vie sur un thread, mais renvoie ce thread et une interface IAsyncResult à ASP.NET lorsqu'elle commence une opération d'E/S. Lorsque l'opération prend fin, la demande informe ASP.NET via IAsyncResult et ASP.NET extrait un autre thread du pool et termine le traitement de la demande. Aucun thread du pool n'est consommé pendant que l'opération d'E/S a lieu. Cela permet d'améliorer considérablement les performances, en évitant que les demandes d'autres pages (lesquelles n'effectuent pas de longues opérations d'E/S) n'aient à attendre dans une file d'attente.

Vous pouvez en savoir plus sur les pages asynchrones dans le numéro d'octobre 2005 de MSDN®Magazine. Toute page liée aux E/S plutôt qu'aux calculs et dont l'exécution prend plus de quelques secondes est un candidat comme page asynchrone.

Lorsque je parle de pages asynchrones aux développeurs, ils me répondent souvent « c'est très bien, mais je n'en ai pas réellement besoin dans mon application ». Ce à quoi je réponds « Certaines de vos pages interrogent-elles une base de données ? Certaines appellent-elles un service Web ? Avez-vous examiné les compteurs de performances ASP.NET concernant les demandes mises en file d'attente et les temps d'attente moyens ? Même si votre application fonctionne bien, est-il possible que la charge augmente à mesure que vous obtenez de nouveaux clients ? »

En réalité, peu d'applications ASP.NET réelles n'ont aucun besoin de pages asynchrones. Notez-le !

Haut de pageHaut de page

Usurpation d'identité et autorisation ACL

Voici une directive de configuration simple, qui me fait néanmoins froncer les sourcils lorsque je la vois dans web.config :

        <identity impersonate="true" />
      

Cette directive permet l'usurpation d'identité du client dans une application ASP.NET. Elle associe les jetons d'accès représentant les clients aux threads qui traitent les demandes, de sorte que les vérifications de sécurité effectuées par le système d'exploitation agissent par rapport à l'identité du client plutôt que l'identité du processus de traitement. L'usurpation d'identité est rarement nécessaire dans une application ASP.NET ; lorsque les développeurs l'activent, ils le font généralement pour de mauvaises raisons. Voici pourquoi.

Trop souvent, les développeurs activent l'usurpation d'identité dans les applications ASP.NET afin de pouvoir utiliser les autorisations du système de fichiers pour limiter l'accès aux pages. Si Bob n'a pas l'autorisation d'afficher Salaries.aspx, les développeurs activent l'usurpation d'identité afin d'empêcher Bob d'afficher Salaries.aspx, en configurant la liste de contrôle d'accès (ACL, Access Control List) afin de refuser à Bob l'autorisation de lecture. Mais il y a un problème : l'usurpation d'identité n'est pas nécessaire pour l'autorisation ACL. Lorsque l'authentification Windows est activée dans une application ASP.NET, ASP.NET examine automatiquement la liste ACL pour chaque page .aspx demandée et refuse la demande si l'appelant n'a pas l'autorisation de lire le fichier. Il le fait même si l'usurpation d'identité est désactivée.

Il existe cependant des cas dans lesquels l'usurpation d'identité est justifiée. Mais vous pouvez généralement l'éviter par une bonne conception. Par exemple, supposons que Salaries.aspx interroge une base de données afin d'obtenir des informations de salaire qui ne doivent être disponibles que pour la direction. Avec l'usurpation d'identité, vous pouvez utiliser des autorisations de base de données afin de refuser aux autres la possibilité d'interroger les données de salaire. Mais vous pouvez également limiter l'accès aux données de salaire en configurant la liste ACL de Salaries.aspx afin que seuls les membres de la direction disposent de l'autorisation en lecture. Cette dernière approche offre de meilleures performances, car elle évite l'usurpation d'identité. Elle supprime également des accès inutiles à la base de données. Pourquoi interroger une base de données uniquement pour se voir refuser l'accès pour des raisons de sécurité ?

Il m'est arrivé de contribuer à résoudre des problèmes sur une application ASP classique qui redémarrait de temps en temps, en raison d'une consommation non limitée de la mémoire. Un développeur junior avait remplacé une instruction SELECT ciblée par une instruction SELECT *, en oubliant le fait que la table interrogée contenait des images (volumineuses et nombreuses). Le problème était exacerbé par une fuite mémoire non détectée. Mon royaume pour du code géré ! Soudain, une application qui avait fonctionné correctement pendant des années commençait à s'essouffler, parce que les instructions SELECT qui renvoyaient précédemment un ou deux kilo-octets de données en renvoyaient à présent plusieurs mégaoctets. Associé à un contrôle de version inapproprié, cela rendait passionnante la vie de l'équipe de développement, du moins si vous considérez comme ennuyeux le fait de dormir la nuit.

En théorie, les fuites de mémoire classiques ne peuvent pas se produire dans les applications ASP.NET entièrement composées de code géré. Mais une utilisation inefficace de la mémoire a un impact sur les performances, en forçant le nettoyage plus fréquent de la mémoire. Même dans les applications ASP.NET, méfiez-vous des instructions SELECT * !

Haut de pageHaut de page

N'ayez pas une confiance aveugle : profilez votre base de données !

En tant que consultant, je suis souvent amené à intervenir lorsqu'une application n'offre pas les performances attendues. Récemment, mon équipe a été amenée à déterminer pourquoi une application ASP.NET n'offrait qu'un centième du débit (requêtes par seconde) requis par les exigences. Ce que nous avons découvert était typique de ce que nous trouvons dans les applications Web offrant des performances médiocres, avec une leçon pour chacun d'entre nous.

Nous avons exécuté SQL Server Profiler et nous avons observé les interactions entre l'application et la base de données back-end. Dans l'un des cas les plus extrêmes, un simple clic sur un bouton entraînait plus de 1 500 aller-retour vers la base de données. Vous ne pouvez pas créer une application performante de cette façon. Une bonne architecture commence toujours par une bonne conception de base de données. Peu importe l'efficacité de votre code, elle ne sert à rien si votre base de données est mal conçue.

Les mauvaises architectures d'accès aux données ont généralement une ou plusieurs des causes suivantes :

Mauvaise conception de la base de données (généralement conçue par des développeurs, et non des administrateurs de base de données).

Utilisation de DataSets et DataAdapters, en particulier DataAdapter.Update, intéressant pour les applications Windows Forms et autres clients lourds, mais généralement pas idéal pour les applications Web.

Couches d'accès aux données (DAL, Data Access Layers) mal conçues, avec une mauvaise factorisation et trop de cycles processeur pour des opérations relativement simples.

Les problèmes doivent être identifiés pour pouvoir être traités. L'un des moyens d'identifier les problèmes d'accès aux données consiste à exécuter SQL Server Profiler ou un outil équivalent afin de déterminer ce qui se passe en coulisses. L'optimisation des performances passe également par l'examen du trafic entre l'application et la base de données. Essayez, vous serez peut-être surpris de ce que vous découvrirez.

Haut de pageHaut de page

Conclusion

Vous avez vu quelques-uns des problèmes que vous pouvez être amené à rencontrer lors de la création d'applications ASP.NET de production, ainsi que les solutions. L'étape suivante consiste à examiner d'un peu plus près votre propre code et à tenter d'éviter certains des problèmes décrits ici. ASP.NET place la barre moins haut pour les développeurs Web, mais il n'y a pas de raison pour que vos applications ne soient pas rapides et robustes. Prenez le temps de réfléchir afin d'éviter ces erreurs de débutant.

La figure 8 propose une check-list afin d'éviter les pièges décrits dans cet article. Vous pouvez créer une check-list similaire pour les pièges liés à la sécurité. Par exemple :

Avez-vous des sections de configuration cryptées contenant des données sensibles ?

Vérifiez-vous et validez-vous les saisies utilisées dans les opérations de base de données, et encodez-vous en HTML l'entrée utilisée comme sortie ?

Certains de vos répertoires virtuels contiennent-ils des fichiers avec des extensions non protégées ?

Il s'agit là de questions importantes si vous tenez à l'intégrité de votre site Web, des serveurs qui hébergent votre site et des ressources back-end sur lesquelles ils reposent.


Jeff Prosise participe à MSDN Magazine et est l'auteur de plusieurs livres, notamment Programming Microsoft .NET (Programmation Microsoft .Net, éditions Microsoft Press, 2002). Il est également co-fondateur de Wintellect, une entreprise de conseil et de formation en informatique.

Article de juillet 2006 de MSDN Magazine.


Haut de pageHaut de page