-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
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>
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
-
In Microsoft.WinFx.targets, the GenerateTemporaryTargetAssembly target generates a randomly temporary project name if $(IncludePackageReferencesDuringMarkupCompilation) is not false.
wpf/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/Microsoft.WinFX.targets
Lines 373 to 378 in 70126b9
<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> -
The GenerateTemporaryTargetAssembly task receives both the original assembly name (resolved via Directory.Build.props) and the randomly generated temporary project name.
wpf/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/Microsoft.WinFX.targets
Lines 440 to 458 in 70126b9
<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)" > -
This task generates a temporary project file, which includes the original assembly name.
-
This task start build of temporary project, but this building the temporary project file, Directory.Build.props is applied again.
-
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.
-
A temporary assembly is generated with this random name.
-
The MarkupCompilePass2 target in Microsoft.WinFx.targets attempts to resolve local type names using this temporary assembly.
-
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.
I investigated why the temporary project file is generated in this particular order.
Lines 267 to 287 in 70126b9
// 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.
Lines 830 to 840 in 70126b9
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.
Lines 763 to 764 in 70126b9
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.