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

loresoft / FluentCommand / 18517443474

15 Oct 2025 04:06AM UTC coverage: 55.189% (-0.04%) from 55.225%
18517443474

push

github

pwelter34
update tests

1736 of 3650 branches covered (47.56%)

Branch coverage included in aggregate %.

4390 of 7450 relevant lines covered (58.93%)

210.23 hits per line

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

32.43
/src/FluentCommand/Reflection/TypeAccessor.cs
1
using System.Collections.Concurrent;
2
using System.ComponentModel.DataAnnotations.Schema;
3
using System.Linq.Expressions;
4
using System.Reflection;
5

6
namespace FluentCommand.Reflection;
7

8
/// <summary>
9
/// Provides efficient access to type reflection information, including dynamic creation, property, field, and method accessors,
10
/// and metadata such as table mapping. Caches accessors for performance and supports late-bound operations on types.
11
/// </summary>
12
public class TypeAccessor
13
{
14
    private static readonly ConcurrentDictionary<Type, TypeAccessor> _typeCache = new();
4✔
15
    private readonly ConcurrentDictionary<string, IMemberAccessor> _memberCache = new();
24✔
16
    private readonly ConcurrentDictionary<int, IMethodAccessor> _methodCache = new();
24✔
17
    private readonly ConcurrentDictionary<int, IEnumerable<IMemberAccessor>> _propertyCache = new();
24✔
18

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

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

32
        Type = type;
24✔
33
        _constructor = new Lazy<Func<object>>(() => ExpressionFactory.CreateConstructor(Type));
24✔
34
        _tableAttribute = new Lazy<TableAttribute>(() => type.GetCustomAttribute<TableAttribute>(true));
46✔
35
    }
24✔
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; }
67✔
42

43
    /// <summary>
44
    /// Gets the name of the type.
45
    /// </summary>
46
    /// <value>The name of the type.</value>
47
    public string Name => Type.Name;
6✔
48

49
    /// <summary>
50
    /// Gets the name of the table the class is mapped to, as specified by the <see cref="TableAttribute"/>.
51
    /// If not specified, returns the type name.
52
    /// </summary>
53
    /// <value>The name of the mapped table.</value>
54
    public string TableName => _tableAttribute.Value?.Name ?? Type.Name;
110✔
55

56
    /// <summary>
57
    /// Gets the schema of the table the class is mapped to, as specified by the <see cref="TableAttribute"/>.
58
    /// </summary>
59
    /// <value>The schema of the mapped table, or <c>null</c> if not specified.</value>
60
    public string TableSchema => _tableAttribute.Value?.Schema;
139✔
61

62
    /// <summary>
63
    /// Creates a new instance of the type represented by this accessor using the default constructor.
64
    /// </summary>
65
    /// <returns>A new instance of the type.</returns>
66
    /// <exception cref="InvalidOperationException">Thrown if a parameterless constructor is not found.</exception>
67
    public object Create()
68
    {
69
        var constructor = _constructor.Value;
×
70
        if (constructor == null)
×
71
            throw new InvalidOperationException($"Could not find constructor for '{Type.Name}'.");
×
72

73
        return constructor.Invoke();
×
74
    }
75

76
    #region Method
77

78
    /// <summary>
79
    /// Finds a method with the specified <paramref name="name"/> and no parameters.
80
    /// </summary>
81
    /// <param name="name">The name of the method.</param>
82
    /// <returns>An <see cref="IMethodAccessor"/> for the method, or <c>null</c> if not found.</returns>
83
    public IMethodAccessor FindMethod(string name)
84
    {
85
        return FindMethod(name, Type.EmptyTypes);
×
86
    }
87

88
    /// <summary>
89
    /// Finds a method with the specified <paramref name="name"/> and parameter types.
90
    /// </summary>
91
    /// <param name="name">The name of the method.</param>
92
    /// <param name="parameterTypes">The method parameter types.</param>
93
    /// <returns>An <see cref="IMethodAccessor"/> for the method, or <c>null</c> if not found.</returns>
94
    public IMethodAccessor FindMethod(string name, params Type[] parameterTypes)
95
    {
96
        return FindMethod(name, parameterTypes, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
×
97
    }
98

99
    /// <summary>
100
    /// Finds a method with the specified <paramref name="name"/>, parameter types, and binding flags.
101
    /// </summary>
102
    /// <param name="name">The name of the method.</param>
103
    /// <param name="parameterTypes">The method parameter types.</param>
104
    /// <param name="flags">The binding flags to use for the search.</param>
105
    /// <returns>An <see cref="IMethodAccessor"/> for the method, or <c>null</c> if not found.</returns>
106
    public IMethodAccessor FindMethod(string name, Type[] parameterTypes, BindingFlags flags)
107
    {
108
        int key = MethodAccessor.GetKey(name, parameterTypes);
×
109
        return _methodCache.GetOrAdd(key, n => CreateMethodAccessor(name, parameterTypes, flags));
×
110
    }
111

112
    private IMethodAccessor CreateMethodAccessor(string name, Type[] parameters, BindingFlags flags)
113
    {
114
        var info = FindMethod(Type, name, parameters, flags);
×
115
        return info == null ? null : CreateAccessor(info);
×
116
    }
117

118
    private static MethodInfo FindMethod(Type type, string name, Type[] parameterTypes, BindingFlags flags)
119
    {
120
        if (type == null)
×
121
            throw new ArgumentNullException(nameof(type));
×
122
        if (name == null)
×
123
            throw new ArgumentNullException(nameof(name));
×
124

125
        if (parameterTypes == null)
×
126
            parameterTypes = Type.EmptyTypes;
×
127

128
        var typeInfo = type.GetTypeInfo();
×
129

130
        //first try full match
131
        var methodInfo = typeInfo.GetMethod(name, parameterTypes);
×
132
        if (methodInfo != null)
×
133
            return methodInfo;
×
134

135
        // next, get all that match by name
136
        var methodsByName = typeInfo.GetMethods(flags)
×
137
          .Where(m => m.Name == name)
×
138
          .ToList();
×
139

140
        if (methodsByName.Count == 0)
×
141
            return null;
×
142

143
        // if only one matches name, return it
144
        if (methodsByName.Count == 1)
×
145
            return methodsByName.FirstOrDefault();
×
146

147
        // next, get all methods that match param count
148
        var methodsByParamCount = methodsByName
×
149
            .Where(m => m.GetParameters().Length == parameterTypes.Length)
×
150
            .ToList();
×
151

152
        // if only one matches with same param count, return it
153
        if (methodsByParamCount.Count == 1)
×
154
            return methodsByParamCount.FirstOrDefault();
×
155

156
        // still no match, make best guess by greatest matching param types
157
        MethodInfo current = methodsByParamCount.FirstOrDefault();
×
158
        int matchCount = 0;
×
159

160
        foreach (var info in methodsByParamCount)
×
161
        {
162
            var paramTypes = info.GetParameters()
×
163
                .Select(p => p.ParameterType)
×
164
                .ToArray();
×
165

166
            // unsure which way IsAssignableFrom should be checked?
167
            int count = paramTypes
×
168
                .Select(t => t.GetTypeInfo())
×
169
                .Where((t, i) => t.IsAssignableFrom(parameterTypes[i]))
×
170
                .Count();
×
171

172
            if (count <= matchCount)
×
173
                continue;
174

175
            current = info;
×
176
            matchCount = count;
×
177
        }
178

179
        return current;
×
180
    }
181

182
    private static IMethodAccessor CreateAccessor(MethodInfo methodInfo)
183
    {
184
        return methodInfo == null ? null : new MethodAccessor(methodInfo);
×
185
    }
186
    #endregion
187

188
    #region Find
189

190
    /// <summary>
191
    /// Searches for the public property or field with the specified name.
192
    /// </summary>
193
    /// <param name="name">The name of the property or field to find.</param>
194
    /// <returns>An <see cref="IMemberAccessor"/> for the property or field if found; otherwise, <c>null</c>.</returns>
195
    public IMemberAccessor Find(string name)
196
    {
197
        return Find(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
×
198
    }
199

200
    /// <summary>
201
    /// Searches for the specified property or field using the specified binding flags.
202
    /// </summary>
203
    /// <param name="name">The name of the property or field to find.</param>
204
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
205
    /// <returns>An <see cref="IMemberAccessor"/> for the property or field if found; otherwise, <c>null</c>.</returns>
206
    public IMemberAccessor Find(string name, BindingFlags flags)
207
    {
208
        return _memberCache.GetOrAdd(name, n => CreateAccessor(n, flags));
×
209
    }
210

211
    private IMemberAccessor CreateAccessor(string name, BindingFlags flags)
212
    {
213
        // first try property
214
        var property = FindProperty(Type, name, flags);
×
215
        if (property != null)
×
216
            return CreateAccessor(property);
×
217

218
        // next try field
219
        var field = FindField(Type, name, flags);
×
220
        if (field != null)
×
221
            return CreateAccessor(field);
×
222

223
        return null;
×
224
    }
225
    #endregion
226

227
    #region Column
228
    /// <summary>
229
    /// Searches for the public property with the specified column name.
230
    /// </summary>
231
    /// <param name="name">The name of the property or field to find.</param>
232
    /// <returns>An <see cref="IMemberAccessor"/> instance for the property or field if found; otherwise <c>null</c>.</returns>
233
    public IMemberAccessor FindColumn(string name)
234
    {
235
        return FindColumn(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
2✔
236
    }
237

238
    /// <summary>
239
    /// Searches for the property with the specified column name and binding flags, using <see cref="ColumnAttribute"/> if present.
240
    /// </summary>
241
    /// <param name="name">The name of the property or column to find.</param>
242
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
243
    /// <returns>An <see cref="IMemberAccessor"/> for the property if found; otherwise, <c>null</c>.</returns>
244
    public IMemberAccessor FindColumn(string name, BindingFlags flags)
245
    {
246
        return _memberCache.GetOrAdd(name, n => CreateColumnAccessor(n, flags));
3✔
247
    }
248

249
    private IMemberAccessor CreateColumnAccessor(string name, BindingFlags flags)
250
    {
251
        var typeInfo = Type.GetTypeInfo();
1✔
252

253
        foreach (var p in typeInfo.GetProperties(flags))
3!
254
        {
255
            // try ColumnAttribute
256
            var columnAttribute = p.GetCustomAttribute<System.ComponentModel.DataAnnotations.Schema.ColumnAttribute>();
1✔
257
            if (columnAttribute != null && name.Equals(columnAttribute.Name, StringComparison.OrdinalIgnoreCase))
1!
258
                return CreateAccessor(p);
1✔
259

260
            if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
×
261
                return CreateAccessor(p);
×
262
        }
263

264
        return null;
×
265
    }
266
    #endregion
267

268
    #region Property
269
    /// <summary>
270
    /// Searches for the property using a property expression.
271
    /// </summary>
272
    /// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
273
    /// <param name="propertyExpression">The property expression (e.g. <c>p =&gt; p.PropertyName</c>).</param>
274
    /// <returns>An <see cref="IMemberAccessor"/> for the property if found; otherwise, <c>null</c>.</returns>
275
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="propertyExpression"/> is <c>null</c>.</exception>
276
    /// <exception cref="ArgumentException">Thrown if the expression is not a valid property access expression.</exception>
277
    public IMemberAccessor FindProperty<T>(Expression<Func<T>> propertyExpression)
278
    {
279
        if (propertyExpression == null)
×
280
            throw new ArgumentNullException(nameof(propertyExpression));
×
281

282
        if (propertyExpression.Body is UnaryExpression unaryExpression)
×
283
            return FindProperty(unaryExpression.Operand as MemberExpression);
×
284
        else
285
            return FindProperty(propertyExpression.Body as MemberExpression);
×
286
    }
287

288
    /// <summary>
289
    /// Searches for the property using a property expression.
290
    /// </summary>
291
    /// <typeparam name="TSource">The object type containing the property specified in the expression.</typeparam>
292
    /// <typeparam name="TValue">The type of the value.</typeparam>
293
    /// <param name="propertyExpression">The property expression (e.g. <c>p =&gt; p.PropertyName</c>).</param>
294
    /// <returns>An <see cref="IMemberAccessor"/> for the property if found; otherwise, <c>null</c>.</returns>
295
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="propertyExpression"/> is <c>null</c>.</exception>
296
    /// <exception cref="ArgumentException">Thrown if the expression is not a valid property access expression.</exception>
297
    public IMemberAccessor FindProperty<TSource, TValue>(Expression<Func<TSource, TValue>> propertyExpression)
298
    {
299
        if (propertyExpression == null)
202!
300
            throw new ArgumentNullException(nameof(propertyExpression));
×
301

302
        if (propertyExpression.Body is UnaryExpression unaryExpression)
202✔
303
            return FindProperty(unaryExpression.Operand as MemberExpression);
10✔
304
        else
305
            return FindProperty(propertyExpression.Body as MemberExpression);
192✔
306
    }
307

308
    /// <summary>
309
    /// Searches for the public property with the specified name.
310
    /// </summary>
311
    /// <param name="name">The name of the property to find.</param>
312
    /// <returns>An <see cref="IMemberAccessor"/> for the property if found; otherwise, <c>null</c>.</returns>
313
    public IMemberAccessor FindProperty(string name)
314
    {
315
        return FindProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
202✔
316
    }
317

318
    /// <summary>
319
    /// Searches for the property with the specified name and binding flags.
320
    /// </summary>
321
    /// <param name="name">The name of the property to find.</param>
322
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
323
    /// <returns>An <see cref="IMemberAccessor"/> for the property if found; otherwise, <c>null</c>.</returns>
324
    public IMemberAccessor FindProperty(string name, BindingFlags flags)
325
    {
326
        return _memberCache.GetOrAdd(name, n => CreatePropertyAccessor(n, flags));
251✔
327
    }
328

329
    /// <summary>
330
    /// Gets the property member accessors for the type.
331
    /// </summary>
332
    /// <returns>An <see cref="IEnumerable{IMemberAccessor}"/> for the type's properties.</returns>
333
    public IEnumerable<IMemberAccessor> GetProperties()
334
    {
335
        return GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
53✔
336
    }
337

338
    /// <summary>
339
    /// Gets the property member accessors for the type using the specified binding flags.
340
    /// </summary>
341
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
342
    /// <returns>An <see cref="IEnumerable{IMemberAccessor}"/> for the type's properties.</returns>
343
    public IEnumerable<IMemberAccessor> GetProperties(BindingFlags flags)
344
    {
345
        return _propertyCache.GetOrAdd((int)flags, k =>
53✔
346
        {
53✔
347
            var typeInfo = Type.GetTypeInfo();
9✔
348
            var properties = typeInfo.GetProperties(flags);
9✔
349
            return properties.Select(GetAccessor);
9✔
350
        });
53✔
351
    }
352

353

354
    private IMemberAccessor GetAccessor(PropertyInfo propertyInfo)
355
    {
356
        if (propertyInfo == null)
614!
357
            throw new ArgumentNullException(nameof(propertyInfo));
×
358

359
        return _memberCache.GetOrAdd(propertyInfo.Name, n => CreateAccessor(propertyInfo));
740✔
360
    }
361

362
    private IMemberAccessor CreatePropertyAccessor(string name, BindingFlags flags)
363
    {
364
        var info = FindProperty(Type, name, flags);
49✔
365
        return info == null ? null : CreateAccessor(info);
49!
366
    }
367

368
    private IMemberAccessor FindProperty(MemberExpression memberExpression)
369
    {
370
        if (memberExpression == null)
202!
371
            throw new ArgumentException("The expression is not a member access expression.", nameof(memberExpression));
×
372

373
        var property = memberExpression.Member as PropertyInfo;
202✔
374
        if (property == null)
202!
375
            throw new ArgumentException("The member access expression does not access a property.", nameof(memberExpression));
×
376

377
        // find by name because we can't trust the PropertyInfo here as it could be from an interface or inherited class
378
        return FindProperty(property.Name);
202✔
379
    }
380

381
    private static PropertyInfo FindProperty(Type type, string name, BindingFlags flags)
382
    {
383
        if (type == null)
49!
384
            throw new ArgumentNullException(nameof(type));
×
385

386
        if (name == null)
49!
387
            throw new ArgumentNullException(nameof(name));
×
388

389
        var typeInfo = type.GetTypeInfo();
49✔
390
        // first try GetProperty
391
        var property = typeInfo.GetProperty(name, flags);
49✔
392
        if (property != null)
49!
393
            return property;
49✔
394

395
        // if not found, search while ignoring case
396
        property = typeInfo
×
397
            .GetProperties(flags)
×
398
            .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
×
399

400
        return property;
×
401
    }
402

403
    private static IMemberAccessor CreateAccessor(PropertyInfo propertyInfo)
404
    {
405
        return propertyInfo == null ? null : new PropertyAccessor(propertyInfo);
176!
406
    }
407
    #endregion
408

409
    #region Field
410
    /// <summary>
411
    /// Searches for the specified field with the specified name.
412
    /// </summary>
413
    /// <param name="name">The name of the field to find.</param>
414
    /// <returns>
415
    /// An <see cref="IMemberAccessor"/> instance for the field if found; otherwise <c>null</c>.
416
    /// </returns>
417
    public IMemberAccessor FindField(string name)
418
    {
419
        return FindField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
×
420
    }
421

422
    /// <summary>
423
    /// Searches for the field with the specified name and binding flags.
424
    /// </summary>
425
    /// <param name="name">The name of the field to find.</param>
426
    /// <param name="flags">A bitmask comprised of one or more <see cref="BindingFlags"/> that specify how the search is conducted.</param>
427
    /// <returns>An <see cref="IMemberAccessor"/> for the field if found; otherwise, <c>null</c>.</returns>
428
    public IMemberAccessor FindField(string name, BindingFlags flags)
429
    {
430
        return _memberCache.GetOrAdd(name, n => CreateFieldAccessor(n, flags));
×
431
    }
432

433
    private IMemberAccessor CreateFieldAccessor(string name, BindingFlags flags)
434
    {
435
        var info = FindField(Type, name, flags);
×
436
        return info == null ? null : CreateAccessor(info);
×
437
    }
438

439
    private static FieldInfo FindField(Type type, string name, BindingFlags flags)
440
    {
441
        if (type == null)
×
442
            throw new ArgumentNullException(nameof(type));
×
443

444
        if (name == null)
×
445
            throw new ArgumentNullException(nameof(name));
×
446

447
        // first try GetField
448
        var typeInfo = type.GetTypeInfo();
×
449
        var field = typeInfo.GetField(name, flags);
×
450
        if (field != null)
×
451
            return field;
×
452

453
        // if not found, search while ignoring case
454
        return typeInfo
×
455
            .GetFields(flags)
×
456
            .FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
×
457
    }
458

459
    private static IMemberAccessor CreateAccessor(FieldInfo fieldInfo)
460
    {
461
        return fieldInfo == null ? null : new FieldAccessor(fieldInfo);
×
462
    }
463
    #endregion
464

465

466
    /// <summary>
467
    /// Gets the <see cref="TypeAccessor"/> for the specified Type.
468
    /// </summary>
469
    /// <typeparam name="T">The Type to get the accessor for.</typeparam>
470
    /// <returns></returns>
471
    public static TypeAccessor GetAccessor<T>()
472
    {
473
        return GetAccessor(typeof(T));
90✔
474
    }
475

476
    /// <summary>
477
    /// Gets the <see cref="TypeAccessor"/> for the specified <see cref="Type"/>.
478
    /// </summary>
479
    /// <param name="type">The type to get the accessor for.</param>
480
    /// <returns>The <see cref="TypeAccessor"/> for the specified type.</returns>
481
    public static TypeAccessor GetAccessor(Type type)
482
    {
483
        return _typeCache.GetOrAdd(type, t => new TypeAccessor(t));
114✔
484
    }
485
}
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