Following the lead from @JL0PD I ended up converting:
t => t.Member (ie employee => employee.Name)
into:
(t, v) => {
var c = t.<Clone>$();
c.Member = v;
return c;
}
ie:
(employee, newName) => {
var c = employee.<Clone>$();
c.Name=newName;
return c;
}
Below is a full implemetation of a record Lens including caching of delegates
Note that this does not cover nested mutators so my question above still stands
static class RecLens<T, TMember> {
public static (Func<T, TMember> Selector, Func<T, TMember, T> Mutator) Get(Expression<Func<T, TMember>> expression) {
if (!IsExpressionValid(expression.Body)) throw new Exception($"Lens Invalid expression ({expression})");
// create unique cache key, calc same key for x=>x.p and y=>y.p
var exprStr = expression.Body.ToString();
var dotPos = exprStr.IndexOf(Type.Delimiter);
var cacheKey = typeof(T).FullName + '|' + (dotPos > 0 ? exprStr.Remove(0, exprStr.IndexOf(Type.Delimiter) + 1) : "root");
if (!Cache.TryGetValue(cacheKey, out var res)) {
res = (expression.Compile(), CalcMutator(expression));
Cache = Cache.Add(cacheKey, res);
}
return res;
}
// key: "{srcType.FullName}|{member}" , ie: "Test.Organization|DevelopmentDepartment.Manager"
static ImmutableDictionary<string, (Func<T, TMember>, Func<T, TMember, T>)> Cache = ImmutableDictionary<string, (Func<T, TMember>, Func<T, TMember, T>)>.Empty;
// create delegate: (t, v) => { var c=t.<Clone>$(); c.Member = v; return c; }
static Func<T, TMember, T> CalcMutator(Expression<Func<T, TMember>> expression) {
var result = Expression.Variable(typeof(T), "c");
var typeParam = Expression.Parameter(typeof(T), "t");
var valueParam = Expression.Parameter(typeof(TMember), "v");
var cloneMethod = typeof(T).GetMethod("<Clone>$");
if (cloneMethod is null) throw new Exception($"CalcMutatorNo Clone method on {typeof(T)}");
var cloneCall = Expression.Call(typeParam, cloneMethod);
var assignResult = Expression.Assign(result, cloneCall);
var memberInfo = (expression.Body as MemberExpression)!.Member;
var resultMemberAccess = Expression.MakeMemberAccess(result, memberInfo);
var assign = Expression.Assign(resultMemberAccess, valueParam);
var block = Expression.Block(new[] { result }, assignResult, assign, result);
var assignLambda = (Expression<Func<T, TMember, T>>)Expression.Lambda(block, typeParam, valueParam);
return assignLambda.Compile();
}
// verify that expr is a member expression of its parameter
static bool IsExpressionValid(Expression expr, bool first = true) {
if (expr is ParameterExpression) return !first;
if (expr is MemberExpression memberExpr && memberExpr.Expression is object) return IsExpressionValid(memberExpr.Expression, false);
return false;
}
}
To use:
record Employee(string Name, int Age);
var (Selector, Mutator) = RecLens<Employee, string>.Get(e => e.Name);
var dave = new Employee("Dave", 30);
var name = Selector(dave); // "Dave"
var john = Mutator(dave, "John"); // Employee("John", 30)