MSBuild: By Example—Reusing MSBuild Project Elements

Microsoft .NET Framework, ASP.NET, Visual C# (CSharp, C Sharp, C-Sharp) Developer Training, Visual Studio


Jump to: navigation, search
CSharp-Online.NET:Articles
Visual Studio Articles

MSBuild: By Example

© 2006 Hashimi & Hashimi

Reusing MSBuild Project Elements

As you create MSBuild project files, you will want to reuse some of the elements that you defined previously. For instance, let’s say you have created a new target that deploys your application to a set of servers. You will need to reuse this target in more than one project. How can you do this effectively? The most obvious answer is to copy and paste this target into every project file that needs it. This is probably the easiest solution, but it’s the most difficult to maintain as well. Imagine that in your organization you have 30 projects that use this target, and you need to change the location of one of the servers. You’ll have to search for all these project files and update them. Consolidating this functionality in a single place would be much better. With MSBuild you can achieve this. The element that you will need is the Import project element. So, we’ll now show a simple example of using the Import element. This sample will include the Import1.proj file, which is as follows:

<Project 
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <FtpServer1URL>ftp.sedodream.com</FtpServer1URL>
        <FtpServer2URL>ftp.apress.old.com</FtpServer2URL>
    </PropertyGroup>
</Project>

In this project file you can see that two properties are defined that point to FTP sites. These may be two servers that applications need to be deployed in. The file that uses this, which is named ImportEx1.proj, is as follows:

<Project 
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="Import1.proj"/>
 
    <Target Name="PropertyImport1">
        <Message Text="FtpServer1URL: $(FtpServer1URL)"/>
        <Message Text="FtpServer2URL: $(FtpServer2URL)"/>
    </Target>
 
</Project>

In this project file you are importing the Import1.proj file with the <Import Project="Import1.proj"/> statement. Typically when you create files that are to be imported into other projects, you’ll want to store them in a special location. This is because you want to avoid copying these files to many different locations. This project file has a single target that simply prints the values for the properties defined in the imported project file. You can execute the PropertyImport1 target as follows: >msbuild ImportEx1.proj /t:PropertyImport1. Figure 3-16 shows the result of this execution.



Figure 3-16. Output from the PropertyImport1 target


In Figure 3-16 you can see that the properties defined in the Import1.proj file were successfully imported into the current project. Before you explore this feature in more detail, let’s examine a related issue. When you are creating MSBuild project files, what happens if there is a conflict of the properties, items, or targets? When two or more MSBuild elements have the same name, the last one defined takes precedence. You can see this in action very clearly in the following simple project file:

<Project 
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <FtpServer1URL>files.sedodream.com</FtpServer1URL>
    </PropertyGroup>
 
    <Target Name="PrintFtp">
        <Message Text="FtpServer1URL: $(FtpServer1URL)"/>
    </Target>
 
    <PropertyGroup>
        <FtpServer1URL>ftp.sedodream.com</FtpServer1URL>
    </PropertyGroup>
</Project>

This project file has the property FtpServer1URL being defined twice and a target, PrintFtp, that will print its value. Based on what was stated previously, what value do you expect to be printed? To find out, invoke this target by executing the following at the command line: >msbuild OverrideEx1.proj /t:PrintFtp. Figure 3-17 shows the result of this execution.



Figure 3-17. Output from the PrintFtp target


From the execution of this target you can see that the last defined FtpServer1URL, ftp.sedodream.com, was taken as the value of that property. As you can see, there was no error or warning issued to notify you about this naming conflict. This is an expected behavior of MSBuild files. Similar to the idea of overriding methods in object-oriented programming, it is not an error if you override the value of a previously defined property.

Now we’ll show what happens if you override a target. Does it have the same behavior? To determine this, refer to the following simple MSBuild project file:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <Email>sayed.hashimi@gmail.com</Email>
    </PropertyGroup>
 
    <Target Name="EmailAdmin">
        <Message Text="Emailing: $(Email)"/>
    </Target>
 
    <Target Name="EmailAdmin">
        <Message Text="Email sent to : $(Email)"/>
    </Target>
 
</Project>

The previous target has a property defined, Email, and a target, EmailAdmin, defined twice. According to what we stated previously, the last EmailAdmin target defined should be executed. You can check this by invoking this target with >msbuild OverrideEx2.proj /t:EmailAdmin. Figure 3-18 shows the output from this.



Figure 3-18. Output from the EmailAdmin target


In Figure 3-18 it is clear that indeed the last EmailAdmin target was executed. Before returning to the main point of this section, we’ll mention one more aspect of this. Let’s modify the previous project file a bit; refer to the following:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <Email>sayed.hashimi@gmail.com</Email>
    </PropertyGroup>
    <PropertyGroup>
        <Email>Sayed I. Hashimi [$(Email)]</Email>
    </PropertyGroup>
    <Target Name="EmailAdmin">
        <Message Text="Emailing: $(Email)"/>
    </Target>
 
    <Target Name="EmailAdmin">
        <Message Text="Email sent to : $(Email)"/>
    </Target>
 
</Project>

This is almost the same project file as you saw previously, but the second definition of the Email property is <Email>Sayed I. Hashimi [$(Email)]</Email>. This property looks like it is referencing itself! Think of the Email property as a property defined as a class member, such as in C#. When MSBuild loads this file, it will process the first Email property and set its value. When the second definition is encountered, it gets the current Email property value and adds to it. To execute this target, specify >msbuild OverrideEx3.proj /t:EmailAdmin at the command line. Figure 3-19 shows the result from this execution.



Figure 3-19. Output from the EmailAdmin target


As you can see from Figure 3-19, overriding targets has the same behavior as overriding properties. When a target is defined, it will take precedence over any previously defined target with the same name. Now, you are not likely to override a property or target within the same file, so you are probably wondering why we are discussing this. The reason is that you may run into this situation when your project imports other MSBuild project files.

Let’s talk about what happens when you import another MSBuild project file into yours. When MSBuild starts processing a project file, it will do this in a node-by-node manner, from the top of your project file to the bottom. When an Import element is encountered, these steps then take place:

1. The working directory is changed to that of the imported project file.

2. Project element attributes are processed.

3. Project element nodes are processed.

4. The working directory returns to importing the previous value.

The first step is to change the working directory to that of the imported project file—assuming that it has a different working directory from the current project file, that is. This is necessary to properly handle any relatively defined import statements, or to resolve locations of assemblies when the UsingTask element is inserted, within that project file. This change is not used for item declarations. Let’s move on to the following step.

The next thing after that is for the Project element attributes to be processed. Possible attributes are DefaultTargets and InitialTargets. If a value is already assigned to DefaultTargets, then this is ignored; otherwise, it becomes the value of DefaultTargets. If the InitialTargets attribute is present, then that list of targets will be appended to the current list of InitialTargets. As mentioned, the DefaultTargets attribute is a list of targets to execute if no target is specified; this can be valued from only one project file. The InitialTargets attribute is a list of targets to be executed before any other targets. The value from InitialTargets can be from many different source project files.

Following the Project element attribute processing, the child nodes are processed. The same processing is performed if those nodes were immediate children in the importing file. Finally, for the last step after processing the child nodes, the directory is changed to its previous value, and processing on the original file continues. When you import a project file, it is as if you actually took that file and injected its contents inside of the file that was importing it. This is true with the exception of the Project element itself, which was previously discussed, and with issues related to the working directory, which also were previously discussed. How does this affect reusing elements across project files?

When you reuse elements in project files, you have to be aware of what happens in the case of a naming conflict. We have discussed the behavior of values overridden in the same file. Now that you know how project files are imported, we can discuss what happens when project elements are overridden across project files. The behavior of both is the same; all you have to remember is that the last element defined is the one that gives the value. Let’s clear this up with a simple example. The following is the OverrideEx5.proj project file on which you will invoke MSBuild:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 
    <PropertyGroup>
        <DeployURL>http://www.sayedhashimi.com</DeployURL>
    </PropertyGroup>
 
    <Import Project="ImportOverride1.targets"/>
 
    <Target Name="PrintDeployURL">
        <Message Text="Deployment URL: $(DeployURL)"/>
    </Target>
</Project>

This file contains one property, DeployURL, and one target, PrintDeployURL. This file imports the simple ImportOverride1.targets file, which is as follows:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <DeployURL>http://www.sedodream.com</DeployURL>
    </PropertyGroup>
</Project>

This file simply defines the DeployURL property—the same one defined in the importing project file. You can find the value of this property by executing the PrintDeployURL target. What value do you expect to be printed? Let’s find out by executing the PrintDeployURL target by using >msbuild OverrideEx5.proj /t:PrintDeployURL at the Visual Studio command prompt. Figure 3-20 shows the output from this execution.



Figure 3-20. Output from the PrintDeployURL target


As you can see, the value from the ImportOverride1.target file was used instead of the value defined inside the OverrideEx5.proj file. Is this what you expected? This value is used because it was encountered after the first value of DeployURL. Essentially, a project file was processed with the following contents:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <DeployURL>http://www.sayedhashimi.com</DeployURL>
    </PropertyGroup>
 
    <PropertyGroup>
        <DeployURL>http://www.sedodream.com</DeployURL>
    </PropertyGroup>
    <Target Name="PrintDeployURL">
        <Message Text="Deployment URL: $(DeployURL)"/>
    </Target>
</Project>

In the previous snippet, the import statement was replaced with the contents of ImportOverride1.target, stripping away the Project element, of course. This is exactly how MSBuild will treat your imported files. If the import statement had been before the DeployURL declaration, then the value http://www.sayedhashimi.com would have been used instead. This behavior is the same for targets that are overridden. You can give it a shot on your own to verify this. If you are importing files and you have properties or targets that you must ensure are defined as is, then you should place them after all import statements. When you execute targets on that file, then you can be sure you are using the definition that you intend. If that file is imported into other files, then you can’t be so sure, but you could throw an error if that condition exists.

To summarize, when you share project files, be careful where you place your import statements, and always be aware of how MSBuild will treat them. A good method to mitigate the risk of wrongfully overridden targets is to place hooks into your targets to be overridden instead of overriding the target itself. For instance, Visual Studio provides the BeforeBuild and AfterBuild targets that are empty. Their sole purpose is to be overridden by you. You can employ a similar strategy at your organization. Another method is to use naming conventions; for instance, targets that exist for the sole purpose of being overridden could start with an underscore (_) character.


Previous_Page_.gif Next_Page_.gif

Personal tools