Search code examples
c#dictionarymsbuildhashtable

Hashtable/Dictionary parameters in MSBuild Task


I'm trying to send/share a Dictionary or Hashtable between MSBuildtasks.

I have the following two Custom tasks, Get that produces a Hashtable and Set that should consume it.

Get.cs

public class Get : Task
{
    [Output]
    public Hashtable Output { get; set; }

    public override bool Execute()
    {
        Output = new Hashtable();
        return true;
    }
}

Set.cs

public class Set : Task
{
    [Required]
    public Hashtable Output { get; set; }

    public override bool Execute()
    {
        var items = Output.Cast<DictionaryEntry>().ToDictionary(d => d.Key.ToString(), d => d.Value.ToString());

        foreach(var item in items)
        {
            //Do Something
        }

        return true;
    }
}

The above classes build fine into Assembly.dll

I then use that Assembly.dll in the following build target script to call the Get and Set custom tasks:

MyTarget.targets

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask TaskName="Get" AssemblyFile=".\Assembly.dll"/>
  <UsingTask TaskName="Set" AssemblyFile=".\Assembly.dll"/>

  <Target Name="Get">

    <Get>
      <Output TaskParameter="Output" ItemName="Output" />
    </Get>
    <Set Output=@(Output) />

  </Target>

</Project>

When I build the project with the above target MSBuild shows the following error:

The "System.Collections.Hashtable" type of the "Output" parameter of the "Get" task is not supported by MSBuild

How can I use an Hashtable or Dictionary in a property for a custom MSBuild task?


Solution

  • The Parameters that can go in or out a Task are limited to either ITaskItem or an array of ITaskItem's.

    So your properties should change from

    public Hashtable Output { get; set; }
    

    to

    public ITaskItem[] Output { get; set; }
    

    to match that requirement.

    Next you need an implementation class that implements ITaskItem. That allow you to handle your hashset or dictionary. I left that for you that add but a minimal KeyValue class could look like this:

    public class KeyValue: ITaskItem
    {
        string _spec = String.Empty;
    
        public KeyValue(string key, string value)
        {
            _spec = key;
            metadata.Add("value", value);
        }
    
        Dictionary<string,string> metadata = new Dictionary<string,string>();
    
        public string ItemSpec
        {
            get {return _spec;}
            set {}
        }
        public ICollection MetadataNames
        {
            get {return metadata.Keys;}
        }
        public int MetadataCount
        {
            get {return metadata.Keys.Count;}
        }
        public string GetMetadata(string metadataName)
        {
            return metadata[metadataName];
        }
        public void SetMetadata(string metadataName, string metadataValue) 
        {
            metadata[metadataName] = metadataValue;
        }
        public void RemoveMetadata(string metadataName)
        {
        }
        public void CopyMetadataTo(ITaskItem destinationItem)
        {
        }
        public IDictionary CloneCustomMetadata()
        {
              return metadata;
        }
    }
    

    This class will produce and Item that will look look this if it was done in plane MSBuild script:

     <Item Include="key">
        <value>some value</value>
     </Item>
    

    Next you can adapt the Set and Get Task to use this new class KeyValue:

    public class Set : Task
    {
    
        TaskLoggingHelper log;
    
        public Set() {
            log = new TaskLoggingHelper(this);
        }
    
        [Required]
        public ITaskItem[] Output { get; set; }
    
        public override bool Execute()
        {
            log.LogMessage("start set");
            foreach(var item in Output)
            {
                log.LogMessage(String.Format("Set sees key {0} with value {1}.",item.ItemSpec, item.GetMetadata("value")));
            }
            log.LogMessage("end set");
            return true;
        }
    }
    
    public class Get : Task
    {
    
        // notice this property no longer is called Output
        // as that gave me errors as the property is reserved       
        [Output]
        public ITaskItem[] Result { get; set; }
    
        public override bool Execute()
        {
            // convert a Dictionary or Hashset to an array of ITaskItems
            // by creating instances of the class KeyValue.
            // I use a simple list here, I leave it as an exercise to do the other colletions
            Result = new List<ITaskItem> { new KeyValue("bar", "bar-val"), new KeyValue("foo","foo val") }.ToArray();
            return true;
        }
    }
    

    The build file I used to test above code:

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <UsingTask TaskName="Get" AssemblyFile=".\cb.dll"/>
      <UsingTask TaskName="Set" AssemblyFile=".\cb.dll"/>
    
      <Target Name="Get">
    
        <Get>
          <Output  TaskParameter="Result"  ItemName="GetResult" />
        </Get>
    
        <!-- lets see what we've got -->
        <Message Importance="high" Text="key: @(GetResult) :: value: %(value)" />
    
        <Set Output="@(GetResult)">
    
        </Set>
    
      </Target>
    
    </Project>
    

    When run the result will be:

    Build started 24-12-2017 21:26:17.
    Project "C:\Prj\bld\test.build" on node 1 (default targets).
    Get:
      key: bar :: value: bar-val
      key: foo :: value: foo val
      start set
      Set sees key bar with value bar-val.
      Set sees key foo with value foo val.
      end set
    Done Building Project "C:\Prj\bld\test.build" (default targets).
    
    
    Build succeeded.
        0 Warning(s)
        0 Error(s)