Skip to content

Commit 302a49a

Browse files
Enhance collectible assembly handling in XmlSerializer (#127695)
This pull request improves the handling of collectible assemblies and types within the XML serialization infrastructure, particularly when dealing with generic containers (like `List<T>` or arrays) that may contain collectible types. The changes ensure that serializers and related caches correctly associate with the appropriate collectible AssemblyLoadContext (ALC), preventing memory leaks and enabling proper unloading of collectible assemblies. Additionally, new tests have been added to verify these scenarios. **Improvements to collectible assembly handling:** * Added the `FindCollectibleAssembly` helper method in `Compilation.cs` to recursively locate the correct collectible assembly for a given type, including generic arguments and element types. This ensures that serializer generation and caching are tied to the correct ALC, especially for generic or array types whose main assembly may not be collectible. * Updated serializer generation (`GenerateRefEmitAssembly`), cache lookup (`TempAssemblyCache.GetTempAssembly`), and cache insertion (`TempAssemblyCache.Add`) logic to use the collectible assembly found via `FindCollectibleAssembly`, rather than relying solely on `t.Assembly`. This change ensures that collectible types within generic containers are handled correctly and can be unloaded. [[1]](diffhunk://#diff-e4d354039ba8ec739e290407b136c6af32d4d38e07fa9420078f662590ea0da2L448-R488) [[2]](diffhunk://#diff-e4d354039ba8ec739e290407b136c6af32d4d38e07fa9420078f662590ea0da2L705-R746) [[3]](diffhunk://#diff-e4d354039ba8ec739e290407b136c6af32d4d38e07fa9420078f662590ea0da2L720-R776) **Improvements to context-aware tables:** * Adjusted logic in `ContextAwareTables.GetOrCreateValue` to use the default table only for non-collectible types and non-collectible load contexts, and to use a `ConditionalWeakTable` for collectible types or collectible load contexts, improving resource management for collectible scenarios. [[1]](diffhunk://#diff-ddd028dcb1c03a409ec83ec7247b1c4793ebbee56e379a0dab16696fedf1503dL37-R38) [[2]](diffhunk://#diff-ddd028dcb1c03a409ec83ec7247b1c4793ebbee56e379a0dab16696fedf1503dL50-R50) **Testing enhancements:** * Added new tests (`Xml_CollectibleTypeInGenericContainer`) to verify that collectible types inside generic containers (like `List<T>` or arrays) are correctly handled and can be unloaded, preventing memory leaks. This includes a helper method `ExecuteCollectibleContainerAndUnload` that exercises these scenarios. [[1]](diffhunk://#diff-cb25d8cca02e9c7959514c29f15823cfbdcae1b51cd148ddc69e2a8a6fb780e4R2623-R2646) [[2]](diffhunk://#diff-cb25d8cca02e9c7959514c29f15823cfbdcae1b51cd148ddc69e2a8a6fb780e4R2823-R2883) **Minor changes:** * Added a missing `using System.Diagnostics;` directive to support new assertions. These changes collectively improve the reliability and correctness of XML serialization in scenarios involving collectible assemblies and types, especially when used in generic containers. Fixes #100518 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 65b7467 commit 302a49a

4 files changed

Lines changed: 254 additions & 14 deletions

File tree

src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,12 +1276,14 @@ private IfState PopIfState()
12761276
}
12771277

12781278
[RequiresDynamicCode(XmlSerializer.AotSerializationWarning)]
1279-
internal static AssemblyBuilder CreateAssemblyBuilder(string name)
1279+
internal static AssemblyBuilder CreateAssemblyBuilder(string name, bool collectible)
12801280
{
12811281
AssemblyName assemblyName = new AssemblyName();
12821282
assemblyName.Name = name;
12831283
assemblyName.Version = new Version(1, 0, 0, 0);
1284-
return AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
1284+
return AssemblyBuilder.DefineDynamicAssembly(
1285+
assemblyName,
1286+
collectible ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);
12851287
}
12861288

12871289
internal static ModuleBuilder CreateModuleBuilder(AssemblyBuilder assemblyBuilder, string name)

src/libraries/System.Private.Xml/src/System/Xml/Serialization/Compilation.cs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.Globalization;
89
using System.IO;
@@ -433,6 +434,32 @@ internal static bool GenerateSerializerToStream(XmlMapping[] xmlMappings, Type?[
433434
return true;
434435
}
435436

437+
internal static Assembly? FindCollectibleAssembly(Type t)
438+
{
439+
// Shortcut the common case
440+
if (!t.IsCollectible)
441+
return null;
442+
443+
if (t.Assembly.IsCollectible)
444+
return t.Assembly;
445+
446+
if (t.IsGenericType && !t.IsGenericTypeDefinition)
447+
{
448+
foreach (Type arg in t.GenericTypeArguments)
449+
{
450+
Assembly? found = FindCollectibleAssembly(arg);
451+
if (found is not null)
452+
return found;
453+
}
454+
}
455+
else if (t.HasElementType)
456+
{
457+
return FindCollectibleAssembly(t.GetElementType()!);
458+
}
459+
460+
return null;
461+
}
462+
436463
[RequiresUnreferencedCode("calls GenerateElement")]
437464
[RequiresDynamicCode(XmlSerializer.AotSerializationWarning)]
438465
internal static Assembly GenerateRefEmitAssembly(XmlMapping[] xmlMappings, Type?[] types)
@@ -445,17 +472,37 @@ internal static Assembly GenerateRefEmitAssembly(XmlMapping[] xmlMappings, Type?
445472
TypeScope[] scopes = new TypeScope[scopeTable.Keys.Count];
446473
scopeTable.Keys.CopyTo(scopes, 0);
447474

448-
using (AssemblyLoadContext.EnterContextualReflection(mainAssembly))
475+
// Make sure we enter the correct ALC. If we have any collectible types, we should enter
476+
// the ALC of those types. The mainType's assembly might not satisfy that requirement.
477+
Assembly? collectibleAssembly = null;
478+
foreach (var t in types)
479+
{
480+
if (t?.IsCollectible == true)
481+
{
482+
collectibleAssembly = FindCollectibleAssembly(t);
483+
Debug.Assert(collectibleAssembly != null);
484+
break;
485+
}
486+
}
487+
488+
// Validate collectible types against the same assembly whose ALC we enter below, so
489+
// that a collectible type combined with a non-collectible mainAssembly is checked
490+
// against the collectible context rather than the default one.
491+
Assembly? contextAssembly = collectibleAssembly ?? mainAssembly;
492+
493+
using (AssemblyLoadContext.EnterContextualReflection(contextAssembly))
449494
{
450495
// Before generating any IL, check each mapping and supported type to make sure
451496
// they are compatible with the current ALC
452497
for (int i = 0; i < types.Length; i++)
453-
VerifyLoadContext(types[i], mainAssembly);
498+
VerifyLoadContext(types[i], contextAssembly);
454499
foreach (var mapping in xmlMappings)
455-
VerifyLoadContext(mapping.Accessor.Mapping?.TypeDesc?.Type, mainAssembly);
500+
VerifyLoadContext(mapping.Accessor.Mapping?.TypeDesc?.Type, contextAssembly);
456501

457502
string assemblyName = "Microsoft.GeneratedCode";
458-
AssemblyBuilder assemblyBuilder = CodeGenerator.CreateAssemblyBuilder(assemblyName);
503+
AssemblyBuilder assemblyBuilder = CodeGenerator.CreateAssemblyBuilder(
504+
assemblyName,
505+
collectible: collectibleAssembly is not null);
459506
// Add AssemblyVersion attribute to match parent assembly version
460507
if (mainType != null)
461508
{
@@ -702,7 +749,8 @@ internal sealed class TempAssemblyCache
702749
if (_fastCache.TryGetValue(key, out tempAssembly))
703750
return tempAssembly;
704751

705-
if (_collectibleCaches.TryGetValue(t.Assembly, out var cCache))
752+
Assembly lookupAssembly = TempAssembly.FindCollectibleAssembly(t) ?? t.Assembly;
753+
if (_collectibleCaches.TryGetValue(lookupAssembly, out var cCache))
706754
cCache.TryGetValue(key, out tempAssembly);
707755

708756
return tempAssembly;
@@ -717,17 +765,22 @@ internal void Add(string? ns, Type t, TempAssembly assembly)
717765
if (tempAssembly == assembly)
718766
return;
719767

720-
AssemblyLoadContext? alc = AssemblyLoadContext.GetLoadContext(t.Assembly);
721768
TempAssemblyCacheKey key = new TempAssemblyCacheKey(ns, t);
722769
Dictionary<TempAssemblyCacheKey, TempAssembly>? cache;
723770

724-
if (alc != null && alc.IsCollectible)
771+
// Use the collectible cache for any collectible type so that the cache entry
772+
// can be released when the collectible ALC is unloaded. For generic types like
773+
// List<Bar> where Bar is collectible, t.Assembly is the default ALC assembly
774+
// (System.Collections), so we need to find the actual collectible assembly from
775+
// the type's generic arguments or element type.
776+
Assembly? collectibleAssembly = TempAssembly.FindCollectibleAssembly(t);
777+
if (collectibleAssembly != null)
725778
{
726-
cache = _collectibleCaches.TryGetValue(t.Assembly, out var c) // Clone or create
779+
cache = _collectibleCaches.TryGetValue(collectibleAssembly, out var c)
727780
? new Dictionary<TempAssemblyCacheKey, TempAssembly>(c)
728781
: new Dictionary<TempAssemblyCacheKey, TempAssembly>();
729782
cache[key] = assembly;
730-
_collectibleCaches.AddOrUpdate(t.Assembly, cache);
783+
_collectibleCaches.AddOrUpdate(collectibleAssembly, cache);
731784
}
732785
else
733786
{

src/libraries/System.Private.Xml/src/System/Xml/Serialization/ContextAwareTables.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ internal T GetOrCreateValue(Type t, Func<Type, T> f)
3434
// Not found. Do the slower work of creating the value in the correct collection.
3535
AssemblyLoadContext? alc = AssemblyLoadContext.GetLoadContext(t.Assembly);
3636

37-
// Null and non-collectible load contexts use the default table
38-
if (alc == null || !alc.IsCollectible)
37+
// Use the default table for types that are not collectible and whose load context is either null or not collectible
38+
if (!t.IsCollectible && !(alc?.IsCollectible ?? false))
3939
{
4040
lock (_defaultTable)
4141
{
@@ -47,7 +47,7 @@ internal T GetOrCreateValue(Type t, Func<Type, T> f)
4747
}
4848
}
4949

50-
// Collectible load contexts should use the ConditionalWeakTable so they can be unloaded
50+
// Collectible types or load contexts should use the ConditionalWeakTable so they can be unloaded
5151
else
5252
{
5353
lock (_collectibleTable)

src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.IO;
1111
using System.Linq;
1212
using System.Reflection;
13+
using System.Reflection.Emit;
1314
using System.Runtime.CompilerServices;
1415
using System.Runtime.Loader;
1516
using System.Runtime.Serialization.Tests;
@@ -2701,6 +2702,46 @@ public static void Xml_TypeInCollectibleALC()
27012702
Assert.True(!weakRef.IsAlive);
27022703
}
27032704

2705+
#if !XMLSERIALIZERGENERATORTESTS
2706+
// The Generator tests only cover pre-generated serializers that are based on the types in SerializableAssembly.dll.
2707+
// For this test, we're testing a scenario where one of those types is used inside a generic container - aka, List<SerializationType>.
2708+
// Since 'List<>' is *not* defined in SerializableAssembly.dll, the pre-generated serializers do not include serializers for List<SerializationType>,
2709+
// Therefore, this test is excluded from the Generator tests.
2710+
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.HasAssemblyFiles))]
2711+
[ActiveIssue("https://github.com/dotnet/runtime/issues/34072", TestRuntimes.Mono)]
2712+
[ActiveIssue("https://github.com/dotnet/runtime/issues/95928", typeof(PlatformDetection), nameof(PlatformDetection.IsReadyToRunCompiled))]
2713+
[ActiveIssue("https://github.com/dotnet/runtime/issues/124344", typeof(PlatformDetection), nameof(PlatformDetection.IsAppleMobile), nameof(PlatformDetection.IsCoreCLR))]
2714+
[InlineData("List")]
2715+
[InlineData("Array")]
2716+
public static void Xml_CollectibleTypeInGenericContainer(string containerKind)
2717+
{
2718+
ExecuteCollectibleContainerAndUnload("SerializableAssembly.dll", "SerializationTypes.SimpleType", containerKind, out var weakRef);
2719+
2720+
for (int i = 0; weakRef.IsAlive && i < 10; i++)
2721+
{
2722+
GC.Collect();
2723+
GC.WaitForPendingFinalizers();
2724+
}
2725+
Assert.True(!weakRef.IsAlive);
2726+
}
2727+
2728+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsReflectionEmitSupported))]
2729+
// Mono doesn't support collectible assemblies: RuntimeType.IsCollectible always returns false,
2730+
// so the serializer never engages its collectible weak-caching path. See dotnet/runtime#34072.
2731+
[ActiveIssue("https://github.com/dotnet/runtime/issues/34072", TestRuntimes.Mono)]
2732+
public static void Xml_RunAndCollectType_DoesNotRemainCached()
2733+
{
2734+
ExecuteRunAndCollectAndRelease(out WeakReference weakRef);
2735+
2736+
for (int i = 0; weakRef.IsAlive && i < 10; i++)
2737+
{
2738+
GC.Collect();
2739+
GC.WaitForPendingFinalizers();
2740+
}
2741+
Assert.True(!weakRef.IsAlive);
2742+
}
2743+
#endif
2744+
27042745
[Fact]
27052746
public static void ValidateXElement()
27062747
{
@@ -2877,6 +2918,150 @@ private static void ExecuteAndUnload(string assemblyfile, string typename, out W
28772918
alc.Unload();
28782919
}
28792920

2921+
[MethodImpl(MethodImplOptions.NoInlining)]
2922+
private static void ExecuteCollectibleContainerAndUnload(string assemblyfile, string typename, string containerKind, out WeakReference wref)
2923+
{
2924+
var fullPath = Path.GetFullPath(assemblyfile);
2925+
var alc = new TestAssemblyLoadContext("XmlSerializerTests", true, fullPath);
2926+
wref = new WeakReference(alc);
2927+
2928+
var asm = alc.LoadFromAssemblyPath(fullPath);
2929+
var baseType = asm.GetType(typename);
2930+
Assert.Equal(alc, AssemblyLoadContext.GetLoadContext(baseType.Assembly));
2931+
Assert.NotEqual(alc, AssemblyLoadContext.Default);
2932+
2933+
Type type;
2934+
object obj;
2935+
switch (containerKind)
2936+
{
2937+
case "List":
2938+
type = typeof(List<>).MakeGenericType(baseType);
2939+
// The type is collectible because it has a collectible type argument, but its
2940+
// Assembly is in the default ALC since that's where List<> is defined.
2941+
Assert.True(type.IsCollectible);
2942+
Assert.Equal(AssemblyLoadContext.Default, AssemblyLoadContext.GetLoadContext(type.Assembly));
2943+
2944+
obj = Activator.CreateInstance(type);
2945+
object listItem = Activator.CreateInstance(baseType);
2946+
baseType.GetProperty("P1").SetValue(listItem, "test");
2947+
baseType.GetProperty("P2").SetValue(listItem, 42);
2948+
type.GetMethod("Add").Invoke(obj, [listItem]);
2949+
break;
2950+
2951+
case "Array":
2952+
type = baseType.MakeArrayType();
2953+
Assert.True(type.IsCollectible);
2954+
2955+
var array = Array.CreateInstance(baseType, 1);
2956+
object arrayItem = Activator.CreateInstance(baseType);
2957+
baseType.GetProperty("P1").SetValue(arrayItem, "test");
2958+
baseType.GetProperty("P2").SetValue(arrayItem, 42);
2959+
array.SetValue(arrayItem, 0);
2960+
obj = array;
2961+
break;
2962+
2963+
default:
2964+
throw new ArgumentException($"Unknown container kind: {containerKind}");
2965+
}
2966+
2967+
XmlSerializer serializer = new XmlSerializer(type);
2968+
using var ms = new MemoryStream();
2969+
serializer.Serialize(ms, obj);
2970+
ms.Position = 0;
2971+
var rtobj = serializer.Deserialize(ms);
2972+
Assert.NotNull(rtobj);
2973+
2974+
if (rtobj is Array rtArray)
2975+
Assert.Equal(1, rtArray.Length);
2976+
else if (rtobj is ICollection rtCollection)
2977+
Assert.Equal(1, rtCollection.Count);
2978+
else
2979+
Assert.Fail($"Expected deserialized object to be an Array or ICollection, but got {rtobj.GetType()}.");
2980+
2981+
alc.Unload();
2982+
}
2983+
2984+
[MethodImpl(MethodImplOptions.NoInlining)]
2985+
private static void ExecuteRunAndCollectAndRelease(out WeakReference wref)
2986+
{
2987+
string uniqueName = "XmlSerializerTests.RunAndCollect.Type_" + Guid.NewGuid().ToString("N");
2988+
var assemblyName = new AssemblyName(uniqueName);
2989+
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
2990+
assemblyName,
2991+
AssemblyBuilderAccess.RunAndCollect);
2992+
2993+
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(uniqueName);
2994+
TypeBuilder typeBuilder = moduleBuilder.DefineType(
2995+
uniqueName + ".SimpleType",
2996+
TypeAttributes.Public | TypeAttributes.Class);
2997+
2998+
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
2999+
DefineAutoProperty(typeBuilder, "P1", typeof(string));
3000+
DefineAutoProperty(typeBuilder, "P2", typeof(int));
3001+
3002+
Type type = typeBuilder.CreateTypeInfo()!.AsType();
3003+
3004+
Assert.True(type.IsCollectible);
3005+
Assert.True(type.Assembly.IsCollectible);
3006+
3007+
wref = new WeakReference(type.Assembly);
3008+
3009+
object obj = Activator.CreateInstance(type)!;
3010+
type.GetProperty("P1")!.SetValue(obj, "test");
3011+
type.GetProperty("P2")!.SetValue(obj, 42);
3012+
3013+
XmlSerializer serializer = new XmlSerializer(type);
3014+
using var ms = new MemoryStream();
3015+
serializer.Serialize(ms, obj);
3016+
3017+
ms.Position = 0;
3018+
object? rtobj = serializer.Deserialize(ms);
3019+
Assert.NotNull(rtobj);
3020+
Assert.Equal("test", type.GetProperty("P1")!.GetValue(rtobj));
3021+
Assert.Equal(42, type.GetProperty("P2")!.GetValue(rtobj));
3022+
}
3023+
3024+
private static void DefineAutoProperty(TypeBuilder typeBuilder, string name, Type propertyType)
3025+
{
3026+
FieldBuilder field = typeBuilder.DefineField("_" + name, propertyType, FieldAttributes.Private);
3027+
3028+
MethodAttributes methodAttributes =
3029+
MethodAttributes.Public |
3030+
MethodAttributes.SpecialName |
3031+
MethodAttributes.HideBySig;
3032+
3033+
MethodBuilder getter = typeBuilder.DefineMethod(
3034+
"get_" + name,
3035+
methodAttributes,
3036+
propertyType,
3037+
Type.EmptyTypes);
3038+
3039+
ILGenerator getterIl = getter.GetILGenerator();
3040+
getterIl.Emit(OpCodes.Ldarg_0);
3041+
getterIl.Emit(OpCodes.Ldfld, field);
3042+
getterIl.Emit(OpCodes.Ret);
3043+
3044+
MethodBuilder setter = typeBuilder.DefineMethod(
3045+
"set_" + name,
3046+
methodAttributes,
3047+
null,
3048+
new[] { propertyType });
3049+
3050+
ILGenerator setterIl = setter.GetILGenerator();
3051+
setterIl.Emit(OpCodes.Ldarg_0);
3052+
setterIl.Emit(OpCodes.Ldarg_1);
3053+
setterIl.Emit(OpCodes.Stfld, field);
3054+
setterIl.Emit(OpCodes.Ret);
3055+
3056+
PropertyBuilder property = typeBuilder.DefineProperty(
3057+
name,
3058+
PropertyAttributes.None,
3059+
propertyType,
3060+
null);
3061+
3062+
property.SetGetMethod(getter);
3063+
property.SetSetMethod(setter);
3064+
}
28803065

28813066
private static readonly string s_defaultNs = "http://tempuri.org/";
28823067
private static T RoundTripWithXmlMembersMapping<T>(object requestBodyValue, string memberName, string baseline, bool skipStringCompare = false, string wrapperName = null)

0 commit comments

Comments
 (0)