Skip to content

Fix XmlSerializer ILGen path failing with collectible AssemblyLoadContext types#127598

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/fix-xmlserializer-collectible-assembly
Draft

Fix XmlSerializer ILGen path failing with collectible AssemblyLoadContext types#127598
Copilot wants to merge 2 commits intomainfrom
copilot/fix-xmlserializer-collectible-assembly

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

Description

XmlSerializer's ILGen path throws NotSupportedException: A non-collectible assembly may not reference a collectible assembly when types from a collectible ALC are involved — both the direct case (serializing a collectible type) and the inverted case (creating a serializer from within a collectible ALC context for a type whose root assembly appears to be in the default ALC, e.g. List<CollectibleType>).

The root cause: CodeGenerator.CreateAssemblyBuilder always created a non-collectible dynamic assembly in the default ALC, making it illegal to emit IL referencing collectible types.

Core fix — GenerateRefEmitAssembly / CodeGenerator

  • Capture CurrentContextualReflectionContext before EnterContextualReflection(mainAssembly) overwrites it (critical for the inverted scenario)
  • Scan types[] and all TypeScope.Types for any collectible ALC; fall back to the caller's ALC if none found
  • Enter contextual reflection via collectibleAlc.EnterContextualReflection() when applicable so DefineDynamicAssembly targets the right ALC
  • Add collectible flag to CreateAssemblyBuilder — uses AssemblyBuilderAccess.RunAndCollect when set, allowing the generated assembly to live in the collectible ALC and reference its types

Cache correctness — TempAssemblyCache / ContextAwareTables

Both caches use ConditionalWeakTable to allow collectible entries to be GC'd when the ALC unloads. The inverted scenario (type in default ALC, caller in collectible ALC) was routing entries into _fastCache (strong refs, never GC'd).

  • TempAssemblyCache: adds GetCollectibleKeyAssembly(Type t) — returns t.Assembly for the direct case; for the inverted case returns the first assembly from currentAlc.Assemblies as the weak ConditionalWeakTable key. Using AssemblyLoadContext directly as a key creates a circular dependency that prevents GC; using a collectible Assembly object avoids this.
  • ContextAwareTables: adds _collectibleAlcTable (ConditionalWeakTable<Assembly, Hashtable>) keyed by the same first-assembly heuristic, with a per-type inner hashtable for the inverted scenario.

VerifyLoadContext

Added secondary check against CurrentContextualReflectionContext so collectible types are accepted when the caller's context matches their ALC (previously would throw for the inverted case even with correct assembly setup).

Tests

Three new [ConditionalFact] tests in XmlSerializerTests.cs (ILGen-only, inside #if !ReflectionOnly && !XMLSERIALIZERGENERATORTESTS), each with [MethodImpl(NoInlining)] helpers:

  • Xml_SerializerCreatedInsideCollectibleALC — inverted scenario; verifies ALC unloads after use
  • Xml_FromTypesInsideCollectibleALCXmlSerializer.FromTypes inverted scenario
  • Xml_MultipleCollectibleALCsCanUnloadIndependently — two independent ALCs unload without interfering

Changes

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Compilation.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ContextAwareTables.cs
  • src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs

Testing

Existing Xml_TypeInCollectibleALC test preserved. New tests cover the inverted scenario and FromTypes path.

Note

This PR was created with AI assistance from GitHub Copilot.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • bla
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26211.102/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing e/.dotnet/dotnet .o PPORT=0 8 GET_AMD64 -DTARGET_LINUX -DTARG (dns block)
  • foo
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26211.102/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing e/.dotnet/dotnet .o PPORT=0 8 GET_AMD64 -DTARGET_LINUX -DTARG (dns block)
  • notfound.invalid.corp.microsoft.com
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26211.102/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing e/.dotnet/dotnet .o PPORT=0 8 GET_AMD64 -DTARGET_LINUX -DTARG (dns block)
  • test.test
    • Triggering command: /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26211.102/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing e/.dotnet/dotnet .o PPORT=0 8 GET_AMD64 -DTARGET_LINUX -DTARG (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

Problem

Issues: #100518, #1388

When XmlSerializer is used with types in a collectible AssemblyLoadContext (ALC), the ILGen-based serializer fails with:

System.NotSupportedException: A non-collectible assembly may not reference a collectible assembly.

The root cause is in CodeGenerator.CreateAssemblyBuilder:

// src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs
internal static AssemblyBuilder CreateAssemblyBuilder(string name)
{
    AssemblyName assemblyName = new AssemblyName();
    assemblyName.Name = name;
    assemblyName.Version = new Version(1, 0, 0, 0);
    return AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
}

This always creates a non-collectible dynamic assembly in the default ALC. When the ILGen code then tries to emit IL referencing types from a collectible ALC (declaring locals, castclass, etc.), the CLR throws because a non-collectible assembly cannot reference collectible types.

The reflection-based serializer (SerializationMode.ReflectionOnly) does NOT have this problem because it never generates a dynamic assembly.

Root Cause Analysis

There are two scenarios that trigger this:

  1. "Inverted" case (issue XmlSerializer does not work with collections in collectible ALC #100518): Code running within a collectible ALC creates an XmlSerializer for a type. The type might even be from the default ALC (e.g., List<Bar> where Bar is in the collectible ALC), but the serialization graph includes collectible types.

  2. "Direct" case (issue AssemblyLoadContext crash when collectible assembly use XmlSerializer #1388): Code creates an XmlSerializer for a type that is directly defined in a collectible ALC.

In both cases, GenerateRefEmitAssembly creates the dynamic assembly in the default ALC (non-collectible), and then IL generation fails when it encounters type references to the collectible ALC.

Solution

Use AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect, loadContext) to create the dynamic assembly in the same collectible ALC when collectible types are involved. This way:

  • The dynamic assembly can reference collectible types (they're in the same ALC)
  • The dynamic assembly itself is collectible and will be cleaned up when the ALC unloads

Key Changes Required

1. Capture the caller's ALC early

The caller's ALC context needs to be captured before GenerateRefEmitAssembly calls AssemblyLoadContext.EnterContextualReflection(mainAssembly), which may overwrite it. The best place is in the XmlSerializer constructor or GenerateTempAssembly entry point.

Use AssemblyLoadContext.CurrentContextualReflectionContext or inspect the type's assembly to determine the effective ALC.

2. Determine the effective ALC in GenerateRefEmitAssembly

Before creating the assembly builder, determine if any involved type is collectible OR if the current execution context is collectible:

AssemblyLoadContext? collectibleAlc = null;

// Check the types array for collectible types
foreach (var t in types)
{
    if (t == null) continue;
    var alc = AssemblyLoadContext.GetLoadContext(t.Assembly);
    if (alc != null && alc.IsCollectible)
    {
        collectibleAlc = alc;
        break;
    }
}

// Also check TypeScope types discovered during import
if (collectibleAlc == null)
{
    foreach (TypeScope scope in scopes)
    {
        foreach (Type scopeType in scope.Types)
        {
            var alc = AssemblyLoadContext.GetLoadContext(scopeType.Assembly);
            if (alc != null && alc.IsCollectible)
            {
                collectibleAlc = alc;
                break;
            }
        }
        if (collectibleAlc != null) break;
    }
}

// Check the caller's context (for the "inverted" scenario where caller is in collectible ALC
// but types being serialized might appear to be in the default ALC like List<T>)
// This requires the caller's ALC to have been captured earlier and passed in.
collectibleAlc ??= callerAlc; // if caller's ALC is collectible

3. Modify CodeGenerator.CreateAssemblyBuilder

Add an overload or modify the existing method to accept an optional AssemblyLoadContext:

internal static AssemblyBuilder CreateAssemblyBuilder(string name, AssemblyLoadContext? targetAlc = null)
{
    AssemblyName assemblyName = new AssemblyName();
    assemblyName.Name = name;
    assemblyName.Version = new Version(1, 0, 0, 0);

    if (targetAlc != null && targetAlc.IsCollectible)
    {
        return AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect, targetAlc);
    }

    return AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
}

4. Update TempAssemblyCache to handle collectible scenarios correctly

The TempAssemblyCache also needs to use the effective ALC for its caching decisions. Currently it checks `AssemblyLoadContext.GetLoadContext(t.Assem...

This pull request was created from Copilot chat.

Copilot AI requested review from Copilot and removed request for Copilot April 30, 2026 07:25
@github-actions github-actions Bot added the area-AssemblyLoader-coreclr only use for closed issues label Apr 30, 2026
…rializer created inside collectible ALC

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/1c148b1e-1a19-45fb-93a5-8eb9a309241f

Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 30, 2026 08:17
Copilot AI changed the title [WIP] Fix XmlSerializer for collectible AssemblyLoadContext Fix XmlSerializer ILGen path failing with collectible AssemblyLoadContext types Apr 30, 2026
Copilot AI requested a review from StephenMolloy April 30, 2026 08:18
@teo-tsirpanis teo-tsirpanis added area-Serialization and removed area-AssemblyLoader-coreclr only use for closed issues labels May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants