Skip to content

Incorrect SDK import order in temporary project file #11150

@gekka

Description

@gekka

Description

When a WPF project or its parent directory includes a Directory.Build.props file like the one below, the intermediate build process for WPF generates a random assembly name. As a result, local assembly types cannot be resolved, leading to an MC3050 error.

This issue has also been reported in #10068

Reproduction Steps

The TestDirectoryProps.csproj file does not explicitly define an AssemblyName. Instead, Directory.Build.props dynamically sets the AssemblyName using $(MSBuildProjectName).

<Project>
    <PropertyGroup>
        <Product_RootNameSpace>TestRootName</Product_RootNameSpace>
		
        <AssemblyName >$(Product_RootNameSpace).$(MSBuildProjectName)</AssemblyName>
    </PropertyGroup>
</Project>

Sample repository

Expected behavior

The temporary assembly is referenced, and the type of the local assembly {x:Type} is resolved.

Actual behavior

The build fails and no assembly is generated.

During the XAML-to-BAML conversion, {x:Type} resolution relies on the assembly name. Since the assembly name is random, the lookup fails, resulting in an MC3050 error.

Regression?

No response

Known Workarounds

As a workaround, setting the IncludePackageReferencesDuringMarkupCompilation property to false restores legacy behavior and prevents the error.
However, this breaks the functionality of source generators such as CSWin32.

Impact

No response

Configuration

.Net Farmework 4.7.2, .Net8 ,.Net 9

Other information

Description of Behavior

  1. In Microsoft.WinFx.targets, the GenerateTemporaryTargetAssembly target generates a randomly temporary project name if $(IncludePackageReferencesDuringMarkupCompilation) is not false.

    <PropertyGroup Condition="'$(IncludePackageReferencesDuringMarkupCompilation)' != 'false'">
    <_ParentProjectName>$([System.IO.Path]::GetFileNameWithoutExtension('$(MSBuildProjectFullPath)'))</_ParentProjectName>
    <_ParentProjectExtension>$([System.IO.Path]::GetExtension($(MSBuildProjectFullPath)))</_ParentProjectExtension>
    <_TemporaryTargetAssemblyProjectNameNoExtension>$([System.String]::Join("_", "$(_ParentProjectName)", "$([System.IO.Path]::GetFileNameWithoutExtension($([System.IO.Path]::GetRandomFileName())))", "wpftmp"))</_TemporaryTargetAssemblyProjectNameNoExtension>
    <_TemporaryTargetAssemblyProjectName>$(_TemporaryTargetAssemblyProjectNameNoExtension)$(_ParentProjectExtension)</_TemporaryTargetAssemblyProjectName>
    </PropertyGroup>

  2. The GenerateTemporaryTargetAssembly task receives both the original assembly name (resolved via Directory.Build.props) and the randomly generated temporary project name.

    <GenerateTemporaryTargetAssembly
    CurrentProject="$(MSBuildProjectFullPath)"
    MSBuildBinPath="$(MSBuildBinPath)"
    ReferencePathTypeName="ReferencePath"
    CompileTypeName="Compile"
    AnalyzerTypeName="Analyzer"
    GeneratedCodeFiles="@(_GeneratedCodeFiles)"
    ReferencePath="@(ReferencePath)"
    BaseIntermediateOutputPath="$(BaseIntermediateOutputPath)"
    IntermediateOutputPath="$(_IntermediateOutputPathNoTargetFrameworkOrRID)"
    AssemblyName="$(AssemblyName)"
    CompileTargetName="$(_CompileTargetNameForLocalType)"
    GenerateTemporaryTargetAssemblyDebuggingInformation="$(GenerateTemporaryTargetAssemblyDebuggingInformation)"
    IncludePackageReferencesDuringMarkupCompilation="$(IncludePackageReferencesDuringMarkupCompilation)"
    Analyzers="@(Analyzer)"
    TemporaryTargetAssemblyProjectName="$(_TemporaryTargetAssemblyProjectName)"
    MSBuildProjectExtensionsPath="$(MSBuildProjectExtensionsPath)"
    RootNamespace="$(RootNamespace)"
    >

  3. This task generates a temporary project file, which includes the original assembly name.

  4. This task start build of temporary project, but this building the temporary project file, Directory.Build.props is applied again.

  5. At this point, the AssemblyName is override by the definition in Directory.Build.props, but $(MSBuildProjectName) now refers to the temporary project file name—resulting in a random assembly name.

  6. A temporary assembly is generated with this random name.

  7. The MarkupCompilePass2 target in Microsoft.WinFx.targets attempts to resolve local type names using this temporary assembly.

  8. During the XAML-to-BAML conversion, {x:Type} resolution relies on the assembly name. Since the assembly name is random, the lookup fails, resulting in an MC3050 error.

Actual tempoary project file

<Project>
  <PropertyGroup>
    <AssemblyName>TestRootName.TestDirectoryProps</AssemblyName>
    <IntermediateOutputPath>obj\Debug\</IntermediateOutputPath>
    <BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath>
    <MSBuildProjectExtensionsPath>S:\TestDirectoryProps\Test\TestDirectoryProps\obj\</MSBuildProjectExtensionsPath>
    <_TargetAssemblyProjectName>TestDirectoryProps</_TargetAssemblyProjectName>
    <RootNamespace>TestDirectoryProps</RootNamespace>
  </PropertyGroup>
  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFrameworks>net472;net8.0-windows;net9.0-windows</TargetFrameworks>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  ...
msbuild /bl /t:_CompileTemporaryAssembly TestDirectoryProps_2rop0taq_wpftmp.csproj

As a result of building the temporary project file, an executable named after this temporary project file (e.g., TestDirectoryProps_2rop0taq_wpftmp.exe) is generated.

By inspecting the build log of the temporary project, you can confirm that the original project name in AssemblyName has been override by the temporary project name.

Property reassignment: $(AssemblyName)="TestRootName.TestDirectoryProps_20otaqqp_wpftmp" (previous value: "TestRootName.TestDirectoryProps") at S:\TestDirectoryProps\Test\Directory.Build.props (6,3)

After identifying that the override was caused by Directory.Build.props, I investigated how it gets applied and found that it is imported transitively via Sdk.props.

The import order executed by Sdk.props is as follows, and it includes the import of Directory.Build.props.

<TimedNode Name="Imports" StartTime="0001-01-01T00:00:00.0000000" EndTime="0001-01-01T00:00:00.0000000" NodeId="0">
  <Import Text="C:\Program Files\dotnet\sdk\9.0.304\Sdks\Microsoft.NET.Sdk\Sdk\Sdk.props">
    <NoImport Text="$(AlternateCommonProps)" />
    <Import Text="C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Microsoft.Common.props">
      <NoImport Text="$(CustomBeforeDirectoryBuildProps)" />
      <Import Text="S:\TestDirectoryProps\Test\Directory.Build.props" />
        <Import Text="C:\Program Files\dotnet\sdk\9.0.304\Sdks\Microsoft.NET.Sdk\Sdk\UseArtifactsOutputPath.props">
          <NoImport Text="$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.DefaultArtifactsPath.props" />
        </Import>

This reveals that although the correct AssemblyName is defined within the temporary project file, it is later override by the definition from Directory.Build.props. In other words, an Import placed after the PropertyGroup ends up override the correct property with an incorrect one.

Therefore, the solution is to adjust the property evaluation order so that Directory.Build.props does not override the intended values.

Expected temporary project file

<Project>

  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" /> <!--This must run before PropertyGroup -->

  <PropertyGroup>
    <AssemblyName>TestRootName.TestDirectoryProps</AssemblyName>
    <IntermediateOutputPath>obj\Debug\</IntermediateOutputPath>
    <BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath>
    <MSBuildProjectExtensionsPath>S:\TestDirectoryProps\Test\TestDirectoryProps\obj\</MSBuildProjectExtensionsPath>
    <_TargetAssemblyProjectName>TestDirectoryProps</_TargetAssemblyProjectName>
    <RootNamespace>TestDirectoryProps</RootNamespace>
  </PropertyGroup>
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFrameworks>net472;net8.0-windows;net9.0-windows</TargetFrameworks>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  ...

After applying this fix, the build produced an assembly with the correct file name.

Image

I investigated why the temporary project file is generated in this particular order.

// Replace implicit SDK imports with explicit SDK imports
ReplaceImplicitImports(xmlProjectDoc);
// Add properties required for temporary assembly compilation
var properties = new List<(string PropertyName, string PropertyValue)>
{
( nameof(AssemblyName), AssemblyName ),
( nameof(IntermediateOutputPath), IntermediateOutputPath ),
( nameof(BaseIntermediateOutputPath), BaseIntermediateOutputPath ),
( nameof(MSBuildProjectExtensionsPath), MSBuildProjectExtensionsPath ),
( "_TargetAssemblyProjectName", Path.GetFileNameWithoutExtension(CurrentProject) ),
( nameof(RootNamespace), RootNamespace ),
};
//Removing duplicate AssemblyName
RemovePropertiesByName(xmlProjectDoc, nameof(AssemblyName));
AddNewProperties(xmlProjectDoc, properties);
// Save the xmlDocument content into the temporary project file.
xmlProjectDoc.Save(TemporaryTargetAssemblyProjectName);

The method ReplaceImplicitImports called here inserts an element at the beginning of the project file.

XmlNode nodeImportProps = CreateImportProjectSdkNode(xmlProjectDoc, "Sdk.props", sdkReference);
// Prepend this Import to the root of the XML document
if (previousNodeImportProps == null)
{
previousNodeImportProps = root.PrependChild(nodeImportProps);
}
else
{
previousNodeImportProps = root.InsertAfter(nodeImportProps, previousNodeImportProps);
}

Similarly, AddNewProperties inserts a element at the beginning of the project file.

XmlNode nodeItemGroup = xmlProjectDoc.CreateElement("PropertyGroup", root.NamespaceURI);
root.PrependChild(nodeItemGroup);

Since AddNewProperties is executed after ReplaceImplicitImports, the element ends up appearing after .

Suggest for Fix:

To resolve this issue, the execution order should be modified so that ReplaceImplicitImports runs after AddNewProperties.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions