Fix XmlSerializer ILGen path failing with collectible AssemblyLoadContext types#127598
Draft
Fix XmlSerializer ILGen path failing with collectible AssemblyLoadContext types#127598
Conversation
…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
changed the title
[WIP] Fix XmlSerializer for collectible AssemblyLoadContext
Fix XmlSerializer ILGen path failing with collectible AssemblyLoadContext types
Apr 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
XmlSerializer's ILGen path throwsNotSupportedException: A non-collectible assembly may not reference a collectible assemblywhen 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.CreateAssemblyBuilderalways created a non-collectible dynamic assembly in the default ALC, making it illegal to emit IL referencing collectible types.Core fix —
GenerateRefEmitAssembly/CodeGeneratorCurrentContextualReflectionContextbeforeEnterContextualReflection(mainAssembly)overwrites it (critical for the inverted scenario)types[]and allTypeScope.Typesfor any collectible ALC; fall back to the caller's ALC if none foundcollectibleAlc.EnterContextualReflection()when applicable soDefineDynamicAssemblytargets the right ALCcollectibleflag toCreateAssemblyBuilder— usesAssemblyBuilderAccess.RunAndCollectwhen set, allowing the generated assembly to live in the collectible ALC and reference its typesCache correctness —
TempAssemblyCache/ContextAwareTablesBoth caches use
ConditionalWeakTableto 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: addsGetCollectibleKeyAssembly(Type t)— returnst.Assemblyfor the direct case; for the inverted case returns the first assembly fromcurrentAlc.Assembliesas the weakConditionalWeakTablekey. UsingAssemblyLoadContextdirectly as a key creates a circular dependency that prevents GC; using a collectibleAssemblyobject 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.VerifyLoadContextAdded secondary check against
CurrentContextualReflectionContextso 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 inXmlSerializerTests.cs(ILGen-only, inside#if !ReflectionOnly && !XMLSERIALIZERGENERATORTESTS), each with[MethodImpl(NoInlining)]helpers:Xml_SerializerCreatedInsideCollectibleALC— inverted scenario; verifies ALC unloads after useXml_FromTypesInsideCollectibleALC—XmlSerializer.FromTypesinverted scenarioXml_MultipleCollectibleALCsCanUnloadIndependently— two independent ALCs unload without interferingChanges
src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cssrc/libraries/System.Private.Xml/src/System/Xml/Serialization/Compilation.cssrc/libraries/System.Private.Xml/src/System/Xml/Serialization/ContextAwareTables.cssrc/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.csTesting
Existing
Xml_TypeInCollectibleALCtest preserved. New tests cover the inverted scenario andFromTypespath.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/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/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/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/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
XmlSerializeris used with types in a collectibleAssemblyLoadContext(ALC), the ILGen-based serializer fails with:The root cause is in
CodeGenerator.CreateAssemblyBuilder: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:
"Inverted" case (issue XmlSerializer does not work with collections in collectible ALC #100518): Code running within a collectible ALC creates an
XmlSerializerfor a type. The type might even be from the default ALC (e.g.,List<Bar>whereBaris in the collectible ALC), but the serialization graph includes collectible types."Direct" case (issue AssemblyLoadContext crash when collectible assembly use XmlSerializer #1388): Code creates an
XmlSerializerfor a type that is directly defined in a collectible ALC.In both cases,
GenerateRefEmitAssemblycreates 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:Key Changes Required
1. Capture the caller's ALC early
The caller's ALC context needs to be captured before
GenerateRefEmitAssemblycallsAssemblyLoadContext.EnterContextualReflection(mainAssembly), which may overwrite it. The best place is in theXmlSerializerconstructor orGenerateTempAssemblyentry point.Use
AssemblyLoadContext.CurrentContextualReflectionContextor inspect the type's assembly to determine the effective ALC.2. Determine the effective ALC in
GenerateRefEmitAssemblyBefore creating the assembly builder, determine if any involved type is collectible OR if the current execution context is collectible:
3. Modify
CodeGenerator.CreateAssemblyBuilderAdd an overload or modify the existing method to accept an optional
AssemblyLoadContext:4. Update
TempAssemblyCacheto handle collectible scenarios correctlyThe
TempAssemblyCachealso 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.