diff --git a/src/libraries/System.Private.DataContractSerialization/src/System.Private.DataContractSerialization.csproj b/src/libraries/System.Private.DataContractSerialization/src/System.Private.DataContractSerialization.csproj index 8ee00c3c19581b..bd9a7cc8028184 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System.Private.DataContractSerialization.csproj +++ b/src/libraries/System.Private.DataContractSerialization/src/System.Private.DataContractSerialization.csproj @@ -145,6 +145,7 @@ + @@ -163,6 +164,7 @@ + diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/ContextAware.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/ContextAware.cs new file mode 100644 index 00000000000000..8134c5c3f82c70 --- /dev/null +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/ContextAware.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Runtime.Serialization.DataContracts; + +namespace System.Runtime.Serialization +{ + internal sealed class ContextAwareDataContractIndex + { + private (DataContract? strong, WeakReference? weak)[] _contracts; + private ConditionalWeakTable _keepAlive; + + public int Length => _contracts.Length; + + public ContextAwareDataContractIndex(int size) + { + _contracts = new (DataContract?, WeakReference?)[size]; + _keepAlive = new ConditionalWeakTable(); + } + + public DataContract? GetItem(int index) => _contracts[index].strong ?? (_contracts[index].weak?.TryGetTarget(out DataContract? ret) == true ? ret : null); + + public void SetItem(int index, DataContract dataContract) + { + // Check for unloadability to decide how to store the value + AssemblyLoadContext? alc = AssemblyLoadContext.GetLoadContext(dataContract.UnderlyingType.Assembly); + if (alc == null || !alc.IsCollectible) + { + _contracts[index].strong = dataContract; + } + else + { + _contracts[index].weak = new WeakReference(dataContract); + _keepAlive.Add(dataContract.UnderlyingType, dataContract); + } + } + + public void Resize(int newSize) + { + Array.Resize<(DataContract?, WeakReference?)>(ref _contracts, newSize); + } + } + + internal sealed class ContextAwareDictionary + where TKey : Type + where TValue : class? + { + private readonly ConcurrentDictionary _fastDictionary = new(); + private readonly ConditionalWeakTable _collectibleTable = new(); + + + internal TValue GetOrAdd(TKey t, Func f) + { + TValue? ret; + + // The fast and most common default case + if (_fastDictionary.TryGetValue(t, out ret)) + return ret; + + // Common case for collectible contexts + if (_collectibleTable.TryGetValue(t, out ret)) + return ret; + + // Not found. Do the slower work of creating the value in the correct collection. + AssemblyLoadContext? alc = AssemblyLoadContext.GetLoadContext(t.Assembly); + + // Null and non-collectible load contexts use the default table + if (alc == null || !alc.IsCollectible) + { + return _fastDictionary.GetOrAdd(t, f); + } + + // Collectible load contexts should use the ConditionalWeakTable so they can be unloaded + else + { + lock (_collectibleTable) + { + if (!_collectibleTable.TryGetValue(t, out ret)) + { + ret = f(t); + _collectibleTable.AddOrUpdate(t, ret); + } + } + } + + return ret; + } + } +} diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs index 49b2d19b83d772..1028c02ff4c1a0 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs @@ -4,6 +4,7 @@ using System; using System.Buffers.Binary; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -297,19 +298,16 @@ internal virtual bool IsValidContract() internal class DataContractCriticalHelper { - private static readonly Hashtable s_typeToIDCache = new Hashtable(new HashTableEqualityComparer()); - private static DataContract[] s_dataContractCache = new DataContract[32]; + private static readonly ConcurrentDictionary s_typeToIDCache = new(); + private static readonly ContextAwareDataContractIndex s_dataContractCache = new(32); private static int s_dataContractID; - private static Dictionary? s_typeToBuiltInContract; + private static readonly ContextAwareDictionary s_typeToBuiltInContract = new(); private static Dictionary? s_nameToBuiltInContract; private static Dictionary? s_typeNameToBuiltInContract; private static readonly Hashtable s_namespaces = new Hashtable(); private static Dictionary? s_clrTypeStrings; private static XmlDictionary? s_clrTypeStringsDictionary; - [ThreadStatic] - private static TypeHandleRef? s_typeHandleRef; - private static readonly object s_cacheLock = new object(); private static readonly object s_createDataContractLock = new object(); private static readonly object s_initBuiltInContractsLock = new object(); @@ -337,7 +335,7 @@ internal class DataContractCriticalHelper [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] internal static DataContract GetDataContractSkipValidation(int id, RuntimeTypeHandle typeHandle, Type? type) { - DataContract dataContract = s_dataContractCache[id]; + DataContract? dataContract = s_dataContractCache.GetItem(id); if (dataContract == null) { dataContract = CreateDataContract(id, typeHandle, type); @@ -353,13 +351,13 @@ internal static DataContract GetDataContractSkipValidation(int id, RuntimeTypeHa [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] internal static DataContract GetGetOnlyCollectionDataContractSkipValidation(int id, RuntimeTypeHandle typeHandle, Type? type) { - DataContract dataContract = s_dataContractCache[id] ?? CreateGetOnlyCollectionDataContract(id, typeHandle, type); + DataContract dataContract = s_dataContractCache.GetItem(id) ?? CreateGetOnlyCollectionDataContract(id, typeHandle, type); return dataContract; } internal static DataContract GetDataContractForInitialization(int id) { - DataContract dataContract = s_dataContractCache[id]; + DataContract? dataContract = s_dataContractCache.GetItem(id); if (dataContract == null) { throw new SerializationException(SR.DataContractCacheOverflow); @@ -370,14 +368,14 @@ internal static DataContract GetDataContractForInitialization(int id) internal static int GetIdForInitialization(ClassDataContract classContract) { int id = DataContract.GetId(classContract.TypeForInitialization.TypeHandle); - if (id < s_dataContractCache.Length && ContractMatches(classContract, s_dataContractCache[id])) + if (id < s_dataContractCache.Length && ContractMatches(classContract, s_dataContractCache.GetItem(id))) { return id; } int currentDataContractId = DataContractCriticalHelper.s_dataContractID; for (int i = 0; i < currentDataContractId; i++) { - if (ContractMatches(classContract, s_dataContractCache[i])) + if (ContractMatches(classContract, s_dataContractCache.GetItem(id))) { return i; } @@ -385,7 +383,7 @@ internal static int GetIdForInitialization(ClassDataContract classContract) throw new SerializationException(SR.DataContractCacheOverflow); } - private static bool ContractMatches(DataContract contract, DataContract cachedContract) + private static bool ContractMatches(DataContract contract, DataContract? cachedContract) { return (cachedContract != null && cachedContract.UnderlyingType == contract.UnderlyingType); } @@ -393,36 +391,29 @@ private static bool ContractMatches(DataContract contract, DataContract cachedCo internal static int GetId(RuntimeTypeHandle typeHandle) { typeHandle = GetDataContractAdapterTypeHandle(typeHandle); - s_typeHandleRef ??= new TypeHandleRef(); - s_typeHandleRef.Value = typeHandle; - object? value = s_typeToIDCache[s_typeHandleRef]; - if (value != null) - return ((IntRef)value).Value; + if (s_typeToIDCache.TryGetValue(typeHandle.Value, out int id)) + return id; try { lock (s_cacheLock) { - value = s_typeToIDCache[s_typeHandleRef]; - if (value != null) - return ((IntRef)value).Value; - - int nextId = s_dataContractID++; - if (nextId >= s_dataContractCache.Length) + return s_typeToIDCache.GetOrAdd(typeHandle.Value, static _ => { - int newSize = (nextId < int.MaxValue / 2) ? nextId * 2 : int.MaxValue; - if (newSize <= nextId) + int nextId = s_dataContractID++; + if (nextId >= s_dataContractCache.Length) { - Debug.Fail("DataContract cache overflow"); - throw new SerializationException(SR.DataContractCacheOverflow); + int newSize = (nextId < int.MaxValue / 2) ? nextId * 2 : int.MaxValue; + if (newSize <= nextId) + { + Debug.Fail("DataContract cache overflow"); + throw new SerializationException(SR.DataContractCacheOverflow); + } + s_dataContractCache.Resize(newSize); } - Array.Resize(ref s_dataContractCache, newSize); - } - IntRef id = new IntRef(nextId); - - s_typeToIDCache.Add(new TypeHandleRef(typeHandle), id); - return id.Value; + return nextId; + }); } } catch (Exception ex) when (!ExceptionUtility.IsFatal(ex)) @@ -436,12 +427,12 @@ internal static int GetId(RuntimeTypeHandle typeHandle) [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] private static DataContract CreateDataContract(int id, RuntimeTypeHandle typeHandle, Type? type) { - DataContract? dataContract = s_dataContractCache[id]; + DataContract? dataContract = s_dataContractCache.GetItem(id); if (dataContract == null) { lock (s_createDataContractLock) { - dataContract = s_dataContractCache[id]; + dataContract = s_dataContractCache.GetItem(id); if (dataContract == null) { type ??= Type.GetTypeFromHandle(typeHandle)!; @@ -508,7 +499,7 @@ private static void AssignDataContractToId(DataContract dataContract, int id) { lock (s_cacheLock) { - s_dataContractCache[id] = dataContract; + s_dataContractCache.SetItem(id, dataContract); } } @@ -519,7 +510,7 @@ private static DataContract CreateGetOnlyCollectionDataContract(int id, RuntimeT DataContract? dataContract = null; lock (s_createDataContractLock) { - dataContract = s_dataContractCache[id]; + dataContract = s_dataContractCache.GetItem(id); if (dataContract == null) { type ??= Type.GetTypeFromHandle(typeHandle)!; @@ -589,17 +580,11 @@ private static RuntimeTypeHandle GetDataContractAdapterTypeHandle(RuntimeTypeHan if (type.IsInterface && !CollectionDataContract.IsCollectionInterface(type)) type = Globals.TypeOfObject; - lock (s_initBuiltInContractsLock) + return s_typeToBuiltInContract.GetOrAdd(type, static (Type key) => { - s_typeToBuiltInContract ??= new Dictionary(); - - if (!s_typeToBuiltInContract.TryGetValue(type, out DataContract? dataContract)) - { - TryCreateBuiltInDataContract(type, out dataContract); - s_typeToBuiltInContract.Add(type, dataContract); - } + TryCreateBuiltInDataContract(key, out DataContract? dataContract); return dataContract; - } + }); } [RequiresDynamicCode(DataContract.SerializerAOTWarning)] @@ -945,19 +930,8 @@ internal static void ThrowInvalidDataContractException(string? message, Type? ty { if (type != null) { - lock (s_cacheLock) - { - s_typeHandleRef ??= new TypeHandleRef(); - s_typeHandleRef.Value = GetDataContractAdapterTypeHandle(type.TypeHandle); - - if (s_typeToIDCache.ContainsKey(s_typeHandleRef)) - { - lock (s_cacheLock) - { - s_typeToIDCache.Remove(s_typeHandleRef); - } - } - } + RuntimeTypeHandle runtimeTypeHandle = GetDataContractAdapterTypeHandle(type.TypeHandle); + s_typeToIDCache.TryRemove(runtimeTypeHandle.Value, out _); } throw new InvalidDataContractException(message); @@ -2515,62 +2489,4 @@ public override int GetHashCode() return _object1.GetHashCode() ^ _object2.GetHashCode(); } } - - internal sealed class HashTableEqualityComparer : IEqualityComparer - { - bool IEqualityComparer.Equals(object? x, object? y) - { - return ((TypeHandleRef)x!).Value.Equals(((TypeHandleRef)y!).Value); - } - - public int GetHashCode(object obj) - { - return ((TypeHandleRef)obj).Value.GetHashCode(); - } - } - - internal sealed class TypeHandleRefEqualityComparer : IEqualityComparer - { - public bool Equals(TypeHandleRef? x, TypeHandleRef? y) - { - return x!.Value.Equals(y!.Value); - } - - public int GetHashCode(TypeHandleRef obj) - { - return obj.Value.GetHashCode(); - } - } - - internal sealed class TypeHandleRef - { - private RuntimeTypeHandle _value; - - public TypeHandleRef() - { - } - - public TypeHandleRef(RuntimeTypeHandle value) - { - _value = value; - } - - public RuntimeTypeHandle Value - { - get => _value; - set => _value = value; - } - } - - internal sealed class IntRef - { - private readonly int _value; - - public IntRef(int value) - { - _value = value; - } - - public int Value => _value; - } } diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonDataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonDataContract.cs index 50db0fd272948b..25654871992db8 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonDataContract.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -151,8 +152,7 @@ internal class JsonDataContractCriticalHelper private static JsonDataContract[] s_dataContractCache = new JsonDataContract[32]; private static int s_dataContractID; - private static readonly TypeHandleRef s_typeHandleRef = new TypeHandleRef(); - private static readonly Dictionary s_typeToIDCache = new Dictionary(new TypeHandleRefEqualityComparer()); + private static readonly ConcurrentDictionary s_typeToIDCache = new(); private DataContractDictionary? _knownDataContracts; private readonly DataContract _traditionalDataContract; private readonly string _typeName; @@ -188,34 +188,26 @@ public static JsonDataContract GetJsonDataContract(DataContract traditionalDataC internal static int GetId(RuntimeTypeHandle typeHandle) { + if (s_typeToIDCache.TryGetValue(typeHandle.Value, out int id)) + return id; + lock (s_cacheLock) { - IntRef? id; - s_typeHandleRef.Value = typeHandle; - if (!s_typeToIDCache.TryGetValue(s_typeHandleRef, out id)) + return s_typeToIDCache.GetOrAdd(typeHandle.Value, static _ => { - int value = s_dataContractID++; - if (value >= s_dataContractCache.Length) + int nextId = s_dataContractID++; + if (nextId >= s_dataContractCache.Length) { - int newSize = (value < int.MaxValue / 2) ? value * 2 : int.MaxValue; - if (newSize <= value) + int newSize = (nextId < int.MaxValue / 2) ? nextId * 2 : int.MaxValue; + if (newSize <= nextId) { Debug.Fail("DataContract cache overflow"); throw new SerializationException(SR.DataContractCacheOverflow); } Array.Resize(ref s_dataContractCache, newSize); } - id = new IntRef(value); - try - { - s_typeToIDCache.Add(new TypeHandleRef(typeHandle), id); - } - catch (Exception ex) when (!ExceptionUtility.IsFatal(ex)) - { - throw new Exception(ex.Message, ex); - } - } - return id.Value; + return nextId; + }); } } diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs b/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs index 8484775ea230a0..0ae22abf361777 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs @@ -19,7 +19,8 @@ using System.Xml.Schema; using Xunit; using System.Runtime.Serialization.Tests; - +using System.Runtime.CompilerServices; +using System.Runtime.Loader; public static partial class DataContractSerializerTests { @@ -1115,6 +1116,88 @@ public static void DCS_TypeNamesWithSpecialCharacters() Assert.Equal(x.PropertyNameWithSpecialCharacters\u6F22\u00F1, y.PropertyNameWithSpecialCharacters\u6F22\u00F1); } + [Fact] +#if XMLSERIALIZERGENERATORTESTS + // Lack of AssemblyDependencyResolver results in assemblies that are not loaded by path to get + // loaded in the default ALC, which causes problems for this test. + [SkipOnPlatform(TestPlatforms.Browser, "AssemblyDependencyResolver not supported in wasm")] +#endif + [ActiveIssue("34072", TestRuntimes.Mono)] + public static void DCS_TypeInCollectibleALC() + { + ExecuteAndUnload("SerializableAssembly.dll", "SerializationTypes.SimpleType", makeCollection: false, out var weakRef); + + for (int i = 0; weakRef.IsAlive && i < 10; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + Assert.True(!weakRef.IsAlive); + } + + [Fact] +#if XMLSERIALIZERGENERATORTESTS + // Lack of AssemblyDependencyResolver results in assemblies that are not loaded by path to get + // loaded in the default ALC, which causes problems for this test. + [SkipOnPlatform(TestPlatforms.Browser, "AssemblyDependencyResolver not supported in wasm")] +#endif + [ActiveIssue("34072", TestRuntimes.Mono)] + public static void DCS_CollectionTypeInCollectibleALC() + { + ExecuteAndUnload("SerializableAssembly.dll", "SerializationTypes.SimpleType", makeCollection: true, out var weakRef); + + for (int i = 0; weakRef.IsAlive && i < 10; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + Assert.True(!weakRef.IsAlive); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ExecuteAndUnload(string assemblyfile, string typename, bool makeCollection, out WeakReference wref) + { + var fullPath = Path.GetFullPath(assemblyfile); + var alc = new TestAssemblyLoadContext("DataContractSerializerTests", true, fullPath); + object obj; + wref = new WeakReference(alc); + + // Load assembly by path. By name, and it gets loaded in the default ALC. + var asm = alc.LoadFromAssemblyPath(fullPath); + + // Ensure the type loaded in the intended non-Default ALC + var type = asm.GetType(typename); + Assert.Equal(AssemblyLoadContext.GetLoadContext(type.Assembly), alc); + Assert.NotEqual(alc, AssemblyLoadContext.Default); + + if (makeCollection) + { + int arrayLength = 3; + var array = Array.CreateInstance(type, arrayLength); + for (int i = 0; i < arrayLength; i++) + { + array.SetValue(Activator.CreateInstance(type), i); + } + type = array.GetType(); + obj = array; + } + else + { + obj = Activator.CreateInstance(type); + } + + // Round-Trip the instance + var dcs = new DataContractSerializer(type); + var rtobj = DataContractSerializerHelper.SerializeAndDeserialize(obj, null, null, () => dcs, true, false); + Assert.NotNull(rtobj); + if (makeCollection) + Assert.Equal(obj, rtobj); + else + Assert.True(rtobj.Equals(obj)); + + alc.Unload(); + } + [Fact] public static void DCS_JaggedArrayAsRoot() { diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/ReflectionOnly/System.Runtime.Serialization.Xml.ReflectionOnly.Tests.csproj b/src/libraries/System.Runtime.Serialization.Xml/tests/ReflectionOnly/System.Runtime.Serialization.Xml.ReflectionOnly.Tests.csproj index bdc0d2c86c22c0..d128172dd2acc6 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/ReflectionOnly/System.Runtime.Serialization.Xml.ReflectionOnly.Tests.csproj +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/ReflectionOnly/System.Runtime.Serialization.Xml.ReflectionOnly.Tests.csproj @@ -3,10 +3,14 @@ $(DefineConstants);ReflectionOnly $(NetCoreAppCurrent) + + + + - + diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/System.Runtime.Serialization.Xml.Tests.csproj b/src/libraries/System.Runtime.Serialization.Xml/tests/System.Runtime.Serialization.Xml.Tests.csproj index 1628ec4ae6ebd1..c299e17295d0a0 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/System.Runtime.Serialization.Xml.Tests.csproj +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/System.Runtime.Serialization.Xml.Tests.csproj @@ -2,9 +2,13 @@ $(NetCoreAppCurrent) + + + + - +