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
+ }
0 commit comments