Every time we create a new C# project, Visual Studio puts there the AssemblyInfo.cs file for us. The file defines the assembly meta-data like its version, configuration, or producer. In small solutions, we can edit these files manually; however, with larger ones, this repetitive and boring task becomes problematic.
The common solution to this problem is a build script that first updates all the AssemblyInfo.cs files and then compiles the source code (often by invoking MSBuild, passing it the sln or csproj file).
In this article, I'd like to present a different approach that takes advantage of the fact that (since VS.NET 2005) the csproj files are just MSBuild scripts - and highly customizable ones. By making minor modifications to the csproj files, we can easily solve the AssemblyInfo.cs problem and create a foundation for further customizations.
Before we start, please install MSBuild Community Tasks which is available from here. They contain custom tasks that the presented script uses.
Creating a custom targets file
We will begin with creating a targets file that will contain the versioning functionality and will be referred by our C# projects. The targets extension is, by convention, used for MSBuild scripts that just provide some features for other scripts (they can be compared to header files in the C or C++ world).
The minimal structure of the file is:
Now, inside the
Project element, we will put this line:
It tells MSBuild that the script uses external tasks that are defined in the specified file.
Next comes the following section:
<My-Producer Condition=" $(My-Producer) == '' ">The Producer</My-Producer>
<My-Copyright Condition=" $(My-Copyright) == '' ">Copyright (C) 2008
<My-Major Condition=" $(My-Major) == '' ">1</My-Major>
<My-Minor Condition=" $(My-Minor) == '' ">0</My-Minor>
<My-Build Condition=" $(My-Build) == '' "></My-Build>
<My-Revision Condition=" $(My-Revision) == '' ">0</My-Revision>
It specifies some properties of all the projects that we will customize. If we want to share the script with multiple solutions, we should not embed version information in it. But that is beyond the scope of this article.
Next, we define
<Output PropertyName="My-VersionNumber" TaskParameter="Value" />
It sets the
My-ConfigureVersionNumber property to the full version number.
Then, we add:
<My-AssemblyInfo Include="$(My-PropertiesDir)\AssemblyInfo.cs" />
<Compile Include="@(My-AssemblyInfo)" />
The XML above defines the location of the AssemblyInfo.cs file. The most interesting part of it is the
Compile item. It instructs MSBuild that the AssemblyInfo.cs file should be passed to the C# compiler. The presence of this line will allow us to remove the AssemblyInfo.cs from the project, but more on this later.
Next comes two targets that make use of the properties and items we defined earlier:
<MakeDir Directories="$(My-PropertiesDir)" />
<Delete Files="@(My-AssemblyInfo)" />
These targets are responsible for creating and deleting the AssemblyInfo.cs, respectively. The
AssemblyInfo task is defined in the MSBuild Community Tasks project.
Notice that the first of the targets has its dependency set. This is to ensure that the
My-VersionNumber property is always initialized.
Finally, the last section:
Here, we update the values of the two properties that are used by the default MSBuild scripts for the C# projects. These properties determine which targets are called when the project is built or cleaned up. The section above prepends our custom targets to the default ones. This ensures that our targets are called always - no matter if the application is compiled from within the IDE or from the command line (or the CI server).
That's all - the targets file is finished and should be saved, we will use it soon.
Customizing csproj files
Now that we have the targets file, we can modify our C# projects.
To make the presentation simpler, we will start with an empty C# project of type Class Library (the method works for other project types too, of course). Please start Visual Studio and create the solution and project. Now, in the Solution Explorer, navigate to the project, expand the Properties folder, and delete the AssemblyInfo.cs file. We won't need it here any more.
Now, please open the csproj file outside Visual Studio - Notepad will be fine (as Visual Studio won't let you edit the file manually). The file is an XML document that, at its top level, contains the
ItemGroup, and the
Import sections. Navigate to the
Import section, and insert a new line below it, to make the fragment look like:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\Versioning.targets" />
This makes the targets file we created previously a part of this project. Make sure that the file is copied to the directory where the sln file resides (one level up from the directory containing the csproj for this project).
Like this, you can customize as many projects as you wish. They will all refer to the same copy of our targets file, which means that if we change it, all projects are affected. This allows us to extend the build further, for instance, by running unit tests. And, we can be sure that our custom targets are called always - no matter how the build was started.
Points of interest
- Extending default scripts that are shipped with MSBuild and used every time a C# project is built.
- Learning some basics of the inner workings of the Visual Studio build process.
- 2008.07.19 - Initial version.