Mark is a senior enterprise architect for a consulting company. He can be contacted at email@example.com.
Making the appropriate selection of an ASP to ASP.NET migration strategy is not always clear cut. As a developer, you would probably be tempted to rewrite the entire web application in Microsoft .NET from scratch. However, your manager might not be as enthusiastic as you are to this idea. The application is already in production and satisfies the requirements. Why follow a more risky and costly path if it is possible to start extending the application in .NET and at the same time preserving an investment in the legacy ASP code? Especially if a complete migration can be achieved gradually in a number of evolutionary migration steps, with every step resolving a concrete migration-justifiable problem.
Migration is not necessarily an "all or nothing" proposition. ASP and ASP.NET can run side-by-side (coexist) at the same time. A web site or web application within a site can contain both ASP and ASP.NET pages. Legacy code can be replaced later in one shot, or gradually as necessary, or never replaced at all. The side-by-side approach has its own pros and cons; see Table 1.
It is important to emphasize that even with a minimalist "do not touch legacy code" strategy, some changes to this code must be done. They are:
- Unpredicted small future updates that are tightly coupled with legacy code and are preferred, compared to the code migration option. In cases other than minor updates, one of the migration strategies is preferred.
- Changes driven by the decision to expose legacy code to .NET code.
- Changes driven by the decision to access .NET code from legacy code.
- Changes that may be necessary to keep both the ASP and ASP.NET parts of an application alive in case of a possible timeout.
- A legacy code update for ASP and ASP.NET state synchronization.
Legacy code can be migrated gradually, while applying migration strategies and running legacy code for a long period of time until a complete migration is achieved. In this case, all problems and benefits of the side-by-side approach continue to exist for the duration of this process.
The idea behind the side-by-side approach relies on the assumption that legacy code and .NET code implement different parts of the application with low coupling between them. There are three possible solutions:
- If common functionality is already incorporated as a COM object in legacy code, and has a proper interface that fits or can be easily adapted to the new architecture of the .NET application, then it can be exposed to .NET code by the Runtime Callable Wrapper (RCW).
- If common functionality is already incorporated as a mix of scripts and/or COM objects and a significant amount of work is necessary to expose it to the .NET application, then this functionality can be removed from legacy code, implemented in the .NET application code, and exposed to legacy code as a COM Callable Wrapper (CCW).
- If removing common functionality from legacy code and replacing it with CCW invocations leads to serious legacy code shuffling, then common functionality duplication can be an option. This decision has to be made while clearly understanding the consequences of maintaining duplicated code only for a certain period of time, until the complete migration process is finished.
Figure 1 illustrates legacy application functionality exposed to the .NET part of a side-by-side application. Note that this article discusses logical layers and not physical tiers of application deployment. Figure 2 illustrates .NET application functionality exposed to the legacy part of a side-by-side application.
The side-by-side approach has its own problem that does not exist for any one ASP or ASP.NET application separately: state synchronization between ASP and ASP.NET environments. ASP and ASP.NET have different state management. This means that a custom application and session state management synchronization has to be developed and minimal legacy code changes have to be made.
There are two major ways to synchronize state in the side-by-side scenario:
- Client-based synchronization.
- Server-based synchronization.
With client-based synchronization, the application and session states ("state") can be shared by passing state data through the browser. This way, the state is passed from page to page with request/response data. The possible methods are:
- Passing state using cookies.
- Passing state in URL strings (URL munging).
- Passing state using hidden form fields.
If a legacy application uses one of these methods to pass state between pages already, then no adjustments in legacy code are needed. Otherwise, if intrinsic Application or Session objects are used, then the object's state can be passed back and forth between ASP and ASP.NET pages using one of the three methods just mentioned.
The first two options have more restrictions, such as the size of the state and disabled cookies, but do not differ conceptually from the last one. Figure 3 illustrates client-based state synchronization. Table 2 lists the pros and cons of client-based synchronization.
The client-based synchronization approach can be implemented by developing two intermediate pages that will sit between ASP and ASP.NET pages involved in client-based synchronization. These pages, named "ClientBasedSync.asp" and "ClientBasedSync.aspx," are in Figure 4. Here, the ASP page uses Server.Transfer to the first intermediate ASP page ClientBasedSync.asp, instead of redirecting directly to the destination ASP.NET page. The ClientBasedSync.asp generates a temporary form on the fly, placing state variable names/value pairs into its hidden fields, and redirects it to the second intermediary ASP.NET page ClientBasedSync.aspx. This page assigns state variables of the ASP.NET part of the application and uses Server.Transfer to the destination ASP.NET page.
Notice, however, that only one trip to the client's browser is needed. A similar approach works in another synchronization directionfrom an ASP.NET to an ASP client-based synchronization implementation. If state contains more complex types, not simple strings or numbers, then they have to be serialized to be understandable by both environments.
With server-based synchronization, the state can be shared by passing data through a common location at the server side. This location can be a:
- Dedicated server process memory space.
ASP.NET supports both options, but classic ASP does not support them. ASP.NET saves the state in a proprietary binary format that can be changed in the future. The ASP.NET state cannot be directly instantiated from classic ASP. This makes it difficult to use an ASP.NET state from an ASP part of the application. The better solution will be to rely on some custom common state format, which both ASP and ASP.NET can access and understand. In this case, a custom server-side synchronization has to be developed in-house or third-party software must be acquired.
The replacement of built-in ASP and ASP.NET state management with a custom one should follow the dictionary pattern by manipulating key/value pairs:
CustomStateObject(theKey) = theValue
This lets you easily replace the native ASP and ASP.NET state implementation by preserving the nature in which both of them access state. Later, after complete migration, this custom solution is easily switched to the native ASP.NET state implementation. Figure 5 shows two possible ways of accessing a common state from legacy code: through the callable wrapper and directly. By accessing state through the CCW, the common code is not duplicated, but an interoperability overhead exists. Table 3 lists the pros and cons of server-based synchronization.
Static versus Dynamic Synchronization
There are two ways to synchronize state that can be achieved by using any one of client-based or server-based synchronization:
- Static synchronization.
- Dynamic synchronization.
Static synchronization can be utilized if Application and/or Session states are updated by only a small percentage of web site pages. This presumes the static nature of the web site state. The static state nature opens a way of saving on state synchronization. Instead of synchronizing state on every new page request, the state can be synchronized while leaving only the pages that update the state.
With static synchronization, both ASP and ASP.NET states have to be constantly in sync and it is the developer's responsibility to maintain them by not forgetting to update the state. Update the ASP.NET state while leaving an ASP page that updated the ASP state and arriving at the ASP.NET page. For the opposite direction, update an ASP state, while leaving an ASP.NET page that updated the ASP.NET state and arriving at the ASP page.
State for static synchronization has to be created in Application_OnStart and Session_OnStart event handlers for both global.asa and global.asax files. This gives a synchronous start to both ASP and ASP.NET components of a side-by-side application.
Static synchronization can be achieved easily by passing state in hidden fields with client-based synchronization. State can also be passed, using server-side synchronization, through the database to all pages, called by the updating state page. Table 4 lists the pros and cons of static synchronization.
Dynamic synchronization has to be used if a significant amount of pages update the state. In this case, there are no performance gains because the state has to be synchronized constantly. Dynamic synchronization should occur automatically on loading and unloading every page, freeing you from adding synchronization code manually. By replacing ASP and ASP.NET states with custom state management, the same state storage is synchronously used by both. Table 5 lists the pros and cons of dynamic synchronization.
The side-by-side approach lets legacy code and new .NET code coexist for a period of time. Even if the existing legacy code is completely satisfactory, in many cases it is better to relocate some common functionality into a new .NET part of a web site.
Migration ranges from local to complete migration of the application as a whole. Different strategies can be applied at different stages of this migration process, ending with a complete migration or the end of application support, whichever comes first.
A partial migration interoperability plays an important role in connecting the unmanaged and managed worlds. RCW and CCW are two major ways to interoperate. The unmanaged code in native Win32 DLLs can also be called through interoperability, known as "Platform Invocation" (P/Invoke). However, the .NET Framework does not support calling from Win32 DLLs into .NET managed code. To call directly from unmanaged to managed code, COM interoperability must be used.
The main migration strategies are local migration, horizontal migration, vertical migration, and rewrite and optimization.
Local migration is the migration of a cohesive part of one of the legacy application layers. Underlying layers' code, tightly coupled with this part, can also be migrated. In most cases, it will be a migration of some part of a business layer, with a corresponding data layer part. Another option includes migrating part of a presentation layer, which is based on complex business logic, left in legacy code. Figure 6 illustrates these options.
Some of the reasons for local migration can be:
- Common business functionality for legacy and .NET parts of an application.
- Performance problems in legacy code.
- Part of a legacy application that has to be improved and/or extended.
- Part of a legacy application that will be used by other applications.
Local migration implies calling between legacy unmanaged code and .NET managed code through the interoperability layer (if no duplication is selected). The RCW and CCW, depending on the direction of invocation, translate data between two environments. Some blittable data types, such as integers, floats, and so on, do not require translation. Nonblittable data types, such as strings, require translation. Nonblittable data-type conversion overhead affects performance.
In many cases, this performance overhead is negligible compared to the amount of work the concrete component (COM or .NET) is doing. If, however, a lightweight component is accessed in a chatty fashion, then this overhead can be significant. Setting and getting different component properties multiple times could have a performance impact, since every call crosses an interoperability boundary.
Another problem can appear if the existing COM interface does not satisfy a .NET client for reasons other then performance; for example, when it has to be redesigned.
Both problems can be resolved by creating a Custom Managed Wrapper (CMW) in front of the RCW. The CMW allows exposure of an existing COM component to a .NET client by including the necessary additional code into this managed adapter. The CMW consumes the RCW internally and delegates most of the calls to COM through it. The legacy code can continue accessing the COM component directly without any change.
CMW enables to take advantage of:
- Movement of COM object functionality into the CMW and elimination of chatty access through an interoperability boundary to improve performance.
- Parameterized constructors.
- Static methods.
- Complex data conversions. For example, between an ADO.NET dataset and an ADO recordset.
- Gradual movement of more and more functionality into the CMW, improving performance and approaching a complete component migration without affecting the .NET client.
- Design better interface.
- Movement of a remoting boundary from DCOM to .NET Remoting, if such a boundary exists. This allows closing DCOM ports in the firewall.
Horizontal migration involves migration of an entire layer of the application. Figure 8 illustrates horizontal migration. Horizontal migration can also be done in pairs: business and data layers, for example. Horizontal migration can imply additional cost and time for mapping data between layers. For example, migrating the data layer and keeping other layers as-is requires conversion between an ADO.NET dataset and an ADO recordset. Table 7 lists the pros and cons of presentation-layer migration.
To transparently replace business layer COM components with .NET classes, without affecting the legacy presentation layer, GUIDs and/or ProgIds of these COM components must be maintained. The interoperability assemblies have to be deployed. Table 8 lists the pros and cons of business-layer migration.
Data-layer COM component migration can be done with the same approach, and in general, repeats the pros and cons of the business-layer COM components migration. There is no need for ADO recordset and ADO.NET dataset conversion if layers were optimized to consume the ADO.NET DataSet and/or DataReader.
Vertical migration involves the migration of an application portion vertically through all application layers. This involves identifying a logical piece of a web application having minimal interaction with other pieces and migrating it. Both ASP code and COM components, implementing rather independent pieces of application functionality, are potentially good candidates for vertical migration. The remaining functionality of the site runs side-by-side with the vertically converted part and can be migrated later, based on project schedules and resources.
The vertically migrated part of the app communicates with the legacy code using .NET interoperability. This interoperability should be minimal, reflecting low coupling between the vertically migrated part and the rest of the application. Figure 9 illustrates a vertical migration, and Table 9 lists the pros and cons of vertical migration.
Rewrite and Optimization
The rewrite/optimization can be done after migration, in parallel with it or instead of it. This lets you gain such .NET benefits as: layered application architecture (in case the legacy application did not have layered architecture prior to migration), caching, separation of HTML views from code behind, server controls and data binding, server events, view state, the .NET exception handling, improved developer productivity, improved maintainability, and so on.
Here are some common migration guidelines and suggestions:
- For simple sites with low application- and session-update rates, client-based static synchronization can be used.
- If application and session state update rate is high, consider using horizontal migration of ASP pages versus custom state-management utilization.
- Move presentation layer inline code to code behind for cleaner MVC architecture separation. By the way, you will receive IntelliSense benefits.
- Complex COM object hierarchies in a business layer should be migrated as a unit to decrease the number of interoperability calls.
- Vertical migration minimizes the work involved in achieving interoperability with ADO.
- Perform code-path analysis before local or vertical migration. The ASP pages and components that are used during the user's single interaction are considered a code path. Distinct code paths are a natural place to consider isolating a piece for local or vertical migration.
- Code paths that share components are good candidates for vertical migration, minimizing code interoperability required.
- Minimize touch point between ASP and ASP.NET for vertical migration.
- Consider the horizontal migration of a business layer if it is well separated from other layers and all its COM objects are strongly coupled.
- Migrate and rewrite read-only functionality first. By doing this, you can quickly take advantage of data binding and caching capabilities of ASP.NET or expose data as web services.
- Vertical migration provides a good test for a new .NET web-application design.
- Rearchitecture and rewrite from scratch if the goal is to achieve a high strategic value and make the best use of the .NET Framework.
The migration lifecycle contains four major steps that are repeated iteratively if necessary:
- Code analysis, deciding what will be in the scope for the current migration stage, and designing.
- Rewrite and optimization. Refactor migrated code and optimize it. Rewrite from scratch if necessary.
- Testing and deploying.
In horizontal migration, these steps are iterated for every layer. In vertical migration, they are iterated for every functional module. In local migration, they are iterated for local functionality. Local migration can be combined with a horizontal or vertical migration strategy. Figure 10 illustrates the dynamics of a migration process.
Migration from ASP to ASP.NET can be done in different ways. Understanding possible migration strategies, their drawbacks and benefits, helps software architects and developers in selecting an optimal migration strategy and its implementation.