wpf: For non-deterministic builds WPF application crashes on resource dictionary load

If project is set to be non-deterministic and AssemblyVersion uses wildcards, the application fails while loading resource dictionaries. It seem to try to parse Version object from wildcard string from meta (?) instead of determining concrete running assembly version, either that or there is a bug in Version parser. Note: I understand there are benefits to deterministic, but that is not the topic, non-deterministic should not be a showstopper for wpf core, especially when porting existing applications and follow-up processes.

Steps to reproduce:

  1. Create .net core WPF application
  2. Modify csproj and switch to wildcard non-deterministic build
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWPF>true</UseWPF>
    <!-- non-deterministic with wildcards -->
    <Deterministic>False</Deterministic>
    <AssemblyVersion>1.0.*</AssemblyVersion>
  </PropertyGroup>
</Project>
  1. Add any resource file to project (e.g. test.xaml), contents is irrelevant.
  2. Load resource from App.xaml
<Application x:Class="WpfApp1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp1"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Test.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>
  1. Build&Run app

Expected behavior A working application?

Actual behavior Application crashes with unhandled exception immediately on start. Stack trace is:

Application: WpfApp1.exe
CoreCLR Version: 4.700.19.56402
.NET Core Version: 3.1.0
Description: The process was terminated due to an unhandled exception.
Exception Info: System.FormatException: Input string was not in a correct format.
   at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)
   at System.Version.TryParseComponent(ReadOnlySpan`1 component, String componentName, Boolean throwOnFailure, Int32& parsedComponent)
   at System.Version.ParseVersion(ReadOnlySpan`1 input, Boolean throwOnFailure)
   at System.Version.Parse(String input)
   at System.Version..ctor(String version)
   at System.Windows.Navigation.BaseUriHelper.GetLoadedAssembly(String assemblyName, String assemblyVersion, String assemblyKey)
   at MS.Internal.AppModel.ResourceContainer.GetResourceManagerWrapper(Uri uri, String& partName, Boolean& isContentFile)
   at MS.Internal.AppModel.ResourceContainer.GetPartCore(Uri uri)
   at System.IO.Packaging.Package.GetPartHelper(Uri partUri)
   at System.IO.Packaging.Package.GetPart(Uri partUri)
   at System.Windows.Application.GetResourceOrContentPart(Uri uri)
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
   at WpfApp1.App.InitializeComponent() in C:\playground\WpfApp1\WpfApp1\App.xaml:line 1
   at WpfApp1.App.Main()

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 24 (12 by maintainers)

Most upvoted comments

Here is a summary of what we know and what has been discussed.

Issue

  • MarkupCompilePass1 consumes the AssemblyVersion build property and uses it to generate the resource URI in the code for InitializeComponent. This code is generated any time BAML is needed. https://github.com/dotnet/wpf/blob/7e336618311d1f6e1076c800623b01505b8590d5/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/MarkupCompiler.cs#L3001-L3003
    • When this is a wildcard version string (e.g. the developer has defined it as such via the AssemblyVersion property), this will cause a crash at runtime. In the call tree from InitializeComponent, WPF is calling Version.Parse with the wildcard string. Since this is not a valid version string, an exception is thrown.
    • This is not a regression, the same behavior exists in .NET Framework. It is, however, worse to have this behavior in .NET Core as there is a slightly different expectation on how to accomplish wildcard versions as compared to .NET Framework.

Workaround

  • As of right now, if you need to use wildcard versions in .NET Core, you must set GenerateAssemblyVersionAttribute to False in your project and then add the wildcard as an AssemblyVersionAttribute to your AssemblyInfo.cs. (Example Here)
    • When these steps are followed the AssemblyVersion property passed to WPF is an empty string and the generated code for InitializeComponent does not have a version in the resource URI.
    • This is essentially the same process and result (sans turning off generation of AssemblyVersionAttribute) as .NET Framework.

Potential Fix

  • After discussions with @vatsan-madhavan , we decided that a fix can be made for the issue by simply detecting the wildcard version string in GenerateInitializeComponent and ensuring the version string used in the resource URI is empty if a wildcard is in use.
    • This mirrors the state of the URI in both the workaround and the way that you would have to do this in .NET Framework.
    • Uses of WPF with non-wildcard version strings (either via the AssemblyVersion build property or the AssemblyVersionAttribute in AssemblyInfo.cs) are not affected.
    • I have a WIP branch here: https://github.com/rladuca/wpf/tree/pbtwildcards. I have tested this with some basic applications and all is as expected.

@weltkante Yes, I was getting a bit stream of consciousness above so probably a mess of half-conclusions.

My current thinking (have to validate with binlogs) is:

  1. In .NET Framework, you generally would put the wildcard in the AssemblyVersionAttribute and not in the AssemblyVersion property. Using the property there also breaks WPF in the exact same fashion.
  2. This says to me that there is a step in .NET Framework that converts AssemblyVersionAttribute to the AssemblyVersion property and that this step occurs prior to PresentationBuildTasks using AssemblyVersion.
  3. In .NET Core, this doesn’t seem to happen. AssemblyVersion remains empty even when overriding the default generated AssemblyInfo.cs. While the wildcard is filled in when the assembly is built, PresentationBuildTasks sees an empty string. This fixes the originally reported issue, but breaks my workaround since the resource URI is no longer valid to load the resource.
  4. In either .NET Framework or .NET Core, if you set the AssemblyVersion property, PresentationBuildTasks sees not the filled in version, but the original wildcard string. Meaning it was always too early in the build process when using the property.

Again, this is just from experimentation right now, I have to comb through logs to see what is happening when (unless someone knows offhand).

Hopefully that condenses and clarifies the word salad I was typing last night : - )