1
+ //*********************************************************************
2
+ //xToolkit
3
+ //Copyright(C) 2023 Xarial Pty Limited
4
+ //Product URL: https://xtoolkit.xarial.com
5
+ //License: https://xtoolkit.xarial.com/license/
6
+ //*********************************************************************
7
+
8
+ using System ;
9
+ using System . Collections . Generic ;
10
+ using System . IO ;
11
+ using System . Linq ;
12
+ using System . Reflection ;
13
+
14
+ namespace Xarial . XToolkit . Reflection
15
+ {
16
+ /// <summary>
17
+ /// Simple resolve for the assembly references
18
+ /// </summary>
19
+ /// <remarks>Resolver will consider the following rules to load missing references
20
+ /// 1 - Loading local references only (if requesting assembly is in the working directory)
21
+ /// 2 - Loading target or newer version
22
+ /// 3 - Loading from work directory and sub-directories
23
+ /// 4 - Loading matched name, public key and culture
24
+ /// 5 - Loading nearest available version</remarks>
25
+ public class AssemblyReferenceResolver : IDisposable
26
+ {
27
+ private class AssemblyNameEqualityComparer : IEqualityComparer < AssemblyName >
28
+ {
29
+ public bool Equals ( AssemblyName x , AssemblyName y )
30
+ => string . Equals ( x . FullName , y . FullName ) ;
31
+
32
+ public int GetHashCode ( AssemblyName obj ) => 0 ;
33
+ }
34
+
35
+ private readonly AppDomain m_AppDomain ;
36
+
37
+ private readonly string m_WorkDir ;
38
+ private readonly string m_LogName ;
39
+
40
+ private readonly Dictionary < AssemblyName , Assembly > m_CachedAssemblies ;
41
+
42
+ private readonly IEqualityComparer < AssemblyName > m_AssmNameEqualityComparer ;
43
+
44
+ /// <summary>
45
+ /// Starts monitoring and loading missin assemblies
46
+ /// </summary>
47
+ /// <param name="appDomain">Application domain</param>
48
+ /// <param name="workDir">Working directories to look for the assmebly to load</param>
49
+ /// <param name="logName">Name of the trace log</param>
50
+ public AssemblyReferenceResolver ( AppDomain appDomain , string workDir , string logName )
51
+ {
52
+ m_WorkDir = workDir ;
53
+
54
+ m_AssmNameEqualityComparer = new AssemblyNameEqualityComparer ( ) ;
55
+ m_CachedAssemblies = new Dictionary < AssemblyName , Assembly > ( m_AssmNameEqualityComparer ) ;
56
+
57
+ m_LogName = logName ;
58
+
59
+ m_AppDomain = appDomain ;
60
+
61
+ m_AppDomain . AssemblyResolve += OnAssemblyResolve ;
62
+ }
63
+
64
+ private Assembly OnAssemblyResolve ( object sender , ResolveEventArgs args )
65
+ {
66
+ var assmName = new AssemblyName ( args . Name ) ;
67
+
68
+ var requestingAssm = args . RequestingAssembly ;
69
+
70
+ if ( ! assmName . Name . EndsWith ( ".resources" )
71
+ && ( requestingAssm == null || IsInDirectory ( requestingAssm . Location , m_WorkDir ) ) )
72
+ {
73
+ return Resolve ( assmName ) ;
74
+ }
75
+ else
76
+ {
77
+ return null ;
78
+ }
79
+ }
80
+
81
+ private Assembly Resolve ( AssemblyName searchAssmName )
82
+ {
83
+ if ( ! m_CachedAssemblies . TryGetValue ( searchAssmName , out var assm ) )
84
+ {
85
+ var matchedAssmNames = new List < AssemblyName > ( ) ;
86
+
87
+ foreach ( var assmName in EnumerateAssemblyByName ( m_WorkDir , true , searchAssmName ) )
88
+ {
89
+ if ( m_AssmNameEqualityComparer . Equals ( assmName , searchAssmName ) )
90
+ {
91
+ Trace ( $ "Loading '{ searchAssmName } ' from '{ assmName . CodeBase } ' as exact match") ;
92
+
93
+ assm = LoadAssembly ( assmName ) ;
94
+ break ;
95
+ }
96
+ else
97
+ {
98
+ if ( ! matchedAssmNames . Contains ( assmName , m_AssmNameEqualityComparer ) )
99
+ {
100
+ matchedAssmNames . Add ( assmName ) ;
101
+ }
102
+ }
103
+ }
104
+
105
+ if ( assm == null )
106
+ {
107
+ var targAssmName = ResolveAmbiguity ( matchedAssmNames , searchAssmName ) ;
108
+
109
+ if ( targAssmName != null )
110
+ {
111
+ assm = LoadAssembly ( targAssmName ) ;
112
+ }
113
+ else
114
+ {
115
+ assm = null ;
116
+ }
117
+ }
118
+
119
+ m_CachedAssemblies . Add ( searchAssmName , assm ) ;
120
+
121
+ return assm ;
122
+ }
123
+ else
124
+ {
125
+ return assm ;
126
+ }
127
+ }
128
+
129
+ private Assembly LoadAssembly ( AssemblyName assmName )
130
+ {
131
+ Trace ( $ "Loading assembly '{ assmName } '") ;
132
+
133
+ return Assembly . Load ( assmName ) ;
134
+ }
135
+
136
+ private AssemblyName ResolveAmbiguity ( IReadOnlyList < AssemblyName > assmNames , AssemblyName searchAssmName )
137
+ {
138
+ Trace ( $ "Resolving ambiguity for '{ searchAssmName } '") ;
139
+
140
+ var targAssmName = assmNames . FirstOrDefault ( a => m_AssmNameEqualityComparer . Equals ( a , searchAssmName ) ) ;
141
+
142
+ if ( targAssmName != null )
143
+ {
144
+ Trace ( $ "Ambiguity for '{ searchAssmName } ' is resolved by exact match") ;
145
+ }
146
+ else
147
+ {
148
+ targAssmName = assmNames . OrderBy ( a => a . Version ) . FirstOrDefault ( ) ;
149
+
150
+ if ( targAssmName != null )
151
+ {
152
+ Trace ( $ "Ambiguity for '{ searchAssmName } ' is resolved by nearest available version of the assembly") ;
153
+ }
154
+ else
155
+ {
156
+ Trace ( $ "Ambiguity for '{ searchAssmName } ' is not resolved") ;
157
+ }
158
+ }
159
+
160
+ return targAssmName ;
161
+ }
162
+
163
+ private IEnumerable < AssemblyName > EnumerateAssemblyByName ( string dir , bool recurse , AssemblyName searchAssmName )
164
+ {
165
+ var probeAssmFilePath = Path . Combine ( dir , searchAssmName . Name + ".dll" ) ;
166
+
167
+ if ( File . Exists ( probeAssmFilePath ) )
168
+ {
169
+ var probeAssmName = AssemblyName . GetAssemblyName ( probeAssmFilePath ) ;
170
+
171
+ if ( Matches ( probeAssmName , searchAssmName ) )
172
+ {
173
+ yield return probeAssmName ;
174
+ }
175
+ }
176
+
177
+ if ( recurse )
178
+ {
179
+ foreach ( var subDir in Directory . EnumerateDirectories ( dir , "*.*" , SearchOption . TopDirectoryOnly ) )
180
+ {
181
+ foreach ( var res in EnumerateAssemblyByName ( subDir , recurse , searchAssmName ) )
182
+ {
183
+ yield return res ;
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ private bool Matches ( AssemblyName probeAssmName , AssemblyName searchAssmName )
190
+ {
191
+ return ( probeAssmName . Name == searchAssmName . Name )
192
+ && ( GetPublicKeyTokenString ( probeAssmName ) == GetPublicKeyTokenString ( searchAssmName ) )
193
+ && ( probeAssmName . CultureName == probeAssmName . CultureName )
194
+ && ( probeAssmName . Version >= searchAssmName . Version ) ;
195
+ }
196
+
197
+ private string GetPublicKeyTokenString ( AssemblyName assmName )
198
+ {
199
+ var token = assmName . GetPublicKeyToken ( ) ;
200
+
201
+ if ( token != null )
202
+ {
203
+ return string . Join ( "" , token . Select ( t => string . Format ( "{0:x2}" , t ) ) ) ;
204
+ }
205
+ else
206
+ {
207
+ return "" ;
208
+ }
209
+ }
210
+
211
+ private bool IsInDirectory ( string thisPath , string parentDir )
212
+ {
213
+ if ( ! parentDir . EndsWith ( "\\ " ) )
214
+ {
215
+ parentDir = parentDir + "\\ " ;
216
+ }
217
+
218
+ return thisPath . StartsWith ( parentDir , StringComparison . CurrentCultureIgnoreCase ) ;
219
+ }
220
+
221
+ private void Trace ( string message )
222
+ => System . Diagnostics . Trace . WriteLine ( message , m_LogName ) ;
223
+
224
+ public void Dispose ( )
225
+ {
226
+ m_AppDomain . AssemblyResolve -= OnAssemblyResolve ;
227
+ }
228
+ }
229
+ }
0 commit comments