MSBuild: How to develop a Custom Build Task

My problem: When compiling a VB.NET project, I wanted to read a key from a .config file. According to the value of that key, decide which native dll's to copy to the target directory.

My solution: First of all, remember that every C#, VB.NET or C++ Visual studio project is an MSBuild script (I guess F# projects too), which is somewhat similar to a Nant build script. It's an XML file with a special schema defined by Microsoft. The XML tags of interest are Target, which are made up mainly of Task tags. In other words, a Target can call Tasks. The .NET framework offers a bunch of predefined MSBuild Tasks, which are documented in the MSDN. However, if those are not enough, we can quite easily write and use our own tasks.

An MSBuild custom task must be defined in a dll, which must be referenced from the MSBuidl script which calls it. We'll see how soon.

So, the first step is creating a C# project (I guess VB.NET or any other pure managed language would work just as well, I'm a C-biased man). The type must be Class Library, that is, a dll. In that project, we must create a class which extends Microsoft.Build.Utilities.Task and override the Execute method. That is where we'll implement what our task does. If our task needs parameters and/or outputs, these must be declared as properties of the class, and tagged with special .NET attributes.

In my case, I wrote a task called ConfigRead, which receives the path to the config file and the name of the key to read as parameters, and return the value of the key in the config file. Once done, the task would be used like this (WARNING: MSBuild is case sensitive! The syntax highlighter I'm using put all my XML identifiers in lowercase, but that's not how they go. The casing should match the one used in the ConfigRead class, see below):

<ConfigRead ConfigFilePath="$(TargetDir)DRA.config" KeyName="DRA.AcquisitionPanel">
      <Output TaskParameter="KeyValue" PropertyName="ReadKeyValue" />
</ConfigRead>
<Message Text="Value Read from DRA.config: $(ReadKeyValue)" />
And this would be the implementation of the ConfigRead task:
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.Configuration;
using System.Diagnostics;
namespace ConfigRead{

    public class ConfigRead : Task{

        [Required]
        public string ConfigFilePath{ get; set; }

        [Required]
        public string KeyName{ get; set; }

        [Output]
        public string KeyValue{ get; set; }

        public override bool Execute(){
            Debug.WriteLine("ConfigFilePath: {0}", ConfigFilePath);
            ExeConfigurationFileMap FileMap = new ExeConfigurationFileMap();
            FileMap.ExeConfigFilename = ConfigFilePath;
            Debug.WriteLine("Key sought: {0}", KeyName);
            Configuration ConfigurationFile =
               ConfigurationManager.OpenMappedExeConfiguration(
                  FileMap, ConfigurationUserLevel.None
               );
            KeyValue = ConfigurationFile.AppSettings.Settings[KeyName].Value;
            Debug.WriteLine("Value read: {0}", KeyValue);
            return true;
        }
    }
}

Finally, in the MSBuild script which calls ConfigRead, we must reference the built dll, which should be somewhere in the file system. This way:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build"
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- Needed for using the ConfigRead Custom Task -->
  <UsingTask TaskName="ConfigRead"
      AssemblyFile="$(SolutionDir)\DRALibraries\CustomBuildTasks\ConfigRead.dll" />
  <Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
  <!-- Cosas predefinidas por el Visual... -->
  <!-- To modify your build process, add your task inside one of the targets below and 
       uncomment it. Other similar extension points exist, see
       Microsoft.Common.targets.-->  
  <Target Name="AfterBuild">
    <ConfigRead ConfigFilePath="$(TargetDir)DRA.config" KeyName="DRA.AcquisitionPanel">
      <Output TaskParameter="KeyValue" PropertyName="ReadKeyValue" />
    </ConfigRead>
    <Message Text="Value Read from DRA.config: $(ReadKeyValue)" />
  </Target>  
</Project>

Comments

Popular posts from this blog

VB.NET: Raise base class events from a derived class

Apache Kafka - I - High level architecture and concepts

Upgrading Lodash from 3.x to 4.x