Target Framework Moniker

Let's start. .NET 5.0 doesn't allow using Windows Forms or WPF by specifying net5.0:

<PropertyGroup>
  <TargetFramework>net5.0</TargetFramework>
  <UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

We get the following error:

C:\Program Files\dotnet\sdk\5.0.201\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.DefaultItems.targets(369,5): error NETSDK1136: The target platform must be set to Windows (usually by including '-windows' in the TargetFramework property) when using Windows Forms or WPF, or referencing projects or packages that do so.

Target Framework Moniker suggests the correct solution by specifying net5.0-windows:

<PropertyGroup>
  <TargetFramework>net5.0-windows</TargetFramework>
  <UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

How does it work ?

During the build, the build files from Microsoft.NET.Sdk\targets are implicitly imported.
Inside dotnet\sdk\5.0\Sdks\Microsoft.NET.Sdk.WindowsDesktop\targets\Microsoft.NET.Sdk.WindowsDesktop.props there are following lines:

    <FrameworkReference Include="Microsoft.WindowsDesktop.App" IsImplicitlyDefined="true"
                        Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' == 'true')"/>

    <FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" IsImplicitlyDefined="true"
                        Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' != 'true')"/>

    <FrameworkReference Include="Microsoft.WindowsDesktop.App.WindowsForms" IsImplicitlyDefined="true"
                        Condition="('$(UseWPF)' != 'true') And ('$(UseWindowsForms)' == 'true')"/>

What is the problem ?

The thing is that FrameworkReference is a transitive dependency: .NET Targeting, Runtime and AppHost Packs, FrameworkReference in NuGet.

This means that when there is an assembly using any type from Windows Forms or from WPF, we must specify 'net5.0-windows' in all dependent projects.

As a result we potentially add unwanted bloat to the output file.

It is not a problem when your program is Windows Forms or WPF, however it is a problem when you want a console application and you end up with additional 60MB.

Our use-case

Class Library project:

using System.Windows.Forms;

namespace Library
{
    public class Demo
    {
        void ShowForm()
        {
            var f = new Form();
            f.Show();
        }
    }
}

Console application with reference to Class Library.

using System;

class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}

We only reference the Class Library project, yet we do not use Library.Demo class.

Let's build using dotnet publish:

dotnet publish ConsoleApp.csproj --self-contained -c Release -r win-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeAllContentForSelfExtract=true

Output size: 81,8MB!

Thanks to IncludeAllContentForSelfExtract when the application is executed we can see the internal content inside %TEMP%\net:

But, why ?
We did not use Library.Demo, we do use PublishTrimmed, and yet we have Windows Forms dlls.

Solution

As we see dotnet publish is not being able to do its job and remove unused dependencies. Let's help !

Step 1

Inside Class Library, we specify framework references explicitly and make it non-transitive.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <!-- Use explicit  -->
    <DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
  </PropertyGroup>


  <ItemGroup>
    <!-- .NET Runtime -->
    <!-- PrivateAssets="all" here doesn't do anything, this reference is always added  -->
    <FrameworkReference Include="Microsoft.NETCore.App" />
    
    <!-- Windows Desktop -->
    <!-- PrivateAssets="all" - non-transitive dependency -->
    <FrameworkReference Include="Microsoft.WindowsDesktop.App" PrivateAssets="all"  />
    
    <!-- We can be more specific
      Microsoft.WindowsDesktop.App.WPF
      Microsoft.WindowsDesktop.App.WindowsForms -->
  </ItemGroup>

</Project>

Documentation of DisableImplicitFrameworkReference

The key point here is PrivateAssets="all" which stops transitive dependency.

Step 2

Replace .net5.0-windows with .net5.0 inside our console application.

The result

Using the same build command as before:

dotnet publish ConsoleApp.csproj --self-contained -c Release -r win-x64 /p:PublishSingleFile=true /p:PublishTrimmed=true /p:IncludeAllContentForSelfExtract=true

The output file is just 18.8MB size with the following content:

Conclusion

Shall I use this in my library ?

Definitely yes !

On the one hand this makes it possible to create a library which uses types from Windows Forms or WPF, on the other hand the dotnet publishing is able to throw away unused framework references and produce a much smaller file.