If you’ve ever tried to apply the same set of settings to all the projects in a solution you’ll know it can be somewhat time consuming. You have to edit each project file and change it’s settings. Let’s say you’d like to run code analysis on Release builds but ignore one or two specific rules. To do this you have to violate the DRY principle and have copies of the same settings in every project file. Pretty soon it becomes quite time consuming to make sure that your projects all have the settings you expect.
I started to think of a mechanism for applying common settings to each project; allowing the author to either merge the new global values in with any existing project specific ones or overwrite the project defaults completely. I also wanted to be group these settings so I could apply them all together and define these groups of settings in a single place. For example I could edit a single file to specify that code analysis would run for all projects and specific warnings would be ignored.
How’s it done?
Because a .csproj file is just another MSBuild project you can import your own targets into it. This means I can import some new settings and use them to modify the existing ones in the project. This is a little trickier than you might expect because of the way the MSBuild PropertyGroup works as we’ll see below.
You have to make a single change to the end of each project file in your solution:
<PropertyGroup Condition="'$(SolutionDir)' == '' or '$(SolutionDir)' == '*undefined*'"> <SolutionDir>..\..\</SolutionDir> </PropertyGroup> <Import Project="$(SolutionDir)CommonSettings.targets" /> </Project>
This will import a targets file and set the SolutionDir property if it’s not already set. IDE builds will set this property for you but command line builds will not. It also imports the targets file that contains logic to update the project’s properties.
What I need is a way to merge the two groups of settings in an explicit way before executing the build. Luckily MSBuild gives you a way to do this. Setting the InitialTargets property will cause the MergeProperties target to run before the default target specified in the .csproj file. The MergeProperties target takes the settings in the build settings items file and either merges them with the project’s existing values or uses them to overwrite those values:
<Project InitialTargets="MergeProperties" xmlns=http://schemas.microsoft.com/developer/msbuild/2003 ToolsVersion="3.5"> <PropertyGroup Condition="'$(BuildSettings)'==''"> <BuildSettings>DefaultSettings</BuildSettings> </PropertyGroup> <Import Project="$(BuildSettings).items" Condition="Exists('$(SolutionDir)$(BuildSettings).items')" /> <Target Name="MergeProperties"> <Error Text="$(SolutionDir)$(BuildSettings).items not found" Condition="!Exists('$(SolutionDir)$(BuildSettings).items')" /> <Message Text="SolutionDir: $(SolutionDir)" /> <Error Text="SolutionDir does not have trailing slash." Condition="!HasTrailingSlash('$(SolutionDir)')" /> <CreateProperty Value="$(CommonCodeAnalysisRules);$(CodeAnalysisRules)"> <Output TaskParameter="Value" PropertyName="CodeAnalysisRules" /> </CreateProperty> <CreateProperty Value="$(OverrideRunCodeAnalysis)" > <Output TaskParameter="Value" PropertyName="RunCodeAnalysis" /> </CreateProperty> <Message Text="SolutionDir: $(SolutionDir)" /> <Message Text="RunCodeAnalysis: $(RunCodeAnalysis)" /> <Message Text="CodeAnalysisRules: $(CodeAnalysisRules)" /> </Target> </Project>
A typical settings file looks something like this, DefaultSettings.items:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5"> <PropertyGroup> <CommonCodeAnalysisRules> -Microsoft.Design#CA1020;-Microsoft.Performance#CA1822; </CommonCodeAnalysisRules> </PropertyGroup> <PropertyGroup> <OverrideRunCodeAnalysis>false</OverrideRunCodeAnalysis> </PropertyGroup> </Project>
Now if I build my solution from the IDE it will pick up the values specified in the Items file but I can also write other items files which set properties correctly for other build scenarios. For example I might have a different set of values for my continuous integration server. I can specify this from the command line
msbuild MyApplication.sln /p:BuildSettings=ContinuousIntegration
Once you’ve modified your project file you will see a security warning dialog next time you try and load the project. This is because Visual Studio detects that your project file is importing additional files that are potentially untrustworthy. To get things to work you need to select the “Load project normally” option. You can find out more about this here. It’s possible to get VS to trust your build file if you don’t want to have to dismiss this dialog each time you load the solution.
Note: Updated later same day to include a slightly better code in the .csproj file which sets the SolutionDir property. SolutionDir is set automatically when building in VS but not from the command line. Having each project know this is useful for some other things you might want to do – a future post perhaps.
Update: Copy-paste of the source text above causes all the quotes in the XML to get modified. You can now download the actual files.
Currently listening to:
Gogol Bordello – Super Taranta!