Welcome to EMC Consulting Blogs Sign in | Join | Help

James Dawson's Blog (2005 - 2011)

I have now left EMC Consulting, if you wish to continue to receive new content then please subscribe to my new blog here: http://www.readsource.co.uk

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:

  <connectionStrings>

    <add name="DB1"

         connectionString="Data Source=.;Initial Catalog=DB1;Integrated Security=SSPI"

         providerName="System.Data.SqlClient" />

  </connectionStrings>

 

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:

image image

Such files appear in Solution Explorer as normal project items, albeit with a shortcut icon.

image

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.

  <ItemGroup>

    <Content Include="..\..\dbConfig.config">

      <Link>dbConfig.config</Link>

    </Content>

  </ItemGroup>

 

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:

  1. When building in Visual Studio, nothing special happens to copy the content files (linked or otherwise)
  2. 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:

  1. Fix the 'Copy' task to not include linked files
  2. 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:

  <!--

  ============================================================

  _CopyWebApplication

  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="@(IntermediateAssembly)"

          DestinationFolder="$(WebProjectOutputDir)\bin"

          SkipUnchangedFiles="true" />

    <Copy SourceFiles="@(AddModules)"

          DestinationFolder="$(WebProjectOutputDir)\bin"

          SkipUnchangedFiles="true" />

    <Copy SourceFiles="$(IntermediateOutputPath)$(_SGenDllName)"

          DestinationFolder="$(WebProjectOutputDir)\%(Content.SubFolder)%(Content.RecursiveDir)"

          SkipUnchangedFiles="true"

          Condition="'$(_SGenDllCreated)'=='true'" />

    <Copy SourceFiles="$(IntermediateOutputPath)$(TargetName).pdb"

          DestinationFolder="$(WebProjectOutputDir)\bin"

          SkipUnchangedFiles="true"

          Condition="'$(_DebugSymbolsProduced)'=='true'" />

    <Copy SourceFiles="@(DocFileItem)"

          DestinationFolder="$(WebProjectOutputDir)\bin"

          SkipUnchangedFiles="true"

          Condition="'$(_DocumentationFileProduced)'=='true'" />

    <Copy SourceFiles="@(IntermediateSatelliteAssembliesWithTargetPath)"

          DestinationFiles="@(IntermediateSatelliteAssembliesWithTargetPath->'$(WebProjectOutputDir)\bin\%(Culture)\$(TargetName).resources.dll')"

          SkipUnchangedFiles="true" />

    <Copy SourceFiles="@(ReferenceComWrappersToCopyLocal); @(ResolvedIsolatedComModules); @(_DeploymentLooseManifestFile); @(NativeReferenceFile)"

          DestinationFolder="$(WebProjectOutputDir)\bin"

          SkipUnchangedFiles="true" />

    <!-- copy any referenced assemblies to _PublishedWebsites\app\bin folder -->

    <Copy SourceFiles="@(ReferenceCopyLocalPaths)"

          DestinationFolder="$(WebProjectOutputDir)\bin"

          SkipUnchangedFiles="true" />

    <!-- MODIFICATION HERE: Copy local content files (i.e. non-linked files) recursively to _PublishedWebsites\app\ folder -->

    <Copy Condition=" '%(Content.Link)' == '' "

          SourceFiles="%(Content.Identity)"

          DestinationFolder="$(WebProjectOutputDir)\%(Content.RelativeDir)" />

  </Target>

 

For the second, we need to add a new target to our project file and override the default dependencies to have it executed:

  <!--

  ============================================================

  CopyLinkedContentFiles

 

  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.

  ============================================================

  -->

  <Target Name="CopyLinkedContentFiles">

    <!-- Remove any old copies of the files -->

    <Delete Condition=" '%(Content.Link)' != '' AND Exists('$(WebProjectOutputDir)\%(Content.Link)') "

            Files="$(WebProjectOutputDir)\%(Content.Link)" />

    <!-- Copy linked content files recursively to the project folder -->

    <Copy Condition=" '%(Content.Link)' != '' "

          SourceFiles="%(Content.Identity)"

          DestinationFiles="$(WebProjectOutputDir)\%(Content.Link)" />

  </Target>

  <!-- Override the default target dependencies to -->

  <!-- include the new _CopyLinkedContentFiles target. -->

  <PropertyGroup>

    <PrepareForRunDependsOn>

      $(PrepareForRunDependsOn);

      _CopyWebApplication;

      CopyLinkedContentFiles;

      _BuiltWebOutputGroupOutput

    </PrepareForRunDependsOn>

  </PropertyGroup>

 

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 Tuesday, June 03, 2008 1:51 AM by james.dawson
Filed under: ,

Attachment(s): LinkedFilesBugWorkaround.zip

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

CodeClimber said:

Bug in MsBuild: Linked files in Web Application projects are not deployed properly

July 7, 2008 1:07 PM
 

Shane Kenyon said:

This works in VS2010 as well.  The only change is that the override section is called _CopyWebApplicationLegacy.  This seems like a crazy MS bug that should be fixed!

August 3, 2012 11:06 PM
 

wizmagister said:

Thanks ! I've been looking at many solutions on the Web and this is very clean (I don't like changing the project file, but at least it stays there forever).

Works with VS 2012.  

You don't need to fix the COPY task. Simply make sure you Linked "Views" files are set to "Do not Copy"

December 13, 2012 9:01 PM
 

Andrii Litvinov said:

Great thanks! You save a lot of my time.

All I changed in my project is copied content of CopyLinkedContentFiles Target to AfterBuild Target. And it works fine is VS 2010 and 2012.

December 25, 2012 1:31 PM
 

Matt Perdeck said:

It seems to me that if it comes to copying linked files at each build, there is a simpler solution:

http://mattperdeck.com/post/Copying-linked-content-files-at-each-build-using-MSBuild.aspx

May 9, 2013 4:57 PM
 

Shalke said:

Oh man, great post! Just what I needed.

July 8, 2013 9:23 AM
 

Floris Westerman said:

Matt Perdeck:

That doesn't work in VS 2012...

August 31, 2013 9:14 PM

Leave a Comment

(required) 
(optional)
(required) 
Submit
Powered by Community Server (Personal Edition), by Telligent Systems