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:

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:

Configuration du répertoire NotDotNet dans la console IIS

Reconfigurez ensuite l'application afin que les extensions .asp soient traitées par le même filtre ISAPI que ASP.NET:

L'extension ASP doit être traitée par le filtre ISAPI d'ASP.NET

Le paramétrage du filtre ISAPI

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:

Le projet sera exposé comme composant COM

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:

Le 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 instructions Response.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.Write est 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 (le null de COM n'est pas le null de .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éthode InvokeMember, 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, GetDefaultMembers et GetProperty, contrairement à InvokeMember, ne passent pas par IDispatch et sont donc incapables de fonctionner sur l'objet COM caché dans literal.

La méthode Response.Write tente d'abord d'utiliser literal comme string, puis comme type natif (on utilise IsPrimitive), 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, AspErrorHandler Cette 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 que CreateScript conserve 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.CookieCollection au lieu de for each cookieName in Request.Cookies

Le 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?

Télécharger le projet NotDotNet.

       Auteur : Bertrand Le Roy

       DotNetGuru - Mars 2003