This solution seems to work..
Included below for persistence:
Using Linked Files with Web Application Projects
A customer recently contacted me saying that they had upgraded to Visual Studio 2008 and had also taken the opportunity to switch from a Web Site Project to a Web Application Project. A while back I’d helped them to centralise some of their configuration so that they only had a single copy of all the configuration sections that were referenced by multiple projects (e.g. the web site, various unit test projects etc.).
This involved creating separate configuration files for each section and referencing them from the main web/app config using the ‘configSource’ attribute, for example:
<!– Reference the shared config file –>
<connectionStrings configSource=”dbConfig.config” />
The above referenced configuration file would something like this:
connectionString=”Data Source=.;Initial Catalog=DB1;Integrated Security=SSPI”
Due to the ‘configSource’ attribute not supporting relative paths, it was necessary to setup pre-Build events to copy the referenced files into the project folder – especially for the Web Site project where a valid web.config is required by the ASPNET compiler.
After the switch to the Web Application project model the client was wondering whether they needed to change how they were handling these shared configuration files. My initial response was to suggest that they discontinue the pre-build scripts and instead use the ‘linked files’ feature that was available with web application projects (much like they have been for other types of projects a long time).
For those of you unfamiliar with this useful feature, here’s how you add a linked file via the ‘Add Existing Item’ option:
Such files appear in Solution Explorer as normal project items, albeit with a shortcut icon.
However, after trying it, they reported that those files were missing from the web site which was causing all manner of problems.
It turns out that when you build the project linked files are not copied into the project folder, which is obviously a problem as it means that the file is not available to the web site at runtime. Your first thought might be to enable the ‘Copy Local’ option for the linked files, however, all this does is to copy the linked files to the ‘bin’ folder… not exactly the result we’re after.
Before carrying on, it’s worth looking at how the linked items are stored in the actual web application project file, as it will later help us understand why this issue occurs.
From this you can see the main path for the item (the ‘Include’ attribute) is a relative path to the shared configuration file, whilst the ‘Link’ attribute tells Visual Studio where the item exists within the project – in this case, the file was added to the root of the project.
To investigate further I took a look at the ‘Microsoft.WebApplication.targets’ file that contains the Web Application project build process and typically lives in the ‘C:\Program Files\MSBuild\Microsoft\VisualStudio\v9.0\WebApplications’ folder. There I found a target called ‘_CopyWebApplication’ which only gets executed when the output directory has been overridden (e.g. what TeamBuild does to redirect all build outputs into a single directory):
<Target Name=”_CopyWebApplication” Condition=”‘$(OutDir)’ != ‘$(OutputPath)'” >
At the end of this Target there is the following call to the MSBuild Copy task:
<!– Copy content files recursively to _PublishedWebsites\app\ folder –>
<Copy SourceFiles=”@(Content)” DestinationFolder=”$(WebProjectOutputDir)\%(Content.RelativeDir)”/>
So when the above Copy task executes it’s going to use the relative path to the linked item as part of the destination path, rather than the path within the project (i.e. the ‘Link’ attribute) – so this explains why the files aren’t getting copied:
- When building in Visual Studio, nothing special happens to copy the content files (linked or otherwise)
- When redirecting the output path, normal Content files are copied fine (their ‘Include’ attribute does not contain a relative path), but the linked content files will get copied to a folder with the same relative path to the actual output folder, as the linked item has to the project file (e.g. ‘MyOutDir\..\..\dbConfig.config’ instead of ‘MyOutDir\dbConfig.config’)
Anyway, let’s cut to the chase… here is my suggested workaround:
- Fix the ‘Copy’ task to not include linked files
- Create another target to handle the copying of linked files using the correct destination path
To achieve the first, we’ll need to override the built-in ‘_CopyWebApplication’ target by pasting it into our web application’s project file and tweaking the ‘Copy’ task mentioned above:
MODIFIED: Ignores linked files as part of normal deployment logic.
This target will copy the build outputs along with the
content files into a _PublishedWebsites folder.
This Task is only necessary when $(OutDir) has been redirected
to a folder other than ~\bin such as is the case with Team Build.
<Target Name=”_CopyWebApplication” Condition=”‘$(OutDir)’ != ‘$(OutputPath)'”>
<!– Log tasks –>
<Message Text=”Copying Web Application Project Files for $(MSBuildProjectName)” />
<!– Create the _PublishedWebsites\app\bin folder –>
<MakeDir Directories=”$(WebProjectOutputDir)\bin” />
<!– Copy build outputs to _PublishedWebsites\app\bin folder –>
<Copy SourceFiles=”@(ReferenceComWrappersToCopyLocal); @(ResolvedIsolatedComModules); @(_DeploymentLooseManifestFile); @(NativeReferenceFile)”
<!– copy any referenced assemblies to _PublishedWebsites\app\bin folder –>
<!– MODIFICATION HERE: Copy local content files (i.e. non-linked files) recursively to _PublishedWebsites\app\ folder –>
<Copy Condition=” ‘%(Content.Link)’ == ” ”
For the second, we need to add a new target to our project file and override the default dependencies to have it executed:
A new target to copy any linked content files into the
web application output folder.
NOTE: This is necessary even when ‘$(OutDir)’ has not been redirected.
<!– Remove any old copies of the files –>
<Delete Condition=” ‘%(Content.Link)’ != ” AND Exists(‘$(WebProjectOutputDir)\%(Content.Link)’) ”
<!– Copy linked content files recursively to the project folder –>
<Copy Condition=” ‘%(Content.Link)’ != ” ”
<!– Override the default target dependencies to –>
<!– include the new _CopyLinkedContentFiles target. –>
With these changes, whenever you build the web project any linked content files will be copied into the web application’s folder structure – whether you override the default output directory or not.
If anyone is interested I’ve attached a sample solution that demonstrates the above workaround (as well as an unmodified project file so you can see the issue).
Part of me wonders whether this is a bug at all, or whether it is in fact just an unexpected feature that was never supposed to be supported… I’ve raised a bug on Connect, so I guess I’ll find out in due course.
Published 03 June 2008 01:51 by james.dawson