Description
A NativeAOT application using a source-generated PolyType shape fails at runtime because the compiler does not retain a reachable generic virtual interface method implementation. The concrete implementation is created by a statically visible factory; there is no reflection or dynamic loading involved.
This was discovered while adding NativeAOT test execution to AArnott/Nerdbank.JsonRpc. It reduces to one POCO and one direct call to the published Nerdbank.Json package.
Reproduction steps
Create Repro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<PublishAot>true</PublishAot>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.Json" Version="0.1.1041-alpha" />
<PackageReference Include="PolyType" Version="1.4.1" />
</ItemGroup>
</Project>
Create Program.cs:
using Nerdbank.Json;
using PolyType;
Poco value = new() { Number = 42 };
Console.WriteLine(new JsonSerializer().Serialize(value, GetShape<Poco, Poco>()));
static ITypeShape<T> GetShape<T, TProvider>()
where TProvider : IShapeable<T>
=> TProvider.GetTypeShape();
[GenerateShape]
internal partial class Poco
{
public int Number { get; init; }
}
Then run:
dotnet publish -c Release -r win-x64
.\bin\Release\net10.0\win-x64\publish\Repro.exe
Expected behavior
The executable prints serialized JSON for Poco.
Actual behavior
Publishing succeeds without trimming/AOT warnings. Running the executable fail-fasts:
Process terminated. Failed to create generic virtual method implementation
Declaring type: Nerdbank.Json.JsonStandardVisitor
Method name: PolyType.Abstractions.ITypeShapeFunc.Invoke
Instantiation:
Argument 00000000: Poco
at System.RuntimeExceptionHelpers.FailFast(...)
at Internal.Runtime.TypeLoader.TypeLoaderEnvironment.ResolveGenericVirtualMethodTarget(...)
at System.Runtime.TypeLoaderExports.CacheMiss(...)
at System.Runtime.TypeLoaderExports.GVMLookupForSlotSlow(...)
at PolyType.Utilities.TypeGenerationContext.GetOrAdd(ITypeShape, Object)
at PolyType.Utilities.TypeCache.AddValue(ITypeShape)
at PolyType.Utilities.TypeCache.GetOrAdd(ITypeShape)
at Nerdbank.Json.ConverterCache.GetOrAddConverter[T](ITypeShape`1)
at Nerdbank.Json.JsonSerializer.Serialize[T](...)
at Program.<Main>$(String[] args)
Reduction and A/B results
The relevant reachable path is:
// Nerdbank.Json configures PolyType's cache with a concrete factory:
ValueBuilderFactory = ctx => new JsonStandardVisitor(this, ctx);
// PolyType later dispatches through the erased shape:
value = typeShape.Invoke(ValueBuilder, state);
// Generated/source shape dispatch reaches this interface GVM:
object? ITypeShapeFunc.Invoke<T>(ITypeShape<T> typeShape, object? state)
=> owner.CreateConverter(typeShape, this);
Observed A/B behavior:
- Adding
<TrimmerRootAssembly Include="Nerdbank.Json" /> makes the executable pass, but broadly rooting a shipping library is not a viable consumer requirement.
- Replacing the
Invoke<T> implementation with the direct expression below gets past the missing-GVM failure and enters normal converter traversal:
object? ITypeShapeFunc.Invoke(ITypeShape typeShape, object? state)
=> typeShape.Accept(this, state);
- Keeping the complete converter-selection logic in
Invoke<T> still fails. A minimal direct Accept body succeeds, while the shipping implementation's delegation to a generic helper fails.
- Moving the visitor to a nested type, making the implementation public, eagerly constructing the cache, and using a static cache factory do not fix it.
- Publishing Nerdbank.Json itself as net10.0 does not fix it.
- PolyType's own
JsonSerializerTS example uses the same MultiProviderTypeCache/ITypeShapeFunc.Invoke<T> pattern and passes NativeAOT. Its implementation directly calls typeShape.Accept(this), matching the successful side of the A/B test.
This looks like an ILC GVM reachability/code-generation issue rather than reflection usage or a missing user type shape. It is distinct from #113664, which fixed dynamic static generic virtual dispatch and is already present in .NET 10.
Configuration
Reproduced on Windows x64 with:
- .NET SDK 10.0.401 / Microsoft.DotNet.ILCompiler 10.0.8
- .NET SDK 11.0.100-preview.5.26302.115 / matching .NET 11 ILCompiler
- Nerdbank.Json 0.1.1041-alpha
- PolyType 1.4.1
The original test suite reproduces on Windows x64, Linux x64, and macOS arm64 under .NET 10 NativeAOT. Its IL executions pass on all three platforms.
Description
A NativeAOT application using a source-generated PolyType shape fails at runtime because the compiler does not retain a reachable generic virtual interface method implementation. The concrete implementation is created by a statically visible factory; there is no reflection or dynamic loading involved.
This was discovered while adding NativeAOT test execution to AArnott/Nerdbank.JsonRpc. It reduces to one POCO and one direct call to the published Nerdbank.Json package.
Reproduction steps
Create
Repro.csproj:Create
Program.cs:Then run:
Expected behavior
The executable prints serialized JSON for
Poco.Actual behavior
Publishing succeeds without trimming/AOT warnings. Running the executable fail-fasts:
Reduction and A/B results
The relevant reachable path is:
Observed A/B behavior:
<TrimmerRootAssembly Include="Nerdbank.Json" />makes the executable pass, but broadly rooting a shipping library is not a viable consumer requirement.Invoke<T>implementation with the direct expression below gets past the missing-GVM failure and enters normal converter traversal:object? ITypeShapeFunc.Invoke(ITypeShape typeShape, object? state)
=> typeShape.Accept(this, state);
Invoke<T>still fails. A minimal directAcceptbody succeeds, while the shipping implementation's delegation to a generic helper fails.JsonSerializerTSexample uses the sameMultiProviderTypeCache/ITypeShapeFunc.Invoke<T>pattern and passes NativeAOT. Its implementation directly callstypeShape.Accept(this), matching the successful side of the A/B test.This looks like an ILC GVM reachability/code-generation issue rather than reflection usage or a missing user type shape. It is distinct from #113664, which fixed dynamic static generic virtual dispatch and is already present in .NET 10.
Configuration
Reproduced on Windows x64 with:
The original test suite reproduces on Windows x64, Linux x64, and macOS arm64 under .NET 10 NativeAOT. Its IL executions pass on all three platforms.