From 4a7a45b0c4ac95664a33603c76dbdde3625c5c6b Mon Sep 17 00:00:00 2001 From: "pablo.martinezbernardo" Date: Mon, 31 Aug 2026 13:16:22 +0200 Subject: [PATCH 1/9] [DuckTyping] Reproduce cross-ALC MissingFieldException --- .../AssemblyLoadContextTests.cs | 98 +++++++++++++++++++ .../GetAssemblyTests.cs | 4 +- 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs 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..aabf062c2d4b --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs @@ -0,0 +1,98 @@ +// +// 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 NETCOREAPP3_0_OR_GREATER + +#nullable enable + +using System; +using System.IO; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.Loader; +using FluentAssertions; +using Xunit; + +namespace Datadog.Trace.DuckTyping.Tests; + +public class AssemblyLoadContextTests +{ + [Fact] + public void DuckFieldThrowsMissingFieldExceptionAcrossAssemblyLoadContexts() + { + var testAssemblyPath = Assembly.GetExecutingAssembly().Location; + var targetContext = new AssemblyLoadContext("DuckTypingTarget"); + var proxyContext = new ProxyLoadContext(testAssemblyPath); + + var target = CreateGeneratedTarget(targetContext, testAssemblyPath); + var proxyAssembly = proxyContext.LoadFromAssemblyPath(testAssemblyPath); + var proxyRunner = proxyAssembly.GetType(typeof(ProxyRunner).FullName!, throwOnError: true)!; + + var exception = Assert.Throws( + () => proxyRunner.GetMethod(nameof(ProxyRunner.AccessField))!.Invoke(null, [target])); + exception.InnerException.Should().BeOfType(); + } + + private static object CreateGeneratedTarget(AssemblyLoadContext targetContext, string testAssemblyPath) + { + var targetAssembly = targetContext.LoadFromAssemblyPath(testAssemblyPath); + var fieldType = targetAssembly.GetType(typeof(FieldValue).FullName!, throwOnError: true)!; + + // An ordinary type also present in the proxy assembly does not reproduce the mismatch. + // Contextual reflection makes this generated type belong only to targetContext. + using (AssemblyLoadContext.EnterContextualReflection(targetAssembly)) + { + var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("DuckTypingTargetAssembly"), AssemblyBuilderAccess.Run); + var type = assembly.DefineDynamicModule("MainModule").DefineType("DuckTypingTarget", TypeAttributes.Public); + type.DefineField("_field", fieldType, FieldAttributes.Private); + return Activator.CreateInstance(type.CreateType()!)!; + } + } + + public static class ProxyRunner + { + internal interface ITargetProxy + { + [DuckField(Name = "_field")] + IFieldValue? Field { get; } + } + + internal interface IFieldValue + { + } + + public static void AccessField(object target) + { + _ = target.DuckCast().Field; + } + } + + public sealed class FieldValue + { + } + + private sealed class ProxyLoadContext : AssemblyLoadContext + { + private readonly string _directory; + + public ProxyLoadContext(string testAssemblyPath) + : base("DuckTypingProxy") + { + _directory = Path.GetDirectoryName(testAssemblyPath)!; + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + if (assemblyName.Name != typeof(DuckType).Assembly.GetName().Name) + { + return null; + } + + return LoadFromAssemblyPath(Path.Combine(_directory, assemblyName.Name + ".dll")); + } + } +} + +#endif diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs index 7e5111d01f92..8139a7492061 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs @@ -56,7 +56,7 @@ public void GetAssemblyTest() #elif NETCOREAPP2_1 asmDuckTypes.Should().Be(1513); #else - asmDuckTypes.Should().Be(1514); + asmDuckTypes.Should().Be(1515); #endif } else @@ -67,7 +67,7 @@ public void GetAssemblyTest() #elif NETCOREAPP2_1 asmDuckTypes.Should().BeGreaterThan(1513); #else - asmDuckTypes.Should().BeGreaterThan(1514); + asmDuckTypes.Should().BeGreaterThan(1515); #endif } } From 1f60c9b2558b841ecbd23567da5b25648f71d6e8 Mon Sep 17 00:00:00 2001 From: "pablo.martinezbernardo" Date: Mon, 31 Aug 2026 17:24:01 +0200 Subject: [PATCH 2/9] [DuckTyping] Use compiled fixtures for cross-ALC repro --- .../Directory.Build.props | 3 ++ ...ce.DuckTyping.Tests.Fixtures.Shared.csproj | 9 +++++ .../Shared/FieldValue.cs | 10 +++++ ...ce.DuckTyping.Tests.Fixtures.Target.csproj | 13 +++++++ .../Target/DuckTypingTarget.cs | 15 +++++++ .../AssemblyLoadContextTests.cs | 39 ++++++++----------- .../Datadog.Trace.DuckTyping.Tests.csproj | 2 + 7 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Directory.Build.props create mode 100644 tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/Datadog.Trace.DuckTyping.Tests.Fixtures.Shared.csproj create mode 100644 tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs create mode 100644 tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/Datadog.Trace.DuckTyping.Tests.Fixtures.Target.csproj create mode 100644 tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/DuckTypingTarget.cs 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..6f0c695b3381 --- /dev/null +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs @@ -0,0 +1,10 @@ +// +// 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 +{ +} 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 index aabf062c2d4b..9d5ef3e126d1 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs @@ -10,7 +10,6 @@ using System; using System.IO; using System.Reflection; -using System.Reflection.Emit; using System.Runtime.Loader; using FluentAssertions; using Xunit; @@ -19,14 +18,30 @@ 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"; + [Fact] public void DuckFieldThrowsMissingFieldExceptionAcrossAssemblyLoadContexts() { var testAssemblyPath = Assembly.GetExecutingAssembly().Location; + var assemblyDirectory = Path.GetDirectoryName(testAssemblyPath)!; + var sharedAssemblyPath = Path.Combine(assemblyDirectory, SharedAssemblyName + ".dll"); + var targetAssemblyPath = Path.Combine(assemblyDirectory, TargetAssemblyName + ".dll"); var targetContext = new AssemblyLoadContext("DuckTypingTarget"); var proxyContext = new ProxyLoadContext(testAssemblyPath); - var target = CreateGeneratedTarget(targetContext, testAssemblyPath); + // The target field and the generated proxy resolve the same dependency identity in different contexts. + var targetSharedAssembly = targetContext.LoadFromAssemblyPath(sharedAssemblyPath); + var proxySharedAssembly = proxyContext.LoadFromAssemblyPath(sharedAssemblyPath); + targetSharedAssembly.FullName.Should().Be(proxySharedAssembly.FullName); + targetSharedAssembly.Should().NotBeSameAs(proxySharedAssembly); + + var targetAssembly = targetContext.LoadFromAssemblyPath(targetAssemblyPath); + var targetType = targetAssembly.GetType(TargetTypeName, throwOnError: true)!; + var target = Activator.CreateInstance(targetType)!; + var proxyAssembly = proxyContext.LoadFromAssemblyPath(testAssemblyPath); var proxyRunner = proxyAssembly.GetType(typeof(ProxyRunner).FullName!, throwOnError: true)!; @@ -35,22 +50,6 @@ public void DuckFieldThrowsMissingFieldExceptionAcrossAssemblyLoadContexts() exception.InnerException.Should().BeOfType(); } - private static object CreateGeneratedTarget(AssemblyLoadContext targetContext, string testAssemblyPath) - { - var targetAssembly = targetContext.LoadFromAssemblyPath(testAssemblyPath); - var fieldType = targetAssembly.GetType(typeof(FieldValue).FullName!, throwOnError: true)!; - - // An ordinary type also present in the proxy assembly does not reproduce the mismatch. - // Contextual reflection makes this generated type belong only to targetContext. - using (AssemblyLoadContext.EnterContextualReflection(targetAssembly)) - { - var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("DuckTypingTargetAssembly"), AssemblyBuilderAccess.Run); - var type = assembly.DefineDynamicModule("MainModule").DefineType("DuckTypingTarget", TypeAttributes.Public); - type.DefineField("_field", fieldType, FieldAttributes.Private); - return Activator.CreateInstance(type.CreateType()!)!; - } - } - public static class ProxyRunner { internal interface ITargetProxy @@ -69,10 +68,6 @@ public static void AccessField(object target) } } - public sealed class FieldValue - { - } - private sealed class ProxyLoadContext : AssemblyLoadContext { private readonly string _directory; 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..27e09b6d142b 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,8 @@ + + $(DefineConstants);INTERFACE_DEFAULTS From e14c3436441902a3b466ee9105d9bed711ab2f6c Mon Sep 17 00:00:00 2001 From: "pablo.martinezbernardo" Date: Tue, 1 Sep 2026 09:38:32 +0200 Subject: [PATCH 3/9] [DuckTyping] Build ALC fixtures for unit tests --- tracer/build/_build/Build.Steps.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tracer/build/_build/Build.Steps.cs b/tracer/build/_build/Build.Steps.cs index ce8ce52e31e2..bb31aebca7db 100644 --- a/tracer/build/_build/Build.Steps.cs +++ b/tracer/build/_build/Build.Steps.cs @@ -1442,6 +1442,7 @@ void PrepareMonitoringHomeLinuxForPackaging(AbsolutePath assetsDirectory, string .DependsOn(CompileManagedLoader) .Executes(() => { + DotnetBuild(TracerDirectory.GlobFiles("test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/*.csproj"), noDependencies: false); DotnetBuild(TracerDirectory.GlobFiles("test/**/*.Tests.csproj")); }); From ebf83071915f8ea72866ffc5bbb1813edd8241c6 Mon Sep 17 00:00:00 2001 From: "pablo.martinezbernardo" Date: Tue, 1 Sep 2026 11:44:23 +0200 Subject: [PATCH 4/9] [DuckTyping] Add ALC fixtures to build solution --- Datadog.Trace.Build.g.sln | 14 ++++++++++++++ Datadog.Trace.sln | 14 ++++++++++++++ 2 files changed, 28 insertions(+) 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} From 01a78e169cabbcc862ba796a76cdc8f69e914dbd Mon Sep 17 00:00:00 2001 From: "pablo.martinezbernardo" Date: Tue, 1 Sep 2026 12:22:27 +0200 Subject: [PATCH 5/9] [DuckTyping] Match ALC repro to production topology --- .../AssemblyLoadContextTests.cs | 59 ++++++++----------- .../Datadog.Trace.DuckTyping.Tests.csproj | 13 +++- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs index 9d5ef3e126d1..de9278776de8 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs @@ -11,6 +11,7 @@ using System.IO; using System.Reflection; using System.Runtime.Loader; +using Datadog.Trace.DuckTyping.Tests.Fixtures.Shared; using FluentAssertions; using Xunit; @@ -25,29 +26,38 @@ public class AssemblyLoadContextTests [Fact] public void DuckFieldThrowsMissingFieldExceptionAcrossAssemblyLoadContexts() { - var testAssemblyPath = Assembly.GetExecutingAssembly().Location; - var assemblyDirectory = Path.GetDirectoryName(testAssemblyPath)!; - var sharedAssemblyPath = Path.Combine(assemblyDirectory, SharedAssemblyName + ".dll"); - var targetAssemblyPath = Path.Combine(assemblyDirectory, TargetAssemblyName + ".dll"); + var defaultContext = AssemblyLoadContext.Default; + 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"); var targetContext = new AssemblyLoadContext("DuckTypingTarget"); - var proxyContext = new ProxyLoadContext(testAssemblyPath); - // The target field and the generated proxy resolve the same dependency identity in different contexts. + var defaultSharedAssembly = typeof(FieldValue).Assembly; + AssemblyLoadContext.GetLoadContext(typeof(DuckType).Assembly).Should().BeSameAs(defaultContext); + AssemblyLoadContext.GetLoadContext(typeof(ProxyRunner).Assembly).Should().BeSameAs(defaultContext); + AssemblyLoadContext.GetLoadContext(defaultSharedAssembly).Should().BeSameAs(defaultContext); + defaultContext.Assemblies.Should().NotContain(assembly => 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 targetSharedAssembly = targetContext.LoadFromAssemblyPath(sharedAssemblyPath); - var proxySharedAssembly = proxyContext.LoadFromAssemblyPath(sharedAssemblyPath); - targetSharedAssembly.FullName.Should().Be(proxySharedAssembly.FullName); - targetSharedAssembly.Should().NotBeSameAs(proxySharedAssembly); + targetSharedAssembly.FullName.Should().Be(defaultSharedAssembly.FullName); + targetSharedAssembly.Should().NotBeSameAs(defaultSharedAssembly); + AssemblyLoadContext.GetLoadContext(targetSharedAssembly).Should().BeSameAs(targetContext); var targetAssembly = targetContext.LoadFromAssemblyPath(targetAssemblyPath); + AssemblyLoadContext.GetLoadContext(targetAssembly).Should().BeSameAs(targetContext); var targetType = targetAssembly.GetType(TargetTypeName, throwOnError: true)!; + var targetField = targetType.GetField("_field", BindingFlags.Instance | BindingFlags.NonPublic)!; + targetField.FieldType.Assembly.Should().BeSameAs(targetSharedAssembly); var target = Activator.CreateInstance(targetType)!; - var proxyAssembly = proxyContext.LoadFromAssemblyPath(testAssemblyPath); - var proxyRunner = proxyAssembly.GetType(typeof(ProxyRunner).FullName!, throwOnError: true)!; + Assert.Throws(() => ProxyRunner.AccessField(target)); - var exception = Assert.Throws( - () => proxyRunner.GetMethod(nameof(ProxyRunner.AccessField))!.Invoke(null, [target])); - exception.InnerException.Should().BeOfType(); + var proxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), targetType).ProxyType!; + proxyType.Assembly.IsDynamic.Should().BeTrue(); + AssemblyLoadContext.GetLoadContext(proxyType.Assembly).Should().BeSameAs(defaultContext); } public static class ProxyRunner @@ -67,27 +77,6 @@ public static void AccessField(object target) _ = target.DuckCast().Field; } } - - private sealed class ProxyLoadContext : AssemblyLoadContext - { - private readonly string _directory; - - public ProxyLoadContext(string testAssemblyPath) - : base("DuckTypingProxy") - { - _directory = Path.GetDirectoryName(testAssemblyPath)!; - } - - protected override Assembly? Load(AssemblyName assemblyName) - { - if (assemblyName.Name != typeof(DuckType).Assembly.GetName().Name) - { - return null; - } - - return LoadFromAssemblyPath(Path.Combine(_directory, assemblyName.Name + ".dll")); - } - } } #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 27e09b6d142b..1805ffb83923 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 @@ -10,7 +10,7 @@ - + $(DefineConstants);INTERFACE_DEFAULTS @@ -26,4 +26,15 @@ + + + + + + + From f275a077728283822d9ba2b61871f7d20ab6ba55 Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Wed, 2 Sep 2026 18:32:59 +0200 Subject: [PATCH 6/9] [DuckTyping] Generate proxies in target load context --- .../DuckTyping/DuckType.Statics.cs | 55 ++++++++++++++++ .../AssemblyLoadContextTests.cs | 66 ++++++++++++++----- .../GetAssemblyTests.cs | 8 +-- 3 files changed, 110 insertions(+), 19 deletions(-) diff --git a/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs b/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs index e88b2515e4f6..e9c42fcdb1f2 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 NET6_0_OR_GREATER +using System.Runtime.Loader; +#endif // ReSharper disable InconsistentNaming namespace Datadog.Trace.DuckTyping @@ -40,6 +43,10 @@ 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 [DebuggerBrowsable(DebuggerBrowsableState.Never)] private static long _assemblyCount; @@ -59,6 +66,9 @@ 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 _assemblyCount = 0; _typeCount = 0; @@ -139,6 +149,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 +205,39 @@ static ModuleBuilder CreateModuleBuilder(string name, Assembly targetAssembly) { var assemblyName = new AssemblyName(name + $"_{++_assemblyCount}"); assemblyName.Version = targetAssembly.GetName().Version; + +#if NET6_0_OR_GREATER + using var contextualReflectionScope = AssemblyLoadContext.EnterContextualReflection(targetAssembly); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); +#elif NETSTANDARD2_0 || NETCOREAPP3_1 + var assemblyBuilder = DefineDynamicAssemblyInTargetLoadContext(assemblyName, targetAssembly.ManifestModule); +#else var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); +#endif return assemblyBuilder.DefineDynamicModule("MainModule"); } + +#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/AssemblyLoadContextTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs index de9278776de8..547e9f29bcaa 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // -#if NETCOREAPP3_0_OR_GREATER +#if NETCOREAPP2_1_OR_GREATER #nullable enable @@ -24,40 +24,71 @@ public class AssemblyLoadContextTests private const string TargetTypeName = "Datadog.Trace.DuckTyping.Tests.Fixtures.Target.DuckTypingTarget"; [Fact] - public void DuckFieldThrowsMissingFieldExceptionAcrossAssemblyLoadContexts() + public void DuckFieldAcrossAssemblyLoadContexts() { var defaultContext = AssemblyLoadContext.Default; - 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"); - var targetContext = new AssemblyLoadContext("DuckTypingTarget"); + var targetContext = new TestAssemblyLoadContext(); var defaultSharedAssembly = typeof(FieldValue).Assembly; AssemblyLoadContext.GetLoadContext(typeof(DuckType).Assembly).Should().BeSameAs(defaultContext); AssemblyLoadContext.GetLoadContext(typeof(ProxyRunner).Assembly).Should().BeSameAs(defaultContext); AssemblyLoadContext.GetLoadContext(defaultSharedAssembly).Should().BeSameAs(defaultContext); - defaultContext.Assemblies.Should().NotContain(assembly => assembly.GetName().Name == TargetAssemblyName); + 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 targetSharedAssembly = targetContext.LoadFromAssemblyPath(sharedAssemblyPath); + var targetType = LoadTargetType(targetContext, out var targetSharedAssembly); targetSharedAssembly.FullName.Should().Be(defaultSharedAssembly.FullName); targetSharedAssembly.Should().NotBeSameAs(defaultSharedAssembly); AssemblyLoadContext.GetLoadContext(targetSharedAssembly).Should().BeSameAs(targetContext); - - var targetAssembly = targetContext.LoadFromAssemblyPath(targetAssemblyPath); - AssemblyLoadContext.GetLoadContext(targetAssembly).Should().BeSameAs(targetContext); - var targetType = targetAssembly.GetType(TargetTypeName, throwOnError: true)!; var targetField = targetType.GetField("_field", BindingFlags.Instance | BindingFlags.NonPublic)!; targetField.FieldType.Assembly.Should().BeSameAs(targetSharedAssembly); var target = Activator.CreateInstance(targetType)!; - Assert.Throws(() => ProxyRunner.AccessField(target)); + ProxyRunner.AccessField(target); var proxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), targetType).ProxyType!; proxyType.Assembly.IsDynamic.Should().BeTrue(); - AssemblyLoadContext.GetLoadContext(proxyType.Assembly).Should().BeSameAs(defaultContext); + 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); + + ProxyRunner.AccessField(Activator.CreateInstance(firstTargetType)!); + ProxyRunner.AccessField(Activator.CreateInstance(secondTargetType)!); + + var firstProxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), firstTargetType).ProxyType!; + var cachedFirstProxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), firstTargetType).ProxyType!; + var secondProxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), secondTargetType).ProxyType!; + + 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)!; } public static class ProxyRunner @@ -77,6 +108,11 @@ public static void AccessField(object target) _ = target.DuckCast().Field; } } + + private sealed class TestAssemblyLoadContext : AssemblyLoadContext + { + protected override Assembly? Load(AssemblyName assemblyName) => null; + } } #endif diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs index 8139a7492061..839a9750351f 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(1519); #else - asmDuckTypes.Should().Be(1515); + asmDuckTypes.Should().Be(1520); #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(1519); #else - asmDuckTypes.Should().BeGreaterThan(1515); + asmDuckTypes.Should().BeGreaterThan(1520); #endif } } From d1aeae51e86661ecfbfcf317813fd9f3b5d8c2ff Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Wed, 2 Sep 2026 19:00:39 +0200 Subject: [PATCH 7/9] [DuckTyping] Update trimming descriptor --- .../src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml | 1 + 1 file changed, 1 insertion(+) 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 @@ + From 79cc7f952f126428dead54b12947a1a0448e457a Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Mon, 7 Sep 2026 16:05:25 +0200 Subject: [PATCH 8/9] [DuckTyping] Address load context review feedback --- tracer/build/_build/Build.Steps.cs | 2 +- .../DuckTyping/DuckType.Statics.cs | 64 +++++++++++++++-- .../Shared/FieldValue.cs | 3 + .../AssemblyLoadContextTests.cs | 70 +++++++++++-------- .../Datadog.Trace.DuckTyping.Tests.csproj | 22 +++--- .../GetAssemblyTests.cs | 8 +-- 6 files changed, 119 insertions(+), 50 deletions(-) diff --git a/tracer/build/_build/Build.Steps.cs b/tracer/build/_build/Build.Steps.cs index bb31aebca7db..a4cca0724159 100644 --- a/tracer/build/_build/Build.Steps.cs +++ b/tracer/build/_build/Build.Steps.cs @@ -1427,6 +1427,7 @@ void PrepareMonitoringHomeLinuxForPackaging(AbsolutePath assetsDirectory, string .Executes(() => { //we need to build in this exact order + DotnetBuild(TracerDirectory.GlobFiles("test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/*.csproj"), noDependencies: false); DotnetBuild(TracerDirectory.GlobFiles("test/**/*TestHelpers.csproj")); DotnetBuild(TracerDirectory.GlobFiles("test/**/*TestHelpers.AutoInstrumentation.csproj")); }); @@ -1442,7 +1443,6 @@ void PrepareMonitoringHomeLinuxForPackaging(AbsolutePath assetsDirectory, string .DependsOn(CompileManagedLoader) .Executes(() => { - DotnetBuild(TracerDirectory.GlobFiles("test/Datadog.Trace.DuckTyping.Tests.Fixtures/Target/*.csproj"), noDependencies: false); DotnetBuild(TracerDirectory.GlobFiles("test/**/*.Tests.csproj")); }); diff --git a/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs b/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs index e9c42fcdb1f2..1b9335e9e261 100644 --- a/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs +++ b/tracer/src/Datadog.Trace/DuckTyping/DuckType.Statics.cs @@ -12,7 +12,7 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; -#if NET6_0_OR_GREATER +#if NETCOREAPP3_1 || NET6_0_OR_GREATER using System.Runtime.Loader; #endif // ReSharper disable InconsistentNaming @@ -47,6 +47,14 @@ public static partial class DuckType [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; @@ -69,6 +77,12 @@ static DuckType() #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; @@ -207,16 +221,58 @@ static ModuleBuilder CreateModuleBuilder(string name, Assembly targetAssembly) assemblyName.Version = targetAssembly.GetName().Version; #if NET6_0_OR_GREATER - using var contextualReflectionScope = AssemblyLoadContext.EnterContextualReflection(targetAssembly); - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + 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 = DefineDynamicAssemblyInTargetLoadContext(assemblyName, targetAssembly.ManifestModule); + 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) { diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs index 6f0c695b3381..f1d217a70315 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests.Fixtures/Shared/FieldValue.cs @@ -7,4 +7,7 @@ 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/AssemblyLoadContextTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs index 547e9f29bcaa..57db118d5acc 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/AssemblyLoadContextTests.cs @@ -23,6 +23,26 @@ public class AssemblyLoadContextTests 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() { @@ -31,7 +51,7 @@ public void DuckFieldAcrossAssemblyLoadContexts() var defaultSharedAssembly = typeof(FieldValue).Assembly; AssemblyLoadContext.GetLoadContext(typeof(DuckType).Assembly).Should().BeSameAs(defaultContext); - AssemblyLoadContext.GetLoadContext(typeof(ProxyRunner).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); @@ -46,9 +66,12 @@ public void DuckFieldAcrossAssemblyLoadContexts() targetField.FieldType.Assembly.Should().BeSameAs(targetSharedAssembly); var target = Activator.CreateInstance(targetType)!; - ProxyRunner.AccessField(target); + var proxy = target.DuckCast(); + var field = proxy.Field; + field.Should().NotBeNull(); + field!.Value.Should().Be(FieldValue.ExpectedValue); - var proxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), targetType).ProxyType!; + var proxyType = proxy.GetType(); proxyType.Assembly.IsDynamic.Should().BeTrue(); AssemblyLoadContext.GetLoadContext(proxyType.Assembly).Should().BeSameAs(targetContext); } @@ -65,13 +88,22 @@ public void CachesProxyPerTargetAssemblyLoadContext() firstTargetType.Assembly.FullName.Should().Be(secondTargetType.Assembly.FullName); firstTargetType.Should().NotBeSameAs(secondTargetType); - ProxyRunner.AccessField(Activator.CreateInstance(firstTargetType)!); - ProxyRunner.AccessField(Activator.CreateInstance(secondTargetType)!); - - var firstProxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), firstTargetType).ProxyType!; - var cachedFirstProxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), firstTargetType).ProxyType!; - var secondProxyType = DuckType.GetOrCreateProxyType(typeof(ProxyRunner.ITargetProxy), secondTargetType).ProxyType!; - + 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); @@ -91,24 +123,6 @@ private static Type LoadTargetType(AssemblyLoadContext targetContext, out Assemb return targetAssembly.GetType(TargetTypeName, throwOnError: true)!; } - public static class ProxyRunner - { - internal interface ITargetProxy - { - [DuckField(Name = "_field")] - IFieldValue? Field { get; } - } - - internal interface IFieldValue - { - } - - public static void AccessField(object target) - { - _ = target.DuckCast().Field; - } - } - private sealed class TestAssemblyLoadContext : AssemblyLoadContext { protected override Assembly? Load(AssemblyName assemblyName) => null; 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 1805ffb83923..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,8 +9,15 @@ - - + + $(DefineConstants);INTERFACE_DEFAULTS @@ -26,15 +33,4 @@ - - - - - - - diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs index 839a9750351f..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(1519); -#else asmDuckTypes.Should().Be(1520); +#else + 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(1519); -#else asmDuckTypes.Should().BeGreaterThan(1520); +#else + asmDuckTypes.Should().BeGreaterThan(1521); #endif } } From 004ce88e80c098a499402019ee13770ae8ffa4a5 Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Mon, 7 Sep 2026 16:20:47 +0200 Subject: [PATCH 9/9] [DuckTyping] Build ALC fixtures without dependencies --- tracer/build/_build/Build.Steps.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tracer/build/_build/Build.Steps.cs b/tracer/build/_build/Build.Steps.cs index a4cca0724159..9dd04a626a9c 100644 --- a/tracer/build/_build/Build.Steps.cs +++ b/tracer/build/_build/Build.Steps.cs @@ -1427,7 +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/Target/*.csproj"), noDependencies: false); + 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")); });