Optimizing your Migrated Site for ASP.NET
Overview
In this article you will learn how to optimize an ASP.NET site ported from ASP using the ASP to ASP.NET Migration Assistant. In a previous article, the Downhill Bikes ASP application was migrated to ASP.NET using the Migration Assistant. The migrated application gained from some benefits of the .NET Framework, but there are still many optimizations that can be made to fully utilize the power of ASP.NET and the .NET Framework. This article covers the optimization of the Downhill Bikes application after it has been ported to ASP.NET.
Code Sample:
The Downhill Bikes code sample and the ASP to ASP.NET Migration Assistant for this paper are available from http://www.asp.net/migrationassistants/asp2aspnet.aspx
Why Optimize?
The migrated ASP.NET version of Downhill Bikes works, so before proceeding with the optimization it is important to understand why you might want to consider optimizing a site after porting it to ASP.NET. The Downhill Bikes application is a good example of several key areas in which optimizing a ported site is beneficial.
COM Interop costs. ASP.NET supports the use of COM components but there is a performance penalty whenever you move data between the .NET world and the COM world. The ideal optimization is to move the logic encapsulated in the COM components into .NET components. When this is not possible (too expensive, third-party components, etc.), you should consider writing a COM wrapper that does as much work as possible each time you move into the COM world. Deciding when to rewrite COM components requires an evaluation of the performance benefit versus the cost to rewrite. A low volume intranet site, for example, might not gain any perceivable performance benefit while a high volume Internet store might significantly increase the number of requests per second that it can serve.
ADO.NET and Managed Providers. The .NET Framework provides a new database access model called ADO.NET. Replacing the use of ADO COM components with ADO.NET managed components eliminates the type of "chatty" use of COM Interop that can be so expensive.
ADO.NET also provides a number of benefits over ADO. ADO.NET is optimized for working with disconnected data. Connections to data sources are only used long enough to perform the current set of operations and ADO.NET provides a richer feature set than a disconnected ADO Recordset. When working with multiple data sources, even just multiple tables in the same database, ADO.NET provides the robust DataSetclass that can contain multiple DataTableobjects complete with relationships and constraints that can be mapped much closer to the data source than a wide Recordsetgenerated from a JOINquery. DataTableobjects also help avoid problems with queries that are not updateable.
ADO.NET has a further advantage over ADO in that ADO.NET is designed to work with XML, making it easy to communicate with disparate systems as long as they understand XML and to consume XML from many different sources.
ADO.NET uses database "managed providers" to communicate with different types of databases. There is an OleDb managed provider for communicating with a wide range of database systems. The .NET Framework also ships with a managed provider for SQL Server, accessible via the System.Data.SqlClientnamespace. This SqlClientprovider exposes specialized versions of ADO.NET classes such as SqlDataReaderthat are optimized for working with SQL Server 7 and higher. An application such as Downhill Bikes that uses SQL Server can benefit from switching from ADO and OLEDB to ADO.NET and the managed provider for SQL Server.
For a more complete discussion of the benefits of ADO.NET versus ADO, visit http://msdn.microsoft.com.
Built-In Authentication Support. ASP.NET has built-in support for several common types of authentication including Windows authentication, Passport authentication, and Forms authentication. The authentication schemes in ASP.NET are all based on a common model so that authorization checks and other identity-related tasks work in a consistent way no matter which type of authentication was used. Forms authentication is used when you have custom logic to verify the identity of a user (e.g., verify user name and password).
ASP.NET authentication makes it extremely easy to manage authentication and authorization using XML elements in the Web.config configuration file for your Web application. One of the benefits is that you can configure access to different parts of your site in the Web.config file rather than doing explicit authentication and authorization checks on every page of your site. If a user tries to access a restricted page, ASP.NET automatically redirects the user to the login page and then redirects the user back to the requested page after being successfully authenticated.
Server Controls. ASP.NET server controls are server-side components that you include in an ASPX page to render HTML output. When a page is executing on the server, these components can be manipulated programmatically before they generate their output. A simple example is the DropDownList, which would appear in the ASPX markup as:
<asp:DropDownList id="DropDownList1" runat="server"></asp:DropDownList>
In a method for the page, items can be added to this drop-down list programmatically:
DropDownList1.Items.Add("One") DropDownList1.Items.Add("Two")
This server control would then generate a <SELECT>element with two <OPTION>elements.
ASP.NET provides more sophisticated server controls including numerous controls that can be bound to a data source to produce complex blocks of HTML markup. Replacing dozens or hundreds of Response.Writestatements with a few server controls and some data binding logic can greatly increase the maintainability of your application by making it easier to read and modify.
Strongly Typed Code and Early Binding. When the Migration Assistant upgrades an ASP application, it attempts to identify data types and create strongly typed variable declarations. When a data type cannot be identified, a variable of type Objectis created. Objectis the generic base class of all .NET objects. Although it is not equivalent to a Variantin VBScript, it generally serves the same purpose. Many ASP applications, including Downhill Bikes, will contain some generic Objectvariables after being ported by the ASP to ASP.NET Migration Assistant. These Objectvariables are late bound, which means the .NET Framework Common Language Runtime (CLR) must do extra work at runtime to verify methods, properties, etc. Furthermore, if an Objectvariable is actually being used to store a primitive data type such as Integer(called value types in the .NET world), an additional "boxing" cost is paid.
Setting an explicit data type for variables helps avoid late binding and boxing penalties. It also makes code more readable and enables Visual Studio .NET 2003 to provide Intellisense assistance when making modifications to the source code.
What to Optimize First?
Choosing what to optimize first in a migrated application is not necessarily a straightforward decision. Some optimizations will have bigger paybacks than others, but they might be more expensive to introduce, especially if they involve components used system-wide such as a Customer object.
For Downhill Bikes, the optimizations to be made are:
Move code to code-behind classes
Move code from include files to classes
Rewrite COM components as .NET components
Switch to ADO.NET (instead of ADO)
Add databound server controls instead of Response.Writestatements to generate HTML
Add ASP.NET Forms authentication
The COM component that will typically get the most use is the Catalog. The Catalogis used on the default and search pages to retrieve Recordsetobjects that are used to generate HTML. The Catalogcomponent could be rewritten in Visual Basic .NET while continuing to use Recordsetsbut that would not eliminate the use of the ADO COM library. Since the plan is to switch to ADO.NET and to use server controls instead of Response.Writestatements to generate HTML, it makes sense to first rewrite the Catalogas a .NET component using ADO.NET and then to rewrite the default and search pages to use server controls and data binding for displaying catalog items.
Note that this first step is not attempting to rewrite all the COM components at once. Instead, one component is being rewritten and only the affected ASPX pages are being modified. This helps keep the changes manageable and easy to test incrementally. After the first component is successfully rewritten, the remaining components can be rewritten incrementally. You would probably not want to deploy the application after rewriting only some of the components since it is only partially optimized, although in some cases that might be possible and even appropriate.
Optimizing Downhill Bikes
Getting Started
The Visual Studio .NET 2003 project created after the migration was called DownhillBikesMigrated. Before making any optimizations to this application, create a copy of the application to work from:
Copy the source files for DownhillBikesMigrated to a new folder called DownhillBikesOptimized.
Rename DownhillBikesMigrated.vbproj to DownhillBikesOptimized.vbproj.
Rename DownhillBikesMigrated.vbproj.WebInfo to DownhillBikesOptimized.vbproj.WebInfo.
Open DownhillBikesOptimized.vbproj.WebInfo with your favorite text editor and change the Web URLPath to http://localhost/DownhillBikesOptimized/DownhillBikesOptimized.vbproj.
Open Internet Information Services and add a new Virtual Directory. Set DownhillbikesOptimized as the Alias and the DownhillBikesOptimized folder from step 1 as the Directory.
Move Code to Code-Behind Classes
ASP.NET fully supports single-file ASPX pages, but the development experience in Visual Studio .NET 2003 is much richer when using code-behind classes. The ability to compile the code, see all build errors at once in Visual Studio .NET 2003, and step through your code using the Visual Studio .NET 2003 debugger can drastically reduce the effort required to maintain and update an application.
In some cases moving code into code-behind classes is a relatively futile exercise, especially when a lot of output is generated from Response.Writestatements and you do not plan on updating the code. But if you are choosing to take advantage of some of the powerful features in ASP.NET such as server controls (e.g., databound controls) and event handling (e.g., Button1_Click) then using code-behind classes can be an effective strategy for separating code from content. Code-behind classes help make a clear distinction between how a page looks (markup in the .aspx file) and how a page behaves (code in the .vb file).
The ASPX pages created by the Migration Assistant in the Downhill Bikes project are all single-file pages that generate a considerable amount of output using Response.Writestatements. Much of the output is simply layout information such as the table cells used to organize that data. That layout information can be easily moved into the template of a databound server control. The Visual Basic .NET code can be reduced to just a few lines for data binding that can be placed in a code-behind class in order to take advantage of the benefits of code-behind such as better debugging and earlier code compilation.
To convert these pages to use code-behind classes requires defining the code-behind class (which must inherit from System.Web.UI.Page) and then referencing the code-behind class in the ASPX page. After creating a code-behind class, any page level variable declarations must be marked Protectedso they are accessible in the ASPX page. If any server objects created with the <object runat="server"> tag are replaced with page level variables, those variables must also be marked as Protected.
Visual Studio .NET 2003 can automatically create code-behind classes for your ASPX pages if you remove the .aspx file from the project and then add it back into the project. First make sure that the Show All Filesbutton is toggled on in the Solution Explorer.
.gif)
Figure 1. Show All Files in Solution Explorer
Right-click on the file in the Solution Explorer and select Exclude From Project. Then right-click on the file and select Include In Project. Visual Studio .NET 2003 will prompt you to create a class file for the page. If you click Yes, Visual Studio .NET 2003 creates your code-behind class and associates the ASPX page with it by updating the @Pagedirective in the ASPX page.
.gif)
.gif)
Figure 2. Exclude and include the .aspx file via Solution Explorer
Moving the code from a script block into the code-behind class is straightforward because the Migration Assistant groups all variables, subroutines, and functions in one script block at the top of your ASPX page. The entire contents of the script block can be cut from the ASPX page and pasted in the code-behind class.
After pasting the code from the script block into the code-behind class, you should change the page level variable declarations (i.e., "global variables") from Dimto either Privateor Protected, depending on how they are used. Variables that are accessed in the ASPX page must be Protected. Variables that are only accessed in the code-behind class can be marked as Private. For the sake of expediency, you can initially mark all page level variables as Protectedand then revisit those declarations later if necessary.
Next you move any server object tags to the code-behind class to take advantage of early binding. Simply remove the <object>tags from the ASPX page and add variable declarations to the code-behind class. For default.aspx in Downhill Bikes, it is just one variable:
Protected catalog As New DownhillBikesASP.Catalog
The "global variables" from the script block are now page level variables marked as Protected. The catalogobject that was previously created with an object server tag is now declared as a strongly-typed page level variable also marked as Protected. The methods in the code-behind class can now access those page level variables.
In this article, all of the code copied from the script blocks is for interacting with COM components and generating HTML. Since the Downhill Bikes site is going to be further optimized to use .NET business objects, server controls, and data binding to generate HTML, most of this code will become irrelevant and will removed from the final optimized site. In many real world applications that will not be the case and code from script blocks has to be preserved. Therefore the steps required to preserve the code migrated from VBScript are being detailed here even though most of it will be replaced. If the code were not being replaced in the optimization, then the data types for variable declarations would need to be reviewed and several Objectvariables replaced with properly typed declarations (e.g., String).
This process must be repeated for each ASPX page in the project. For Downhill Bikes, the pages are checkout.aspx, default.aspx, login.aspx, search.aspx, and thankyou.aspx (note: thankyou.aspx does not contain any server script so it can be excluded from this process if desired). The steps you need to repeat for each page are:
- Exclude the file from the project
- Include the file in the project
- Allow Visual Studio .NET 2003 to create a class file for the page
- Move the code from the server script block to the code-behind class
- Change page level variable declarations to Protected
- Create page level variables to replace server object tags
After moving the code from ASPX pages to code-behind classes, code that refers to variables, constants, or functions defined in include files will no longer be valid. The Visual Basic .NET code in those include files must be moved to classes or modules (modules are essentially classes with only Sharedmembers).
Move code from include files to classes
The Downhill Bikes site uses include directives to include constants and global functions in several pages. ASP.NET fully supports the include directive. Markup, client script, and server script can all be added via include directives. But even though the include directive is supported, moving code for server-side logic into a class or module that can be accessed from both code-behind classes and ASPX pages is generally more appropriate. You will also find that it is often easier to encapsulate markup and client script in ASP.NET Web user controls than to continue to use include files that are great for sequential processing but less than ideal for an event-driven execution model.
For Downhill Bikes, you must first add a new Moduleto the project and then move the code from _common.aspx to the module. The constants, functions, and Moduledeclaration itself must all be marked as Publicso they are accessible from ASPX pages (which are compiled into temporary assemblies that are separate from the assemblies containing the code-behind classes, modules, and other classes).
The function getCurPagethat has just been moved to the module accesses a server variable via the Requestobject:
page = Request.ServerVariables.Item("URL")
Since this code is no longer being included directly in an ASPX page that has a Requestproperty, the Requestobject would have to be accessed via the current HttpContext:
page = System.Web.HttpContext.Current.Request.ServerVariables.Item("URL")
However, the getCurPagefunction is only ever called from the _verify_login.aspx include file. The _verify_login.aspx include file is used to redirect users to the login page if not properly authenticated. Since the optimized Downhill Bikes site will use ASP.NET Forms Authentication, the _verify_login.aspx file can be removed from the project entirely and the getCurPagefunction in the new module can also be removed.
Any calls from ASPX pages to the constants and functions in the module must now be fully qualified with the namespace and module name. In default.aspx, there are two calls to iif_Renamedthat must be changed. If the root namespace for the project is DownhillBikesMigratedand the module is called BikesModule, then the function call would change to:
DownhillBikesMigrated.BikesModule.iif_Renamed
The file _order.aspx is also used as an include file for Downhill Bikes. This file contains a script block and a server object tag. The general approach for this situation would be similar to that used for _common.aspx: the contents of the script block would be moved into a module. But _order.aspx file is only used by the checkout.aspx page so it makes sense to simply move the contents of the script block into the code-behind class for checkout.aspx and create a new page level variable in checkout.aspx.vb to replace the server object tag:
Protected order As New DownhillBikesASP.Order
Rewrite the Catalog as a .NET Component
At this point, most of the Downhill Bikes functionality has been moved into code-behind classes but those classes are still using COM components. To further optimize this site, COM Interop costs can be eliminated by replacing the COM components with .NET components, starting with the frequently used Catalogcomponent.
The Catalogcomponent contains only four short data access functions written in Visual Basic 6. Three functions are used in the default page:
GetCategories Returns all product categories
GetProductList Returns all products in a category
GetProductDetails Returns details for a specific product
The fourth function is called from the search page:
GetSearchList Returns products matching the search criteria
All four of the original functions build a SQL statement and return the resulting ADO Recordset. The GetCategoriesfunction looks like this:
Function GetCategories() As ADODB.Recordset On Error GoTo ErrHandler Dim sql As String sql = "select CategoryID, CategoryName, CategoryImage from Categories" Set GetCategories = ExecuteReturnRS(sql, adCmdText) Exit Function ErrHandler: RaiseError m_modName, "GetCategories" End Function
You can see that GetCategoriesuses a helper function called ExecuteReturnRS. All of the functions in Cataloguse helper functions for creating Recordsetand Parameterobjects. Since the amount of .NET code to be written here is so short, it is not necessary to rewrite those helper functions.
The Visual Basic 6 file containing the database helper functions also contains a hard-coded database connection string. For the optimized ASP.NET site, the connection string will be stored in the Web site's configuration file (Web.config). Custom configuration values can be placed in the <appSettings>section of Web.config and read at runtime using classes from the Configurationnamespace:
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) Application("BikesConnString") = _ ConfigurationSettings.AppSettings("ConnectionString") End Sub
Before adding components to a project, it is usually helpful to create folders within the project to organize the components. To add a new folder to a project in Visual Studio .NET 2003, right-click on the project in Solution Explorer and select Add | New Folder.
To create a new code file to hold the .NET version of the Catalogcomponent, right-click on the new folder and select Add | New Class. If you type "Catalog" as the name in the Add New Item dialog, Visual Studio .NET 2003 creates a new file called Catalog.vb containing a class called Catalog.
.gif)
Figure 3. New Catalog.vb class file
The new Catalogcomponent will use the managed provider for SQL Server to communicate with the Downhill Bikes database. The methods in Catalogwill return DataTableand SqlDataReaderobjects instead of Recordsetobjects. Database connections will be established using SqlConnectionobjects instead of ADO Connectionobjects.
The original Visual Basic 6 functions use On Errorstatements. In a .NET application, Try Catchblocks provide structured exception handling that is more powerful than On Errorstatements. In fact, On Errorstatements in Visual Basic .NET are actually compiled into Try Catchblocks, albeit with extra overhead to emulate the behavior of the VB6 On Errorstatement.
After writing a new GetCategoriesmethod using the SQL Server managed provider for data access and a Try Catchblock, Catalog.vb would look like this:
.gif)
Figure 4. New Catalog.GetCategories method
A SqlDataAdapteris used to populate a DataTablein the Tablescollection of the DataSetobject ds. The SqlDataAdapterautomatically opens and closes the database connection, so there is no need to write that code explicitly. If you have worked with ADO.NET before, you might know that using a DataSetis more expensive that using a SqlDataReader, which is roughly the ADO.NET equivalent to a read-only, forward-only "fire hose" Recordset. A SqlDataReaderis not used here because the categories must be used twice: once to populate a DropDownListand then to retrieve the image for the selected category. Since the list of categories will not change very often, this method can be further optimized by caching the DataTable(using HttpContext.Current.Cache) and only filling a new DataSetonly if the item is not in the cache:
.gif)
Figure 5. Further Optimization Using Caching
Note that under different circumstances you would want to consider output caching for the catalog instead of caching the DataSet. Since the catalog page also shows a shopping cart summary, the entire page output cannot be cached. To use output caching for Downhill Bikes you would have to move the catalog display into a Web user control and use page fragment output caching, which is beyond the scope of this article.
Add Databound Server Controls
Once the Catalogis rewritten as a .NET component, code that uses the old COM Catalogcan be updated to use the new managed Catalog. The original site generates an HTML table as it iterates through the result set returned by GetCategories. One render block, <%emitCategory()%>, produces an HTML table containing a <select>element (i.e., a drop-down list) and an <img>tag for an image for the currently selected category. The resulting output can be seen in the following figure:
.gif)
Figure 6. Drop-drown list and image for categories
This same output can be generated with significantly less code using ASP.NET server controls. ASP.NET offers a rich data binding model and several powerful server controls included in the .NET Framework base class library that offer data binding capabilities. Developers coming from a Visual Basic 6 background may have become accustomed to avoiding data binding for various reasons. The data binding in ASP.NET is very efficient and does almost exactly what you would do if you were writing the code yourself: iterate through a data source and produce HTML from a template, inserting data from the data source in the appropriate places.
The markup in default.aspx to replace the <%emitCategory()%>render block is:
.gif)
Figure 7. DropDownList and HtmlImage server controls
The DropDownListserver control is identified as lstCategories. lstCategorieswill be the name of the variable used in the code-behind class to bind the DropDownListto the DataTable. The AutoPostBackattribute for the DropDownListwill generate client-side script to submit the page when the list selection changes.
The runat="server"attribute has been added to the <img>tag so that ASP.NET will treat it as a server control. You can add runat="server"to standard HTML tags to create HTML server controls. HTML server controls are accessed via the System.Web.UI.HtmlControlsnamespace rather than the System.Web.UI.WebControlsnamespace (which exposes ASP.NET server controls such as DropDownList, Label, and DataList). Because the image tag is now a server control, it can be manipulated programmatically. In this case, we need to change the Srcattribute based on the category chosen by the user from the DropDownList.
The code for binding the DropDownListand setting the category image must now be added to an event handler such as Page_Load:
.gif)
Figure 8. Data binding code
The Page.IsPostBackproperty is used here to determine if the current request is the result of the page posting data back to itself. If it is a postback, the DropDownListdoes not have to be repopulated because the values are maintained automatically in a hidden HTML <input>tag known as the ViewState (visit http://msdn.microsoft.comfor an explanation of the PostBack/ViewState model). If the current request is not a postback, the DataSourceof the DropDownListis set to the DataTablereturned by GetCategoriesFromCache. Then specific columns from the DataTablemust be bound to the DropDownList. For the category list, CategoryNameis the text displayed in the list but CategoryIDis the value that will be posted to the server when the user submits the page. To complete the binding, the DataBindmethod of the bound control must be called. Failing to call DataBindis a common omission made by new ASP.NET developers.
Regardless of whether the current request is a postback, the category image must be set. The relative URL for the image is read from the row in the DataTablethat corresponds to the category selected from the DropDownList. The URL is then assigned to the Srcproperty of the HtmlImageserver control.
Optimize the Rest of Default.aspx
The changes described so far have only optimized the categories dropdown list and the corresponding category image. The default page also contains:
- a product list to display products in the selected category;
- a product details section to display details about the selected product;
- a shopping cart summary; and
- shopping cart controls (clear, recalc, and checkout).
.gif)
Figure 9. Downhill Bikes default page
To optimize the rest of default.aspx, the other functions in Cataloghave to be converted to Visual Basic .NET code using ADO.NET for data access. The render blocks in the ASPX page must then be replaced with data bound server controls. And then event handlers have to be written to change the data bindings as different categories and products are selected. An implementation of default.aspx and default.aspx.vb is included with the sample code for this article. In the sample code, the <%emitCategory%>render block is replaced with a DropDownList(<asp:DropDownList>) as described in the preceding section.
The render block <%emitProducts%>that generates the list of products for a selected category is replaced with a DataListcontrol:
<asp:datalist id="ProductListing" runat="server" borderwidth="0" repeatdirection="horizontal" showfooter="false" showheader="false" DataKeyField="ProductID">
The DataListis bound to a SqlDataReaderto generate the product listing. The data binding is done when the page loads for a non-postback request, when a new category is selected from the dropdown list, and when a new product is selected from the product list.
The render block <%emitSelProduct%>that generates the details section for a selected product is replaced with a Repeatercontrol:
<asp:repeater id="DetailsListing" runat="server">
The Repeateris bound to a SqlDataReaderto generate the product details section after the product listing is generated. A Repeatercontrol is used here because it provides an easy way to bind data to an HTML template. The sample code only ever shows one product in the Repeater but it could be easily modified to show multiple products. For example, the Repeatercould be used to show details for all products in a category instead of having separate product list and product details sections.
The render block <%emitPersonalizationMsg%>that generates a personalization message for logged in users is replaced with a Labelcontrol:
<asp:label id="PersonalizationMsg" Runat="server"></asp:label>
The Textproperty of the Labelis set in Page_Loadif a personalization cookie containing the user's name is present.
The render block <%emitCart%>that generates the shopping cart summary is replaced with a DataGridcontrol:
<asp:datagrid id="DataGrid1" style="..." runat="server" cellspacing="4" BorderWidth="0" AutoGenerateColumns="False">
The DataGridcontains an inner template that defines how the data from its data source is rendered. A collection of BasketItemobjects is bound to the DataGridin the UpdateShoppingCartmethod.
The render block <%emitCartButtons%>that generates the buttons for the shopping cart is replaced with two <span>tags with runat="server", which makes them Html server controls (HtmlGenericControl). These two Html server controls, identified as areaCartButtonsEnabledand areaCartButtonsDisabled, are set to Visible=Trueor Visible=Falsedepending on whether there is anything in the shopping cart.
The sample code for this article introduces a new method in the Catalogcomponent:
Public Function GetProduct(ByVal productID As Integer) As DataRow
This method returns the DataRowfor the requested product ID. In the original ASP site, the data for the currently selected product are saved to global variables as the product list is built by the emitProductsfunction. Then the HTML for the selected product details is generated by the emitSelProductfunction using the data in the global variables. The optimized ASP.NET site uses data binding to generate the product list so the data for the selected item cannot be saved to variables as the product list is built. The method Catalog.GetProduct()returns the data for the selected product which can then be used to generate the details section for the selected product.
The optimized default.aspx page contains significantly less code than the original default.asp or the migrated default.aspx. The optimized default.aspx also contains a much cleaner separation of business logic from the graphic design and HTML layout, which will make it much easier for a graphic designer to update the site and for a programmer to adjust the application logic.
The rest of the Downhill Bikes site can be optimized in a similar way, replacing render blocks that emit HTML with server controls that can be data bound or otherwise manipulated programmatically in a code-behind class. The sample code for this article contains the entire site optimized with .NET components, ADO.NET, server controls, and data binding.
Add ASP.NET Forms Authentication
The Downhill Bikes site is an excellent example of a site for which authentication and authorization can be better managed using ASP.NET Forms Authentication. The existing authorization scheme uses a session object that is only created by the login page. Restricted pages, in this case checkout.aspx, must include the _verify_login.aspx page. The code in _verify_login.aspx redirects the user to the login page if the session object does not exist.
Using an include file and a custom session object is easy to manage for a reasonably small site such as Downhill Bikes. But security for a much larger site can become difficult to manage with includes. As an alternative, ASP.NET Forms Authentication allows you to use custom logic for identifying a user but then ASP.NET takes care of redirecting requests to the login page and restricting access for unauthorized users.
Forms authentication is configured in the Web.config file for the ASP.NET site. The <authentication>element specifies the type of authentication. When using Forms authentication, the <forms>element specifies where to redirect anonymous requests, the name of the authentication cookie stored on the client, and other settings specific to forms authentication.
<authentication mode="Forms"> <forms name="DownhillBikesAuth" loginUrl="Login.aspx" protection="All" /> </authentication>
By default, anonymous users are authorized to access all pages. The <authorization>element is used in Web.config to restrict access to the site. An <authorization>element can be added for the entire site. Specific parts of the site can also be configured using <location>elements. The following <location>element denies access to checkout.aspx for anonymous users:
<location path="Checkout.aspx"> <system.web> <authorization> <deny users="?" /> </authorization> </system.web> </location>
A request for checkout.aspx will be automatically redirected to the page specified in the <authentication>element (login.aspx in this case).
Although the existing login.aspx page could be modified to use Forms authentication with only a few small changes, adding server controls and an event handler in the code-behind class is more inline with the other modifications being made in this article. The username and password text boxes are first changed to TextBoxserver controls (in a production app you would set TextMode="Password"for txtPassword):
<asp:TextBox ID="txtUsername" Runat="server"></asp:TextBox> <asp:TextBox id="txtPassword" Runat="server"></asp:TextBox>
The render block, <%=errMsg%>, previously used to render a "login failed" message, is replaced with a Labelserver control to which an error message can be assigned if needed:
<asp:Label ID="ErrorMessage" Runat="server"></asp:Label>
The image button is changed to an HTML server control:
<input id="btnLogin" type="image" src="images/button_sign_in.gif" border="0" name="btnLogin" runat="server">
Finally, an event handler is written for the ServerClick event of the new btnLoginserver control. In the event handler, the credentials supplied by the user are used to look up the user's customer ID. If the credentials are valid, the customer ID is returned and a Customerobject is created for that customer ID. Once the credentials are verified and any business logic is executed, the user is redirected to the originally requested page by calling FormsAuthentication.RedirectFromLoginPage.
Private Sub btnLogin_ServerClick(ByVal sender As System.Object, _ ByVal e As System.Web.UI.ImageClickEventArgs) _ Handles btnLogin.ServerClick Dim cust As New CustomerDB Dim customerID As String customerID = cust.Login(Me.txtUsername.Text, Me.txtPassword.Text) If customerID Is Nothing Then ' Login failed ErrorMessage.Text = "Login Failed. Please try again." Else Dim customerDetails As Customer customerDetails = cust.GetCustomerDetails(customerID) Response.Cookies("Bikes_FullName").Value = customerDetails.Name Session(Module1.csSessionCust) = customerDetailsFormsAuthentication.RedirectFromLoginPage(txtUsername.Text, False) End If End Sub
The call to RedirectFromLoginPagecauses ASP.NET to add an authentication cookie to the response. ASP.NET will use that cookie to identify the authenticated user during future requests. The authentication cookie can optionally be set as persistent so that the user does not have to login again on subsequent visits.
Other Optimizations
This article has covered some of the basic concepts for optimizing an ASP.NET site like Downhill Bikes after migrating it from ASP. When you optimize other sites, there will often be other opportunities for optimization not covered by this article. For example, there are many ASP applications that use successive string concatenations to build HTML output before calling Response.Write. In .NET applications, excessive string manipulations (i.e., thousands of operations at a time) can degrade performance because of the way memory for strings is managed. Specialized classes that use a string buffer (such as StringBuilder) offer improved performance by reducing the number of times that memory must be allocated and subsequently garbage collected. In ASP.NET, the Responseobject uses a buffered output stream internally. Calling Response.Writefor each string will generally give better performance than successive string concatenations followed by one Response.Writestatement.
The methods in the Catalogcomponent repeatedly retrieve category and product information from the database. Since the number of products in the database is quite small and not likely to change frequently (perhaps once a day at the most), it might make sense to cache the product lists for each category as well as the details for each product. Caching this information makes sense as long as the site is not exposing real-time data such as inventory counts.
Conclusion
There are always trade offs when porting code from one platform to another. Fully optimizing an application for the new platform can involve costly development efforts. Porting from ASP to ASP.NET is relatively "cheap" compared to other migrations. The existing ASP syntax compatibility in ASP.NET makes many simple migrations painless. For more complex cases, the ASP to ASP.NET Migration Assistant helps automate some of the steps in the migration process. After some manual tweaking of the output from the Migration Assistant, you can get a migrated ASP.NET site working with minimal effort but the migrated application may not be optimized for the ASP.NET platform.
The additional effort to optimize a migrated site can be an investment that helps decrease future maintainability costs. Reducing COM Interop costs and introducing server controls, data binding, and strongly typed variables (and therefore early binding) will help improve performance while making code easier to debug and modify.
If an application is important enough to migrate, then it is probably also important enough to optimize for ASP.NET. When deciding what to optimize first and what not to optimize at all, look for components that get a lot of use or that are very expensive when they are used. Cut down on chatty .NET-to-COM interactions and consider moving to ADO.NET to avoid the often chatty interactions with ADO COM objects. Look for variables without an explicit data type or declared as Object.
Remember that the optimizations covered here are not the only ASP.NET optimizations that you can make. As you work with your migrated site(s), watch for things that are in need of improvement and then do some research. ASP.NET puts a tremendous amount of power into your hands as a developer. Often the biggest job is just finding out what the great features are that will really make your application shine.
Finally, if you are porting an ASP site to ASP.NET, then you are looking to gain from the many benefits that ASP.NET has to offer. If you do not even consider making some optimizations after a basic migration, you could be missing out on significant yet easily attained benefits for your migrated application.
Additional Information
Performance Tips and Tricks in .NET
http://msdn.microsoft.com/library/en-us/dndotnet/html/dotnetperftips.asp
Converting ASP to ASP.NET
http://msdn.microsoft.com/library/en-us/dndotnet/html/convertasptoaspnet.asp
Accessing Data
http://msdn.microsoft.com/library/en-us/vbcon/html/vboriIntegratingDataVB.asp
Taking a Bite Out of ASP.NET ViewState
http://msdn.microsoft.com/library/aa902656.aspx