Search code examples
collectionsmsbuilditemgroup

MSBuild Metadata on joined item groups


I have two item groups that I wish to join:

  <ItemGroup>
<ServerTypeA Include="ServerA;ServerB;">
  <MetaDataA>A</MetaDataA>
</ServerTypeA>
</ItemGroup>

  <ItemGroup>
<ServerTypeB Include="ServerB;ServerC;">
  <MetaDataB>B</MetaDataB>
</ServerTypeB>
</ItemGroup>

Using the regular join will give me a collection with 4 items:

ServerA with Metadata A;
ServerB with Metadata A;
ServerB with Metadata B;
ServerC with Metadata B;

How can I create the following collection:

ServerA with Metadata A
ServerB with Metadata A & B
ServerC with Metadata B

Solution

  • It's possible. You have to can manually perform the join.

    Here's an example of how to do it (msbuild 3.5 or greater required):

    <ItemGroup>
      <ServerTypeA Include="ServerA;ServerB;">
        <MetaDataA>A</MetaDataA>
      </ServerTypeA>
    
      <ServerTypeB Include="ServerB;ServerC;">
        <MetaDataB>B</MetaDataB>
      </ServerTypeB>
    </ItemGroup>
    
    <Target Name="JoinServers" DependsOnTargets="ProcessServerTypeA;ProcessServerTypeB">
      <Message Text="%(Joined.Identity) Metadata: %(Joined.MetaDataA)%(Joined.MetaDataB)"/>
    </Target>
    
    <!--Create -->
    <Target Name="ProcessServerTypeA">
      <ItemGroup>
        <Joined Include="%(ServerTypeA.Identity)">
          <MetaDataA>%(ServerTypeA.MetaDataA)</MetaDataA>
        </Joined>
      </ItemGroup>
    </Target>
    
    <!--Need to batch at the target level for this to work-->
    <Target Name="ProcessServerTypeB" Inputs="@(ServerTypeB)" Outputs="%(ServerTypeB.Identity)'">
    
      <PropertyGroup>
        <!--Create Temporary Properties for the Item Metadata-->
        <TempItemName>%(ServerTypeB.Identity)</TempItemName>
        <TempMetaDataB>%(ServerTypeB.MetaDataB)</TempMetaDataB>
        <!--Does the current item already exist?-->
        <TempIsDuplicate Condition="'%(Joined.Identity)' == '$(TempItemName)'">True</TempIsDuplicate>
      </PropertyGroup>
    
      <ItemGroup>
        <!--Update the existing item's metadata if this is a duplicate-->
        <!--Don't provide the include attribute.  This will allow you to update existing items metadata-->
        <!--Have to reference %(Joined.Identity) in the condtion to ensure we only update the correct item-->
        <!--You cannot directly reference metadata from ServerTypeB here.  Hence the need for the temp Properties-->
        <Joined Condition="'%(Joined.Identity)' == '$(TempItemName)'">
          <MetaDataB>$(TempMetaDataB)</MetaDataB>
        </Joined>
    
        <!--Create a new item if current item is not a duplicate-->
        <Joined Include="$(TempItemName)" Condition="'$(TempIsDuplicate)' != 'True'">
          <MetaDataB>$(TempMetaDataB)</MetaDataB>
        </Joined>
      </ItemGroup>
    </Target>
    

    Running the JoinServers target will produce the following output:

    ServerA Metadata: A
    ServerB Metadata: AB
    ServerC Metadata: B
    

    Update with a better answer

    This question pointed me to a much simpler solution.

    Basically you use Transform modifiers with %(Identity) to perform the join.

    You can replace all 3 targets from above with the following to obtain the same output.

    <Target Name="JoinServers">
      <ItemGroup>
        <Joined Include="%(Identity)">
          <MetaDataA>@(ServerTypeA->'%(MetaDataA)')</MetaDataA>
          <MetaDataB>@(ServerTypeB->'%(MetaDataB)')</MetaDataB>
        </Joined>
      </ItemGroup>
    
      <Message Text="%(Joined.Identity) Metadata: %(Joined.MetaDataA)%(Joined.MetaDataB)"/>
    </Target>