Skip to content

Instantly share code, notes, and snippets.

@SergeyTeplyakov
Created February 6, 2024 00:25
Show Gist options
  • Save SergeyTeplyakov/bc29c655a2211054e3b4517562ee70da to your computer and use it in GitHub Desktop.
Save SergeyTeplyakov/bc29c655a2211054e3b4517562ee70da to your computer and use it in GitHub Desktop.
MemberAccessor
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