Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Record 'Lenses' - expression tree for with expression

Tags:

c#

c#-9.0

Is there a way to build an expression tree for the new with operator?

I am trying to implement a 'Lens' feature for records that will only need a selector and will auto generate the mutator

My goal is to convert from a 'selector':

Expression<Func<T, TMember>> expression (ie employee => employee.Name)

To a 'mutator':

(employee, newName) => employee with { Name = newName }

I did manage to do this for the simple case above, see my answer below, however that will not work for a nested case ie:

record Employee(string Name, int Age);
record Manager(String Name, Employee Employee);

Here I want to change ie from

manager => manager.Employee.Name

to

(manager, newEmployeeName) => manager with { Employee = manager.Employee with { Name = newEmployeeName}}

Any help ?

like image 245
kofifus Avatar asked Sep 14 '25 14:09

kofifus


1 Answers

CalcMutator method that can deal with nested properties would look something like this

static Func<T, TMember, T> CalcMutator(Expression<Func<T, TMember>> expression)
{
    var typeParam = expression.Parameters.First();
    var valueParam = Expression.Parameter(typeof(TMember), "v");

    var variables = new List<ParameterExpression>();
    var blockExpressions = new List<Expression>();

    var property = (MemberExpression)expression.Body;
    Expression currentValue = valueParam;
    var index = 0;

    while (property != null)
    {
        var variable = Expression.Variable(property.Expression.Type, $"v_{index}");
        variables.Add(variable);

        var cloneMethod = property.Expression.Type.GetMethod("<Clone>$");
        if (cloneMethod is null) throw new Exception($"CalcMutatorNo Clone method on {typeof(T)}");
        var cloneCall = Expression.Call(property.Expression, cloneMethod);

        var assignClonedToVariable = Expression.Assign(variable, cloneCall);
        
        var accessVariableProperty = Expression.MakeMemberAccess(variable, property.Member);
        var assignVariablePropertyValue = Expression.Assign(accessVariableProperty, currentValue);

        blockExpressions.Add(assignClonedToVariable);
        blockExpressions.Add(assignVariablePropertyValue);

        property = property.Expression as MemberExpression;
        currentValue = variable;
        index++;
    }

    // Return root object
    blockExpressions.Add(currentValue);

    var block = Expression.Block(variables, blockExpressions);
    var assignLambda = (Expression<Func<T, TMember, T>>)Expression.Lambda(block, typeParam, valueParam);
    return assignLambda.Compile();
}

Please keep in mind that Cache implemented with ImmutableDictionary is not thread safe. If you want to ensure that the cached expressions can safely be used in multi-threaded environments, it's better to use ConcurrentDictionary for the cache instead or to apply some synchronization primitives around ImmutableDictionary.

like image 157
Andrii Litvinov Avatar answered Sep 16 '25 05:09

Andrii Litvinov