ASP.NET Deployment – Back to Basics
Some of you may remember that back when Visual Studio .NET was
still in its early beta stages, it was pretty darn unstable. At the
time, it was far more productive to develop ASP.NET applications
using Notepad than it was to wrestle with the crash-prone Visual
Studio .NET IDE. The common development paradigm (oops, I used the
"p" word) was to point your ASPX page to the code-behind source code
file that contained its associated code-behind class using the
"Src" attribute of the @Page directive. You would then deploy both
the ASPX page and the code-behind source code file to your
production web server. The first time that each ASPX page was
navigated to, the code-behind class would be compiled, and the page
would execute.
Jump ahead to the Visual Studio .NET development paradigm (strike
two on the "p" word…one more time, and the next round of drinks is
on me). Visual Studio .NET links ASPX pages to their associated
code-behind classes using the "CodeBehind" and "Inherits" attributes. Since Visual Studio
.NET pre-compiles the entire web application into a single assembly,
it is no longer necessary to deploy the actual code-behind source
code files for your ASPX pages (though I have seen many people make
that mistake). Frankly, I find this über-assembly approach to be a
stupid idea, because it leads to several deployment problems. I will
re-create a couple of them using an (intentionally simplified)
example:
Joe, Molly, and Tom are building a web application that consists
of three ASPX pages. These are JoesPage.aspx,
MollysPage.aspx, and TomsPage.aspx. JoesPage.aspx
allows you to select from a list of fruits and display the
selected fruit in a label:
MollysPage.aspx asks you for your name, and echoes it back
to you along with a salutation:
TomsPage.aspx displays a simple report of the authors in
the pubs SQL Server database:
For this example, assume that Joe, Molly, and Tom are using
Visual Source Safe (VSS) and have invested in a separate server that
they use to compile their web application before deploying it to
production.
The first problem that should be quite apparent, is that whenever
a change is made to any part of the web application, the entire web
application must be re-compiled and deployed. One of the first rules
of software development is that "if it’s working, don’t mess with
it". That tenet is clearly violated by Visual Studio .NET’s
über-assembly approach. But let’s get past the ideological semantics
for a moment. There are some more tangible problems to discuss.
In the example above, assume that both Joe and Molly need to make
changes to their respective ASPX pages. Joe needs to add an
additional fruit to his list of available fruits (for now, forget
the fact that the list shouldn’t have been hard-coded). Molly needs
to add a RequiredFieldValidator
control to her web form to force the user to specify their name
before clicking the Submit button. Joe
finishes his modification on the first day, but Molly has some
difficulty. Her page isn’t working right yet, but before leaving
that night, she still checks her page back into VSS, so that it can
be archived in the nightly tape backup process. The next day, Joe
wants to deploy his fixed ASPX page, so he does a "Get" on the project from VSS to the build
server and inadvertently includes Molly’s broken ASPX page in the
build. If the problem with Molly’s page is rather innocuous, it
stands a good chance of making it through QA (if there is such a
process) and into production. What’s worse, once the problem is
discovered, the entire migration will have to be rolled back,
because it is all contained within the über-assembly. Tom’s code is
being tossed around, and he didn’t even make any changes.
Bummer.
Could this problem have been avoided? It sounds easy enough. Joe
should have kept better track of which files to retrieve from VSS
before compiling the web application on the build server. But what
if your web application consists of hundreds (even thousands) of
ASPX pages, with migrations pertaining to dozens of them each week
(or day)? Eventually, either the wrong files will get migrated to
the build server and compiled into your web application, or you will
discover a critical problem with a piece of code that was
intentionally migrated to production (perhaps Joe incorrectly
assumes that a carrot is a fruit). When that occurs, you will be
force to do a complete rollback of the migration, and you will have
to attempt to reconcile your build server’s files to a point where a
stable version of your web application can once again be compiled.
Forget about doing a complete refresh from VSS, because there is no
way to determine which files are ready to be compiled and migrated
to production.
So, now that I have discredited the deployment method that comes
out of the box with Visual Studio .NET, what method should you use?
I recommend that you go back to basics. Before Visual Studio .NET,
when code-behind source code files were used, you could easily rip
and replace any piece of a web application without affecting any
other piece. The biggest problems with that approach were that you
had to deploy your source-code and it wasn’t pre-compiled (meaning
that you took a performance hit when your page was first navigated
to, and your code-behind class had to be compiled for the first
time). The ideal deployment plan is to merge the best aspects of the
old and the new.
First, as I alluded to in last month’s Back Seat Driver, you shouldn’t use Visual Studio
.NET to compile your production web application. Instead, you should
use batch compile scripts on a clean build server with only the .NET
Framework SDK installed on it. That allows for the greatest
flexibility with regard to build automation and custom
configuration. If you go the batch compile script route, it is just
as easy to compile each code-behind class into its own assembly as
it is to compile the entire web application into a single assembly.
Doing so will give you the benefit of pre-compiled code, while
enabling the ability to do partial migration rollbacks (an absolute
"must have" in fast-moving "migrate first, ask questions later"
corporate intranet environments). Even better, you don’t end up
re-compiling code that isn’t broken over, and over, and over again.
Why take the risk that something will accidentally get thrown in
there? Leave well enough alone.
In the case of Joe, Molly and Tom’s web application, you might
use the following compile scripts (each in its own file with a
".BAT" extension): csc.exe /t:library /out:JoesPage.dll JoesPage.aspx.cs >> results_JoesPage.txt
csc.exe /t:library /out:MollysPage.dll MollysPage.aspx.cs >> results_MollysPage.txt
csc.exe /t:library /out:TomsPage.dll TomsPage.aspx.cs >> results_TomsPage.txt
There are a few things that I should point out. First, before you
can use the command-line compilers, you need to make sure that the
directory containing the compiler executable itself ( csc.exe
for C# in this case) is in your system PATH environment
variable. If it isn’t, you can register all of the necessary
environment variables easily by executing the "vcvars32.bat"
file, which is located in the "/bin" directory of your .NET
installation. Or, if your machine has Visual Studio .NET installed,
it will be in the "C:\Program Files\Microsoft Visual Studio
.NET\Vc7\bin" directory.
The second thing that I’d like to point out is that instead of
having to place all of your compiler switches on a single
command-line, you can opt to use a response file. Response files are
specified using the "@" character on
the command-line, and enable you to place each compiler switch on a
separate line in the file. You can also add comments by prefixing a
line with the "#" character. You can
find out all about how to use response files by looking up
"response files" in the .NET Framework SDK documentation.
Lastly, the ">>" characters
in each compile script above export the output of the compile script
to the file specified after them. This allows you to catch and fix
problems with your compile script and/or your source code. The first
execution of the compile script creates the output file, and
subsequent compiles append their output to the same file (unless you
deleted it).
It takes a bit longer to get this system up and running, but once
you do, your deployment process will be streamlined and ready to
handle the inevitable bumps in the road. My hope is that Microsoft
will allow for greater compilation flexibility in future versions of
Visual Studio .NET, including the ability to see what compile
scripts it is running in the background when you tell it to compile.
That way, you can get your compile process organized and ready in
the Visual Studio .NET environment, which will allow you to set up
your build server environment much more quickly and easily.
Game, Set, MatchEvaluator
The Regex object in the System.Text.RegularExpressions namespace has
a Replace method that allows you to do
string replacements based on a regular expression. What I found most
useful about this method, though, is that you can also specify a
MatchEvaluator delegate that will fire
for each potential replacement, allowing you to determine at runtime
what the replacement text will be, as well as keep track of which
replacements were made. This makes it ideal for token decoding
operations. For example, create a new ASPX page named
TestMatchEvaluator.aspx, and add a TextBox named txtInput, a Label named lblOutput, and a Button named btnSubmit to it. You should end up with
something like this:
Just inside your code-behind class declaration, declare an ArrayList object to hold your token
replacement words: private ArrayList al = new ArrayList();
In the Page_Load event, populate
the token replacement words: private void Page_Load(object sender, System.EventArgs e)
{
al.Add("apples");
al.Add("oranges");
al.Add("bananas");
al.Add("mangoes");
al.Add("grapes");
}
In the Click event for the btnSubmit Button control, place the
following code: private void btnSubmit_Click(object sender,
System.EventArgs e)
{
lblOutput.Text = Regex.Replace(
txtInput.Text,@"<<\d+>>",
new MatchEvaluator(TokenEvaluator));
}
The code above calls the Replace()
method of the Regex object and passes
it a regular expression to match against, and a reference to a MatchEvaluator delegate. It assigns the
result of the replacement operation to the Text property of the lblOutput Label control. The MatchEvaluator delegate (named TokenEvaluator in this example) contains the
following code: private string TokenEvaluator(Match regexMatch)
{
int index = Convert.ToInt32(
regexMatch.Value.Substring(
2,regexMatch.Value.Length - 4));
if(index < al.Count)
{
return al[index].ToString();
}
else
{
return "N/A";
}
}
The TokenEvaluator delegate method
above is called each time the Regex.Replace() method encounters a match
for the specified regular expression. The first thing that the
function above does is parse out the number from the rest of the
token syntax. In this example, I have specified that a token is
represented by a number, enclosed by two less-than signs and two
greater-than signs. For example "<<3>>". The TokenEvaluator delegate method then uses
that token number as an index into the ArrayList containing the token replacement
words. If the token number is larger than the number of items in the
ArrayList, the string "N/A" is returned. Otherwise, the proper
replacement word is returned. An example implementation is
below:
You can modify the example in this article to look up token
replacement words in a database to simulate a more real-world
scenario. You might even want to track how often each token is used
for statistical purposes.
Until next month, buckle up, and be careful which road signs you
follow.
Conversation Starters
1) Have you deployed an ASP.NET website to production? What
strategy did you use? What problems did you encounter, and how did
you solve them?
2) Have you used the Regex object
and/or the MatchEvaluator delegate? Do
you have any tips that you'd like to share? |