One of my favorite things in VS 2005 is the Web Deployment Project. I've used them significantly on two major projects, and now I've discovered some things I do repeatedly that I want to remember for the next time.
Using Configuration Replacement Files
Every project I work on has different settings for the debug and the final (released) version. Configuration Replacement files are the perfect answer for an automatic way to make sure that the released web.config file has the correct compilation mode, connection strings, and what have you. This is one of the major features of Web Deployment Projects, but I've definitely found a way of using this feature that seems to work well for me.
Configurations
Before I get into talking about the replacement of parts of the web.config file, I'm going to quickly cover how I handle different configurations. Most places I've worked follow similar configurations. A configuration is a setting you provide to Visual Studio or MSBuild that informs the build process about the intended deployment of the assembly.
90% of the time, that means I'm building locally so I can test it on my machine. The default configuration for such a build is known as "Debug." However, I found that leaving this as the default configuration doesn't say much about the intended deployment. So I always create a new configuration called "DEV" that implies that the assemblies will be executed on the development machine. Its settings are identical to the Debug configuration, except for the output path for the assemblies. It includes as much debugging and tracing information as I can stand.
Once I've stabilized the DEV build, it's time to put it one another machine so other people can test it. I usually call this phase Testing or QA. So I create another configuration called either "TEST" or "QA", depending on the shop I'm in. This configuration is nearly identical to the DEV configuration, but may have different connection strings. It will also include lots of debugging and tracing information so that as the testers find errors, they can provide me with more information on the error.
After the application has been tested a bunch, it's time to get ready for the public release. The public release really must go perfectly the very first time it happens. To help make sure this happens, I go through several "dry runs" of building a release that is as close the public release as I can get without actually modifying the publicly-used (production) server. I call this phase Staging, and I create a configuration called "STAGE". Sometimes, I've used a User Acceptance Test phase to conduct the Staging phase, and have called the configuration "UAT". Either way, I remove as much debugging information as I can, but I may include tracing information, depending on the policies for the shop's operations.
Finally, I create the publicly releasable configuration, called "PROD". The default configuration for this build is known as "Release". I found that name unhelpful, because in a way, the Staging build is also a release version (at least in the sense I and other developers tend to think of Release as meaning merely "free of debug information").
Finally, I use the Visual Studio Configuration Manager to turn on and off various builds and deployment settings, to help speed things up. In the end, things look like this:
The project that begins with "E:\..." is the Web Site Project. It's the only project I select for Build in the DEV configuration. As long as I have all of my Project Dependencies set correctly, the other projects will get built automatically anyway. You can't change the configuration for a Web Site Project; it's always going to be set to "Debug", even for final release builds.
The other two projects that are set to the "Debug" configuration are projects that I don't control, and so I simply select the best configuration for what I need. Again, in my DEV configuration, I want as much debug information as I can get.
The project that ends with ".database" is a Visual Studio Team System Database Project. I can't recommend these things enough! This thing actually made it almost fun to work with database build and change scripts. However, I don't want to build (create the change script) or deploy (execute the change script on the target database) every time I hit F5 or F6. So I also leave the Build and Deploy boxes unchecked in DEV.
The project that ends with "_deploy" is the Web Deployment Project. As with the Database Project, I don't want to build this in DEV, because my local IIS (or the built-in web server with VS) will almost certainly point to my project's source location (the one at E:\...).
As you can see, I have change the project configurations for some of my projects from Debug to DEV. This is probably only strictly necessary for the Web Deployment Project itself, but it's easier for me to remember how I decided to set configuration-specific project settings for the dependent projects if I just name them according to the solution configuration.
The QA configuration looks identical, except for one thing. On the Web Site Project (the one beginning with "E:\..."), the Build box is unchecked, and on the Web Deployment Project (the one ending with "_deploy"), the Build box is checked. Also, instead of "DEV", the project configurations are named "QA".
The UAT and PROD builds resemble each other, too. Here's the PROD solution configuration shown in the Configuration Manager:
For reasons I can't understand, sometimes these settings and checks get clobbered by Visual Studio, so it's worthwhile to go back into Configuration Manager and double check them periodically.
Replacement
There are always at least two parts of the web.config file that I want to replace for different solution configurations, both in the <system.web> element: <compilation> and <trace>. Frequently, I'll also need to have different settings for the <connectionStrings> and <appSettings> elements, too.
I found that the way that works best for me is to create a Configurations folder at the root folder of the web site, although you can name whatever you like. In the Configurations folder, I place all the replacement configuration files.
This feature of Web Deployment Projects is pretty well documented. There are essentially two ways to conduct replacing part of the web.config file at build time. The first way is to actually replace whole sections in the web.config file, building a new version of web.config for each configuration. The other is to use the configSource attributes available for many elements in the web.config file to link to other .config files.
I use the second method: using the configSource attribute to link in the external .config files. My reasoning is thus: after the build, I like to visually check the web.config file to see whether it has the settings I expect for that configuration. This is a little tricky if I merely replace whole sections of the web.config file, but if I use the configSource attributes, I can instantly determine whether it's using the right files, because of the way I name them.
I name the external configuration files using a simple technique. The first part of the file name is the path of parent element, with underscores separating one level from another if needed, then an underscore, and then the name of the section, another underscore, and the name of the configuration. The final names look like this:
appSettings_DEV.config
appSettings_PROD.config
appSettings_QA.config
appSettings_UAT.config
connectionStrings_DEV.config
connectionStrings_PROD.config
connectionStrings_QA.config
connectionStrings_UAT.config
systemWeb_compilation_DEV.config
systemWeb_compilation_PROD.config
systemWeb_compilation_QA.config
systemWeb_compilation_UAT.config
systemWeb_customErrors_DEV.config
systemWeb_customErrors_PROD.config
systemWeb_customErrors_QA.config
systemWeb_customErrors_UAT.config
systemWeb_trace_DEV.config
systemWeb_trace_PROD.config
systemWeb_trace_QA.config
systemWeb_trace_UAT.config
I use the Web Deployment Project Property Pages to set the web.config replacement settings, like this:
Notice how the check boxes are set. I separated each line in the web.config replacement textbox to make it easier to read, but be sure to use semicolons, because they will be joined into a single line as soon as you close the dialog.
When its built, the web.config file will be easy to check to make sure its got the right settings.
Here's the original web.config file:
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation defaultLanguage="c#" debug="true">
<assemblies>
<add assembly="System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A"/>
</assemblies>
</compilation>
<customErrors mode="Off"/>
<authentication mode="Windows"/>
<authorization>
<allow users="*"/>
</authorization>
<trace enabled="true" mostRecent="true" requestLimit="30" pageOutput="false" localOnly="true" traceMode="SortByTime"/>
<sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424"
sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="20"/>
<globalization requestEncoding="utf-8" responseEncoding="utf-8"/>
<xhtmlConformance mode="Legacy"/>
<pages masterPageFile="~/AllPages.master" autoEventWireup="true" theme="Default" />
<siteMap defaultProvider="MySiteMapProvider">
<providers>
<add name="MySiteMapProvider" type="MySiteMapProvider" securityTrimmingEnabled="true" />
</providers>
</siteMap>
</system.web>
<appSettings>
<add key="Configuration" value="DEV"/>
</appSettings>
<connectionStrings>
<add name="MyConnectionString" connectionString="Data Source=.;Initial Catalog=pubs;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
After it's built, the web.config looks like this (for the UAT solution configuration):
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation configSource="Configurations\systemWeb_compilation_UAT.config" />
<customErrors configSource="Configurations\systemWeb_customErrors_UAT.config" />
<authentication mode="Windows"/>
<authorization>
<allow users="*"/>
</authorization>
<trace configSource="Configurations\systemWeb_trace_UAT.config" />
<sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424"
sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="20"/>
<globalization requestEncoding="utf-8" responseEncoding="utf-8"/>
<xhtmlConformance mode="Legacy"/>
<pages masterPageFile="~/AllPages.master" autoEventWireup="true" theme="Default" />
<siteMap defaultProvider="MySiteMapProvider">
<providers>
<add name="MySiteMapProvider" type="MySiteMapProvider" securityTrimmingEnabled="true" />
</providers>
</siteMap>
</system.web>
<appSettings configSource="Configurations\appSettings_UAT.config" />
<connectionStrings configSource="Configurations\connectionStrings_UAT.config" />
</configuration>
As you can see, it's very easy to tell that the UAT configuration overrides have been applied to this web.config.
Incorporating ApplicationSettings from Class Library assemblies
This part is not exactly specific to Web Deployment Projects, but due to the way it works out of the box, you'll need to make some manual adjustments if you want to use this with Web Deployment Projects.
Here's the basic situation. I like to use configurable settings in Class Libraries, and then incorporate those Class Library assemblies in my web site projects in a way that I can change the configurations from the web.config file (or a linked-in configSource .config file) and have the Class Library pick up the correct settings. Seems like a simple thing, but it wasn't easy to figure out how to make it all work with Web Deployment Projects.
I'll start with the Class Library. In this case, the class library is basically a wrapper for a web service. However, I will need to change the URL to the web service for different build configurations, since I have one web service URL that is meant for development, testing, and staging, and another is the production URL (variations on this idea are, of course, quite possible).
First, I display the Class Library project in the Solution Explorer window in Visual Studio. I expand the project to show the Web References, and I select the Web Reference I originally added. Once selected, I display the Properties window, and check the URL Behavior property to make sure that it is set to Dynamic. With the URL Behavior property set to Dynamic, I should find a Settings class in the Properties folder of the project, as well as an app.config file.
The part that I want is the app.config file. I open it, and see that there are two sections I will need: the configSections element and the applicationSettings element. However, it won't do to copy these straight into my web.config file, because the web.config replacement will choke when it tries to substitute either the applicationSettings element or the settings inside it. So I will forego having the section group (named "applicationSettings") and use only the actual config section contained in it.
Step by step: Open the app.config file:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="MyWebServiceProject.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<applicationSettings>
<MyWebServiceProject.Properties.Settings>
<setting name="MyWebServiceProject_MyWebService_MyService" serializeAs="String">
<value>http://mywebserviceserver/ws/MyService</value>
</setting>
</MyWebServiceProject.Properties.Settings>
</applicationSettings>
</configuration>
Copy the configSections element and paste it into the web.config file, or, if you already have a configSections element in the web.config file, paste only the section element and not the sectionGroup element:
<configSections>
<section name="MyWebServiceProject.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
Go back to the app.config file and copy the contents inside the applicationSettings element, but not the actual applicationSettings tags, and paste it into the web.config file. It should be at the same level from the root as the appSettings element. Here's the web.config file (with some sections collapsed):
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="MyWebServiceProject.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<system.web>[...]
<appSettings>[...]
<MyWebServiceProject.Properties.Settings>
<setting name="MyWebServiceProject_MyWebService_MyService" serializeAs="String">
<value>http://mywebserviceserver/ws/MyService</value>
</setting>
</MyWebServiceProject.Properties.Settings>
<connectionStrings>[...]
</configuration>
That's it! Now you can replace the contents of this section using the web.config replacement feature of Web Deployment Projects just like any other section.
Removing Files
I invariably have files and folders that I don't want to include in the final output. There are two different ways to tackle this, and I like one better than the other. Both ways require working on the .wdproj file directly. The easy way to do that is to right-click the Web Deployment Project in the Solution Explorer window of Visual Studio, and select the Open Project File command.
The way I don't like as much is to put RemoveDir and Delete commands into the AfterBuild or AfterMerge targets. What does that mean? You will need to understand the MSBuild schema a little bit first. To keep things short(er), here is what it would look like in the .wdproj file:
<Target Name="AfterBuild">
<RemoveDir Directories="$(OutputPath)Configurations\" />
<Delete Files="$(OutputPath)MySolution.sln" />
<Delete Files="$(OutputPath)localtestrun.testrunconfig" />
<Delete Files="$(OutputPath)MySolution.vsmdi" />
<RemoveDir Directories="$(OutputPath)TestResults\" />
</Target>
This works fine, and you could even put conditions around it, but it feels wrong, so I use the ExcludeFromBuild command.
If you used the method of including external configuration files I listed above, you may want to get rid of the unused .config files. Here is what my .wdproj file looks like (edited for clarity):
<ItemGroup Condition="'$(Configuration)|$(Platform)' == 'QA|AnyCPU'">
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_DEV.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_PROD.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_UAT.config" />
...
</ItemGroup>
<ItemGroup Condition="'$(Configuration)|$(Platform)' == 'DEV|AnyCPU'">
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_QA.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_PROD.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_UAT.config" />
...
</ItemGroup>
<ItemGroup Condition="'$(Configuration)|$(Platform)' == 'PROD|AnyCPU'">
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_DEV.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_QA.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_UAT.config" />
...
</ItemGroup>
<ItemGroup Condition="'$(Configuration)|$(Platform)' == 'UAT|AnyCPU'">
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_DEV.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_PROD.config" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\Configurations\**\*_QA.config" />
...
</ItemGroup>
I also add an unconditional ItemGroup to handle exclusions for all configurations:
<ItemGroup>
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\MyWebSolution.sln" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\lib\**\*.*" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\CVS\**\*.*" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\**\CVS\**\*.*" />
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\**\.cvsignore" />
</ItemGroup>
The lines for CVS are only useful if you are using CVS for source code control (I don't always).
Copying Files
Some files are not picked up automatically by the Web Deployment Project. For example, I needed to copying licensing files (*.lic) from a popular 3rd party control library.
In this case, the AfterBuild (or AfterMerge) target is perfect:
<Target Name="AfterBuild">
<!-- This XCopy command is required to add the license files to the output -->
<Exec Command="xcopy "$(_FullSourceWebDir)\Licenses\*.lic" "$(OutputPath)Licenses"" />
</Target>
Notice that the " entities should be used around the source and destination paths; they must be used if you have any spaces in the computed paths of either the source or destination.
Building
Once you have all this in place, it's pretty easy to use MSBuild rather than Visual Studio to build the web site. However, if you go this route, you might want to make sure that any dependencies your web site has on other projects is by File Reference, and not by Project Reference. I've encountered problems building Web Deployment Projects for web sites that have project references. Sometimes it works, sometimes it doesn't. Seems to have something to do with the project configuration names. The usual error I get complains that I haven't set the OutputPath property for the dependent project (like that makes any sense).
The command line couldn't be simpler, but it helps a lot to start it from the Visual Studio 2005 Command Prompt (which is at Start > All Programs > Microsoft Visual Studio 2005 > Visual Studio Tools > Visual Studio 2005 Command Prompt):
msbuild /Property:Configuration=PROD MyWebSolution.sln
Be sure to run MSBuild on the solution (.sln) file, not the .wdproj file. You can also abbreviate /Property to /P if you like.
Update 2007-09-19T12:30:
After reading a little more on MSBuild and Web Deployment Projects, it's pretty apparent that Web Deployment Projects (and by extrapolation, MSBuild) will choke when it hits this condition:
- Have a Web Site Project that depends on any other project, such as a class library.
- Use a Solution Configuration that uses a different Project Configuration for the Web Deployment Project than for the dependent project
For example, let's say your Web Site Project, E:\MyWebSite, depends on the class library project MyClassLibraryProject.csproj. Furthermore, let's say you followed my suggestion to create a DEV solution configuration, and then modified the Web Deployment Project so that its project configuration is also set to DEV, but did not modify the MyClassLibraryProject project configuration, so it remains set to Debug.
When you build with this solution configuration, MSBuild will complain that you did not set the OutputPath property:
The OutputPath property is not set for this project. Please check to make sure that you have specified a valid Configuration/Platform combination. Configuration='DEV' Platform='AnyCPU'
The workaround for this (aside from gettin' crazy with the web deployment project file) is to either modify the project configurations for the dependent projects so that they match the project configuration for the web deployment project, or to switch back to a Debug or Release project configuration for the web deployment project. Either way, the project configuration names have to match.