Deploying a .NET 4 MVC 2 Application to Windows Server 2003 Using WiX 3.5

Warning: WiX 3.5 is still under development, so you may encounter things that do not work with your specific build of WiX, this article is based on build 1602.

When you are in a tightly controlled corporate environment it is not always possible to deploy your web applications to Windows Server 2008. I recently found myself in the situation where I had to deploy a .NET 4 MVC 2 application to Windows Server 2003. This article deals with how to handle this using the WiX toolset, because there are some extra things you have to deal with.

Before we begin though I am using two WiX extensions, so these have to be set up first. Start by adding references to WixIIsExtension.dll and WixUtilExtension.dll to the WiX project, these can be found in the bin directory where you installed WiX. You must also set up the namespaces by starting your WiX with the following root node:

 <Wix xmlns="https://schemas.microsoft.com/wix/2006/wi"
     xmlns:iis="https://schemas.microsoft.com/wix/IIsExtension"
     xmlns:util="https://schemas.microsoft.com/wix/UtilExtension">

You need to consider that your shiny new application runs on .NET Framework 4.0, while the server may already be running other ASP.NET applications based on earlier versions of the framework; to reduce risk you may want to keep them running on that earlier version rather than upgrade everything to 4.0. I am however assuming that the server already has version 4.0 of the runtime installed, the WiX code I provide just includes a check, if you want to install the framework at the same time then you need to consider a bootstrap setup program to do this for you. Here is how I check for .NET 4 as a pre-requisite:

 <Property Id='FXINSTALLED' Value='0'>
  <RegistrySearch Id='FxInstalledRegistry'
          Key='SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
          Name='TargetVersion'
          Root='HKLM'
          Type='raw'/>
</Property>
<Condition Message='Requires the Microsoft .NET Framework 4'>
Installed OR FXINSTALLED>="4.0.0"
</Condition>

The following describes the particular considerations that apply to deploying an MVC 2 application to IIS 6 when using .NET 4.

The first thing is to make sure that the application has all its dependencies available. Clearly you need to include any of your own dependant assemblies. However, MVC 2 applications also have a dependency on System.Web.Mvc, which uses the 2.0 runtime, this is not included in the 4.0 framework. So unless you already have this assembly in the GAC on the server for some other MVC 2 application, you will need to include this assembly in the MSI, placing it in the application’s bin directory. Alternatively, if you want to install the runtime and share it among applications, you can get it from here: https://www.microsoft.com/downloads/details.aspx?FamilyID=c9ba1fe1-3ba8-439a-9e21-def90a8615a9&displaylang=en.

Another consequence of running on .NET 4 is that the new application will need to run in a different application pool to any .NET 2/3/3.5 web applications running on the same web server. For that you may need a new application pool. This application pool needs to run under a suitable identity, I normally use an unprivileged domain user account for this purpose. The WiX to do this is:

 <util:User
    Id="AppPoolUser"
    Domain="[WSUSERDOMAIN]"
    Name="[WSUSERNAME]"
    Password="[WSPASSWORD]"
    CreateUser="no">
    <util:GroupRef Id="IIS_WPG"/>
</util:User>
<iis:WebAppPool
    Id="MvcWebAppPool"
    Identity="other"
    Name="MVC App Pool"
    User="AppPoolUser"/>

The above is specific to Windows Server 2003 and IIS 6 (IIS_WPG is replaced by IIS_IUSRS on Windows Server 2008), to make the above work you also need to define the IIS_WPG group as follows:

 <util:Group Id='IIS_WPG' Name='IIS_WPG'/>

Note also that I used properties to pass in the name of the service account and its password.

However, just creating a new application pool is not enough, the web application defined for the web site also needs to be set to run under ASP.NET 4.0. This is the trickiest bit because to do this you need to run aspnet_regiis.exe. The first step is to get the path to the correct version as follows:

 <Property Id='FXDIR' Value='0'>
  <RegistrySearch Id='FxInstallPathRegistry'
          Key='SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
          Name='InstallPath'
          Root='HKLM'
          Type='raw'/>
</Property>

You may need be careful here, if the destination platform is 64-bit then you need to add Win64=’yes’ to the <RegistrySearch> element or set the <Package> element’s Platform attribute to ‘x64’, otherwise the wrong version of aspnet_regiis.exe will be executed later.

To invoke aspnet_regiis.exe we need to know the site ID, which is a simple integer identifying the particular web site. For example to set the 4.0 framework for the second web site on a particular machine you need to invoke it as follows:

 aspnet_regiis -s W3SVC/2/ROOT

The problem is that you don’t necessarily know the ID of your newly created web site. I use the following VBScript in a custom action to find the site ID from the friendly name you see in IIS Manager. Note that using VBScript custom actions is not always recommended (see VBScript (and Jscript) MSI CustomActions suck), but if you are in a controlled environment and you are not writing an MSI that will deployed on more than a handful of computers then I think it is acceptable to use short VBScript custom actions.

 Dim websvc, sitetoset
Dim siteName, fxdir
Dim WshShell

Set WshShell = CreateObject("Wscript.Shell")

ArgString = Session.Property("CustomActionData")
Args = Split(ArgString, ",")
siteName = Args(0)
fxdir = Args(1)

set sitetoset = nothing
set websvc = GetObject("IIS://localhost/W3svc")

for each site in websvc
    if site.class = "IIsWebServer" then
        if site.ServerComment = siteName then
            set sitetoset = site
        end if
    end if
next

if (sitetoset is nothing) then
    WScript.Quit (GENERAL_FAILURE)
end if

cmd = fxdir + "aspnet_regiis -s W3SVC/"+sitetoset.Name+"/ROOT"
Set oExec = WshShell.Exec(cmd)
Do While oExec.Status = 0 ' Busy wait because Sleep fails when running during install
Loop

This script takes a single parameter, passed from the MSI, which is a comma-separated list of two parameters, the friendly name of the site, and the directory where aspnet_regiis.exe is located. The reason I pass the path to the directory is because there are 32 and 64 bit versions of aspnet_regiis.exe and the script does not know which one to run. I put the script in a file called RegAspNet40.vbs and include it in the MSI as follows:

 <Binary Id="RegAspNet40Script" SourceFile="RegAspNet40.vbs"/>

Then I invoke the script as follows:

 <CustomAction Id="SetRegAspNet40" Property="RegAspNet40" Value="[SITENAME],[FXDIR]"/>
<CustomAction Id="RegAspNet40" Execute="deferred"
              BinaryKey="RegAspNet40Script" VBScriptCall=""/>

<InstallExecuteSequence>
  <Custom Action="SetRegAspNet40" After="CostFinalize"/>
  <Custom Action="RegAspNet40" After="PublishProduct">&amp;MvcAppFeature=3</Custom>
</InstallExecuteSequence>

The SITENAME property is the friendly name for the web site and FXDIR is the property we set earlier to tell us the location of the .NET Framework. “MvcAppFeature” is the name I chose to give to the feature which installs the MVC 2 web application.

All of this does not matter because an MVC 2 application will not work on IIS 6 without some other measures being taken. Typically an MVC 2 application benefits from using the Integrated Pipeline mode available in IIS 7 on Windows Server 2008, but this is not available in IIS 6 and you have to use Classic Mode instead. To do this you need to register an extension and modify your global.asax too. The global.asax modifications are described in the ASP.NET MVC with Different Versions of IIS tutorial. In addition if you want the home page to work without having to use a url like https://www.contoso.com/Home.mvc/Index then you need to take the default.aspx page from an original MVC application and include the aspx extension too. This is also shown below.

The extensions are registered in the <WebApplicationExtension> element in the fragment below. Note the use of the FXDIR property so that the appropriate version of the runtime is located.

 <iis:WebSite Id="MvcWebWebSite" Description="[SITENAME]" Directory="INSTALLLOCATION">
    <iis:WebDirProperties
        Id="MvcWebSiteDirProperties"
        WindowsAuthentication="yes"
        Read="yes"
        Script="yes"
        Execute="yes"/>
    <iis:WebAddress
        Id="AllUnassigned"
        Header="[HOSTHEADER]"
        Port="[PORT]" />
    <iis:WebApplication
        Id="MvcServiceApplication"
        Name="MVC Service"
        WebAppPool="MvcWebAppPool">
        <iis:WebApplicationExtension
            CheckPath="no"
            Extension="mvc"
            Executable="[FXDIR]\aspnet_isapi.dll"/>
        <iis:WebApplicationExtension
            CheckPath="no"
            Extension="aspx"
            Executable="[FXDIR]\aspnet_isapi.dll"/>
    </iis:WebApplication>
</iis:WebSite>

One final thing, you may need to enable the ASP.NET 4 web service extension. To do this you need the following:

 <iis:WebServiceExtension
  Id="EnableASPNet4Extension"
  Allow="yes"
  File="[FXDIR]aspnet_isapi.dll"
  Group="ASP.NET v4.0.30319"
  Description="ASP.NET v4.0.30319"
  UIDeletable="no"/>

Note that the Description attribute is critical, do not omit it.

Hopefully with this information you can now easily deploy MVC 2 applications on .NET 4 running on Windows Server 2003.

Written by Rob Jarratt