Skip to content

Commit 1722487

Browse files
authored
Add tests for assembly resolution downgrading via extension mechanisms (#118040)
The runtime supports assembly version downgrading (resolving a request for A, Version=3.0 with A, Version=1.0) but only through extension mechanisms: - AppDomain.AssemblyResolve event handler - AssemblyLoadContext.Resolving event handler - Custom AssemblyLoadContext.Load override However, there were no tests specifically validating this downgrading behavior. This change adds tests to validate that assembly resolution extension mechanisms can successfully downgrade assembly version requests, while normal runtime resolution cannot.
1 parent b8ed7bb commit 1722487

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using System.Reflection;
6+
using Microsoft.DotNet.RemoteExecutor;
7+
using Xunit;
8+
9+
namespace System.Runtime.Loader.Tests
10+
{
11+
public class AssemblyResolutionDowngradeTest : FileCleanupTestBase
12+
{
13+
private const string TestAssemblyName = "System.Runtime.Loader.Test.VersionDowngrade";
14+
15+
/// <summary>
16+
/// Test that AppDomain.AssemblyResolve can resolve a higher version request with a lower version assembly.
17+
/// This tests the scenario where code requests assembly version 3.0.0 but the resolver provides 1.0.0.
18+
/// </summary>
19+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
20+
public void AppDomainAssemblyResolve_CanDowngradeVersion()
21+
{
22+
RemoteExecutor.Invoke(() => {
23+
string assemblyV1Path = GetTestAssemblyPath("System.Runtime.Loader.Test.AssemblyVersion1");
24+
25+
bool resolverCalled = false;
26+
27+
ResolveEventHandler handler = (sender, args) =>
28+
{
29+
Assert.Same(AppDomain.CurrentDomain, sender);
30+
Assert.NotNull(args);
31+
Assert.NotNull(args.Name);
32+
33+
var requestedName = new AssemblyName(args.Name);
34+
if (requestedName.Name == TestAssemblyName)
35+
{
36+
resolverCalled = true;
37+
// Request is for version 3.0, but we return version 1.0 (downgrade)
38+
Assert.Equal(new Version(3, 0, 0, 0), requestedName.Version);
39+
return Assembly.LoadFile(assemblyV1Path);
40+
}
41+
return null;
42+
};
43+
44+
AppDomain.CurrentDomain.AssemblyResolve += handler;
45+
46+
try
47+
{
48+
// Request version 3.0.0 but expect to get 1.0.0 via downgrade
49+
var requestedAssemblyName = new AssemblyName($"{TestAssemblyName}, Version=3.0.0.0");
50+
Assembly resolvedAssembly = Assembly.Load(requestedAssemblyName);
51+
52+
Assert.NotNull(resolvedAssembly);
53+
Assert.True(resolverCalled, "Assembly resolver should have been called");
54+
55+
// Verify we got the 1.0.0 assembly (downgrade successful)
56+
Assert.Equal(new Version(1, 0, 0, 0), resolvedAssembly.GetName().Version);
57+
58+
// Verify the assembly works as expected
59+
Type testType = resolvedAssembly.GetType("System.Runtime.Loader.Tests.VersionTestClass");
60+
Assert.NotNull(testType);
61+
62+
string version = (string)testType.GetMethod("GetVersion").Invoke(null, null);
63+
Assert.Equal("1.0.0", version);
64+
}
65+
finally
66+
{
67+
AppDomain.CurrentDomain.AssemblyResolve -= handler;
68+
}
69+
}).Dispose();
70+
}
71+
72+
/// <summary>
73+
/// Test that AssemblyLoadContext.Resolving event can resolve a higher version request with a lower version assembly.
74+
/// </summary>
75+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
76+
public void AssemblyLoadContextResolving_CanDowngradeVersion()
77+
{
78+
RemoteExecutor.Invoke(() => {
79+
string assemblyV1Path = GetTestAssemblyPath("System.Runtime.Loader.Test.AssemblyVersion1");
80+
81+
bool resolverCalled = false;
82+
83+
Func<AssemblyLoadContext, AssemblyName, Assembly> handler = (context, name) =>
84+
{
85+
if (name.Name == TestAssemblyName)
86+
{
87+
resolverCalled = true;
88+
// Request is for version 3.0, but we return version 1.0 (downgrade)
89+
Assert.Equal(new Version(3, 0, 0, 0), name.Version);
90+
return context.LoadFromAssemblyPath(assemblyV1Path);
91+
}
92+
return null;
93+
};
94+
95+
AssemblyLoadContext.Default.Resolving += handler;
96+
97+
try
98+
{
99+
// Request version 3.0.0 but expect to get 1.0.0 via downgrade
100+
var requestedAssemblyName = new AssemblyName($"{TestAssemblyName}, Version=3.0.0.0");
101+
Assembly resolvedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(requestedAssemblyName);
102+
103+
Assert.NotNull(resolvedAssembly);
104+
Assert.True(resolverCalled, "Assembly resolver should have been called");
105+
106+
// Verify we got the 1.0.0 assembly (downgrade successful)
107+
Assert.Equal(new Version(1, 0, 0, 0), resolvedAssembly.GetName().Version);
108+
109+
// Verify the assembly works as expected
110+
Type testType = resolvedAssembly.GetType("System.Runtime.Loader.Tests.VersionTestClass");
111+
Assert.NotNull(testType);
112+
113+
string version = (string)testType.GetMethod("GetVersion").Invoke(null, null);
114+
Assert.Equal("1.0.0", version);
115+
}
116+
finally
117+
{
118+
AssemblyLoadContext.Default.Resolving -= handler;
119+
}
120+
}).Dispose();
121+
}
122+
123+
/// <summary>
124+
/// Test that a custom AssemblyLoadContext.Load override can resolve a higher version request with a lower version assembly.
125+
/// </summary>
126+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
127+
public void CustomAssemblyLoadContextLoad_CanDowngradeVersion()
128+
{
129+
RemoteExecutor.Invoke(() => {
130+
string assemblyV1Path = GetTestAssemblyPath("System.Runtime.Loader.Test.AssemblyVersion1");
131+
132+
var customContext = new DowngradeAssemblyLoadContext(assemblyV1Path);
133+
134+
// Request version 3.0.0 but expect to get 1.0.0 via downgrade
135+
var requestedAssemblyName = new AssemblyName($"{TestAssemblyName}, Version=3.0.0.0");
136+
Assembly resolvedAssembly = customContext.LoadFromAssemblyName(requestedAssemblyName);
137+
138+
Assert.NotNull(resolvedAssembly);
139+
Assert.True(customContext.LoadCalled, "Custom Load method should have been called");
140+
141+
// Verify we got the 1.0.0 assembly (downgrade successful)
142+
Assert.Equal(new Version(1, 0, 0, 0), resolvedAssembly.GetName().Version);
143+
144+
// Verify the assembly works as expected
145+
Type testType = resolvedAssembly.GetType("System.Runtime.Loader.Tests.VersionTestClass");
146+
Assert.NotNull(testType);
147+
148+
string version = (string)testType.GetMethod("GetVersion").Invoke(null, null);
149+
Assert.Equal("1.0.0", version);
150+
151+
// Verify that the correct ALC loaded the assembly
152+
Assert.Equal(customContext, AssemblyLoadContext.GetLoadContext(resolvedAssembly));
153+
}).Dispose();
154+
}
155+
156+
/// <summary>
157+
/// Test that normal runtime resolution (without extension mechanisms) will NOT allow downgrades.
158+
/// This test verifies the baseline behavior that downgrades only work via extension mechanisms.
159+
/// Note: On Mono, downgrades are allowed even in normal resolution, so this test behaves differently.
160+
/// </summary>
161+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
162+
public void NormalResolution_CannotDowngradeVersion()
163+
{
164+
RemoteExecutor.Invoke(() => {
165+
string assemblyV1Path = GetTestAssemblyPath("System.Runtime.Loader.Test.AssemblyVersion1");
166+
167+
// First, load the version 1.0.0 assembly into the default context
168+
Assembly loadedV1 = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyV1Path);
169+
Assert.Equal(new Version(1, 0, 0, 0), loadedV1.GetName().Version);
170+
171+
// Now try to load version 3.0.0
172+
var requestedAssemblyName = new AssemblyName($"{TestAssemblyName}, Version=3.0.0.0");
173+
174+
if (PlatformDetection.IsMonoRuntime)
175+
{
176+
// On Mono, normal resolution allows downgrades, so this should succeed
177+
// and return the already-loaded 1.0.0 assembly
178+
Assembly resolvedAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(requestedAssemblyName);
179+
Assert.NotNull(resolvedAssembly);
180+
Assert.Equal(new Version(1, 0, 0, 0), resolvedAssembly.GetName().Version);
181+
Assert.Same(loadedV1, resolvedAssembly);
182+
}
183+
else
184+
{
185+
// On CoreCLR, normal resolution should NOT automatically
186+
// downgrade to the already-loaded 1.0.0 version, it should fail
187+
Assert.Throws<FileNotFoundException>(() =>
188+
AssemblyLoadContext.Default.LoadFromAssemblyName(requestedAssemblyName));
189+
}
190+
}).Dispose();
191+
}
192+
193+
private static string GetTestAssemblyPath(string assemblyProject)
194+
{
195+
// Map project names to actual embedded resource names
196+
string resourceName = assemblyProject switch
197+
{
198+
"System.Runtime.Loader.Test.AssemblyVersion1" => "System.Runtime.Loader.Tests.AssemblyVersion1.dll",
199+
_ => throw new ArgumentException($"Unknown test assembly project: {assemblyProject}")
200+
};
201+
202+
// Extract the embedded assembly to a temporary file
203+
string tempPath = Path.Combine(Path.GetTempPath(), $"{assemblyProject}_{Guid.NewGuid()}.dll");
204+
205+
using (Stream resourceStream = typeof(AssemblyResolutionDowngradeTest).Assembly.GetManifestResourceStream(resourceName))
206+
{
207+
if (resourceStream is null)
208+
{
209+
throw new FileNotFoundException($"Could not find embedded resource: {resourceName}");
210+
}
211+
212+
using (FileStream fileStream = File.Create(tempPath))
213+
{
214+
resourceStream.CopyTo(fileStream);
215+
}
216+
}
217+
218+
return tempPath;
219+
}
220+
221+
/// <summary>
222+
/// Custom AssemblyLoadContext that can downgrade version requests.
223+
/// </summary>
224+
private class DowngradeAssemblyLoadContext : AssemblyLoadContext
225+
{
226+
private readonly string _downgradePath;
227+
228+
public bool LoadCalled { get; private set; }
229+
230+
public DowngradeAssemblyLoadContext(string downgradePath) : base("DowngradeContext")
231+
{
232+
_downgradePath = downgradePath;
233+
}
234+
235+
protected override Assembly Load(AssemblyName assemblyName)
236+
{
237+
LoadCalled = true;
238+
239+
if (assemblyName.Name == TestAssemblyName)
240+
{
241+
// Request is for version 3.0, but we return version 1.0 (downgrade)
242+
Assert.Equal(new Version(3, 0, 0, 0), assemblyName.Version);
243+
return LoadFromAssemblyPath(_downgradePath);
244+
}
245+
246+
return null;
247+
}
248+
}
249+
}
250+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0</TargetFrameworks>
4+
<AssemblyVersion>1.0.0.0</AssemblyVersion>
5+
<FileVersion>1.0.0.0</FileVersion>
6+
<Version>1.0.0</Version>
7+
<AssemblyName>System.Runtime.Loader.Test.VersionDowngrade</AssemblyName>
8+
<NuGetAudit>false</NuGetAudit>
9+
</PropertyGroup>
10+
<ItemGroup>
11+
<Compile Include="VersionTestClass.cs" />
12+
</ItemGroup>
13+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Runtime.Loader.Tests
5+
{
6+
public class VersionTestClass
7+
{
8+
public static string GetVersion()
9+
=> typeof(VersionTestClass).Assembly.GetName().Version?.ToString(3) ?? "Unknown";
10+
}
11+
}

src/libraries/System.Runtime.Loader/tests/System.Runtime.Loader.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<Compile Include="ResourceAssemblyLoadContext.cs" />
3131
<Compile Include="SatelliteAssemblies.cs" />
3232
<Compile Include="LoaderLinkTest.cs" />
33+
<Compile Include="AssemblyResolutionDowngradeTest.cs" />
3334
<Compile Include="$(CommonTestPath)TestUtilities\System\DisableParallelization.cs" Link="Common\TestUtilities\System\DisableParallelization.cs" />
3435
<EmbeddedResource Include="MainStrings*.resx" />
3536
</ItemGroup>
@@ -50,6 +51,7 @@
5051
<ProjectReference Include="ReferencedClassLibNeutralIsSatellite\ReferencedClassLibNeutralIsSatellite.csproj" />
5152
<ProjectReference Include="LoaderLinkTest.Shared\LoaderLinkTest.Shared.csproj" />
5253
<ProjectReference Include="LoaderLinkTest.Dynamic\LoaderLinkTest.Dynamic.csproj" />
54+
<ProjectReference Include="System.Runtime.Loader.Test.AssemblyVersion1\System.Runtime.Loader.Test.AssemblyVersion1.csproj" ReferenceOutputAssembly="false" OutputItemType="EmbeddedResource" Link="AssemblyVersion1.dll" />
5355
</ItemGroup>
5456

5557
<!-- ActiveIssue https://github.com/dotnet/runtime/issues/114526 deadlocks on linux CI -->

0 commit comments

Comments
 (0)