ASP TO ASP.NET migration,a new approach (Reprinted)

Introduction
Even though it’s possible to execute asp and asp.net pages on the same server and even in the same directory,
the communication between the two systems is difficult. For example, the Session and Application objects are
not shared.
This coexistence in quasi-sealed execution containers limits the simplicity one could expect from a migration
from ASP to ASP.NET. Many solutions exist to this problem, but this memo proposes a new one that consists in
the creation of an ASP execution environment inside the ASP.NET infrastructure.
Existing solutions
Renaming pages to .aspx / compatibility mode
The oldest solution has been a part of ASP.NET from the start. It consists in renaming asp pages to give them
the .aspx extension and setting up ASP compatibility mode (<%@ Page aspcompat=true %>) if the pages
use STA mode COM components. In this case, the constraints are heavy because the differences between the
two environments are too important for the ASP code to run without modifications.
The differences are precisely the same that have to be addressed by code migration tools. Among them:
• Update all internal and external inbound hyperlinks to .aspx
• Arrays start at 0
• Replace Request.QueryString(“values”)(1) expressions with Request.QueryString(“values”)(0)
(careful with checkboxes)
• Eliminate html blocs from fonctions and procedures. This code is illegal:
Sub DisplayCount
for i = 1 to 10
%
><%=i%><br><%
next
End Sub

 

• Parentheses become mandatory around procedure arguments, whereas they were forbidden before.
• Visual Basic.NET is strongly typed.
• Parameters are passed by value instead of by reference.
• Objects don’t have default properties: you can’t write RS(“Column”) anymore; you have to write RS
(“Column”).Value.
One of the key reasons not to use this method is that it leads to leaving badly adapted and optimized ASP code
in the .NET environment.
Alternate shared Session objects
Such solutions appeared very soon in the .NET history. They replace the intrinsic Session object with a new
object that stores its values into a database, and that exposes both a COM and a .NET interface. The problem
of this kind of approach is that the ASP and/or the ASP.NET applications have to be modified to use the new
Session object. Furthermore, storing the Session into a database has a performance impact in some cases. The
alternate Session objects are not always clean implementations and can lead to other compatibility or
performance issues.
Microsoft proposes such an object in a MSDN article: How to Share Session State Between Classic ASP and
ASP.NET.
This approach has the great advantage of leaving the ASP code in its natural execution environment, requiring
little change in the code except for the use of the Session. Thus, regressions are not a big risk.
ASP Interop, a new approach
This new approach enables the ASP scripts to execute in an environment that is as close as possible to the
original ASP environment. Almost no code modification is necessary from the ASP and ASP.NET sides. Even
references to the Session can be kept unchanged in the source code.
The MsScriptControl control
To execute an ASP page, IIS uses the Microsoft scripting engine. This engine is one of the simplest ways to
build a scriptable application. All you have to do is instantiate a COM component, MsScriptControl, and feed it
the intrinsic application-specific COM objects that will be accessible to the scripts. For IIS, these objects are
Response, Request, Server, Application and Session.
All we’re doing here is make ASP.NET scriptable the same way ASP made IIS scriptable in the first place.
Instead of exposing the IIS intrinsic objects, we’ll expose .NET intrinsic objects in a COM wrapper.
IIS configuration
For the ASP scripts to be directed to ASP.NET, we need to change the IIS configuration so that the Web
application uses the ASP.NET ISAPI filter for the .asp extension. This is done from the IIS MMC console, in the
web site’s property sheet:

 

Then, we have to tell ASP.NET which HttpHandler to use for the asp extension. This can be made by adding this
line to the web.config or machine.config file:
<add verb=*” path=*.asp” type=”AspInterop.AspHandlerFactory,AspInterop” />

 

Exposing the intrinsic objects
The Session and other intrinsic .NET objects that we want to expose to the scripting engine are unfortunately
not ready to be exposed as COM components. One has to write wrappers around. This also enables us to make
their API closer to their ASP equivalents, so that the original scripts don’t have to be modified in most
situations.
To expose the objects as COM components, we sign the assembly and register it.
The object model
The proxy classes that we expose to the scripting engine are:
• AspApplication
• AspCookie
• AspCookieCollection
• AspError
• AspRequest
• AspResponse
• AspServer
• AspSession
Of course, we expose these classes to the scripting engine under their ASP names (Application, Cookie, etc.).
The particular implementations of each of these objects make them more than simple proxies. For example, the
lack of default properties in the .NET world forbids us to expose an object such as Request.Form at the same
time as a collection and as a string (as far as I know; perhaps .NET COM wrappers enable the specification of a
default property; to investigate). This is one of the very few compatibility issues that we’ll encounter with this
tool.
Please note that exposing the .NET Session object to ASP enables us to provide ASP scripts with the advanced .
NET session modes (database session, or session servers). One could even expose the Cache object to ASP
scripts.
From ASP to VBScript
To make the ASP page executable, we need to transform the mixed HTML and script into pure VBScript. This is
what the CreateScript method does. This method is today far from being optimized or even complete. It just
transforms HTML blocks into Response.Write instructions, while keeping the original line numbers in sync
with the original script (this is to simplify error treatment, as we’ll see). It only understands script blocks
delimited by <% %> and <%= %>, and ignores <script runat=server> blocks, @Page directives and
#includes. It only treats VBScript for now, and would need to be rewritten for Javascript or other script
languages. It is clearly the part of the system that needs the most changes and ameliorations, but it is enough
as a proof of concept.
Finally, it’s worth noting that Response.Write is often used in ASP to transmit potentially null parameters (for
example in database applications). This is a problem for COM Marshaling, that triggers an exception if the
parameter is transmitted as a string (the COM null is not the null .NET object, and is thus inhomogeneous with
string). Reflexion brings the solution to this problem through the InvokeMember method, which addresses
the default property of a COM object if an empty string is provided as the method name to invoke:
Ot.InvokeMember(“”, BindingFlags.GetProperty, null, literal, new Object[] {});

 

GetDefaultMEmbers and GetProperty, as opposed to InvokeMember, do not use IDispatch and are thus
unable to properly access the COM object that hides under the literal that’s been passed to Response.Write.
The Response.Write method first tries to use the literal as a string, then as a native type (using IsPrimitive),
and finally invokes the default property if everything else failed.
Error treatment
To enable the scripting engine’s error messages to display, we must attach a delegate to MSScriptControl to
handle errors: AspErrorHandler.
This function searches errors in the original script as well as in the parsed script the line that caused the error.
It is to make this search easier that CreateScript conserves the original line numbers when interpreting an
ASP page.
Test and Validation
To test and validate the system, I’ve added two ASP test pages (test.asp and exec.asp), and one ASP.NET page
(test.aspx). These pages test most ASP features (including ADO database interaction), and validate the
successful bidirectional communication between ASP and ASP.NET using Application, Session and Cookies.
Test.asp is a very typical ASP script, but it works just as well, and with no modification to the code both with
the original ASP engine and with our new engine.
Limitations
The first limitation comes from the very nature of the system: to work properly, and expose native .NET objects
to the COM scripting engine, all parameters (and thus all output) must use COM marshaling, which is probably
a big performance hit in comparison with the original engine, which stays in the COM world throughout the
whole page lifecycle.
The inexistence of default properties for .NET objects will force the users to change some ASP scripts for them
to work with the new engine. For example, to enumerate the cookies, one will have to write
for each cookiename in Request.CookieCollection

 

instead of
for each cookieName in Request.Cookies

 

This could perhaps be corrected by using non-trivial COM declarations of the .NET objects. It is a question to
investigate.
The engine is for the moment limited to <% %> blocks and to VBScript, and it doesn’t handle the global.asa
file or include files.
I also would have like to implement a cache system for the ASP scripts. But is it feasible to serialize COM
objects into the ASP.NET cache? We could still cache the parsed ASP pages. One would also have to investigate
if it would be interesting for performance to keep a pool of MSScriptControl objects.
It should be noted that the performance issues (that are still to be measured) are probably not a very big issue
as the common scenario for such a component is a temporary one: people will use ASP Interop to progressively
migrate a project to ASP.NET. The performance hit will only last while the migration is not finished and will
even attenuate as the project evolves to completion.
Conclusion
This system is only a proof of concept in its present state. Still, it is by far the easiest migration path from ASP
to ASP.NET. If works surprisingly well, with very few ill effects, and it is still very light and easy to set up.
There a lot to do to make it a tool that can safely be used in a production environment, though.
Source code
The source code can be downloaded from:
http://www.dotnetguru.org/articles/Reflexion/AspVersAspDotNet/images/NotDotNet.zip
An addition to the original paper:
ASP Interop as a way to use legacy ASP classes from .NET
Exactly in the same way that the framework provides a tool to generate proxy classes to call Web Services as if
they were native local .NET objects, we could provide a tool that generates a proxy class to call legacy ASP,
VBScript or Jscript functions and classes from the .NET environment.
This would enable complex migration scenarios that no session-sharing tool can provide.
Here’s a working example of a proxy class that successfully executes an ASP function from .NET:
代码
namespace NotDotNet.AspInterop {
/// <summary>
/// Calling an ASP function from .NET using the Script Control
/// </summary>
public class TestInterop {
private TestInterop() {
}
// Calls the Add function from Add.asp
public static int CallAdd(int a, int b) {
// Prepare the script control
ScriptControlClass scc = new ScriptControlClass();
scc.AllowUI 
= false;
scc.Language 
= "vbscript";
scc.UseSafeSubset 
= false;
// Read the source code
FileStream fs = null;
StreamReader sr 
= null;
string asp = "";
try {
fs 
= new FileStream(HttpContext.Current.Server.MapPath("~/Add.asp"),
FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
sr 
= new StreamReader(fs, Encoding.Default);
asp 
= sr.ReadToEnd();
}
catch (Exception e) {
throw new FileLoadException("Couldn't read""~/Add.asp", e);
}
finally {
sr.Close();
fs.Close();
}
scc.AddCode(asp);
// Add intrinsec variables
AspMarshaler Return = new AspMarshaler("Return");
scc.AddObject(Return.Name, Return, 
true);
AspMarshaler A 
= new AspMarshaler("A", a);
scc.AddObject(A.Name, A, 
true);
AspMarshaler B 
= new AspMarshaler("B", b);
scc.AddObject(B.Name, B, 
true);
// Call the ASP function
scc.ExecuteStatement("Return.Value=Add(A.Value, B.Value)");
// Return the results from the Asp function
return (int)Return.Value;
}
}
}
TestInterop.cs

 

The exemple calls the following trivial ASP function, defined in add.asp:
Function Add(ByVal a, ByVal b)
Add 
= a + b
End Function

 

The code that calls this function from .NET is in testadd.aspx.cs and is very simple, similar to a Web Service
call:
private void btnAdd_Click(object sender, EventArgs e) {
int a = int.Parse(tbA.Text);
int b = int.Parse(tbB.Text);
lblResult.Text 
= TestInterop.CallAdd(a, b).ToString();
}

 

The proxy class is defined in TestInterop.cs. The interesting part of the class is the code that prepares the
parameter as intrinsic COM objects for the script engine:
代码
// Add intrinsec variables
AspMarshaler Return = new AspMarshaler("Return");
scc.AddObject(Return.Name, Return, 
true);
AspMarshaler A 
= new AspMarshaler("A", a);
scc.AddObject(A.Name, A, 
true);
AspMarshaler B 
= new AspMarshaler("B", b);
scc.AddObject(B.Name, B, 
true);
// Call the ASP function
scc.ExecuteStatement("Return.Value=Add(A.Value, B.Value)");
// Return the results from the Asp function
return (int)Return.Value;

 

The AspMarshaler object is a very simple object that has a name and a value property, and is exposed as a
COM object to the scripting engine. The parameters and the return value are exposed as intrinsic objects to the
scripting engine so that it can transmit the values of the .NET variables to the ASP function.
The scripting engine provides all the necessary data about the contents of a given script file (a feature similar
to reflection) to build an automated proxy-generating tool.
I think this migration scenario has not even been considered by our users, even though it would be an
extremely easy one.


 

源PDF文件: asp2aspnet.pdf

asp2aspnet

 

示例代码:  NotDotNet.zip

 

posted on 2010-06-12 18:11  小呆也行  阅读(362)  评论(0编辑  收藏  举报

导航