When using the aspnet_compiler.exe and having a theme defined in your web.config file, the compiler puts that theme into the page directive for every page file.

Not sure why they do this? I may be a performance thing. In my case however it is not helpful because after I uploaded my pre-compiled site to the production server, I'm using it for several different actual web sites which all use different themes. Because the page directive overrides the web.config setting all the sites end up with the same theme, the one that was set in web.config on my development machine.

So I added another step to my deployment script, after compiling the site I call:

rxfind.exe c:\projects\mysite\wwwroot\*.as?x /P:" theme=\"[a-z0-1]*\" " /R:" " /I /S /B:2

rxfind is a nice little .net based replace strings in files tool.


 
Categories: ASP.Net

So after finally migrating to Web Site projects and deploying my site to the production server things seem to work fine.

But I noticed dates like '13 Dezember 2006' were showing up on my pages, what's that all about? The production server has a German OS, that's why I have the following code in Global.asax.cs

protected void Application_BeginRequest(Object sender, EventArgs e)
{
   System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
   System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
}

This makes sure that whatever OS or user is running the app, the dates will always be in British format.

Why isn't this working anymore? It seems global.asax is not working correctly.

As far as I understand in the pre-compiled Web site projects you don't have a global.asax file in the root of your site anymore but instead a file called 'App_global.asax.compiled' in the bin directory. This was there alright.

When I copied global.asax back into the root I got the error that my Global class is defined in two places, once in the main precompiled assembly and secondly in a dynamic assembly for 'App_global.asax' and the runtime can't figure out which one to use.

The type 'ASP.global_asax' exists in both ...WebUI.DLL and ...8725\App_global.asax.ocwfm9jz.dll

So it seemed I can't have global.asax there but the runtime also doesn't fire 'bin/App_global.asax.compiled'!

I fired up Process Monitor (be carefull when running this on a production server, it slows things down) to have a look at the access to 'App_global.asax'. I set a filter 'Process Name is w3wp.exe' to limit the output. I couldn't see any references to 'App_global.asax' but saw that a file called 'PrecompiledApp.config' was not found. I remembered this file being created during the compilation of the site but my deployment script doesn't include it in the zip file that goes on to the server.

Well turns out, it has to be there. I copied it over and suddenly my dates worked. I guess the runtime looks for that file and only when it is found actually looks for the *.compiled files in /bin/.

 UPDATE:

Once again I noticed German month names in some XLST output, again an indication that Global.asax is not firing. You can actually fix this by setting the cultures in web config:

   <system.web>
      <globalization uiculture="de" culture="de-DE">
   <system.web>

 But I do more stuff in global.asax, especially global error handling.

So I spend some hours figuring out why global.asax is not executing on the live server. It worked fine on staging and also for other applications on the server. It didn't throw any errors anywhere but just didn't execute all at. I couldn't find anything about this problem online and changing web sites and application pools didn't help either. There were also no problems accessing any of the files. While looking at the assemblies in Reflector to check whether all the classes are there I noticed that there was a reference to Microsoft.Web.Extension, I had just replaced ASP.NET Ajax Beta 2 with RC1 where they changed the assembly to System.Web.Extension. The bin directory on staging had both assemblies while the live site only had the new one. After removing the reference to Microsoft.Web.Extension and rebuilding the assembly, the site worked fine. I wonder why this didn't threw an error and the rest of the site worked but global.asax didn't.


 
Categories: ASP.Net

After my last post about switching between these two types I actually started using it and found a major flaw in my solution. The problem is the declaration of asp.net and html control variables. Is WS these are done magically in the background and are in a partial class file generated during run-time. In WAP you put them in default.aspx.designer.cs files. So after using WS for a while there is no easy way to synchronize the designer.cs files with the controls on the aspx page. It would be nice if Visual Studio could do this and it doesn't at least not in my version which reports "Warning 24 Generation of designer file failed: Object reference not set to an instance of an object." a lot if not always. I also need to be Visual Studio independent.

So I did some more research and found out about VS 2005 Web Deployment Projects, which allow you do compile and merge all the assemblies in a WS project into a single assembly, better yet it is using the command line tool 'aspnet.merge.exe' so you could do this without Visual Studio.

So here's how I now build my deployment package for the staging and production servers.

For development in Visual Studio or Notepad2 I'm now always using WS, it is so much easier not having to recompile all the time. When I done with testing I run a batch file to create a new zip file with the files I need to copy to the servers.

As usualy I'm using some 4NT specific features so the batches wont work with cmd.exe but you get the idea.

I first compile the site with aspnet_compiler.exe into a new directory

I then use aspnet_merger.exe to merge all the assemblies into a single one for which I can specify a name.

I then run a second batch which compares the new directory with the previous version and adds all changed files into a zip file which I can then upload to the live server.

I ran into some small problems:

1. Apparently you can specify an assembly.cs file in aspnet_merger to specify the meta data for the single assembly. This didn't work but I needed to combine two different files anyway because I am using a common SolutionInfo.cs for all the different assemblies.

2. After I create the compiled site and try to run it, I get an error "Could not load type '__ASPNET_INHERITS'." in global.asax. The file has a single line "<%@ application inherits="__ASPNET_INHERITS" %>" what the heck is that? I found out that global.asax is not even suppose to be there anymore because the file bin/App_global.asax.compiled points the runtime to the correct class in the single assembly. So I just deleted global.asax and it works fine.

Here's the batch file for a specific project:

@ECHO OFF

SETLOCAL

set app=admin.twee.net
set target=%tnHostsDir\Deployment\%app\compiled
set iisSite=/LM/W3SVC/236937994/Root/Topas

:: empty target
rmdir /S /Q %target

:: create new assemblyinfo.cs
copy %tnHostsDir\Topas\SolutionInfo.cs %tnHostsDir\Topas\TweeNet.Topas.UI.Web.Info.cs %tnHostsDir\Topas\TweeNet.Topas.UI.Web\App_Code\AssemblyInfo.cs

:: compile site
%tnDnDir\aspnet_compiler.exe -m %iisSite %target -u -nologo

:: merge assemblies
%tnBinDir\aspnet_merge.exe %target -o TweeNet.Topas.WebUI -copyattrs

:: clean bin directory
PUSHD %target\bin
del /Q /E SongTitles.* WinTopas.* topas.*.* *.xml nunit*.* TweeNet.Topas.Unittests.* *.pdb 
POPD
:: and some files in the root
PUSHD %target
del /Q /E *.user *.sln *.csproj PrecompiledApp.config global.asax
POPD

:: now call the main script to find files to be updated
CALL %tnHostsDir\scripts\DeploymentPackageCommon.cmd %app

ENDLOCAL

@ECHO ON

And here's a more generic one to find the updated files:

@ECHO OFF

:: TweeNet Topas Deployment Synchronizer

:: Description:
:: =============
:: Batch to find all updated files to be copied to the live server.
:: The is done by comparing a source and a master directory.
:: 1. Find out which files are newer in the source
:: 2. Create a batch file to list those files which is 
::    then used to copy them but also as a history log
::    to see which files have been updated.
:: 3. Copy the new files into a unique directory
:: 4. Zip the content of that directory
:: 5. Copy the new files into the master directory
:: 6. Manually upload and deploy the files on the live server.

:: Usage:
:: =======
:: TopasDeploySync.cmd appName
:: where appName is one of the applications we use.

:: Requirements:
:: ==============
:: 4NT.exe 7.0 or higher is required for this
:: Robocopy.exe Version XP010 or higher
:: Windows NT 5.0 or higher

:: Assumptions:
:: ============= 
:: - All related files are in X:\hosts
:: - The source files are in X:\hosts\Deployment\appname\compiled
:: - The folders X:\hosts\Deployment\appname\wwwroot exists
:: - in the source \bin\TweeNet.Topas.Business.dll exists

:: History:
:: =========
:: Version 1.0 /  4-Oct-2006 Singapore
:: Version 1.1 / 12-Dec-2006 Singapore

:: Credits:
:: =========
:: written by Peter Hahndorf, contact me at www.twee.net/contact/ 

SETLOCAL

SET scriptVersion=1.0.10.0
SET scriptName=TopasDeploySync.cmd

:: we pass in the folder name for the application
SET appName=%1

:: the second parameter is optional for the hosts directory
:: if not specified we use \hosts on the current drive
iff %# == 2 then
	SET hostDir=%2
else
	SET hostDir=%@LEFT[1,%_CWD]:\hosts   
endiff

:: the third parameter is optional for the wwwroot directory
:: if not specified we use wwwroot on the current drive
iff %# == 3 then
	SET wwwrootDir=%3
else
	SET wwwrootDir=wwwroot   
endiff

:: sourceDir is the folder with the latest files
SET sourceDir=%hostDir\Deployment\%[appName]\compiled
:: masterDir the folder with the last updated version
SET masterDir=%hostDir\Deployment\%[appName]\wwwroot

:: get the version from the main assembly in the source
SET TopasVersion=%@VERINFO[%[sourceDir]\bin\TweeNet.Topas.Business.dll]

iff "%@files[%[sourceDir]\bin\TweeNet.Topas.Business.dll]" == "0" then
	@ECHO No Version information found at ^n%[sourceDir]\bin\TweeNet.Topas.Business.dll
	EXIT /B 15
endiff

:: if we have run this before, we need a new unique deployement directory
 iff "%@files[%hostDir\Deployment\%[appName]\%[TopasVersion]\]" == "0" then
	:: first time, deploy directory is simple the name and version
	SET uniqueToken=
 else
	:: Standard named directory is already there, use a different one
	:: winticks is pretty unique
	SET uniqueToken=_%_WINTICKS
 endiff

:: use the unique token we got above to build the name
SET DeployDir=%hostDir\Deployment\%[appName]\%[TopasVersion]%uniqueToken\

:: the zip file to use, u- stands for update, f- would be full
:: we use the same unique token as above
SET zipFile=u-%[appName]-vs%[TopasVersion]%[uniqueToken].zip 

:: the batch file we create and use to copy the files to be updated
SET batchFile=%hostDir\Deployment\%[appName]\TopasSync_vs%[TopasVersion].cmd

:: temp file to store list of files to be copied
SET tempFile=%TEMP\%[appName]-vs%[TopasVersion].temp
:: temp file to redirect robocopy output into
SET tempFileSuppressOutput=%TEMP\%[appName]-vs%[TopasVersion]_output.temp

:: display some info
@ECHO ^n%scriptName Version %scriptVersion^n
@ECHO Create Deployment package for %[appName] %TopasVersion
@ECHO Source: %sourceDir
@ECHO Target: %DeployDir
@ECHO Master: %masterDir

:: get all files that are relevant and different, we use the fabulous robocopy to do this:
:: the /N* switches suppresses output.
:: the /L switch means we don't actually copy anything, just get a list of the files.
:: we only copy files that we really want on the live server, so loads of filters
robocopy.exe %[sourceDir] %[masterDir] /S /R:3 /W:10 /XX /L /NC /NS /NJH /NJS /NP /NDL *.aspx *.asmx *.ascx *.asax *.gif *dll *.jpg *.xml *.xsl? *.js *.ico *.css *.master *.skin /XD old obj topas /XF nunit.framework.dll TweeNet.*.xml TweeNet.Topas.UnitTests.dll Microsoft.Web.Atlas.dll WebDev.WebHost.dll app.css get_aspx_ver.aspx /Log:%tempFile >%[tempFileSuppressOutput]

:: we also want some *.exe files in the root
:: robocopy.exe %[sourceDir] %masterDir /R:3 /W:10 /XX /L /NC /NS /NJH /NJS /NP /NDL *.exe /Log+:%tempFile >>%[tempFileSuppressOutput]

:: count the number of lines, which represents the number of files.
:: if we have files in both directory, we get one extra blank line
SET numberOfNewFiles=%@LINES[%tempFile]

:: we now have a file with the list of all files to be copied

:: built content of the new batch file
@ECHO :: Topas Deployment Batch File >%batchFile
@ECHO :: created by %scriptName Version %scriptVersion >>%batchFile
@ECHO ::       created: %_ISODATE >>%batchFile
@ECHO :: Topas Version: %TopasVersion >>%batchFile
@ECHO ::   Application: %[appName] >>%batchFile ^n

@ECHO :: Files to be copied: ^n >>%batchFile
:: go through every line of the file and just output the filename
for /f %a in (@%tempFile) echo :: %@replace[%sourceDir,, %a] >>%batchFile

:: the copy command expects existing directories, robocopy can create them
:: but needs directory as input, not files, so we have to remove the file names
:: which is a bit messy with 4NT commands. dummy.xyz is the file to copy, 
:: it never exist, so robocopy just creates directories for us.
@ECHO ^n::Create directory structure with robocopy:>>%batchFile
for /f %a in (@%tempFile) echo robocopy.exe %@replace[%@filename[%a],,%a] ^t %@replace[%@filename[%@replace[%sourceDir,%DeployDir, %a]],,%@replace[%sourceDir,%DeployDir, %a]] dummy.xyz /NC /NS /NJH /NJS /NP /NDL >>%batchFile

:: this creates the actual copy commands
@ECHO ^n::Copy the files to deployment >>%batchFile
for /f %a in (@%tempFile) echo copy /Q %a ^t %@replace[%sourceDir,%DeployDir, %a] >>%batchFile

:: we also want to copy the files to the master directory to bring it up to date.
:: first directories, which may not be there
@ECHO ^n::Create directory structure with robocopy:>>%batchFile
for /f %a in (@%tempFile) echo robocopy.exe %@replace[%@filename[%a],,%a] ^t %@replace[%@filename[%@replace[%sourceDir,%masterDir, %a]],,%@replace[%sourceDir,%masterDir, %a]] dummy.xyz /NC /NS /NJH /NJS /NP /NDL >>%batchFile

:: and the files themselves
@ECHO ^n::Copy the files to master>>%batchFile
for /f %a in (@%tempFile) echo copy /Q %a ^t %@replace[%sourceDir,%masterDir, %a] >>%batchFile

:: delete the temp file
DEL /Q %tempFile

:: execute the sync batchfile we just created
:: this does all the copying, we don't want any output
CALL %batchFile >>%[tempFileSuppressOutput]

:: delete the temp file for outpur suppression
DEL /Q %[tempFileSuppressOutput]

:: check whether there were some new files
:: if not the Deploy directory has not been created
 iff "%@files[%DeployDir]" == "0" then
	:: delete the empty batch file
	DEL /Q %batchFile
	:: tell the user about this
	@ECHO ^nNo new files to be deployed
 else
	:: we have some new files
	:: now zip the target
	:: move into the directory above the content,
	:: this way we get a nice structure in the zip file.
	pushd %DeployDir
	:: create a new zip file
	zip -r -q %zipFile *.*
	
	:: copy the zip file to the news folder for easy upload
	copy /Q %zipFile %tnNewsDir
	
	:: move the batch file into the folder
	move /Q %batchFile %DeployDir
	
	:: back to our previous location
	popd
	
	:: tell the user we're done
	@ECHO ^N %numberOfNewFiles new files found
	@ECHO ^N Deployment package ready for upload at^N %tnNewsDir\%zipFile

 endiff

ENDLOCAL

 
Categories: ASP.Net

Visual Studio 2005 now supports two different asp.net project types, the original file system based 'Web Site' (WS) type and as a web-download but included in Service Pack 1 the 'ASP.NET Web Application project' (WAP) type which is pretty much the way we used to do asp.net projects in .Net 1.x.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvs05/html/WAP.asp describes this type and shows the pros and cons between the two types. So far I as only using WAP because that's how I always did it and it was much easier to port my applications from 1.1 to 2.0. However I liked the fact 'dynamic compilation' of the code-behind files in WS. If you have a bigger site it takes a while to compile all your code-behind code into that single assembly, and you have to do that all the time.

I played with WS a little but because of the deployment model for my main site, I decided to stick with WAP.
But I thought it would be nice to use WS during development and WAP for test, staging and production sites. The asp.net runtime looks at the page directive of an requested aspx page. If it finds the 'CodeFile=' attribute it knows it has to compile the class in that file and deals with a WS project. So by simply changing all CodeBehind= to CodeFile I could convert my WAP project into a WS project and could use the benefit of the dynamic compilation of the aspx.cs files.

Switching between the two. To switch between the two types I came up with a little bit of scripting:

The main task the batch files does is to replace CodeBehind= with CodeFile= and back. I also had to rename the folder 'App_code' to something else like 'AppCode', otherwise you get two copies of the same classes.

One other thing I had to change in my site was an HttpModule which was part of the single assembly. I guess it has to be in a well known assembly because it has to be set up in web.config. So I just moved the class out into it's own little assembly.

I started by using a plain old batch file and some external tools like 4nt.exe and RxFind  as well as Windows scripting host but the next day I ported the whole thing to a Windows Powershell script.

The script supports both IIS and the Visual Studio internal web server because I use both of the at different times. I first delete all temporary asp.net files because otherwise we may end up with two copies of the same classes.

The I check for the existance of my single web assembly, if is exists, I want to move from WAP to WS, if not its the other way.

I then replace all codebehind with codefile or the other way around, it's pretty easy to do in Powershell.

At the end I start the Application poll or the web server process again.

The solution doesn't require Visual Studio at all, but so far I only tested on a Windows 2003 Server, when using XP you don't have AppPools, just kill the ddlhost.exe process hosting the asp.net runtime. Also because you stop an AppPool you need to be an administrator, which is a pain because of course we all never ever log on as one of those. I usually always have one console window that runs under an admin account so I run the batch there.

Here's the script, save it as switch.ps1

# Powershell script to switch an asp.net project from a 'Web Site' type
# to a 'Application Web Project' type and back.
# Peter Hahndorf
# Singapore 11-Dec-2006
# www.twee.net/contact

# Configuration
$AppPoolName="DefaultAppPool"   # The IIS Pool used for the app
$WwwRootDir="c:\hosts\Topas\Tweenet.Topas.UI.Web" # The path to the web files
$WebUIAssembly="TweeNet.Topas.UI.Web.DLL" # The Name of the assembly
$VirtualDirName="Topas" # The name of the virtual directory
$ProjectFileName="TweeNet.Topas.UI.Web.csproj" # Name of the project file
$useIIS = $false # set to $true when using IIS or $false when using Cassini

#======================================================================

# Build some paths
$DotNetDirectory = $env:windir + "\Microsoft.NET\Framework\v2.0.50727"
$assemblyfile = $WwwRootDir + "\bin\" + $WebUIAssembly

# a function to replace text, I thought I had to use
# .net for this, but it's all built in.
function ReplaceString([string]$fileName,[string]$find,[string]$replaceWith)
{
  (Get-Content $fileName) | foreach-object {$_ -replace $find, $replaceWith} | set-content $fileName
}

# function to loop through files
function SwitchFileContent([bool] $toApplication)
{

	$files = get-childitem  $WwwRootDir -Include *.as?x -Recurse
	
	foreach ($file in $files) 
	{
	   if ($toApplication)
	   {
		  ReplaceString $file.fullname " CodeFile=" " CodeBehind="
	   }
	   else
	   {
		  ReplaceString $file.fullname " CodeBehind=" " CodeFile="
	   }
	}

	$files = get-childitem  $WwwRootDir -Include *.master -Recurse
	
	foreach ($file in $files) 
	{
	   if ($toApplication)
	   {
		  ReplaceString $file.fullname " CodeFile=" " CodeBehind="
	   }
	   else
	   {
		  ReplaceString $file.fullname " CodeBehind=" " CodeFile="
	   }
	}

}

if ($useIIS)
{
	# get the IIS Application Pool
	$pool = get-wmiobject -namespace root/MicrosoftIISv2 -class IIsApplicationPool|where {$_.name -match $AppPoolName}
	
	# Stop the application Pool
	$pool.Stop()
}
else
{
	# don't show error if the process does not exist.
	$server = get-process -name webdev.webserver -ErrorAction SilentlyContinue
	
	if ($server -ne $null )
	{
		stop-process -name "WebDev.WebServer"
	} 

}

# Delete all temporary asp.net files for this app
$tempDir = $DotNetDirectory + "\Temporary ASP.NET Files\" + $VirtualDirName + "\"

if ((test-path $tempDir))
{
   Remove-Item $tempDir -Recurse -force
}

if ((test-path $assemblyfile))
{
   "Switch to Web Site Project..."

    # delete the web assembly
    Remove-Item $assemblyfile
	
	# Enable automatic compilation and additional classes in app_code
	$cmdLine = $WwwRootDir + "\AppCode\"
	Rename-Item -Path $cmdLine -newName "App_Code"
	
    SwitchFileContent $false
}
else
{
    "Switch to ASP.Net Web Application Project..."

    # replace the attributes
    SwitchFileContent $true

	# disable App_Code
	$cmdLine = $WwwRootDir + "\App_Code\"
	Rename-Item -Path $cmdLine -newName "AppCode"

	# Use MsBuild to compile the single web assembly
	$startInfo = new-object System.Diagnostics.ProcessStartInfo
	$startInfo.FileName = $DotNetDirectory + "\msbuild.exe "
	$startInfo.Arguments = $WwwRootDir + "\" + $ProjectFileName + " /t:Build /nologo"
	$startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden

	$process = [system.Diagnostics.Process]::Start($startInfo)	

}

if ($useIIS)
{
	# Start the pool again
	$pool.Start()
}
else
{
	$startInfo = new-object System.Diagnostics.ProcessStartInfo
	$startInfo.FileName = $DotNetDirectory + "/WebDev.WebServer.exe"
	$startInfo.Arguments = " /port:80 /path:" + $WwwRootDir
	$startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden

	$process = [system.Diagnostics.Process]::Start($startInfo)
}

"Done"




 
Categories: ASP.Net