This is helper extension method which allows to append column to existing projection. It is useful when you need to add column to projection dynamically.
For example, if you have projection like this:
Expression<Fun<MyTable,MyDto>> projection = p => new MyDto()
{
Column1 = p.Column1,
Column2 = p.Column2,
};
var newProjection = projection.AppendProperty(p => p.Extra, "Column3");
As result you will get projection like this:
Expression<Fun<MyTable,MyDto>> newProjection = p => new MyDto()
{
Column1 = p.Column1,
Column2 = p.Column2,
Extra = p.Column3,
};
Dynamic property can be nested, path is separated by dot:
var newProjection = projection.AppendProperty(p => p.Extra, "Navigation.Column");
Function works with nested properties as well:
var newProjection = projection.AppendProperty(p => p.ExtraNested.Value, "Column3");
With nested property it will create nested object:
Expression<Fun<MyTable,MyDto>> newProjection = p => new MyDto()
{
Column1 = p.Column1,
Column2 = p.Column2,
ExtraNested = new NestedDto
{
Value = p.Column3
},
};
It is also possible to replace existing column:
var newProjection = projection.AppendProperty(p => p.Extra, "Column3");
newProjection = newProjection.AppendProperty(p => p.Extra, "Column4");
And extension method itself:
public static class ProjectionExtensions
{
public static Expression<Func<TSource, TDest>> AppendProperty<TSource, TDest>(this Expression<Func<TSource, TDest>> projection, Expression<Func<TDest, object>> destColumn, string propPath)
{
if (projection.Body is not MemberInitExpression mi)
throw new NotImplementedException($"Support of {projection.NodeType} is not implemented.");
var entityParam = projection.Parameters[0];
var destBody = UnwrapConvert(destColumn.Body);
var path = new List<MemberExpression>();
if (destBody is not MemberExpression me)
throw new InvalidOperationException($"Expected MemberExpression as {nameof(destColumn)} argument.");
var current = me;
while (true)
{
path.Insert(0, current);
if (current.Expression is MemberExpression subMember)
current = subMember;
else if (current.Expression == destColumn.Parameters[0])
break;
else
throw new InvalidOperationException($"Unexpected expression {current.Expression}");
}
var newMemberInit = mi.Update(mi.NewExpression, MergeBinding(mi.Bindings, path, 0, MakePropPath(entityParam, propPath)));
var newProjection = Expression.Lambda<Func<TSource, TDest>>(newMemberInit, projection.Parameters);
return newProjection;
}
static List<MemberBinding> MergeBinding(IEnumerable<MemberBinding> bindings, List<MemberExpression> path, int index, Expression withExpression)
{
var currentMember = path[index];
var result = new List<MemberBinding>(bindings);
MemberBinding? foundBinding = null;
for (int i = 0; i < result.Count; i++)
{
var binding = result[i];
if (binding.Member == currentMember.Member)
{
foundBinding = binding;
if (path.Count - 1 == index)
{
result[i] = Expression.Bind(currentMember.Member, withExpression);
}
else
{
if (binding is not MemberAssignment ma)
throw new InvalidOperationException("Only MemberAssignment is supported.");
if (ma.Expression is not MemberInitExpression mi)
throw new InvalidOperationException("Only MemberInit in binding is supported.");
var assignExpression = Expression.MemberInit(Expression.New(currentMember.Type),
MergeBinding(mi.Bindings, path, index + 1, withExpression));
result[i] = Expression.Bind(currentMember.Member, assignExpression);
}
break;
}
}
if (foundBinding == null)
{
if (path.Count - 1 == index)
{
result.Add(Expression.Bind(currentMember.Member, withExpression));
}
else
{
var assignExpression = Expression.MemberInit(Expression.New(currentMember.Type),
MergeBinding(Enumerable.Empty<MemberBinding>(), path, index + 1, withExpression));
result.Add(Expression.Bind(currentMember.Member, assignExpression));
}
}
return result;
}
[return: NotNullIfNotNull(nameof(ex))]
public static Expression? UnwrapConvert(Expression? ex)
{
if (ex == null)
return null;
switch (ex.NodeType)
{
case ExpressionType.ConvertChecked :
case ExpressionType.Convert :
{
if (((UnaryExpression)ex).Method == null)
return UnwrapConvert(((UnaryExpression)ex).Operand);
break;
}
}
return ex;
}
static Expression MakePropPath(Expression objExpression, string path)
{
return path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);
}
}