Problem Description & Requirements
As part of a recent e-Commerce project I was asked to provide an auditing trail for certain critical application processes, such as:
- The signup of a new member.
- A product purchase.
- The agreement of a user to terms and conditions.
Typically, such requirements have been addressed by saving the data associated with the transaction, adding a timestamp and the corresponding user id etc. While these measures are definitely appropriate and needed, it would be beneficial if in addition to the this information we could create a permanent record of the web page, as it was shown to the user. This record would contain:
- All of the page's HTML elements (text boxes, radio buttons etc), with the content as filled in by the user.
- All of the images that are part of the page.
- The exact look & feel of the page, including any CSS styling.
Any solution should have a minimal impact on the existing flow of the application, and should require as little as possible development effort for every page that needs to be audited. Also, the run-time overhead of the solution should be minimal.
We looked at a number of ways to implement this, including the application of a HttpModule to capture the Html that was sent back to the browser. At the end, it was concluded that we really should capture the Html that was shown to the user, since this is ultimately a true representation of what the user saw at the time of the transaction.
An Ajax-based solution seems like a very natural fit for this problem. Using Ajax technology, we have the ability to asynchronously call Web Services directly from the client. So, we simply need to get the contents of the Html document, and pass this content to the Web Service. The Web Service can then archive this content in some type of persistent storage, such as a the file system or a relational database.
To keep our example simple, we will save the content of the Html Pages in the file system of the Web Server, where the base name of the created file is the URL of the page. We will append the current time in ticks to base name to create a unique file name.
We will use the beta 2 version of Ajax.Net as our Ajax implementation. Beta 2 shipped a couple of weeks ago, hot on the heels of Beta 1. The CTP version of this technology was also known as Microsoft ATLAS.
ASP.Net AJAX allows the developer to create browser-neutral pages with a rich, responsive UI and and an asynchronous communication model. The framework is tightly integrated with ASP.NET, and this integration will be improved even more by the time Orcas (the next version of Visual Studio) will ship sometime next year. You can download Beta 2 from this location.
Create the Web Application Project
To implement this solution, we will first create an ASP.NET Web Application. Make sure that you have the Web Application project model installed. Alternatively, you can use a standard "Web Site" project. I have gotten to really like the Web Application Project model, and I'm currently using it for most of my web projects.
In this case, we will call our project "AjaxAuditing", as is shown below:
Configure the Project for ASP.Net AJAX
After the project has been created, we first should go ahead and configure our application for ASP.Net AJAX. When you install Beta 2, a sample web.config file will be provided for you in the following directory:
C:\Program Files\Microsoft ASP.NET\ASP.NET 2.0 AJAX Extensions\v1.0.61025
You can either copy the entire contents of this file, or you can "lift out" those elements that are required for your project. In our case, we only need scripting and Web Service support. This implies that we need the following configuration elements:
- A <sectionGroup> element in the <configsections> outer element, which configures the scripting component of ASP.Net AJAX.
- A <httpHandlers> element to configure Web Service support.
- A <httpModules> element to configure the AJAX Script Module.
For a complete listing of the web.config file of our sample project, I refer the reader to the sample code for this project.
We also need to add a reference to the 'Microsoft.Web.Extensions.dll' assembly. This assembly will be located in the program files folder named above. Copy this file into your /bin directory, and add a reference to it, as shown below:
Note that the 'Microsoft.Web.Extensions' assembly is installed in the GAC, we are simply referencing it here for build purposes. If you don't like running from the GAC, you can set the "copy local" attribute for this assembly to true, and you will be running with a local copy.
Add a Web Service to the Project
Next, we will add the Web Service which will save our Html content to the file system to our project. To do this, follow these steps:
- Since we want to separate our Web Services from our main .ASPX code, we will add a new folder named 'WebServices' to our project.
- To this folder, we add our Web Service, named 'SaveHtml.asmx', as show below:
In our code, we need a using statement for the Microsoft.Web.Script.Services assembly:
public class SaveHtml : System.Web.Services.WebService
The complete code for the Web Service is shown below:
Our Web Method takes two parameters:
- The URL of the page. This URL is used to build the first part of the base name of the created file.
- The html content itself.
We create a unique file name by concatenating the current time in ticks, map this file into our web context and write the contents of the Html to the file.
Create our Test Web Page
Next, we will enable our Default.aspx Web Page to call our Web Service, using ASP.Net AJAX as follows:
- First, we need to add a register statement for the Microsoft.Web.Extensions assembly. This will import the AJAX asp extensions, which we need to add a reference to the Script Manager (see next).
- Next, we add an <asp:ScriptManager> element to the page. The ScriptManager should always be located in the <form> tag, and should be the first element after the form element. Inside the ScriptManager element, we add a reference to our Web Service by specifying it's relative path:
In the CallSaveHtml() method, we invoke our Web Service by specifying the fully-qualified name of our Web Service method, passing in the following parameters:
- The name of our page (in this case Default.aspx).
- The html content of our page.
- The final argument is NOT an argument of our web service, but rather is the name of the method, which should be called by the ASP.Net AJAX framework when the web service call completes. Remember, by default all AJAX calls are made asynchronously. In this case, we specify the name of the 'SaveCompleted()' method, which displays a 'Save Completed' message in the status bar of the browser.
Testing our Page
Before we run our page, we need to create an "Auditing" directory in our project. This is the directory which will contain our audit files. Now, we are finally ready to test our page. When we run the page:
and click the "Submit" button, you will notice that a file is created in the "Auditing" sub-directory:
Navigating to this page in the browser will show an exact replica of our original page, including the image and the text in our TextBox, producing the exact audit as specified in our requirements:
Also, because we added the call to the 'CallSaveHtml' in the 'OnClientClick' attribute of our submit button, we preserve the standard postback behavior of the button, so we are not affecting the standard flow of the ASP.NET application, which was another one of our requirements.
- It has the name of our Page hard-coded as the first argument to the web service call.
A better solution would be the following:
- Move this code into a base class, from which every page with auditing requirements can inherit.
Note that we use Request.Url.AbsolutePath to get the Url of the page, thereby avoiding the hard-coding we had to do previously.
Now, any page which needs auditing can simply inherit from this base class, as shown in the class diagram below:
In a real-world application, you would probably not use the file system to store your page audit. You would probably want to store the audit pages in a relational database, and add additional context information such as:
- The name of the current user
- The current date & time