![]()
Migration ASP vers ASP.NET : une nouvelle approche par Bertrand Le Roy
S'il est possible d'exécuter des pages ASP et des pages ASP.NET sur le même serveur (et même dans le même répertoire), la communication entre les deux système est difficile. Les objets Session et Application, par exemple, ne sont pas partagés: une valeur stockée sous ASP ne peut être récupérée sous ASP.NET, et vice-versa.
Cette cohabitation dans des conteneurs quasi-étanches des deux infrastructures d'exécution limite la simplicité de migration d'une application ASP vers ASP.NET. De nombreuses solutions existent toutefois à ce problème. Cet article en propose une tout à fait nouvelle (à notre connaissance) en créant un environnement d'exécution ASP au sein de l'infrastructure ASP.NET.
Les solutions actuelles
Renommer ses pages ASP en .aspx
La solution la plus ancienne fait partie intégrante d'ASP.NET et consiste simplement à renommer les pages ASP pour leur donner l'extension .aspx, et à mettre en place le mode de compatibilité ASP (
<%@Page aspcompat=true%>) si les pages font appel à des composants COM en mode STA (Single Threaded Apartment, typiquement des composants VB, ce qui a un impact sur les performances de l'application). Dans ce cas, les contraintes sont nombreuses car les différences entre les deux environnements sont trop importantes pour que le code ASP s'exécute sans modifications. Parmi celles-là, citons:
- La mise à jour de tous les liens du site (ainsi que ceux de sites externes pointant vers des pages du site).
- Le passage des tableaux en index 0.
- Le remplacement des expressions du type
Request.QueryString("values")(1)parRequest.QueryString.GetValues("values")(0)(attention aux checkboxes)- La disparition des blocs html dans les fonctions et procédures:
Sub DisplayCount
for i=1 to 10
%><%=i%><br><%
next
end Sub
est illégal.- Les parenthèses sont obligatoires autour des arguments de procédures, alors qu'ils étaient auparavant interdits.
- Le Visual Basic.NET est fortement typé.
- Les paramètres sont passés par défaut par valeur, et plus par référence.
- Les objets n'ont plus de propriétés par défaut (
RS("Colonne").Valuedoit remplacerRS("Colonne"), par exemple).Pour plus de détails, je vous invite à consulter cet article MSDN: Migrating to ASP.NET: Key Considerations. Mais une des meilleures raisons de ne pas faire appel à cette solution est qu'elle conduit à entériner en environnement .NET du code ASP très mal adapté et optimisé pour ce nouvel environnement.
Utiliser un objet Session alternatif utilisable par les deux environnements
Des solutions de ce type sont apparues très tôt après la sortie de .NET. Elles consistent à remplacer l'objet Session intrinsèque par un nouvel objet stockant ses valeurs en base de données, et exposant une interface COM et une interface .NET. On peut même être tenté d'exposer vers ASP la session ASP.NET stockée sous SQL Server via un objet COM. Le problème de ce type d'approche est que les appplications ASP et ASP.NET doivent être modifiées pour remplacer toute allusion à la session par le nouvel objet. De plus, les implémentations ne sont pas toujours très propres, et le stockage de la session en base de données n'est pas optimal dans toutes les situations. Une implémentation très propre de ce principe a toutefois été récemment présentée par Microsoft dans cet article: How to Share Session State Between Classic ASP and ASP.NET.
Cette approche présente l'avantage de conserver l'exécution des scripts ASP dans leur environnement natif. Par conséquent, les régressions sont rares côté ASP. La pénalité à payer avec cette méthode est que toutes les allusions à la session doivent en général être modifiées, dans les deux mondes. Il se peut de plus que des différences gênantes apparaîssent entre la session native et la session alternative. Enfin, le stockage de la session en base de données peut être jugée comme trop pénalisant ou trop lourd.
NotDotNet
L'approche que je propose ici est expérimentale, et je n'encourage donc pas en l'état sa mise en place en environnement de production. Elle est toutefois prometteuse puisqu'elle permet de conserver l'exécution des scripts ASP dans leur environnement d'exécution d'origine, sans nécessiter (dans la majorité des cas) de modifications du code, ni du côté .NET, ni du côté ASP.
Le contrôle MsScriptControl
Pour exécuter une page ASP, IIS utilise le moteur de script de Microsoft. Ce moteur constitue sans doute la manière la plus simple de rendre une application scriptable. Il suffit pour cela d'instancier un composant COM, MsScriptControl, de lui fournir des objets COM intrinsèques spécifiques de l'application (pour IIS, Response, Request, Server, Application, Session) grâce à la méthode
AddObject, et un script à exécuter grâce àExecuteStatement.Nous allons ici tout simplement rendre ASP.NET scriptable grâce à ce même contrôle. Les objets que nous allons exposer sont bien sûr Response, Request, Server, Application et Session, mais cette fois-ci, ce sont les objets .NET qui vont être exposés aux scripts ASP. Nous hébergeons donc le moteur de script au sein de l'environnement .NET, en lui exposant des objets .NET sous une enveloppe COM.
Configuration IIS et HttpHandler
Pour qu'ASP.NET puisse prendre en charge les scripts ASP, deux opérations sont nécessaires. Il faut tout d'abord déclarer sous IIS que l'extension .asp doit être dirigée vers le filtre ISAPI d'ASP.NET. Pour cela, sélectionnez le dossier dans lequel vous souhaitez utiliser NotDotNet dans la console d'administration IIS et faites-en une application:
Reconfigurez ensuite l'application afin que les extensions .asp soient traitées par le même filtre ISAPI que ASP.NET:
Il nous suffit maintenant d'écrire un HttpHandler, AspHandler, capable d'interpréter les scripts ASP. Notre Handler sera fabriqué à chaque requête par une IHttpHandlerFactory très simple, AspHandlerFactory. Enfin, la factory est déclarée à ASP.NET dans le web.config par la ligne suivante dans la section httpHandlers:
<add verb="*" path="*.asp" type="NotDotNet.AspHandlerFactory,NotDotNet" />Importation du moteur de script
Pour pouvoir utiliser le moteur de script depuis notre application .NET, il nous suffit d'ajouter une référence COM vers "Microsoft Script Control". Visual Studio.NET génère pour nous une assembly "proxy" enveloppant le composant COM.
Exposition des objets intrinsèques
Lors de mes premiers essais, j'ai tenté d'exposer directement les objets .NET au moteur de script. Malheureusement, ces objets ne sont pas faits pour être exposés en tant que composants COM. Il est donc nécessaire d'envelopper ces objets dans un ensemble de classes "proxy" regroupées dans une DLL que nous exposerons sous COM. Pour exposer une assembly comme composant COM, il est nécessaire de la signer avec un nom fort (strong name). Le .NET Framework SDK nous fournit un utilitaire, sn.exe, permettant de générer des clés. Nous déclarons ensuite cette clé d'une part dans AssemblyInfo.cs:
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile(@"C:\Inetpub\wwwroot\NotDotNet\NotDotNet.snk")]et d'autre part dans les propriétés du projet Visual Studio.NET comme clé pour l'enveloppe ActiveX / COM de l'assembly:
L'implémentation de ces classes "proxy" sera aussi l'occasion de créer un modèle objet le plus proche possible de l'ancien modèle ASP, afin que les scripts existants aient le moins possible besoin de modifications pour tourner dans le nouvel environnement.
Le modèle objet
Les classes proxy directement ou indirectement exposées au moteur de script sont:
AspApplication AspCookie AspCookieCollection AspError AspRequest AspResponse AspServer AspSessionLe code permettant d'exposer ces classes est le suivant:
AspServer aserv=null;
AspRequest areq=null;
AspResponse aresp=null;
AspApplication aapp=null;
AspSession ases=null;
ScriptControlClass scc=null;
string script=CreateScript(context.Request.PhysicalPath, out asp);
scc=new ScriptControlClass();
scc.AllowUI=false;
scc.Language="vbscript";
scc.UseSafeSubset=false;
areq=new AspRequest(context);
scc.AddObject("Request", areq, true);
aresp=new AspResponse(context);
scc.AddObject("Response", aresp, true);
aserv=new AspServer(context, scc);
scc.AddObject("Server", aserv, true);
aapp=new AspApplication(context);
scc.AddObject("Application", aapp, true);
ases=new AspSession(context);
scc.AddObject("Session", ases, true);L'implémentation de chacun de ces objets comporte quelques particularités qui en font plus que de simples proxys. En particulier, .NET ne permettant pas d'avoir une propriété par défaut sur chaque objet, il nous est impossible d'exposer un objet comme
Request.Formà la fois comme collection et comme chaîne de caractères. C'est là l'origine des très rares icompatibilités du système avec le moteur ASP d'origine et ce qui nécessitera quelques modifications du code ASP.Remarquons en passant que la session exposée étant la session native ASP.NET, il est tout à fait possible de faire bénéficier aux scripts ASP des modes de persistance de session par serveur de session ou en base de données. Il n'est pas exclu non plus de profiter du cache ASP.NET.
D'ASP vers VBScript
Il nous faut pour finir transformer le script ASP qui mixe HTML et code VBScript en code VBScript pur. C'est le rôle de la méthode statique
CreateScript. Cette méthode est aujourd'hui loin d'être optimisée ou complète. Elle se contente de transformer les blocs HTML en instructionsResponse.Write, en conservant les lignes du script original (pour simplifier le traitement d'erreurs, comme on le verra plus loin). Elle ne traite que les blocs de scripts délimités par<% %>ou<%= %>, et n'interprète pour l'instant ni les blocs<script runat=server>, ni les directives@Page, ni les#includes. De plus, elle ne traite que le VBScript, et aurait besoin d'être réécrite pour d'autres langages comme le javascript. C'est donc la partie du système qui nécessite le plus d'améliorations, mais elle suffit à valider le principe.Enfin, il me faut signaler une dernière particularité de la méthode:
Response.Writeest souvent utilisé en ASP pour transmettre des paramètres potentiellement égaux ànull(par exemple dans des interfaçages de bases de données). Cela pose un problème au Marshaling COM, qui provoque une exception si le paramètre est transmis en tant que string (lenullde COM n'est pas lenullde .NET et n'est donc pas homogène avec string). Merci à David Ebbo de m'avoir fourni la solution à ce problème, qui consiste à utiliser la réflection et en particulier la méthodeInvokeMember, qui adresse la propriété par défaut d'un objet COM lorsqu'on lui fournit une chaîne vide comme nom pour la propriété à invoquer:
ot.InvokeMember("", BindingFlags.GetProperty, null, literal, new Object[] {})Curieusement,
GetDefaultMembersetGetProperty, contrairement àInvokeMember, ne passent pas par IDispatch et sont donc incapables de fonctionner sur l'objet COM caché dans literal.La méthode
Response.Writetente d'abord d'utiliserliteralcomme string, puis comme type natif (on utiliseIsPrimitive), et enfin invoque la propriété par défaut.Traitement d'erreurs
Afin de permettre l'affichage des messages d'erreur du moteur de script, nous devons attacher à MSScriptControl un delegate vers une fonction de traitement d'erreur,
AspErrorHandlerCette fonction recherche dans le script original et dans le script parsé la ligne qui a causé l'erreur, afin d'afficher un message d'erreur proche dans sa forme à ce qu'expose le moteur ASP d'origine. C'est pour faciliter cette recherche queCreateScriptconserve les numéros de ligne dans son interprétation des pages ASP.Test et validation
Pour tester et valider le fonctionnement du système, j'ai ajouté au projet deux fichiers ASP, test.asp et exec.asp, et un fichier ASP.NET, test.aspx. Ces différents fichiers testent la plupart des fonctionnalités d'ASP (y compris l'interfaçage de bases de données par ADO), et valident la transmission d'informations entre ASP et ASP.NET par l'Application, la Session et les cookies.
Test.asp est un script ASP assez typique, mais fonctionne aussi bien avec NotDotNet qu'avec le moteur ASP d'origine.
Limitations
La première limitation est dûe à la nature même du système: pour fonctionner, et exposer au moteur de script COM les objets .NET, tous les passages de paramètres font appel au marshaling COM, ce qui est assez pénalisant en principe au niveau des performances par rapport au moteur d'origine qui reste dans le monde COM du début à la fin de la chaîne.
L'inexistance de propriétés par défaut sous .NET oblige à modifier certains scripts ASP. Par exemple, pour énumérer les cookies, on écrira
for each cookieName in Request.CookieCollectionau lieu defor each cookieName in Request.CookiesLe moteur est pour l'instant limité aux blocs <% %> et au VBScript, et il ne traite pas le global.asa.
J'aurais aimé implémenter un système de cache des scripts ASP. Malheureusement, les objets COM comme MSScriptControl ne sont pas sérialisables et ne peuvent de ce fait pas être mis dans le cache ASP.NET. La mise en cache devrait se limiter aux scripts transformés en VBScript, ce qui est beaucoup moins intéressant. La seule façon, semble-t-il, d'implémenter un cache, serait un mécanisme propriétaire à base par exemple d'une HashTable statique. Cette partie reste à implémenter, mais dans nos très modestes tests, le parsing des scripts ne nous a pas paru être très lourd en termes de charge CPU.
Toutefois, il faut relativiser ces inconvénients dans la mesure où un tel système ne se justifie que transitoirement, le temps de migrer totalement une application sous ASP.NET.
Conclusion
Le système proposé ici n'est que l'exploration d'une nouvelle voie pour migrer progressivement une application ASP vers ASP.NET. Cela fonctionne remarquablement bien, avec très peu d'effets secondaires, et sans mettre en oeuvre de lourdes infrastructures (pas de base de données pour la session), mais il reste beaucoup à faire pour obtenir un système d'une robustesse prouvée et adaptée à une utilisation en environnement de production.
Toutefois, si vous êtes intéressés par le problème et séduits par cette approche, je vous invite à jouer avec le système dans un environnement de tests, et à me faire part de vos remarques, voire à me communiquer vos améliorations. Si la communauté le souhaite, pourquoi ne pas démarrer un projet sur SourceForge?
Auteur : Bertrand Le Roy
DotNetGuru - Mars 2003