Search code examples
c#genericslambda

building generic property selector / setter in C# if you only know the type at runtime


I'm working with API that want expressions as parameters to identitfy/modify properties on an object. This works fine if I know the type at compile Time. E.g. the APi requires an Expression<Func<T, object>>

and I can use it using an expression like x => x.Id

But, in a generic world, I have an object obj to work with and I know the name of the property. How do I build the express Expression<Func<T, object>>?

Likewise, the API I'm dealing with also needs an expression to set a given property on an object to a given value.

The API looks like this and it's an instance method:

void Patch<T, TProperty>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value)

when I know T and how the object looks like, I can

class MyClass { internal string Id {get; set;} }
Patch<MyClass, string>("some_id", x => x.Id, "someValue");

(with the generic definition of Patch being

Patch<T, TProperty>("some_id", x => x.Id, someValue);

where x is of T and someValue is of TProperty)

But, if I don't know T and TProperty at compile time (but can determine them at runtime), I need to formulate the proper expression.

Given the APIs I'm working with, I cannot use PropertyInfo.GetValue/SetValue (for which I do have a solution)


Solution

  • First of all: You state the method signature is void Patch<T, U>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value), where T and TProperty are the meaningfull parameters, but U is an unreferenced parameter: I assume that U should be TProperty.

    Now, what I think you need is an extension method to call the Patch method specifying the target PropertyInfo or specifying its name along with the declaring type. These two alternatives are the minimum to build the expected Expression<T, TProperty>.

    Take a look at the following code:

    Probable original class definition (assuming the described Patch method comes from an interface):

    public interface IPatcher
    {
        void Patch<T, TProperty>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value);
    }
    public class PatcherClass : IPatcher
    {
        public void Patch<T, TProperty>(string id, Expression<Func<T, TProperty>> fieldPath, TProperty value)
        {
        }
    }
    

    Extension class

    public static partial class IPatcherExtensions
    {
        // Represents a callback to a convenient compiled method
        private delegate void PatchCallback(IPatcher self, string id, object? value);
    
        // Creates an instance of PatchCallback
        private static PatchCallback CreatePatchCallback(PropertyInfo info)
        {
            var parSelf = Expression.Parameter(typeof(IPatcher), "self");
            var parId = Expression.Parameter(typeof(string), "id");
            var parValue = Expression.Parameter(typeof(object), "value");
    
            var parTarget = Expression.Parameter(info.DeclaringType, "target");
            var exprGetter = Expression.Lambda(typeof(Func<,>).MakeGenericType(info.DeclaringType, info.PropertyType), Expression.Property(parTarget, info), parTarget);
    
            var exprValue = Expression.Convert(parValue, info.PropertyType);
            var body = Expression.Call(parSelf, nameof(IPatcher.Patch), new Type[] { info.DeclaringType, info.PropertyType }, parId, exprGetter, exprValue);
    
            var lambda = Expression.Lambda<PatchCallback>(body, parSelf, parId, parValue);
    
            return lambda.Compile();
        }
    
        // Caches PatchCallback instances for specific PropertyInfos
        private static readonly ConcurrentDictionary<PropertyInfo, PatchCallback> CacheForPropertyInfo = new();
        public static void Patch(this IPatcher self, string id, PropertyInfo info, object? value)
        {
            var callback = CacheForPropertyInfo.GetOrAdd(info, CreatePatchCallback);
            callback(self, id, value);
        }
    
        // Caches PatchCallback instances for pairs of Type/property names
        private static readonly ConcurrentDictionary<(Type TargetType, string PropertyName), PatchCallback> CacheForTypeAndPropertyName = new();
        public static void Patch(this IPatcher self, string id, Type targetType, string propertyName, object? value)
        {
            var callback = CacheForTypeAndPropertyName.GetOrAdd((targetType, propertyName), Factory);
            callback(self, id, value);
            static PatchCallback Factory((Type TargetType, string PropertyName) key)
            {
                var (targetType, propertyName) = key;
                var info = targetType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
                Debug.Assert(info is not null);
                return CreatePatchCallback(info);
            }
        }
    }
    

    Usage example:

    IPatcher patcher = new PatcherClass();
    
    // original
    patcher.Patch<MyClass, string>("some_id", x => x.Id, "someValue");
    
    // with type and property name
    var declaringType = typeof(MyClass);
    var propertyName = nameof(MyClass.Id);
    patcher.Patch("some_id", declaringType, propertyName, "someValue");
    
    // with property info
    var propertyInfo = typeof(MyClass).GetProperty(nameof(MyClass.Id), BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    patcher.Patch("some_id", propertyInfo, "someValue");
    

    Note that there are no sanity checks or error catching: You should implement them to intercept any invalid PropertyInfo or Type/name pairs.