MSBuild Property Evaluation

I needed a reference on MSBuild property evaluation to link to from another post I've been working on, and discovered that such a thing doesn't really exist!  (Or, alternatively, that my web search skills are not what they once were)  Unfortunately, this is a rather confusing topic.  If you don't believe me, try out the following quiz without looking at the answers:

Given the following MSBuild script, predict the output of each Message task invocation given the following command lines:

  1. > msbuild sample.proj
  2. > set MyProperty=Environment Variable Value
    > msbuild sample.proj
  3. > msbuild sample.proj /p:"MyProperty=Command Line Value"

Here are the contents of sample.proj (note that $(MSBuildProjectFile) is the currently executing project file - namely sample.proj - so the MSBuild task invocations call back into the same project):

 <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <MyProperty Condition=" '$(MyProperty)'=='' ">Declared Value</MyProperty>
  </PropertyGroup>

  <Target Name="Build">
    <Message Text="Build 1, MyProperty = $(MyProperty)" />

    <CreateProperty Value="Programmatic Value">
      <Output TaskParameter="Value" PropertyName="MyProperty"/>
    </CreateProperty>

    <Message Text="Build 2, MyProperty = $(MyProperty)" />

    <CallTarget Targets="Internal" />

    <MSBuild Projects="$(MSBuildProjectFile)" Targets="Internal" />
    
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="Internal" Properties="Foo=Bar" />

    <MSBuild Projects="$(MSBuildProjectFile)" Targets="Internal" Properties="MyProperty=MSBuild Task Value" />
  </Target>

  <Target Name="Internal">
    <Message Text="Internal, MyProperty = $(MyProperty)" />
  </Target>
  
</Project>

 Before getting to the answers, here are (roughly speaking) the property evaluation rules for MSBuild:

  1. Each property passed in on the command line becomes a global property.  Global properties in general have higher precedence than other properties, and additionally are inherited by all subsequent invocations (see 5 below).
  2. Each environment variable in the calling context becomes a property (unless a global property already exists).
  3. A pass is made (top to bottom) through the project file (and all <Import> -ed targets files), and all declared properties are evaluated.  Global properties always override declared property values.  Earlier values are overwritten by later ones.
  4. Target execution begins.  Programmatic values (outputs of tasks, including the CreateProperty task) overwrite current values for all properties, including global properties.  (Within-target declarations, introduced in MSBuild 3.5, are functionally equivalent to programmatic value assignments)
  5. CallTarget and MSBuild task invocations are similar to command line msbuild invocations, except that they inherit any existing global properties.  Values passed into the MSBuild task via its Properties property themselves become global properties.

So - what are the outputs of the above project file?  Here they are, at normal verbosity...

Case 1 (> msbuild sample.proj):

Project "c:\sample.proj" (default targets):

Target Build:
    Build 1, MyProperty = Declared Value
    Build 2, MyProperty = Programmatic Value
    Target Internal:
        Internal, MyProperty = Declared Value
    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    Target Internal:
        Internal, MyProperty = Declared Value
    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    Target Internal:
        Internal, MyProperty = MSBuild Task Value

In this case, MyProperty takes on its declarative value (as per rule 3) and is then overridden programmatically (as per rule 4).  The CallTarget invocation repeats the process, and in this new scope MyProperty takes on its declarative value again.  The first MSBuild invocation doesn't even execute, since MSBuild has already successfully executed the Internal target with the exact set of global properties (see this forum thread for details).  The second MSBuild invocation has the same behavior as the CallTarget invocation.  The final MSBuild invocation explicitly specifies a value for MyProperty, and this value overrides the declarative value.

Case 2 (> set MyProperty=Environment Variable Value, > msbuild sample.proj):

Project "c:\sample.proj" (default targets):

Target Build:
    Build 1, MyProperty = Environment Variable Value
    Build 2, MyProperty = Programmatic Value
    Target Internal:
        Internal, MyProperty = Environment Variable Value
    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    Target Internal:
        Internal, MyProperty = Environment Variable Value
    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    Target Internal:
        Internal, MyProperty = MSBuild Task Value

In this case, MyProperty takes on a value from the environment (as per rule 2).  Because it already has a value, the condition on the declarative value evaluates to false and the environment variable value is preserved.

Case 3 (> msbuild sample.proj /p:MyProperty="Command Line Value"):

Project "c:\sample.proj" (default targets):

Target Build:
    Build 1, MyProperty = Command Line Value
    Build 2, MyProperty = Programmatic Value
    Target Internal:
        Internal, MyProperty = Command Line Value
    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    Target Internal:
        Internal, MyProperty = Command Line Value
    __________________________________________________
    Project "c:\sample.proj" is building "c:\sample.proj" (Internal target(s)):

    Target Internal:
        Internal, MyProperty = MSBuild Task Value

In this case, MyProperty is passed in via the command-line and becomes a global property (as per rule 1).  As such, it can only be overwritten programmatically (as per rule 4) or by being specified again as a global property via the MSBuild task (as per rule 5). 

Did you get them all right?  If so, you are an official MSBuild master...  If not, you are not alone - this stuff is pretty confusing.  Please let me know if you have advice on how I can make the explanations here easier to understand, etc.