MSBuild Metadata doesn't support string instance methods

In MSBuild, string instance methods are directly supported for properties but not for item metadata values. This is a longstanding issue. Properties have property functions which include string instance properties and methods. $(Prop.Length), for example, will invoke the string.Length property of the string instance for the MSBuild Property named Prop. Items have item functions that operate on the vector. @(Items->get_Length()) will invoke the string.Length property of the string instance for the Identity metadata value for each item in the ItemGroup reference. But to use a metadata value other than Identity and/or to operate on a metadata reference instead of a ItemGroup reference is not directly supported.

The work around is to create a string from the metadata reference and then apply a property function to the string. Common approaches are to 'copy' the string or to use the ValueOrDefault function.

An example of copying the string would be

$([System.String]::Copy('%(Filename)').Length)

An example of using the ValueOrDefault function would be

$([MSBuild]::ValueOrDefault('%(Filename)', '').Length)

At this point in time, the copy technique is considered more idiomatic and preferred and there is an optimization in the MSBuild internals to not actually create a copy of the string.

An expanded example that contrasts Property functions, Item functions, and changing metadata values to strings and applying Property functions follows.

<!-- stringinstance.targets -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- MSBuild Property (Scalar) -->
  <PropertyGroup>
    <Prop>FooBar</Prop>
  </PropertyGroup>

  <PropertyGroup>
    <!-- Examples of string instance methods as 'Property Functions' -->
    <PropLength>$(Prop.Length)</PropLength>
    <PropRemoveFirstChar>$(Prop.Substring(1))</PropRemoveFirstChar>
    <PropRemoveLastChar>$(Prop.Substring(0, $([MSBuild]::Subtract($(Prop.Length), 1))))</PropRemoveLastChar>
  </PropertyGroup>

  <!-- MSBuild Item (Vector) -->
  <ItemGroup>
    <Items Include="FooBar.txt;Quux.txt"/>
  </ItemGroup>

  <PropertyGroup>
    <!-- Examples of string instance methods as 'Item Functions' -->
    <ItemLengths>@(Items->get_Length())</ItemLengths>
    <ItemRemoveFirstChars>@(Items->Substring(1))</ItemRemoveFirstChars>
  </PropertyGroup>

  <ItemGroup>
    <!-- Examples of string instance methods on metadata values -->
    <Items2 Include="@(Items)">
      <FilenameLength>$([System.String]::Copy('%(Filename)').Length)</FilenameLength>
      <FilenameRemoveFirstChar>$([System.String]::Copy('%(Filename)').Substring(1))</FilenameRemoveFirstChar>
      <FilenameRemoveLastChar>$([System.String]::Copy('%(Filename)').Substring(0, $([MSBuild]::Subtract($([System.String]::Copy('%(Filename)').Length), 1))))</FilenameRemoveLastChar>
    </Items2>
  </ItemGroup>

  <Target Name="ShowResults">
    <Message Text="Property Functions"/>
    <Message Text="PropLength           = $(PropLength)"/>
    <Message Text="PropRemoveFirstChar  = $(PropRemoveFirstChar)"/>
    <Message Text="PropRemoveLastChar   = $(PropRemoveLastChar)"/>
    <Message Text="Item Functions"/>
    <Message Text="ItemLengths          = $(ItemLengths)"/>
    <Message Text="ItemRemoveFirstChars = $(ItemRemoveFirstChars)"/>
    <Message Text="Items2 with Metadata"/>
    <Message Text="@(Items2->'  Identity = %(Identity), FilenameLength = %(FilenameLength), FilenameRemoveFirstChar = %(FilenameRemoveFirstChar), FilenameRemoveLastChar = %(FilenameRemoveLastChar)', '%0d%0a')"/>
  </Target>
  <!--
    Output:
      ShowResults:
        Property Functions
        PropLength           = 6
        PropRemoveFirstChar  = ooBar
        PropRemoveLastChar   = FooBa
        Item Functions
        ItemLengths          = 6;4
        ItemRemoveFirstChars = ooBar;uux
        Items2 with Metadata
          Identity = FooBar, ItemLength = 6, ItemRemoveFirstChar = ooBar, ItemRemoveLastChar = FooBa
          Identity = Quux, ItemLength = 4, ItemRemoveFirstChar = uux, ItemRemoveLastChar = Quu
    -->

</Project>

Imagine a scenario with a set of files where some of the filenames use a special suffix.

<!-- suffixexample.targets -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <ItemGroup>
    <SourceList Include="a.txt;b.txt;a-alt.txt;b-alt.txt"/>
  </ItemGroup>

  <PropertyGroup>
    <suffix>-alt</suffix>
  </PropertyGroup>

  <ItemGroup>
    <Files Include="@(SourceList)">
      <HasSuffix>$([System.String]::Copy('%(Filename)').Endswith('$(suffix)'))</HasSuffix>
      <PrimaryName Condition="%(HasSuffix)">$([System.String]::Copy('%(Filename)').Substring(0, $([MSBuild]::Subtract($([System.String]::Copy('%(Filename)').Length), $(suffix.Length)))))</PrimaryName>
      <PrimaryName Condition="!%(HasSuffix)">%(Filename)</PrimaryName>
    </Files>
  </ItemGroup>

  <Target Name="ListFilesWithSuffix">
    <Message Text="@(Files->'%(Identity) PrimaryName = %(PrimaryName)','%0d%0a')" Condition="%(HasSuffix)" />
  </Target>
  <!--
    Output:
      ListFilesWithSuffix:
        a-alt.txt PrimaryName = a
        b-alt.txt PrimaryName = b
    -->

</Project>

The Files ItemGroup is created with metadata that provides a boolean indicating if the special suffix is present or not and metadata for the filename sans suffix (regardless of whether the suffix is present). The Target 'ListFilesWithSuffix' uses metadata batching to display the files that have the suffix.