Created
February 6, 2024 00:25
-
-
Save SergeyTeplyakov/bc29c655a2211054e3b4517562ee70da to your computer and use it in GitHub Desktop.
MemberAccessor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace MemberAccessors | |
{ | |
using System.Collections.Concurrent; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
#nullable enable | |
/// <summary> | |
/// A helper class for accessing fields and properties of an object in a performant way. | |
/// </summary> | |
public class MemberAccessor<T> | |
{ | |
private readonly ConcurrentDictionary<string, Func<T, object>?> cache = new(); | |
/// <summary> | |
/// Tries getting a given <paramref name="fieldOrPropertyName"/> for <paramref name="instance"/>. | |
/// </summary> | |
public bool TryGetFieldOrProperty(T? instance, string fieldOrPropertyName, out object? value) | |
{ | |
if (instance is null) | |
{ | |
value = null; | |
return false; | |
} | |
var func = cache.GetOrAdd( | |
fieldOrPropertyName, | |
static (fieldOrPropertyName, instance) => | |
{ | |
// Generating the following delegate: | |
// Func<T, object> func = param => {var instanceExpr = (ActualT)param; return instanceExpr.FieldOrProp;} | |
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase; | |
var objParameterExpr = Expression.Parameter(typeof(T)); | |
var type = instance!.GetType(); | |
Expression? memberExpression = null; | |
// Generating the cast, since the 'instance' might be a derived type of T. | |
var instanceExpr = Expression.TypeAs(objParameterExpr, instance.GetType()); | |
PropertyInfo propertyInfo = type.GetProperty(fieldOrPropertyName, flags); | |
if (propertyInfo != null) | |
{ | |
memberExpression = Expression.Property(instanceExpr, propertyInfo); | |
} | |
else | |
{ | |
FieldInfo fieldInfo = type.GetField(fieldOrPropertyName, flags); | |
if (fieldInfo != null) | |
{ | |
memberExpression = Expression.Field(instanceExpr, fieldInfo); | |
} | |
} | |
if (memberExpression == null) | |
{ | |
// Field or property are not found. | |
return null; | |
} | |
return Expression.Lambda<Func<T, object>>(memberExpression, objParameterExpr).Compile(); | |
}, | |
instance); | |
if (func == null) | |
{ | |
value = null; | |
return false; | |
} | |
value = func(instance); | |
return true; | |
} | |
} | |
} | |
// Benchmark results | |
/* | |
| Method | Mean | Error | StdDev | Median | | |
|------------------------------------ |------------:|-----------:|-----------:|------------:| | |
| Field_Direct_Access | 0.2954 ns | 0.1047 ns | 0.3087 ns | 0.1817 ns | | |
| Property_Direct_Access | 0.2602 ns | 0.1002 ns | 0.2955 ns | 0.1298 ns | | |
| Property_Func_Access | 16.7612 ns | 1.2221 ns | 3.6033 ns | 17.9320 ns | | |
| MemberAccessor_Field_Access | 66.1821 ns | 3.2530 ns | 9.5915 ns | 69.1015 ns | | |
| MemberAccessor_Property_Access | 80.1140 ns | 3.4362 ns | 10.1317 ns | 81.4857 ns | | |
| MemberAccesss_Unknown_Member_Access | 58.6942 ns | 3.5154 ns | 10.3652 ns | 60.9069 ns | | |
| TryFindValue_Field | 639.8839 ns | 32.1623 ns | 94.8313 ns | 662.6848 ns | | |
| TryFindValue_Property | 459.3306 ns | 26.6561 ns | 78.5961 ns | 439.8595 ns | | |
| TryFindValue_Unknown_Member | 419.1841 ns | 30.0418 ns | 88.5788 ns | 382.3306 ns | | |
*/ | |
// Benchmarks | |
public class MyClass | |
{ | |
public readonly string Field = "Field Value"; | |
public string Property { get; } = "Property Value"; | |
} | |
public class MemberAccessBenchmarks | |
{ | |
private readonly MyClass _config = new MyClass(); | |
private readonly MemberAccessor<MyClass> _memberAccessor = new(); | |
[Benchmark] | |
public string Field_Direct_Access() => _config.Field; | |
[Benchmark] | |
public string Property_Direct_Access() => _config.Property; | |
[Benchmark] | |
public string Property_Func_Access() => ((Func<string>)(() => _config.Property))(); | |
[Benchmark] | |
public string MemberAccessor_Field_Access() | |
{ | |
_memberAccessor.TryGetFieldOrProperty(_config, nameof(MyClass.Field), out var value); | |
return value as string; | |
} | |
[Benchmark] | |
public string MemberAccessor_Property_Access() | |
{ | |
_memberAccessor.TryGetFieldOrProperty(_config, nameof(MyClass.Property), out var value); | |
return value as string; | |
} | |
[Benchmark] | |
public string MemberAccesss_Unknown_Member_Access() | |
{ | |
_memberAccessor.TryGetFieldOrProperty(_config, "Field2", out var value); | |
return value as string; | |
} | |
[Benchmark] | |
public string TryFindValue_Field() | |
{ | |
// TryFindValue uses reflection without caching. | |
TryFindValue(_config, nameof(MyClass.Field), out var value); | |
return value as string; | |
} | |
[Benchmark] | |
public string TryFindValue_Property() | |
{ | |
TryFindValue(_config, nameof(MyClass.Property), out var value); | |
return value as string; | |
} | |
[Benchmark] | |
public bool TryFindValue_Unknown_Member() | |
{ | |
return TryFindValue(_config, "Field1", out _); | |
} | |
private static bool TryFindValue(MyClass config, string FieldName, out object value) | |
{ | |
if (string.IsNullOrWhiteSpace(FieldName) || config == null) | |
{ | |
value = null; | |
return false; | |
} | |
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase; | |
Type type = config.GetType(); | |
PropertyInfo propertyInfo = type.GetProperty(FieldName, flags); | |
if (propertyInfo != null) | |
{ | |
value = propertyInfo.GetValue(config, null); | |
return true; | |
} | |
FieldInfo fieldInfo = type.GetField(FieldName, flags); | |
if (fieldInfo != null) | |
{ | |
value = fieldInfo.GetValue(config); | |
return true; | |
} | |
value = null; | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment