Summary
When a project multi-targets net10.0 and net472 (or netstandard2.0) and references Microsoft.Windows.CsWin32,
the generator emits the IComIID interface and attaches it to every generated COM struct only on the .NET 7+ TFMs. On net472:
- The
Windows.Win32.IComIID interface itself is not generated.
- Generated COM struct wrappers (e.g.
IRunningObjectTable, IMoniker, IUnknown, …) do not list IComIID in their base list, so generic helpers built around where T : unmanaged, IComIID cannot accept them.
This forces consumers to hand-author two polyfills per project:
- An instance-based
IComIID interface declaration gated #if !NET.
- A
partial struct Foo : IComIID { Guid IComIID.Guid => IID_Guid; } entry for every generated COM struct that flows through ComScope<T>, gated #if !NET.
Each new COM type added to NativeMethods.txt requires a corresponding partial. The pattern is mechanical and lends itself to generator support.
Steps to reproduce
Create the project as described below and build it.
The net10.0 build succeeds. The net472 build fails with:
error CS0246: The type or namespace name 'IComIID' could not be found
error CS0311: The type 'IRunningObjectTable' cannot be used as type parameter 'T' in
the generic type or method 'Program.GetIID<T>()'. There is no implicit
reference conversion from 'IRunningObjectTable' to 'IComIID'.
Expected
CsWin32 emits IComIID and attaches it to generated COM structs on all supported target frameworks, in a shape appropriate for the language version (static-abstract on .NET 7+, instance member on older targets, matching how dotnet/winforms polyfills the type today).
Actual
The interface and its partial attachments are only emitted on TFMs where static abstract interface members are supported, leaving older targets without any substitute.
Environment
| Component |
Version |
Microsoft.Windows.CsWin32 |
0.3.275 |
| .NET SDK |
10.0.100 |
| TFMs in repro |
net10.0;net472 |
How the workaround looks in production
dotnet/msbuild multi-targets net10.0 and net472 and currently maintains two hand-written polyfill files:
src/Framework/Polyfills/IComIID.cs — declares the instance-based IComIID interface under #if !NET.
src/Framework/Polyfills/IComIIDPolyfills.cs — one internal partial struct Foo : IComIID { readonly Guid IComIID.Guid => IID_Guid; } entry per generated COM type used through ComScope<T>, all under #if !NET.
Every time a new COM interface name is added to NativeMethods.txt a matching partial must be added here. This is the same workaround applied in [dotnet/winforms] https://github.com/dotnet/winforms/blob/main/src/System.Private.Windows.Core/src/Framework/Windows/Win32/System/Com/IDataObject.cs), which is referenced from msbuild's polyfill source.
References
Suggested fix
Use #if NET to generate the existing IComIID signature and #else to generate the down-level implementation. In each struct generate the implementation in the same #if blocks. The .NET (core) implementation is optimized for a pattern that pulls straight from metadata but requires .NET runtime features.
Sources
Drop these four files into a fresh folder and run dotnet build.
IComIIDMissing.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;net472</TargetFrameworks>
<OutputType>Exe</OutputType>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.275" PrivateAssets="all" />
<PackageReference Include="System.Memory" Version="4.5.5" Condition="'$(TargetFramework)' == 'net472'" />
</ItemGroup>
</Project>
NativeMethods.txt
GetRunningObjectTable
IRunningObjectTable
IMoniker
CoCreateInstance
NativeMethods.json
{ "$schema": "https://aka.ms/CsWin32.schema.json", "allowMarshaling": false, "useSafeHandles": false }
Program.cs
using System;
using Windows.Win32;
using Windows.Win32.System.Com;
internal static unsafe class Program
{
private static int Main()
{
// Constraint `T : IComIID` fails to bind on net472: neither IComIID nor
// the partial attaching it to IRunningObjectTable are generated there.
Guid iid = GetIID<IRunningObjectTable>();
Console.WriteLine(iid);
return 0;
}
private static Guid GetIID<T>() where T : unmanaged, IComIID => T.Guid;
}
Summary
When a project multi-targets
net10.0andnet472(ornetstandard2.0) and referencesMicrosoft.Windows.CsWin32,the generator emits the
IComIIDinterface and attaches it to every generated COM struct only on the .NET 7+ TFMs. Onnet472:Windows.Win32.IComIIDinterface itself is not generated.IRunningObjectTable,IMoniker,IUnknown, …) do not listIComIIDin their base list, so generic helpers built aroundwhere T : unmanaged, IComIIDcannot accept them.This forces consumers to hand-author two polyfills per project:
IComIIDinterface declaration gated#if !NET.partial struct Foo : IComIID { Guid IComIID.Guid => IID_Guid; }entry for every generated COM struct that flows throughComScope<T>, gated#if !NET.Each new COM type added to
NativeMethods.txtrequires a corresponding partial. The pattern is mechanical and lends itself to generator support.Steps to reproduce
Create the project as described below and build it.
The
net10.0build succeeds. Thenet472build fails with:Expected
CsWin32 emits
IComIIDand attaches it to generated COM structs on all supported target frameworks, in a shape appropriate for the language version (static-abstract on .NET 7+, instance member on older targets, matching howdotnet/winformspolyfills the type today).Actual
The interface and its
partialattachments are only emitted on TFMs wherestatic abstractinterface members are supported, leaving older targets without any substitute.Environment
Microsoft.Windows.CsWin320.3.275net10.0;net472How the workaround looks in production
dotnet/msbuildmulti-targetsnet10.0andnet472and currently maintains two hand-written polyfill files:src/Framework/Polyfills/IComIID.cs— declares the instance-basedIComIIDinterface under#if !NET.src/Framework/Polyfills/IComIIDPolyfills.cs— oneinternal partial struct Foo : IComIID { readonly Guid IComIID.Guid => IID_Guid; }entry per generated COM type used throughComScope<T>, all under#if !NET.Every time a new COM interface name is added to
NativeMethods.txta matching partial must be added here. This is the same workaround applied in [dotnet/winforms] https://github.com/dotnet/winforms/blob/main/src/System.Private.Windows.Core/src/Framework/Windows/Win32/System/Com/IDataObject.cs), which is referenced from msbuild's polyfill source.References
https://github.com/dotnet/msbuild/blob/main/src/Framework/Polyfills/IComIID.cs
and
https://github.com/dotnet/msbuild/blob/main/src/Framework/Polyfills/IComIIDPolyfills.cs
https://github.com/dotnet/winforms/blob/main/src/System.Private.Windows.Core/src/Framework/Windows/Win32/System/Com/IDataObject.cs
https://github.com/dotnet/msbuild/blob/main/.github/skills/cswin32-com/SKILL.md#icomiid-polyfill-for-net472
Suggested fix
Use
#if NETto generate the existingIComIIDsignature and#elseto generate the down-level implementation. In each struct generate the implementation in the same#ifblocks. The .NET (core) implementation is optimized for a pattern that pulls straight from metadata but requires .NET runtime features.Sources
Drop these four files into a fresh folder and run
dotnet build.IComIIDMissing.csprojNativeMethods.txtNativeMethods.json{ "$schema": "https://aka.ms/CsWin32.schema.json", "allowMarshaling": false, "useSafeHandles": false }Program.cs