{"id":76004,"date":"2017-11-17T16:22:56","date_gmt":"2017-11-17T16:22:56","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=76004"},"modified":"2021-05-11T15:56:11","modified_gmt":"2021-05-11T15:56:11","slug":"msbuild-targeting-needs","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/development\/dotnet-development\/msbuild-targeting-needs\/","title":{"rendered":"MSBuild: Targeting Your Needs"},"content":{"rendered":"<p>Sometimes it\u2019s worth it to step out of your comfort zone and to mess a little with Microsoft Build Engine (MSBuild) &#8211; the .NET Build Tool. It is a very comprehensive tool, but only some of its options can be set or invoked from within Microsoft Visual Studio. You might use MSBuild to display custom warnings, upload build results on the server, replace a compiler, or many more useful tasks.<\/p>\n<p>When I started my adventure as a software engineer, I thought that I would just write code to make business applications do what they are supposed to do, and I had never really bothered about all the mystical and arcane stuff that happens within the build processes. Then I landed a job working on the <a href=\"https:\/\/revdebug.com\/\">RevDeBug<\/a> project in which I had to learn how the Roslyn compiler works, how to hook into it, how to write Visual Studio Extensions, and how to extend the .NET application to build a pipeline. It turns out this knowledge is also quite useful when you write typical business applications. Now, after more than a year of doing all that, it\u2019s not arcane to me anymore and a lot more fascinating.<\/p>\n<p>I won\u2019t introduce the basics of how MSBuild works as many <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/dd637714.aspx\">resources<\/a> are available, but I\u2019ll show you several tasks which can then become a template to automate some of the more mundane steps in the build process.<\/p>\n<h2>Custom Warnings<\/h2>\n<p>For the first case, assume that I want to systematically update my old projects to the newer version of the .NET Framework, at least to version 4.0. It might not be suitable to upgrade them all at once, especially for some large codebases. It would be great if Visual Studio could warn that some of the projects use an outdated framework version.<\/p>\n<p>This can be easily achieved with MSBuild. I can write a task that will display a warning during the build process for every project targeting a framework lower than 4.0. This approach makes the problem more visible and, hopefully, will lead to a faster upgrade process.<\/p>\n<p>First, I\u2019ll write code for the warning, and next I\u2019ll show you how to hook it up to the build process.<\/p>\n<p>An empty project file with the targets extension will be the entry point for the custom build step and looks like this:<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;Project xmlns=\"http:\/\/schemas.microsoft.com\/developer\/msbuild\/2003\"&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>Most of the declarations within an MSBuild project file can be conditioned using Boolean expressions. In this case, I want to create an additional property which will be set to true when an older framework is used during compilation. The <strong>&lt;PropertyGroup&gt;<\/strong> tag is used for that purpose. During the build, the information about the .NET Framework version is already available, I just need to gather it. You can find a list of more common properties <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/bb629394.aspx\">here<\/a>.<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true  \">   &lt;PropertyGroup Condition=\"'$(TargetFrameworkIdentifier)' == '.NETFramework' \r\n           and  ('$(TargetFrameworkVersion)' == 'v2.0'\r\n           or '$(TargetFrameworkVersion)' == 'v3.0' or '$(TargetFrameworkVersion)' == 'v3.5')\"&gt;\r\n        &lt;LowFrameworkVerUsed&gt;true&lt;\/LowFrameworkVerUsed&gt;     \r\n    &lt;\/PropertyGroup&gt;\r\n<\/pre>\n<p>The code reads the properties <strong>TargetFrameworkIdentifier<\/strong> and <strong>TargetFrameworkVersion<\/strong> (they are always set in MSBuild) and checks their values to determine if a warning should display.<\/p>\n<p>The <strong>&lt;PropertyGroup&gt;<\/strong> tag declares new properties, but another task must be called that will emit a warning message. Those calls can be invoked only within a <strong>&lt;Target&gt;<\/strong> tag. The following code is a target that calls a predefined <strong>Warning<\/strong> task. A list of useful predefined tasks can be found <a href=\"https:\/\/msdn.microsoft.com\/en-us\/library\/7z253716.aspx\">here<\/a>.<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">    &lt;Target Name=\"LowFrameworkVerUsedWarn\"&gt;\r\n        &lt;Warning Text=\r\n          \"Project '$(AssemblyName)' needs it\u2019s framework upgraded. \r\n          Version used now: '$(TargetFrameworkVersion)'.\"\/&gt;\r\n    &lt;\/Target&gt;\r\n<\/pre>\n<p>The next thing to do is to add the code to the build pipeline so the created target will be triggered during the build. To do that, override one of the <strong>*DependsOn<\/strong> properties. The <strong>*DependsOn<\/strong> properties consist of a list of tasks which must be finished before a certain target is called, e.g. <strong>CleanDependsOn<\/strong> is executed before each clean. All the steps that are available within a standard build process for .NET projects are defined in the <strong>Microsoft.Common.CurrentVersion.targets<\/strong> file which can be found in the MSBuild bin directory. The goal is to emit a warning if the compilation used an obsolete framework version. To do so, I\u2019ll search <strong>Microsoft.Common.CurrentVersion.targets<\/strong> for a task that runs after the compilation phase. The description of the <strong>PrepareForRun<\/strong> task, \u2018Copy the build outputs to the final directory if they have changed\u2019 indicates this is the place to hook into.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1060\" height=\"725\" class=\"wp-image-76005\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2017\/11\/word-image-35.png\" \/><\/p>\n<p>To check the version property and call the warning, add a task to the <strong>PrepareForRunDependsOn<\/strong> property:<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">    &lt;PropertyGroup Condition=\"'$(LowFrameworkVerUsed)' == 'true'\"&gt;\r\n        &lt;PrepareForRunDependsOn&gt;\r\n               $(PrepareForRunDependsOn);\r\n               LowFrameworkVerUsedWarn;\r\n        &lt;\/PrepareForRunDependsOn&gt;\r\n    &lt;\/PropertyGroup&gt;\r\n<\/pre>\n<p>Notice that this <strong>&lt;PropertyGroup&gt;<\/strong> checks the <strong>LowFrameworkVerUsed<\/strong> value in its condition. Therefore, I wrote <strong>$(PrepareForRunDependsOn)<\/strong> before adding the target.\u00a0Be careful to always list <strong>$(PrepareForRunDependsOn)<\/strong> before actually adding new targets as I have done above, or you&#8217;ll override the property effectively removing all previous values which will lead almost inevitably to a broken build.<\/p>\n<p>When done, the <strong>LowFrameworkVerUsed.targets<\/strong> file looks like this.<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true \">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;Project xmlns=\"http:\/\/schemas.microsoft.com\/developer\/msbuild\/2003\"&gt;\r\n\r\n    &lt;PropertyGroup Condition=\"'$(TargetFrameworkIdentifier)' == '.NETFramework' and  \r\n            ('$(TargetFrameworkVersion)' == 'v2.0'\r\n             or '$(TargetFrameworkVersion)' == 'v3.0' \r\n             or '$(TargetFrameworkVersion)' == 'v3.5')\"&gt;\r\n        &lt;LowFrameworkVerUsed&gt;true&lt;\/LowFrameworkVerUsed&gt;     \r\n    &lt;\/PropertyGroup&gt;\r\n\r\n    &lt;PropertyGroup Condition=\"'$(LowFrameworkVerUsed)' == 'true'\"&gt;\r\n        &lt;PrepareForRunDependsOn&gt;\r\n            $(PrepareForRunDependsOn);\r\n            LowFrameworkVerUsedWarn;\r\n        &lt;\/PrepareForRunDependsOn&gt;\r\n    &lt;\/PropertyGroup&gt;\r\n\r\n    &lt;Target Name=\"LowFrameworkVerUsedWarn\"&gt;\r\n        &lt;Warning Text=\"Project '$(AssemblyName)' targets older .NET framework \r\n        version '$(TargetFrameworkVersion)'.\"\/&gt;\r\n    &lt;\/Target&gt;\r\n&lt;\/Project&gt;\r\n<\/pre>\n<p>The very last step is to tell MSBuild to use the file during the build. There are two approaches to do so. One way is to add an import tag to the project\u2019s csproj file:<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">  &lt;Import Project=\"C:\\Common-build\\LowFrameworkVerUsed.targets\" \/&gt;.\r\n<\/pre>\n<p>This will work only for projects with the modification in the project file, so it\u2019s not really that useful. The second solution hooks up to every project automatically. Just copy the <strong>LowFrameworkVerUsed.targets<\/strong> file to the directory where MsBuild looks for them by default which is <strong>%localappdata%\\Microsoft\\MSBuild\\15.0\\Microsoft.Common.Targets\\ImportAfter\\<\/strong>.<\/p>\n<p>If such a folder doesn\u2019t exist, you can create it. Just remember to point to the proper version of MSBuild. It is the same as the Visual Studio version if running 2013 or higher, 15.0 in my case.<\/p>\n<p>Now I can be lazy updating my projects without worrying that any of them will slip by me, plus I have my own Visual Studio warnings, yay!<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"712\" height=\"239\" class=\"wp-image-76006\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2017\/11\/word-image-36.png\" \/><\/p>\n<h2>Creating a Build History<\/h2>\n<p>That example was relatively easy, so now I\u2019ll show you how to implement something a little bit more complex and use some C# while doing it. This time I want the last 10 build artefacts to be preserved in a predefined directory creating a local history of builds.<\/p>\n<p>To implement this custom MSBuild task, you could write a regular .NET library that will contain a class extending <strong>Microsoft.Build.Utilities.Task<\/strong> with all the logic in it. Afterwards, you\u2019ll just have to call it from the <strong>targets<\/strong> file:<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">   &lt;UsingTask\r\n        AssemblyFile=\"C:\\MyLibrary.dll\"\r\n        TaskName=\"Namespace.MyClassExtendingTask\" \/&gt;\r\n\r\n   &lt;Target Name=\"CallMyTask\"&gt;\r\n        &lt;MyClassExtendingTask\/&gt;\r\n    &lt;\/Target&gt;\r\n<\/pre>\n<p>The main problem with this approach is that you must provide the assembly somehow to the build process. It shouldn\u2019t be a problem in smaller projects or for local-only solutions, but managing build specific assemblies can quickly become problematic, especially when you just want to add a small task that doesn\u2019t have too much code. Another way to achieve the same effect is to write .NET code directly within the MSBuild project file. I\u2019ll use this approach below just to have it as a self-contained example.<\/p>\n<p>First I\u2019ll start small with just a task that will compress and copy build outputs to chosen directory. To make it work, import a reference to Microsoft.Build.Tasks.v4.0.dll, define input parameters, and include code that will do the intended work. Here is how it should look:<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">   &lt;UsingTask TaskName=\"ArchiveBuildTask\" TaskFactory=\"CodeTaskFactory\" AssemblyFile=\"$(MSBuildToolsPath)\\Microsoft.Build.Tasks.v4.0.dll\"&gt;\r\n        &lt;ParameterGroup&gt;\r\n            &lt;PublishPath ParameterType=\"System.String\" Required=\"true\" \/&gt;\r\n            &lt;ArtifactsPath ParameterType=\"System.String\" Required=\"true\" \/&gt;\r\n            &lt;ArtifactsCount ParameterType=\"System.Int32\" Required=\"true\" \/&gt;\r\n        &lt;\/ParameterGroup&gt;\r\n        &lt;Task&gt;\r\n            &lt;Reference Include=\"System.IO.Compression.FileSystem\"\/&gt; \r\n            &lt;Using Namespace=\"System\"\/&gt; \r\n            &lt;Using Namespace=\"System.IO\"\/&gt; \r\n            &lt;Using Namespace=\"System.Linq\"\/&gt;\r\n            &lt;Using Namespace=\"System.IO.Compression\"\/&gt;\r\n            &lt;Code Type=\"Fragment\" Language=\"cs\"&gt;&lt;![CDATA[\r\n                int fileCount = 0;\r\n                var sortedFiles = new DirectoryInfo(ArtifactsPath).GetFiles()\r\n                .OrderByDescending(f =&gt; f.LastWriteTime).ToList();\r\n\r\n                foreach (var fi in sortedFiles)\r\n                {\r\n                    fileCount++;\r\n                    if (fileCount&gt;ArtifactsCount-1)\r\n                    {\r\n                        fi.Delete();\r\n                    } \r\n                }\r\n\t\tZipFile.CreateFromDirectory(Path.GetDirectoryName(PublishPath), \r\n                ArtifactsPath + @\u201d\\\u201d + \u201dPublish\u201d + \r\n                DateTime.Now.ToString(\u201cyyyyMMddHHmmss\u201d + \u201d.zip\u201d);\r\n                ]]&gt;&lt;\/Code&gt;\r\n        &lt;\/Task&gt;\r\n  &lt;\/UsingTask&gt;\r\n<\/pre>\n<p>Next, create a target, that will call <strong>ArchiveBuildTask<\/strong> and include it in the build process pipeline.<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">   &lt;Target Name=\"ArchiveBuild\" Condition=\"'$(ArchivePath)' != '' and $(ArchiveCount) &gt; '0'\"&gt;\r\n        &lt;ArchiveBuildTask ArtifactsPath=\"$(ArchivePath)\" PublishPath=\"$(OutputPath)\"\r\n        ArtifactsCount=\"$(ArchiveCount)\" \/&gt;\r\n    &lt;\/Target&gt;\r\n\r\n&lt;PropertyGroup&gt;\r\n    &lt;PrepareForRunDependsOn&gt;\r\n        $(PrepareForRunDependsOn);\r\n        ArchiveBuild;\r\n    &lt;\/PrepareForRunDependsOn&gt;\r\n&lt;\/PropertyGroup&gt;\r\n<\/pre>\n<p>Condition <strong>&#8216;$(ArchivePath)&#8217; != &#8221; and $(ArchiveCount) &gt; &#8216;0&#8217;<\/strong> states that the target won\u2019t run as long as parameters are not set. This way you can control when archiving will take place.<\/p>\n<p>The complete targets file then looks like this:<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true  \">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;Project xmlns=\"http:\/\/schemas.microsoft.com\/developer\/msbuild\/2003\"&gt; \r\n\r\n&lt;PropertyGroup&gt;\r\n    &lt;PrepareForRunDependsOn&gt;\r\n        $(PrepareForRunDependsOn);\r\n        ArchiveBuild;\r\n    &lt;\/PrepareForRunDependsOn&gt;\r\n&lt;\/PropertyGroup&gt;\r\n\r\n &lt;UsingTask TaskName=\"ArchiveBuildTask\" TaskFactory=\"CodeTaskFactory\" \r\n        AssemblyFile=\"$(MSBuildToolsPath)\\Microsoft.Build.Tasks.v4.0.dll\"&gt;\r\n        &lt;ParameterGroup&gt;\r\n            &lt;PublishPath ParameterType=\"System.String\" Required=\"true\" \/&gt;\r\n            &lt;ArtifactsPath ParameterType=\"System.String\" Required=\"true\" \/&gt;\r\n            &lt;ArtifactsCount ParameterType=\"System.Int32\" Required=\"true\" \/&gt;\r\n        &lt;\/ParameterGroup&gt;\r\n        &lt;Task&gt;\r\n            &lt;Reference Include=\"System.IO.Compression.FileSystem\" \/&gt;\r\n            &lt;Using Namespace=\"System\"\/&gt; \r\n            &lt;Using Namespace=\"System.IO\"\/&gt; \r\n            &lt;Using Namespace=\"System.Linq\"\/&gt;\r\n            &lt;Using Namespace=\"System.IO.Compression\"\/&gt;\r\n            &lt;Code Type=\"Fragment\" Language=\"cs\"&gt;&lt;![CDATA[\r\n                int fileCount = 0;\r\n                var sortedFiles = new DirectoryInfo(ArtifactsPath).GetFiles()\r\n                     .OrderByDescending(f =&gt; f.LastWriteTime).ToList();\r\n\r\n                foreach (var fi in sortedFiles)\r\n                {\r\n                    fileCount++;\r\n                    if (fileCount&gt;ArtifactsCount-1)\r\n                    {\r\n                        fi.Delete();\r\n                    } \r\n                }\r\n        ZipFile.CreateFromDirectory(Path.GetDirectoryName(PublishPath),\r\n        @ArtifactsPath + @\"\\\" + \"Publish\" + DateTime.Now.ToString(\"yyyyMMddHHmmss\") + \".zip\");\r\n                ]]&gt;&lt;\/Code&gt;\r\n        &lt;\/Task&gt;\r\n  &lt;\/UsingTask&gt;\r\n  \r\n    &lt;Target Name=\"ArchiveBuild\" Condition=\"'$(ArchivePath)' != '' and $(ArchiveCount) &gt; '0'\"&gt;\r\n        &lt;ArchiveBuildTask ArtifactsPath=\"$(ArchivePath)\" PublishPath=\"$(OutputPath)\" \r\n        ArtifactsCount=\"$(ArchiveCount)\" \/&gt;\r\n    &lt;\/Target&gt;\r\n  &lt;\/Project&gt;\r\n<\/pre>\n<p>I have saved it as <strong>ArchiveBuild.targets<\/strong> inside the<\/p>\n<p><strong>%localappdata%\\Microsoft\\MSBuild\\15.0\\Microsoft.Common.Targets\\ImportAfter\\<\/strong> folder. That way MSBuild will be able to find it automatically no matter which project I\u2019ll build.<\/p>\n<p>Now just set the <strong>ArchivePath<\/strong> and <strong>ArchiveCount<\/strong> properties in the project <strong>csproj<\/strong> or <strong>csproj.user<\/strong> file to make it work.<\/p>\n<pre class=\"theme:vs2012 lang:xml decode:true\">    &lt;PropertyGroup&gt;\r\n        &lt;ArchivePath&gt;C:\/PrivateBuilds\/RevDeBug&lt;\/ArchivePath&gt;\r\n        &lt;ArchiveCount&gt;10&lt;\/ArchiveCount&gt;\r\n    &lt;\/PropertyGroup&gt;\r\n<\/pre>\n<p>Each build will then cause the results to be saved to <strong>C:\\PrivateBuilds\\RevDeBug<\/strong>, and I can use it in as many projects as I\u2019ll want to.<\/p>\n<p>As you can see the MSBuild pipeline is so versatile, the possibilities are countless. You can use it to enforce rules, automate all the mundane tasks, generate code, or pull some resources from external sources. In my organization we are using it for several solutions including to pull translation resources from databases which keep the latest versions, compile a user manual using files located on various network shares, and to create virtual machines with the new build installed. I also saw it being used to check whether the latest binaries are not significantly smaller than the last ones (which might indicate a problem with build file or dependencies) and automatically producing release notes suitable for the web page. I\u2019m quite sure you\u2019ll find a handful of tasks that can be automated with MSBuild right away.<\/p>\n<p>A good starting point is the Microsoft <a href=\"https:\/\/docs.microsoft.com\/en-us\/visualstudio\/msbuild\/msbuild\">documentation<\/a> for MSBuild. You can also quickly find interesting build properties examining the diagnostic output of MSBuild in Visual Studio. To do so, open \u2018<em>Tools &gt; Options &gt; Projects and Solutions &gt; Build and Run\u2019 <\/em>And set \u2018<em>MSBuild project build output verbosity\u2019<\/em> to <em>Diagnostic<\/em>. Now, whenever you build a project, you will see all the MSBuild properties with their values in the build output.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"860\" height=\"349\" class=\"wp-image-76007\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2017\/11\/word-image-37.png\" \/><\/p>\n<p>It\u2019s also worth mentioning, that MSBuild sees environmental variables, so you can write a target that will run only if a specified variable is set in your system.<\/p>\n<h2>Summary<\/h2>\n<p>MSBuild can be extended far beyond the basic scenario in which you only clean, build, and deploy. You can use it to execute pre-defined tasks, console applications, or even new tasks written directly in .NET. You can easily tailor the build process to fit your needs. <br \/>\n If there are any actions you repeat more than once during build, there\u2019s a great chance you should automate it with MSBuild to save time and to make your build less error-prone. One less thing to remember during build is one less thing you must worry about. As Scott Hanselman once said \u201cThe most powerful tool we have as developers is automation\u201d.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Microsoft Build Engine (MSBuild) works seamlessly within Visual Studio, but it can also be used to build software where Visual Studio is not installed. It\u2019s possible to create custom tasks that will run during the build process, saving time and decreasing the chances of error. Hubert Kuffel demonstrates how to create two useful tasks and how to easily is it to apply these to all your .NET projects.&hellip;<\/p>\n","protected":false},"author":316669,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143538],"tags":[],"coauthors":[50595],"class_list":["post-76004","post","type-post","status-publish","format-standard","hentry","category-dotnet-development"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/76004","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/316669"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=76004"}],"version-history":[{"count":15,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/76004\/revisions"}],"predecessor-version":[{"id":76034,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/76004\/revisions\/76034"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=76004"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=76004"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=76004"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=76004"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}