• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / EntityChange / 14044984722

24 Mar 2025 08:13PM UTC coverage: 45.672% (-0.7%) from 46.358%
14044984722

push

github

pwelter34
bug fix

294 of 734 branches covered (40.05%)

Branch coverage included in aggregate %.

5 of 6 new or added lines in 1 file covered. (83.33%)

8 existing lines in 1 file now uncovered.

566 of 1149 relevant lines covered (49.26%)

73.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

16.74
/src/EntityChange/Reflection/TypeAccessor.cs
1
#nullable disable
2

3
using System.Collections.Concurrent;
4
using System.ComponentModel.DataAnnotations.Schema;
5
using System.Linq.Expressions;
6
using System.Reflection;
7

8
namespace EntityChange.Reflection;
9

10
/// <summary>
11
/// A class for accessing type reflection information.
12
/// </summary>
13
public class TypeAccessor
14
{
15
    private static readonly ConcurrentDictionary<Type, TypeAccessor> _typeCache = new();
1✔
16
    private readonly ConcurrentDictionary<string, IMemberAccessor> _memberCache = new();
9✔
17
    private readonly ConcurrentDictionary<int, IMethodAccessor> _methodCache = new();
9✔
18
    private readonly ConcurrentDictionary<int, IEnumerable<IMemberAccessor>> _propertyCache = new();
9✔
19

20
    private readonly Lazy<Func<object>> _constructor;
21
    private readonly Lazy<TableAttribute> _tableAttribute;
22

23
    /// <summary>
24
    /// Initializes a new instance of the <see cref="TypeAccessor"/> class.
25
    /// </summary>
26
    /// <param name="type">The <see cref="Type"/> this accessor is for.</param>
27
    public TypeAccessor(Type type)
9✔
28
    {
29
        if (type == null)
9!
30
            throw new ArgumentNullException(nameof(type));
×
31

32
        Type = type;
9✔
33
        _constructor = new Lazy<Func<object>>(() => ExpressionFactory.CreateConstructor(Type));
9✔
34
        _tableAttribute = new Lazy<TableAttribute>(() => type.GetCustomAttribute<TableAttribute>(true));
9✔
35
    }
9✔
36

37
    /// <summary>
38
    /// Gets the <see cref="Type"/> this accessor is for.
39
    /// </summary>
40
    /// <value>The <see cref="Type"/> this accessor is for.</value>
41
    public Type Type { get; }
9✔
42

43
    /// <summary>
44
    /// Gets the name of the Type.
45
    /// </summary>
46
    /// <value>
47
    /// The name of the Type.
48
    /// </value>
49
    public string Name => Type.Name;
×
50

51
    /// <summary>
52
    /// Gets the name of the table the class is mapped to.
53
    /// </summary>
54
    /// <value>
55
    /// The name of the table the class is mapped to.
56
    /// </value>
57
    public string TableName => _tableAttribute.Value?.Name ?? Type.Name;
×
58

59
    /// <summary>
60
    /// Gets the schema of the table the class is mapped to.
61
    /// </summary>
62
    /// <value>
63
    /// The schema of the table the class is mapped to.
64
    /// </value>
65
    public string TableSchema => _tableAttribute.Value?.Schema;
×
66

67
    /// <summary>
68
    /// Creates a new instance of accessors type.
69
    /// </summary>
70
    /// <returns>A new instance of accessors type.</returns>
71
    public object Create()
72
    {
73
        var constructor = _constructor.Value;
×
74
        if (constructor == null)
×
75
            throw new InvalidOperationException($"Could not find constructor for '{Type.Name}'.");
×
76

77
        return constructor.Invoke();
×
78
    }
79

80

81
    #region Method
82
    /// <summary>
83
    /// Finds the method with the spcified <paramref name="name"/>.
84
    /// </summary>
85
    /// <param name="name">The name of the method.</param>
86
    /// <returns>An <see cref="IMemberAccessor"/> for the method.</returns>
87
    public IMethodAccessor FindMethod(string name)
88
    {
89
        return FindMethod(name, Type.EmptyTypes);
×
90
    }
91

92
    /// <summary>
93
    /// Finds the method with the spcified <paramref name="name" />.
94
    /// </summary>
95
    /// <param name="name">The name of the method.</param>
96
    /// <param name="parameterTypes">The method parameter types.</param>
97
    /// <returns>
98
    /// An <see cref="IMemberAccessor" /> for the method.
99
    /// </returns>
100
    public IMethodAccessor FindMethod(string name, params Type[] parameterTypes)
101
    {
102
        return FindMethod(name, parameterTypes, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
×
103
    }
104

105
    /// <summary>
106
    /// Finds the method with the spcified <paramref name="name" />.
107
    /// </summary>
108
    /// <param name="name">The name of the method.</param>
109
    /// <param name="parameterTypes">The method parameter types.</param>
110
    /// <param name="flags">The binding flags to search.</param>
111
    /// <returns>
112
    /// An <see cref="IMemberAccessor" /> for the method.
113
    /// </returns>
114
    public IMethodAccessor FindMethod(string name, Type[] parameterTypes, BindingFlags flags)
115
    {
116
        int key = MethodAccessor.GetKey(name, parameterTypes);
×
117
        return _methodCache.GetOrAdd(key, n => CreateMethodAccessor(name, parameterTypes, flags));
×
118
    }
119

120
    private IMethodAccessor CreateMethodAccessor(string name, Type[] parameters, BindingFlags flags)
121
    {
122
        var info = FindMethod(Type, name, parameters, flags);
×
123
        return info == null ? null : CreateAccessor(info);
×
124
    }
125

126
    private static MethodInfo FindMethod(Type type, string name, Type[] parameterTypes, BindingFlags flags)
127
    {
128
        if (type == null)
×
129
            throw new ArgumentNullException(nameof(type));
×
130
        if (name == null)
×
131
            throw new ArgumentNullException(nameof(name));
×
132

133
        if (parameterTypes == null)
×
134
            parameterTypes = Type.EmptyTypes;
×
135

136
        var typeInfo = type.GetTypeInfo();
×
137

138
        //first try full match
139
        var methodInfo = typeInfo.GetMethod(name, parameterTypes);
×
140
        if (methodInfo != null)
×
141
            return methodInfo;
×
142

143
        // next, get all that match by name
144
        var methodsByName = typeInfo.GetMethods(flags)
×
145
          .Where(m => m.Name == name)
×
146
          .ToList();
×
147

148
        if (methodsByName.Count == 0)
×
149
            return null;
×
150

151
        // if only one matches name, return it
152
        if (methodsByName.Count == 1)
×
153
            return methodsByName.FirstOrDefault();
×
154

155
        // next, get all methods that match param count
156
        var methodsByParamCount = methodsByName
×
157
            .Where(m => m.GetParameters().Length == parameterTypes.Length)
×
158
            .ToList();
×
159

160
        // if only one matches with same param count, return it
161
        if (methodsByParamCount.Count == 1)
×
162
            return methodsByParamCount.FirstOrDefault();
×
163

164
        // still no match, make best guess by greatest matching param types
165
        MethodInfo current = methodsByParamCount.FirstOrDefault();
×
166
        int matchCount = 0;
×
167

168
        foreach (var info in methodsByParamCount)
×
169
        {
170
            var paramTypes = info.GetParameters()
×
171
                .Select(p => p.ParameterType)
×
172
                .ToArray();
×
173

174
            // unsure which way IsAssignableFrom should be checked?
175
            int count = paramTypes
×
176
                .Select(t => t.GetTypeInfo())
×
177
                .Where((t, i) => t.IsAssignableFrom(parameterTypes[i]))
×
178
                .Count();
×
179

180
            if (count <= matchCount)
×
181
                continue;
182

183
            current = info;
×
184
            matchCount = count;
×
185
        }
186

187
        return current;
×
188
    }
189

190
    private static IMethodAccessor CreateAccessor(MethodInfo methodInfo)
191
    {
192
        return methodInfo == null ? null : new MethodAccessor(methodInfo);
×
193
    }
194
    #endregion
195

196
    #region Find
197
    /// <summary>
198
    /// Searches for the public property or field with the specified name.
199
    /// </summary>
200
    /// <param name="name">The name of the property or field to find.</param>
201
    /// <returns>An <see cref="IMemberAccessor"/> instance for the property or field if found; otherwise <c>null</c>.</returns>
202
    public IMemberAccessor Find(string name)
203
    {
204
        return Find(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
×
205
    }
206

207
    /// <summary>
208
    /// Searches for the specified property or field, using the specified binding constraints.
209
    /// </summary>
210
    /// <param name="name">The name of the property or field to find.</param>
211
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
212
    /// <returns>
213
    /// An <see cref="IMemberAccessor"/> instance for the property or field if found; otherwise <c>null</c>.
214
    /// </returns>
215
    public IMemberAccessor Find(string name, BindingFlags flags)
216
    {
217
        return _memberCache.GetOrAdd(name, n => CreateAccessor(n, flags));
×
218
    }
219

220
    private IMemberAccessor CreateAccessor(string name, BindingFlags flags)
221
    {
222
        // first try property
223
        var property = FindProperty(Type, name, flags);
×
224
        if (property != null)
×
225
            return CreateAccessor(property);
×
226

227
        // next try field
228
        var field = FindField(Type, name, flags);
×
229
        if (field != null)
×
230
            return CreateAccessor(field);
×
231

232
        return null;
×
233
    }
234
    #endregion
235

236
    #region Column
237
    /// <summary>
238
    /// Searches for the public property with the specified column name.
239
    /// </summary>
240
    /// <param name="name">The name of the property or field to find.</param>
241
    /// <returns>An <see cref="IMemberAccessor"/> instance for the property or field if found; otherwise <c>null</c>.</returns>
242
    public IMemberAccessor FindColumn(string name)
243
    {
244
        return FindColumn(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
×
245
    }
246

247
    /// <summary>
248
    /// Searches for the specified property the specified column name and binding constraints.
249
    /// </summary>
250
    /// <param name="name">The name of the property to find.</param>
251
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
252
    /// <returns>
253
    /// An <see cref="IMemberAccessor"/> instance for the property if found; otherwise <c>null</c>.
254
    /// </returns>
255
    public IMemberAccessor FindColumn(string name, BindingFlags flags)
256
    {
257
        return _memberCache.GetOrAdd(name, n => CreateColumnAccessor(n, flags));
×
258
    }
259

260
    private IMemberAccessor CreateColumnAccessor(string name, BindingFlags flags)
261
    {
262
        var typeInfo = Type.GetTypeInfo();
×
263

264
        // first try GetProperty
265
        var property = typeInfo.GetProperty(name, flags);
×
266
        if (property != null)
×
267
            return CreateAccessor(property);
×
268

269
        // if not found, search
270
        foreach (var p in typeInfo.GetProperties(flags))
×
271
        {
272
            if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
×
273
                return CreateAccessor(p);
×
274

275
            // try ColumnAttribute
276
            var columnAttribute = p.GetCustomAttribute<System.ComponentModel.DataAnnotations.Schema.ColumnAttribute>();
×
277
            if (columnAttribute != null && name.Equals(columnAttribute.Name, StringComparison.OrdinalIgnoreCase))
×
278
                return CreateAccessor(p);
×
279
        }
280

281
        return null;
×
282
    }
283
    #endregion
284

285
    #region Property
286
    /// <summary>
287
    /// Searches for the property using a property expression.
288
    /// </summary>
289
    /// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
290
    /// <param name="propertyExpression">The property expression (e.g. p => p.PropertyName)</param>
291
    /// <returns>An <see cref="IMemberAccessor"/> instance for the property if found; otherwise <c>null</c>.</returns>
292
    /// <exception cref="ArgumentNullException">Thrown if the <paramref name="propertyExpression"/> is null.</exception>
293
    /// <exception cref="ArgumentException">Thrown when the expression is:<br/>
294
    ///     Not a <see cref="MemberExpression"/><br/>
295
    ///     The <see cref="MemberExpression"/> does not represent a property.<br/>
296
    ///     Or, the property is static.
297
    /// </exception>
298
    public IMemberAccessor FindProperty<T>(Expression<Func<T>> propertyExpression)
299
    {
300
        if (propertyExpression == null)
×
301
            throw new ArgumentNullException(nameof(propertyExpression));
×
302

303
        if (propertyExpression.Body is UnaryExpression unaryExpression)
×
304
            return FindProperty(unaryExpression.Operand as MemberExpression);
×
305
        else
306
            return FindProperty(propertyExpression.Body as MemberExpression);
×
307
    }
308

309
    /// <summary>
310
    /// Searches for the property using a property expression.
311
    /// </summary>
312
    /// <typeparam name="TSource">The object type containing the property specified in the expression.</typeparam>
313
    /// <typeparam name="TValue">The type of the value.</typeparam>
314
    /// <param name="propertyExpression">The property expression (e.g. p =&gt; p.PropertyName)</param>
315
    /// <returns>
316
    /// An <see cref="IMemberAccessor"/> instance for the property if found; otherwise <c>null</c>.
317
    /// </returns>
318
    /// <exception cref="ArgumentNullException">Thrown if the <paramref name="propertyExpression"/> is null.</exception>
319
    ///
320
    /// <exception cref="ArgumentException">Thrown when the expression is:<br/>
321
    /// Not a <see cref="MemberExpression"/><br/>
322
    /// The <see cref="MemberExpression"/> does not represent a property.<br/>
323
    /// Or, the property is static.
324
    ///   </exception>
325
    public IMemberAccessor FindProperty<TSource, TValue>(Expression<Func<TSource, TValue>> propertyExpression)
326
    {
327
        if (propertyExpression == null)
14!
328
            throw new ArgumentNullException(nameof(propertyExpression));
×
329

330
        if (propertyExpression.Body is UnaryExpression unaryExpression)
14!
331
            return FindProperty(unaryExpression.Operand as MemberExpression);
×
332
        else
333
            return FindProperty(propertyExpression.Body as MemberExpression);
14✔
334
    }
335

336
    /// <summary>
337
    /// Searches for the <see langword="public"/> property with the specified <paramref name="name"/>.
338
    /// </summary>
339
    /// <param name="name">The name of the property to find.</param>
340
    /// <returns>An <see cref="IMemberAccessor"/> instance for the property if found; otherwise <c>null</c>.</returns>
341
    public IMemberAccessor FindProperty(string name)
342
    {
343
        return FindProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
14✔
344
    }
345

346
    /// <summary>
347
    /// Searches for the specified property, using the specified binding constraints.
348
    /// </summary>
349
    /// <param name="name">The name of the property to find.</param>
350
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
351
    /// <returns>
352
    /// An <see cref="IMemberAccessor"/> instance for the property if found; otherwise <c>null</c>.
353
    /// </returns>
354
    public IMemberAccessor FindProperty(string name, BindingFlags flags)
355
    {
356
        return _memberCache.GetOrAdd(name, n => CreatePropertyAccessor(n, flags));
14✔
357
    }
358

359
    /// <summary>
360
    /// Gets the property member accessors for the Type.
361
    /// </summary>
362
    /// <returns>
363
    /// An <see cref="T:System.Collections.Generic.IEnumerable`1"/> of <see cref="IMemberAccessor"/> instances for the Type.
364
    /// </returns>
365
    public IEnumerable<IMemberAccessor> GetProperties()
366
    {
367
        return GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
37✔
368
    }
369

370
    /// <summary>
371
    /// Gets the property member accessors for the Type using the specified flags.
372
    /// </summary>
373
    /// <returns>
374
    /// An <see cref="T:System.Collections.Generic.IEnumerable`1"/> of <see cref="IMemberAccessor"/> instances for the Type.
375
    /// </returns>
376
    public IEnumerable<IMemberAccessor> GetProperties(BindingFlags flags)
377
    {
378
        return _propertyCache.GetOrAdd((int)flags, k =>
37✔
379
        {
37✔
380
            var typeInfo = Type.GetTypeInfo();
9✔
381
            var properties = typeInfo.GetProperties(flags);
9✔
382
            return properties.Select(GetAccessor);
9✔
383
        });
37✔
384
    }
385

386

387
    private IMemberAccessor GetAccessor(PropertyInfo propertyInfo)
388
    {
389
        if (propertyInfo == null)
294!
390
            throw new ArgumentNullException(nameof(propertyInfo));
×
391

392
        return _memberCache.GetOrAdd(propertyInfo.Name, n => CreateAccessor(propertyInfo));
336✔
393
    }
394

395
    private IMemberAccessor CreatePropertyAccessor(string name, BindingFlags flags)
396
    {
UNCOV
397
        var info = FindProperty(Type, name, flags);
×
UNCOV
398
        return info == null ? null : CreateAccessor(info);
×
399
    }
400

401
    private IMemberAccessor FindProperty(MemberExpression memberExpression)
402
    {
403
        if (memberExpression == null)
14!
404
            throw new ArgumentException("The expression is not a member access expression.", nameof(memberExpression));
×
405

406
        var property = memberExpression.Member as PropertyInfo;
14✔
407
        if (property == null)
14!
408
            throw new ArgumentException("The member access expression does not access a property.", nameof(memberExpression));
×
409

410
        // find by name because we can't trust the PropertyInfo here as it could be from an interface or inherited class
411
        return FindProperty(property.Name);
14✔
412
    }
413

414
    private static PropertyInfo FindProperty(Type type, string name, BindingFlags flags)
415
    {
UNCOV
416
        if (type == null)
×
417
            throw new ArgumentNullException(nameof(type));
×
418

UNCOV
419
        if (name == null)
×
420
            throw new ArgumentNullException(nameof(name));
×
421

UNCOV
422
        var typeInfo = type.GetTypeInfo();
×
423
        // first try GetProperty
UNCOV
424
        var property = typeInfo.GetProperty(name, flags);
×
UNCOV
425
        if (property != null)
×
UNCOV
426
            return property;
×
427

428
        // if not found, search while ignoring case
429
        property = typeInfo
×
430
            .GetProperties(flags)
×
431
            .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
×
432

433
        return property;
×
434
    }
435

436
    private static IMemberAccessor CreateAccessor(PropertyInfo propertyInfo)
437
    {
438
        return propertyInfo == null ? null : new PropertyAccessor(propertyInfo);
42!
439
    }
440
    #endregion
441

442
    #region Field
443
    /// <summary>
444
    /// Searches for the specified field with the specified name.
445
    /// </summary>
446
    /// <param name="name">The name of the field to find.</param>
447
    /// <returns>
448
    /// An <see cref="IMemberAccessor"/> instance for the field if found; otherwise <c>null</c>.
449
    /// </returns>
450
    public IMemberAccessor FindField(string name)
451
    {
452
        return FindField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
×
453
    }
454

455
    /// <summary>
456
    /// Searches for the specified field, using the specified binding constraints.
457
    /// </summary>
458
    /// <param name="name">The name of the field to find.</param>
459
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
460
    /// <returns>
461
    /// An <see cref="IMemberAccessor"/> instance for the field if found; otherwise <c>null</c>.
462
    /// </returns>
463
    public IMemberAccessor FindField(string name, BindingFlags flags)
464
    {
465
        return _memberCache.GetOrAdd(name, n => CreateFieldAccessor(n, flags));
×
466
    }
467

468
    private IMemberAccessor CreateFieldAccessor(string name, BindingFlags flags)
469
    {
470
        var info = FindField(Type, name, flags);
×
471
        return info == null ? null : CreateAccessor(info);
×
472
    }
473

474
    private static FieldInfo FindField(Type type, string name, BindingFlags flags)
475
    {
476
        if (type == null)
×
477
            throw new ArgumentNullException(nameof(type));
×
478

479
        if (name == null)
×
480
            throw new ArgumentNullException(nameof(name));
×
481

482
        // first try GetField
483
        var typeInfo = type.GetTypeInfo();
×
484
        var field = typeInfo.GetField(name, flags);
×
485
        if (field != null)
×
486
            return field;
×
487

488
        // if not found, search while ignoring case
489
        return typeInfo
×
490
            .GetFields(flags)
×
491
            .FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
×
492
    }
493

494
    private static IMemberAccessor CreateAccessor(FieldInfo fieldInfo)
495
    {
496
        return fieldInfo == null ? null : new FieldAccessor(fieldInfo);
×
497
    }
498
    #endregion
499

500
    /// <summary>
501
    /// Gets the <see cref="TypeAccessor"/> for the specified Type.
502
    /// </summary>
503
    /// <typeparam name="T">The Type to get the accessor for.</typeparam>
504
    /// <returns></returns>
505
    public static TypeAccessor GetAccessor<T>()
506
    {
507
        return GetAccessor(typeof(T));
×
508
    }
509

510
    /// <summary>
511
    /// Gets the <see cref="TypeAccessor"/> for the specified Type.
512
    /// </summary>
513
    /// <param name="type">The Type to get the accessor for.</param>
514
    /// <returns></returns>
515
    public static TypeAccessor GetAccessor(Type type)
516
    {
517
        return _typeCache.GetOrAdd(type, t => new TypeAccessor(t));
49✔
518
    }
519
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc