-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReflectionHelpers.cs
More file actions
422 lines (379 loc) · 13.2 KB
/
ReflectionHelpers.cs
File metadata and controls
422 lines (379 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
using Microsoft.Extensions.Configuration;
using System.ComponentModel.DataAnnotations;
namespace DigitalRuby.SimpleDi;
/// <summary>
/// Delegate for object activator
/// </summary>
/// <typeparam name="T">Type of object</typeparam>
/// <param name="args">Args</param>
/// <returns>Created object</returns>
public delegate T ObjectActivator<T>(params object[] args);
/// <summary>
/// Reflection helper methods
/// </summary>
public static class ReflectionHelpers
{
private static HashSet<Assembly>? allAssemblies;
private static readonly ConcurrentDictionary<string, Type?> typeCache = new();
/// <summary>
/// Parses the properties of an object into a dictionary, unless the object is a dictionary in which case it is simply returned.
/// </summary>
/// <param name="obj">Object</param>
/// <returns>Dictionary, or empty dictionary if obj is null</returns>
public static IDictionary<string, object?> ParseProperties(this object? obj)
{
if (obj is IDictionary<string, object?> objAsDictionary)
{
return objAsDictionary;
}
else if (obj is IDictionary<string, string?> objAsStringDictionary)
{
return objAsStringDictionary.ToDictionary(x => x.Key, x => (object?)x.Value);
}
Dictionary<string, object?> dictionary = new();
if (obj is not null)
{
var properties = TypeDescriptor.GetProperties(obj);
foreach (PropertyDescriptor? property in properties)
{
if (property != null)
{
dictionary.Add(property.Name, property.GetValue(obj));
}
}
}
return dictionary;
}
/// <summary>
/// Returns the property value of an object.
/// </summary>
/// <param name="obj">Object</param>
/// <param name="propertyName">Property name</param>
/// <returns>Value or null if obj is null or no property exists</returns>
public static object? GetPropertyValue(this object? obj, string propertyName)
{
if (obj is null || string.IsNullOrWhiteSpace(propertyName))
{
return null;
}
PropertyInfo? propertyInfo = obj.GetType().GetProperty(propertyName);
if (propertyInfo is null)
{
return null;
}
return propertyInfo.GetValue(obj, null);
}
#nullable disable
/// <summary>
/// Returns the value of a given selector on an object if it is not null, otherwise returns the default of the selected value.
/// </summary>
/// <param name="obj">Object</param>
/// <param name="selector">Selector</param>
/// <returns>Property</returns>
public static TProperty ValueOrDefault<T, TProperty>(this T obj, Func<T, TProperty> selector)
{
return EqualityComparer<T>.Default.Equals(obj, default) ? default : selector(obj);
}
#nullable restore
/// <summary>
/// Returns the default value for a given Type.
/// </summary>
/// <param name="type">Type</param>
/// <returns>Object or null if type is null</returns>
/// <exception cref="ArgumentException">Invalid type is specified</exception>
public static object? GetDefault(this Type? type)
{
// If no Type was supplied, if the Type was a reference type, or if the Type was a System.Void, return null
if (type is null || !type.IsValueType || type == typeof(void))
{
return null;
}
// If the supplied Type has generic parameters, its default value cannot be determined
if (type.ContainsGenericParameters)
{
throw new ArgumentException($"The type '{type}' contains generic parameters, so the default value cannot be retrieved.");
}
// If the Type is a primitive type, or if it is another publicly-visible value type (i.e. struct/enum), return a
// default instance of the value type
if (type.IsPrimitive || !type.IsNotPublic)
{
try
{
return Activator.CreateInstance(type);
}
catch (Exception ex)
{
throw new ArgumentException($"Could not create a default instance of the type '{type}'.", ex);
}
}
// Fail with exception
throw new ArgumentException($"The type '{type}' is not a publicly-visible type, so the default value cannot be retrieved.");
}
/// <summary>
/// Get all assemblies, including referenced assemblies. This method will be cached beyond the first call.
/// </summary>
/// <returns>All referenced assemblies</returns>
public static IEnumerable<Assembly> GetAllAssemblies()
{
// this method can be called concurrently it will eventually settle
if (allAssemblies is not null)
{
return allAssemblies;
}
var allAssembliesHashSet = new HashSet<Assembly>();
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies().ToArray())
{
allAssembliesHashSet.Add(assembly);
AssemblyName[] references = assembly.GetReferencedAssemblies();
foreach (AssemblyName reference in references)
{
try
{
Assembly referenceAssembly = Assembly.Load(reference);
allAssembliesHashSet.Add(referenceAssembly);
}
catch
{
// don't care, if the assembly can't be loaded there's nothing more to be done
}
}
}
// get referenced assemblies does not include every assembly if no code was referenced from that assembly
string path = AppContext.BaseDirectory;
// grab all dll files in case they were not automatically referenced by the app domain
foreach (string dllFile in Directory.GetFiles(path).Where(f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)))
{
try
{
bool exists = false;
foreach (Assembly assembly in allAssembliesHashSet)
{
try
{
exists = assembly.Location.Equals(dllFile, StringComparison.OrdinalIgnoreCase);
if (exists)
{
break;
}
}
catch
{
// some assemblies will throw upon attempt to access Location property...
}
}
if (!exists)
{
allAssembliesHashSet.Add(Assembly.LoadFrom(dllFile));
}
}
catch
{
// nothing to be done
}
}
return allAssemblies = allAssembliesHashSet;
}
private static KeyValuePair<Type, ConfigurationAttribute>[]? typesAndConfigAttributes;
/// <summary>
/// Get all configuration attributes and types annotated. This call is computed only once and will never change after that,
/// even if the namespaceFilterRegex parameter is changed.
/// </summary>
/// <param name="namespaceFilterRegex">Namespace filter regex</param>
/// <returns>Types and configuration attributes</returns>
public static IReadOnlyCollection<KeyValuePair<Type, ConfigurationAttribute>> GetConfigurationAttributes(string? namespaceFilterRegex = null)
{
if (typesAndConfigAttributes is not null)
{
return typesAndConfigAttributes;
}
List<KeyValuePair<Type, ConfigurationAttribute>> results = new();
foreach (var type in ReflectionHelpers.GetAllTypes(namespaceFilterRegex))
{
var configAttr = type.GetCustomAttribute<ConfigurationAttribute>();
if (configAttr is not null)
{
// fix config path
configAttr.ConfigPath = (string.IsNullOrWhiteSpace(configAttr.ConfigPath) ? type.FullName! : configAttr.ConfigPath);
// add it
results.Add(new KeyValuePair<Type, ConfigurationAttribute>(type, configAttr));
}
}
return typesAndConfigAttributes = results.ToArray();
}
private static readonly ConcurrentDictionary<string, DisplayAttribute?> getDisplayAttributes = new();
/// <summary>
/// Get display attribute from a config path
/// </summary>
/// <param name="configPath">Config path</param>
/// <returns>Display attribute</returns>
public static DisplayAttribute? GetDisplayAttribute(string configPath)
{
return getDisplayAttributes.GetOrAdd(configPath, _configPath =>
{
var configAttr = GetConfigurationAttributes();
DisplayAttribute? displayAttr = null;
foreach (var config in configAttr)
{
if (configPath.StartsWith(config.Value.ConfigPath + ":"))
{
// we are in the right root type
var subConfigPath = configPath[(config.Value.ConfigPath.Length + 1)..];
var segments = subConfigPath.Split(':');
Type? currentType = config.Key;
PropertyInfo? prop = null;
foreach (var segment in segments)
{
prop = currentType?.GetProperty(segment, BindingFlags.Public | BindingFlags.Instance);
currentType = prop?.PropertyType;
}
if (prop is not null)
{
displayAttr = prop.GetCustomAttribute<DisplayAttribute>();
break;
}
}
}
return displayAttr;
});
}
private static string GetDefaultNamespaceFilter()
{
string entryName = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name ?? string.Empty;
// special case for tests, just load everything
if (entryName.Equals("testhost", StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
// only auto bind from assemblies related to our root namespace
string? namespaceFilter = entryName;
int dot = namespaceFilter.IndexOf('.');
if (dot >= 0)
{
namespaceFilter = namespaceFilter[..dot];
}
return "^" + namespaceFilter.Replace(".", "\\.");
}
private static IReadOnlyCollection<Assembly> GetAssemblies(string? namespaceFilterRegex)
{
namespaceFilterRegex ??= GetDefaultNamespaceFilter();
var allAssemblies = GetAllAssemblies();
return allAssemblies.Where(a =>
// no filter, grab the types
string.IsNullOrWhiteSpace(namespaceFilterRegex) ||
// no full name, grab the types
a.FullName is null ||
// assembly name matches filter, grab the types
Regex.IsMatch(a.FullName, namespaceFilterRegex, RegexOptions.IgnoreCase | RegexOptions.Singleline)).ToArray();
}
/// <summary>
/// Get all types from all assemblies
/// </summary>
/// <param name="namespaceFilterRegex">Optional namespace filter (regex), null for default filter, empty for all assemblies (this will increase memory usage)</param>
/// <returns>All types</returns>
public static IReadOnlyCollection<Type> GetAllTypes(string? namespaceFilterRegex = null)
{
namespaceFilterRegex ??= GetDefaultNamespaceFilter();
var allTypesList = new List<Type>();
var matchedAssemblies = GetAssemblies(namespaceFilterRegex);
foreach (Assembly a in matchedAssemblies)
{
try
{
foreach (Type t in a.GetTypes())
{
allTypesList.Add(t);
}
}
catch
{
// ignore, assemblies like intellitrace throw
}
}
return allTypesList;
}
/// <summary>
/// Search all assemblies for a type. Results, including not found types, will be cached permanently in memory.
/// </summary>
/// <param name="fullName">Type full name</param>
/// <param name="namespaceFilterRegex">Optional namespace filter (regex), null for default filter, empty for all assemblies (this will increase memory usage)</param>
/// <returns>Type or null if none found</returns>
public static Type? GetType(string fullName, string? namespaceFilterRegex = null)
{
Type? type = typeCache.GetOrAdd(fullName + "|" + namespaceFilterRegex, _key =>
{
_key = _key[.._key.IndexOf('|')];
Type? type = Type.GetType(_key);
if (type is not null)
{
return type;
}
foreach (Assembly assembly in GetAssemblies(namespaceFilterRegex))
{
type = assembly.GetType(_key);
if (type is not null)
{
return type;
}
}
return null;
});
return type;
}
/// <summary>
/// Get an object creator that is much faster than Activator.CreateInstance
/// </summary>
/// <typeparam name="T">Type of object</typeparam>
/// <param name="ctor">Constructor to use</param>
/// <returns>Activator</returns>
/// <remarks>You can get a constructor by doing <code>typeof(T)?.GetConstructor(types);</code></remarks>
public static ObjectActivator<T>? GetActivator<T>(this ConstructorInfo ctor)
{
if (ctor is null)
{
return null;
}
ParameterInfo[] paramsInfo = ctor.GetParameters();
// create a single param of type object[]
ParameterExpression param = Expression.Parameter(typeof(object[]), "args");
Expression[] argsExp = new Expression[paramsInfo.Length];
// pick each arg from the params array and create a typed expression of them
for (int i = 0; i < paramsInfo.Length; i++)
{
Expression index = Expression.Constant(i);
Type paramType = paramsInfo[i].ParameterType;
Expression paramAccessorExp = Expression.ArrayIndex(param, index);
Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType);
argsExp[i] = paramCastExp;
}
// make a NewExpression that calls the ctor with the args we just created
NewExpression newExp = Expression.New(ctor, argsExp);
// create a lambda with the new expression as body and our param object[] as arg
LambdaExpression lambda = Expression.Lambda(typeof(ObjectActivator<T>), newExp, param);
// compile it
ObjectActivator<T> compiled = (ObjectActivator<T>)lambda.Compile();
return compiled;
}
/// <summary>
/// Determine if a type is an anonymous type
/// </summary>
/// <param name="type">Type</param>
/// <returns>True if anonymous, false otherwise</returns>
public static bool IsAnonymousType(this Type type)
{
if (type.FullName is null)
{
return false;
}
bool hasCompilerGeneratedAttribute = type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Any();
bool nameContainsAnonymousType = type.FullName.Contains("AnonymousType", StringComparison.OrdinalIgnoreCase);
bool isAnonymousType = hasCompilerGeneratedAttribute && nameContainsAnonymousType;
return isAnonymousType;
}
/// <summary>
/// Clear all caches, free up memory
/// </summary>
public static void ClearCaches()
{
typeCache.Clear();
}
}