diff --git a/Datadog.Trace.Build.g.sln b/Datadog.Trace.Build.g.sln index f705ab4f9040..438b3aaffbca 100644 --- a/Datadog.Trace.Build.g.sln +++ b/Datadog.Trace.Build.g.sln @@ -175,6 +175,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks.Trace", "tracer\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Trace.DuckTyping.Tests", "tracer\test\Datadog.Trace.DuckTyping.Tests\Datadog.Trace.DuckTyping.Tests.csproj", "{91E50134-0E55-4D22-B180-6967174FCE0B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Trace.DuckTyping.Tests.Fixtures.Shared", "tracer\test\Datadog.Trace.DuckTyping.Tests.Fixtures\Shared\Datadog.Trace.DuckTyping.Tests.Fixtures.Shared.csproj", "{AF81B51B-C799-468D-9488-2D30C7A435B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Trace.DuckTyping.Tests.Fixtures.Target", "tracer\test\Datadog.Trace.DuckTyping.Tests.Fixtures\Target\Datadog.Trace.DuckTyping.Tests.Fixtures.Target.csproj", "{DE30EF39-4373-4485-8DC2-8CC08233CEA1}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallTargetNativeTest", "tracer\test\test-applications\instrumentation\CallTargetNativeTest\CallTargetNativeTest.csproj", "{021EFBA6-C4BA-4DE5-BF3F-C263EE9E20DB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.AspNet472.LoaderOptimizationRegKey", "tracer\test\test-applications\aspnet\Samples.AspNet472.LoaderOptimizationRegKey\Samples.AspNet472.LoaderOptimizationRegKey.csproj", "{BBB60B0F-BF01-4499-936A-4A299A9ACFD4}" @@ -369,6 +373,14 @@ Global {91E50134-0E55-4D22-B180-6967174FCE0B}.Debug|Any CPU.Build.0 = Debug|Any CPU {91E50134-0E55-4D22-B180-6967174FCE0B}.Release|Any CPU.ActiveCfg = Release|Any CPU {91E50134-0E55-4D22-B180-6967174FCE0B}.Release|Any CPU.Build.0 = Release|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Release|Any CPU.Build.0 = Release|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Release|Any CPU.Build.0 = Release|Any CPU {021EFBA6-C4BA-4DE5-BF3F-C263EE9E20DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {021EFBA6-C4BA-4DE5-BF3F-C263EE9E20DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {021EFBA6-C4BA-4DE5-BF3F-C263EE9E20DB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -602,6 +614,8 @@ Global {C00D8070-A38A-4267-9730-E9985CAE77DF} = {9E5F0022-0A50-40BF-AC6A-C3078585ECAB} {B8D132F6-43E3-44D9-902C-78051DBAD01C} = {E5439139-6F94-44FA-9590-C32FCC1C7A93} {91E50134-0E55-4D22-B180-6967174FCE0B} = {8CEC2042-F11C-49F5-A674-2355793B600A} + {AF81B51B-C799-468D-9488-2D30C7A435B6} = {8CEC2042-F11C-49F5-A674-2355793B600A} + {DE30EF39-4373-4485-8DC2-8CC08233CEA1} = {8CEC2042-F11C-49F5-A674-2355793B600A} {021EFBA6-C4BA-4DE5-BF3F-C263EE9E20DB} = {933F1D4B-1216-4BC1-956E-8C30818BAA0F} {BBB60B0F-BF01-4499-936A-4A299A9ACFD4} = {AFA0AB23-64F0-4AC1-9050-6CE8FE06F580} {78004AA7-26DD-44DB-A2C7-C287A5BBE5D6} = {A0C5FBBB-CFB2-4FB9-B8F0-55676E9DCF06} diff --git a/Datadog.Trace.sln b/Datadog.Trace.sln index 5582c2937e50..8dd1047bbd81 100644 --- a/Datadog.Trace.sln +++ b/Datadog.Trace.sln @@ -227,6 +227,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dependency-libs", "dependen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Trace.DuckTyping.Tests", "tracer\test\Datadog.Trace.DuckTyping.Tests\Datadog.Trace.DuckTyping.Tests.csproj", "{91E50134-0E55-4D22-B180-6967174FCE0B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Trace.DuckTyping.Tests.Fixtures.Shared", "tracer\test\Datadog.Trace.DuckTyping.Tests.Fixtures\Shared\Datadog.Trace.DuckTyping.Tests.Fixtures.Shared.csproj", "{AF81B51B-C799-468D-9488-2D30C7A435B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Trace.DuckTyping.Tests.Fixtures.Target", "tracer\test\Datadog.Trace.DuckTyping.Tests.Fixtures\Target\Datadog.Trace.DuckTyping.Tests.Fixtures.Target.csproj", "{DE30EF39-4373-4485-8DC2-8CC08233CEA1}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{A0C5FBBB-CFB2-4FB9-B8F0-55676E9DCF06}" ProjectSection(SolutionItems) = preProject tracer\build\PackageVersionsGeneratorDefinitions.json = tracer\build\PackageVersionsGeneratorDefinitions.json @@ -871,6 +875,14 @@ Global {91E50134-0E55-4D22-B180-6967174FCE0B}.Debug|Any CPU.Build.0 = Debug|Any CPU {91E50134-0E55-4D22-B180-6967174FCE0B}.Release|Any CPU.ActiveCfg = Release|Any CPU {91E50134-0E55-4D22-B180-6967174FCE0B}.Release|Any CPU.Build.0 = Release|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF81B51B-C799-468D-9488-2D30C7A435B6}.Release|Any CPU.Build.0 = Release|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE30EF39-4373-4485-8DC2-8CC08233CEA1}.Release|Any CPU.Build.0 = Release|Any CPU {D79491F0-CA92-439B-98CE-7AF9F57EBEB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D79491F0-CA92-439B-98CE-7AF9F57EBEB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {D79491F0-CA92-439B-98CE-7AF9F57EBEB0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1661,6 +1673,8 @@ Global {AFA0AB23-64F0-4AC1-9050-6CE8FE06F580} = {9518425A-36A5-4B8F-B0B8-6137DB88441D} {8683D82A-2BBE-4199-9C36-C59F48804F90} = {BAF8F246-3645-42AD-B1D0-0F7EAFBAB34A} {91E50134-0E55-4D22-B180-6967174FCE0B} = {8CEC2042-F11C-49F5-A674-2355793B600A} + {AF81B51B-C799-468D-9488-2D30C7A435B6} = {8CEC2042-F11C-49F5-A674-2355793B600A} + {DE30EF39-4373-4485-8DC2-8CC08233CEA1} = {8CEC2042-F11C-49F5-A674-2355793B600A} {CC53E5C5-9D3E-4AD9-A9CA-D2190463EB5B} = {A0C5FBBB-CFB2-4FB9-B8F0-55676E9DCF06} {E5439139-6F94-44FA-9590-C32FCC1C7A93} = {8CEC2042-F11C-49F5-A674-2355793B600A} {D79491F0-CA92-439B-98CE-7AF9F57EBEB0} = {498A300E-D036-49B7-A43D-821D1CAF11A5} diff --git a/tracer/build/_build/Build.Steps.cs b/tracer/build/_build/Build.Steps.cs index ce8ce52e31e2..9dd04a626a9c 100644 --- a/tracer/build/_build/Build.Steps.cs +++ b/tracer/build/_build/Build.Steps.cs @@ -1427,6 +1427,8 @@ void PrepareMonitoringHomeLinuxForPackaging(AbsolutePath assetsDirectory, string .Executes(() => { //we need to build in this exact order + DotnetBuild(TracerDirectory.GlobFiles("test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/*.csproj")); + DotnetBuild(TracerDirectory.GlobFiles("test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/*.csproj")); DotnetBuild(TracerDirectory.GlobFiles("test/**/*TestHelpers.csproj")); DotnetBuild(TracerDirectory.GlobFiles("test/**/*TestHelpers.AutoInstrumentation.csproj")); }); diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml index f7ee6a2f3b32..8809ff7afd33 100644 --- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml +++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml @@ -1107,6 +1107,7 @@ + diff --git a/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs b/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs index e88b2515e4f6..1b9335e9e261 100644 --- a/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs +++ b/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs @@ -12,6 +12,9 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; +#if NETCOREAPP3_1 || NET6_0_OR_GREATER +using System.Runtime.Loader; +#endif // ReSharper disable InconsistentNaming namespace Datadog.Trace.DuckTyping @@ -40,6 +43,18 @@ public static partial class DuckType private static readonly MethodInfo? _methodBuilderGetToken; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private static readonly ConstructorInfo? _ignoresAccessChecksToAttributeCtor; +#if NETSTANDARD2_0 || NETCOREAPP3_1 + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly MethodInfo? _defineDynamicAssemblyMethodInfo; +#endif +#if NETSTANDARD2_0 + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly MethodInfo? _getAssemblyLoadContextMethodInfo; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly object? _duckTypeAssemblyLoadContext; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private static readonly Dictionary AssembliesInDuckTypeLoadContext; +#endif [DebuggerBrowsable(DebuggerBrowsableState.Never)] private static long _assemblyCount; @@ -59,6 +74,15 @@ static DuckType() _methodBuilderGetToken = typeof(MethodBuilder).GetMethod("GetToken", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) ?? typeof(MethodBuilder).GetProperty("MetadataToken", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)?.GetMethod; _ignoresAccessChecksToAttributeCtor = typeof(IgnoresAccessChecksToAttribute).GetConstructor(new[] { typeof(string) }); +#if NETSTANDARD2_0 || NETCOREAPP3_1 + _defineDynamicAssemblyMethodInfo = typeof(AssemblyBuilder).GetMethod(nameof(AssemblyBuilder.DefineDynamicAssembly), new[] { typeof(AssemblyName), typeof(AssemblyBuilderAccess) }); +#endif +#if NETSTANDARD2_0 + var assemblyLoadContextType = Type.GetType("System.Runtime.Loader.AssemblyLoadContext, System.Runtime.Loader", throwOnError: false); + _getAssemblyLoadContextMethodInfo = assemblyLoadContextType?.GetMethod("GetLoadContext", BindingFlags.Public | BindingFlags.Static, binder: null, [typeof(Assembly)], modifiers: null); + _duckTypeAssemblyLoadContext = _getAssemblyLoadContextMethodInfo?.Invoke(obj: null, [typeof(DuckType).Assembly]); + AssembliesInDuckTypeLoadContext = new(); +#endif _assemblyCount = 0; _typeCount = 0; @@ -139,6 +163,21 @@ private static ConstructorInfo IgnoresAccessChecksToAttributeCtor } } +#if NETSTANDARD2_0 || NETCOREAPP3_1 + private static MethodInfo DefineDynamicAssemblyMethodInfo + { + get + { + if (_defineDynamicAssemblyMethodInfo is null) + { + DuckTypeException.Throw($"{nameof(AssemblyBuilder)}.{nameof(AssemblyBuilder.DefineDynamicAssembly)}() cannot be found."); + } + + return _defineDynamicAssemblyMethodInfo; + } + } +#endif + /// /// Gets the ModuleBuilder instance from a target type. (.NET Framework / Non AssemblyLoadContext version) /// @@ -180,9 +219,81 @@ static ModuleBuilder CreateModuleBuilder(string name, Assembly targetAssembly) { var assemblyName = new AssemblyName(name + $"_{++_assemblyCount}"); assemblyName.Version = targetAssembly.GetName().Version; + +#if NET6_0_OR_GREATER + AssemblyBuilder assemblyBuilder; + if (IsAssemblyInCurrentLoadContext(targetAssembly)) + { + assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + } + else + { + using var contextualReflectionScope = AssemblyLoadContext.EnterContextualReflection(targetAssembly); + assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + } +#elif NETSTANDARD2_0 || NETCOREAPP3_1 + var assemblyBuilder = IsAssemblyInCurrentLoadContext(targetAssembly) + ? AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run) + : DefineDynamicAssemblyInTargetLoadContext(assemblyName, targetAssembly.ManifestModule); +#else var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); +#endif return assemblyBuilder.DefineDynamicModule("MainModule"); } + +#if NETCOREAPP3_1 || NET6_0_OR_GREATER + static bool IsAssemblyInCurrentLoadContext(Assembly targetAssembly) + { +#if NET6_0_OR_GREATER + var currentLoadContext = AssemblyLoadContext.CurrentContextualReflectionContext + ?? AssemblyLoadContext.GetLoadContext(typeof(DuckType).Assembly); +#else + var currentLoadContext = AssemblyLoadContext.GetLoadContext(typeof(DuckType).Assembly); +#endif + return ReferenceEquals(AssemblyLoadContext.GetLoadContext(targetAssembly), currentLoadContext); + } +#elif NETSTANDARD2_0 + static bool IsAssemblyInCurrentLoadContext(Assembly targetAssembly) + { + // AssemblyLoadContext is not part of the .NET Standard 2.0 contract. Resolve it at runtime + // so the fast path does not add a System.Runtime.Loader dependency to Datadog.Trace. + if (_getAssemblyLoadContextMethodInfo is null) + { + return true; + } + + if (!AssembliesInDuckTypeLoadContext.TryGetValue(targetAssembly, out var isInDuckTypeLoadContext)) + { + var targetAssemblyLoadContext = _getAssemblyLoadContextMethodInfo.Invoke(obj: null, [targetAssembly]); + isInDuckTypeLoadContext = ReferenceEquals(targetAssemblyLoadContext, _duckTypeAssemblyLoadContext); + AssembliesInDuckTypeLoadContext.Add(targetAssembly, isInDuckTypeLoadContext); + } + + return isInDuckTypeLoadContext; + } +#endif + +#if NETSTANDARD2_0 || NETCOREAPP3_1 + static AssemblyBuilder DefineDynamicAssemblyInTargetLoadContext(AssemblyName assemblyName, Module targetModule) + { + // Before .NET 6, DefineDynamicAssembly does not honor contextual reflection. Associating the + // call site with the target module makes the runtime create the proxy in the target load context. + var createAssemblyMethod = new DynamicMethod( + "CreateDuckTypeAssembly", + typeof(AssemblyBuilder), + [typeof(AssemblyName)], + targetModule, + skipVisibility: false); + var il = createAssemblyMethod.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4, (int)AssemblyBuilderAccess.Run); + il.Emit(OpCodes.Call, DefineDynamicAssemblyMethodInfo); + il.Emit(OpCodes.Ret); + + var createAssembly = (Func)createAssemblyMethod.CreateDelegate(typeof(Func)); + return createAssembly(assemblyName); + } +#endif } /// diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Directory.Build.props b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Directory.Build.props new file mode 100644 index 000000000000..c13ea1ed0a66 --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Directory.Build.props @@ -0,0 +1,3 @@ + + + diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/Datadog.Trace.DuckTyping.Tests.Fixtures.Shared.csproj b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/Datadog.Trace.DuckTyping.Tests.Fixtures.Shared.csproj new file mode 100644 index 000000000000..e62ba593a204 --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/Datadog.Trace.DuckTyping.Tests.Fixtures.Shared.csproj @@ -0,0 +1,9 @@ + + + + netstandard2.0 + false + enable + + + diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs new file mode 100644 index 000000000000..f1d217a70315 --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs @@ -0,0 +1,13 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +namespace Datadog.Trace.DuckTyping.Tests.Fixtures.Shared; + +public sealed class FieldValue +{ + public const int ExpectedValue = 42; + + public int Value => ExpectedValue; +} diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/Datadog.Trace.DuckTyping.Tests.Fixtures.Target.csproj b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/Datadog.Trace.DuckTyping.Tests.Fixtures.Target.csproj new file mode 100644 index 000000000000..65a5d262232f --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/Datadog.Trace.DuckTyping.Tests.Fixtures.Target.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + false + enable + + + + + + + diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/DuckTypingTarget.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/DuckTypingTarget.cs new file mode 100644 index 000000000000..f194821034e4 --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/DuckTypingTarget.cs @@ -0,0 +1,15 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +using Datadog.Trace.DuckTyping.Tests.Fixtures.Shared; + +namespace Datadog.Trace.DuckTyping.Tests.Fixtures.Target; + +public sealed class DuckTypingTarget +{ + private readonly FieldValue _field = new(); + + public object FieldValue => _field; +} diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs new file mode 100644 index 000000000000..57db118d5acc --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs @@ -0,0 +1,132 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#if NETCOREAPP2_1_OR_GREATER + +#nullable enable + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using Datadog.Trace.DuckTyping.Tests.Fixtures.Shared; +using FluentAssertions; +using Xunit; + +namespace Datadog.Trace.DuckTyping.Tests; + +public class AssemblyLoadContextTests +{ + private const string SharedAssemblyName = "Datadog.Trace.DuckTyping.Tests.Fixtures.Shared"; + private const string TargetAssemblyName = "Datadog.Trace.DuckTyping.Tests.Fixtures.Target"; + private const string TargetTypeName = "Datadog.Trace.DuckTyping.Tests.Fixtures.Target.DuckTypingTarget"; + + internal interface ITargetProxy + { + [DuckField(Name = "_field")] + IFieldValue? Field { get; } + } + + internal interface IFieldValue + { + int Value { get; } + } + + [Fact] + public void DuckProxyInDefaultAssemblyLoadContext() + { + var proxy = new FieldValue().DuckCast(); + + proxy.Value.Should().Be(FieldValue.ExpectedValue); + AssemblyLoadContext.GetLoadContext(proxy.GetType().Assembly).Should().BeSameAs(AssemblyLoadContext.Default); + } + + [Fact] + public void DuckFieldAcrossAssemblyLoadContexts() + { + var defaultContext = AssemblyLoadContext.Default; + var targetContext = new TestAssemblyLoadContext(); + + var defaultSharedAssembly = typeof(FieldValue).Assembly; + AssemblyLoadContext.GetLoadContext(typeof(DuckType).Assembly).Should().BeSameAs(defaultContext); + AssemblyLoadContext.GetLoadContext(typeof(ITargetProxy).Assembly).Should().BeSameAs(defaultContext); + AssemblyLoadContext.GetLoadContext(defaultSharedAssembly).Should().BeSameAs(defaultContext); + AppDomain.CurrentDomain.GetAssemblies().Should().NotContain( + assembly => AssemblyLoadContext.GetLoadContext(assembly) == defaultContext && assembly.GetName().Name == TargetAssemblyName); + + // The target field and the generated proxy resolve the same dependency identity in different contexts, + // matching Azure Functions where Event Grid has a separate Azure.Core copy in its load context. + var targetType = LoadTargetType(targetContext, out var targetSharedAssembly); + targetSharedAssembly.FullName.Should().Be(defaultSharedAssembly.FullName); + targetSharedAssembly.Should().NotBeSameAs(defaultSharedAssembly); + AssemblyLoadContext.GetLoadContext(targetSharedAssembly).Should().BeSameAs(targetContext); + var targetField = targetType.GetField("_field", BindingFlags.Instance | BindingFlags.NonPublic)!; + targetField.FieldType.Assembly.Should().BeSameAs(targetSharedAssembly); + var target = Activator.CreateInstance(targetType)!; + + var proxy = target.DuckCast(); + var field = proxy.Field; + field.Should().NotBeNull(); + field!.Value.Should().Be(FieldValue.ExpectedValue); + + var proxyType = proxy.GetType(); + proxyType.Assembly.IsDynamic.Should().BeTrue(); + AssemblyLoadContext.GetLoadContext(proxyType.Assembly).Should().BeSameAs(targetContext); + } + + [Fact] + public void CachesProxyPerTargetAssemblyLoadContext() + { + var firstContext = new TestAssemblyLoadContext(); + var secondContext = new TestAssemblyLoadContext(); + var firstTargetType = LoadTargetType(firstContext, out _); + var secondTargetType = LoadTargetType(secondContext, out _); + + firstTargetType.FullName.Should().Be(secondTargetType.FullName); + firstTargetType.Assembly.FullName.Should().Be(secondTargetType.Assembly.FullName); + firstTargetType.Should().NotBeSameAs(secondTargetType); + + var firstTarget = Activator.CreateInstance(firstTargetType)!; + var secondTarget = Activator.CreateInstance(secondTargetType)!; + var firstProxy = firstTarget.DuckCast(); + var cachedFirstProxy = firstTarget.DuckCast(); + var secondProxy = secondTarget.DuckCast(); + + var firstField = firstProxy.Field; + var secondField = secondProxy.Field; + firstField.Should().NotBeNull(); + firstField!.Value.Should().Be(FieldValue.ExpectedValue); + secondField.Should().NotBeNull(); + secondField!.Value.Should().Be(FieldValue.ExpectedValue); + + var firstProxyType = firstProxy.GetType(); + var cachedFirstProxyType = cachedFirstProxy.GetType(); + var secondProxyType = secondProxy.GetType(); + cachedFirstProxyType.Should().BeSameAs(firstProxyType); + secondProxyType.Should().NotBeSameAs(firstProxyType); + AssemblyLoadContext.GetLoadContext(firstProxyType.Assembly).Should().BeSameAs(firstContext); + AssemblyLoadContext.GetLoadContext(secondProxyType.Assembly).Should().BeSameAs(secondContext); + } + + private static Type LoadTargetType(AssemblyLoadContext targetContext, out Assembly targetSharedAssembly) + { + var assemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + var fixtureDirectory = Path.Combine(assemblyDirectory, "AssemblyLoadContextFixtures"); + var sharedAssemblyPath = Path.Combine(fixtureDirectory, SharedAssemblyName + ".dll"); + var targetAssemblyPath = Path.Combine(fixtureDirectory, TargetAssemblyName + ".dll"); + + targetSharedAssembly = targetContext.LoadFromAssemblyPath(sharedAssemblyPath); + var targetAssembly = targetContext.LoadFromAssemblyPath(targetAssemblyPath); + AssemblyLoadContext.GetLoadContext(targetAssembly).Should().BeSameAs(targetContext); + return targetAssembly.GetType(TargetTypeName, throwOnError: true)!; + } + + private sealed class TestAssemblyLoadContext : AssemblyLoadContext + { + protected override Assembly? Load(AssemblyName assemblyName) => null; + } +} + +#endif diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/Datadog.Trace.DuckTyping.Tests.csproj b/tracer/test/Datadog.Trace.DuckTyping.Tests/Datadog.Trace.DuckTyping.Tests.csproj index 4d0b91086b2b..f2426473d096 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/Datadog.Trace.DuckTyping.Tests.csproj +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/Datadog.Trace.DuckTyping.Tests.csproj @@ -9,6 +9,15 @@ + + $(DefineConstants);INTERFACE_DEFAULTS diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs index 7e5111d01f92..619465008427 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs @@ -54,9 +54,9 @@ public void GetAssemblyTest() #if NETFRAMEWORK asmDuckTypes.Should().Be(1510); #elif NETCOREAPP2_1 - asmDuckTypes.Should().Be(1513); + asmDuckTypes.Should().Be(1520); #else - asmDuckTypes.Should().Be(1514); + asmDuckTypes.Should().Be(1521); #endif } else @@ -65,9 +65,9 @@ public void GetAssemblyTest() #if NETFRAMEWORK asmDuckTypes.Should().BeGreaterThan(1510); #elif NETCOREAPP2_1 - asmDuckTypes.Should().BeGreaterThan(1513); + asmDuckTypes.Should().BeGreaterThan(1520); #else - asmDuckTypes.Should().BeGreaterThan(1514); + asmDuckTypes.Should().BeGreaterThan(1521); #endif } }