Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
<Compile Include="$(CommonPath)System\CodeDom\CodeTypeReference.cs" />
<Compile Include="$(CommonPath)System\CodeDom\CodeTypeReferenceCollection.cs" />
<Compile Include="$(CommonPath)System\CodeDom\CodeObject.cs" />
<Compile Include="System\Runtime\Serialization\ContextAware.cs" />
</ItemGroup>

<ItemGroup>
Expand All @@ -163,6 +164,7 @@
<Reference Include="System.Reflection.Primitives" />
<Reference Include="System.Runtime" />
<Reference Include="System.Runtime.Intrinsics" />
<Reference Include="System.Runtime.Loader" />
<Reference Include="System.Runtime.Serialization.Formatters" />
<Reference Include="System.Runtime.Serialization.Primitives" />
<Reference Include="System.Text.Encoding.Extensions" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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<DataContract>? weak)[] _contracts;
private ConditionalWeakTable<Type, DataContract> _keepAlive;

public int Length => _contracts.Length;

public ContextAwareDataContractIndex(int size)
{
_contracts = new (DataContract?, WeakReference<DataContract>?)[size];
_keepAlive = new ConditionalWeakTable<Type, DataContract>();
}

public DataContract? GetItem(int index) => _contracts[index].strong ?? (_contracts[index].weak?.TryGetTarget(out DataContract? ret) == true ? ret : null);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only a style thing, but did you consider having providing an indexer property instead of switching to Get and Set methods?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort of an evolution of the context-aware side of the design that got away from the original usage pattern. An indexer would fit the original code. But the nullable notation of the original code was incorrect, even if our code was dilligent about null-checking. Maybe I've still got Monday brain, but I don't know of a way to make the getter nullable and the setter non-nullable... notation-wise anyway.

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>(dataContract);
_keepAlive.Add(dataContract.UnderlyingType, dataContract);
}
}

public void Resize(int newSize)
{
Array.Resize<(DataContract?, WeakReference<DataContract>?)>(ref _contracts, newSize);
}
}

internal sealed class ContextAwareDictionary<TKey, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TValue>
where TKey : Type
where TValue : class?
{
private readonly ConcurrentDictionary<TKey, TValue> _fastDictionary = new();
private readonly ConditionalWeakTable<TKey, TValue> _collectibleTable = new();


internal TValue GetOrAdd(TKey t, Func<TKey, TValue> 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;
Comment thread
StephenMolloy marked this conversation as resolved.

// 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)
{
// The create delegate here could be quite expensive. ConcurrentDictionary semantics would let use
// do this without a lock and not corrupt the dictionary, but the delegate still might be called multiple
// times. So we use a lock to ensure the delegate is only called once. Keep the lock off the hot path.
if (!_fastDictionary.TryGetValue(t, out ret))
{
Comment thread
StephenMolloy marked this conversation as resolved.
lock (_fastDictionary)
{
return _fastDictionary.GetOrAdd(t, f);
}
}
}

// Collectible load contexts should use the ConditionalWeakTable so they can be unloaded
else
{
if (!_collectibleTable.TryGetValue(t, out ret))
{
lock (_collectibleTable)
{
return _collectibleTable.GetValue(t, k => f(k));
}
}
}

return ret;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,9 @@ internal virtual bool IsValidContract()
internal class DataContractCriticalHelper
{
private static readonly ConcurrentDictionary<nint, int> s_typeToIDCache = new();
private static DataContract[] s_dataContractCache = new DataContract[32];
private static readonly ContextAwareDataContractIndex s_dataContractCache = new(32);
private static int s_dataContractID;
private static readonly ConcurrentDictionary<Type, DataContract?> s_typeToBuiltInContract = new();
private static readonly ContextAwareDictionary<Type, DataContract?> s_typeToBuiltInContract = new();
private static Dictionary<XmlQualifiedName, DataContract?>? s_nameToBuiltInContract;
private static Dictionary<string, DataContract?>? s_typeNameToBuiltInContract;
private static readonly Hashtable s_namespaces = new Hashtable();
Expand Down Expand Up @@ -335,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);
Expand All @@ -351,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);
Expand All @@ -368,22 +368,22 @@ 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;
}
}
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);
}
Expand All @@ -410,7 +410,7 @@ internal static int GetId(RuntimeTypeHandle typeHandle)
Debug.Fail("DataContract cache overflow");
throw new SerializationException(SR.DataContractCacheOverflow);
}
Array.Resize<DataContract>(ref s_dataContractCache, newSize);
s_dataContractCache.Resize(newSize);
}
return nextId;
});
Expand All @@ -427,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)!;
Expand Down Expand Up @@ -499,7 +499,7 @@ private static void AssignDataContractToId(DataContract dataContract, int id)
{
lock (s_cacheLock)
{
s_dataContractCache[id] = dataContract;
s_dataContractCache.SetItem(id, dataContract);
}
}

Expand All @@ -510,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)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<object>(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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
<DefineConstants>$(DefineConstants);ReflectionOnly</DefineConstants>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Microsoft.XmlSerializer.Generator\tests\SerializableAssembly.csproj" />
<TrimmerRootAssembly Include="SerializableAssembly" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(CommonTestPath)System\Runtime\Serialization\Utils.cs" />
<Compile Include="$(TestSourceFolder)..\DataContractSerializerStressTests.cs" />
<Compile Include="$(TestSourceFolder)..\SerializationTypes.cs" />
<None Include="$(TestSourceFolder)..\SerializationTypes.cs" />
<Compile Include="$(TestSourceFolder)..\SerializationTypes.RuntimeOnly.cs" />
<Compile Include="$(TestSourceFolder)..\DataContractSerializer.cs" />
<Compile Include="$(TestSourceFolder)..\MyResolver.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
<PropertyGroup>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.XmlSerializer.Generator\tests\SerializableAssembly.csproj" />
<TrimmerRootAssembly Include="SerializableAssembly" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(TestSourceFolder)DataContractSerializerStressTests.cs" />
<Compile Include="$(TestSourceFolder)SerializationTypes.cs" />
<None Include="$(TestSourceFolder)SerializationTypes.cs" />
<Compile Include="$(TestSourceFolder)SerializationTypes.RuntimeOnly.cs" />
<Compile Include="$(TestSourceFolder)DataContractSerializer.cs" />
<Compile Include="$(TestSourceFolder)MyResolver.cs" />
Expand Down