Reputation: 1099
I am trying to replace the parameter type in a lambda expression from one type to another.
I have found other answers on stackoverflow i.e. this one but I have had no luck with them.
Imagine for a second you have a domain object and a repository from which you can retrieve the domain object.
however the repository has to deal with its own Data transfer objects and then map and return domain objects:
ColourDto.cs
public class DtoColour {
public DtoColour(string name)
{
Name = name;
}
public string Name { get; set; }
}
DomainColour.cs
public class DomainColour {
public DomainColour(string name)
{
Name = name;
}
public string Name { get; set; }
}
Repository.cs
public class ColourRepository {
...
public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
{
// Context.Colours is of type ColourDto
return Context.Colours.Where(predicate).Map().ToList();
}
}
As you can see this will not work as the predicate is for the domain model and the Collection inside the repository is a collection of Data transfer objects.
I have tried to use an ExpressionVisitor
to do this but cannot figure out how to just change the type of the ParameterExpression
without an exception being thrown for example:
Test scenario
public class ColourRepository {
...
public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
{
var visitor = new MyExpressionVisitor();
var newPredicate = visitor.Visit(predicate) as Expression<Func<ColourDto, bool>>;
return Context.Colours.Where(newPredicate.Complie()).Map().ToList();
}
}
public class MyExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitParameter(ParameterExpression node)
{
return Expression.Parameter(typeof(ColourDto), node.Name);
}
}
finally here is the exception:
System.ArgumentException : Property 'System.String Name' is not defined for type 'ColourDto'
Hope someone can help.
EDIT: Here is a dotnetfiddle
still doesnt work.
Edit: Here is a working dotnetfiddle
Thanks Eli Arbel
Upvotes: 13
Views: 5039
Reputation: 1323
Eli's answer is great, but will throw
System.InvalidCastException: Unable to cast object of type
'System.Linq.Expressions.Expression1`1[System.Func`2[...,<>f__AnonymousType0`1[...]]]'
to type
'System.Linq.Expressions.Expression`1[System.Func`2[...,System.Object]]'.
when the expression body is returning anonymous type like sharplab.io:
Expression<Func<C, object?>> expr = c => new {c.A};
Console.WriteLine((Expression<Func<C, object?>>)expr);
var visitor = new ReplaceParameterTypeVisitor<C, C>();
Console.WriteLine((Expression<Func<C, object?>>)visitor.Visit(expr));
public class C
{
public int A { get; }
};
which is used widely in EF Core as selectors.
You may notice the first casting is working and marked as redundant since a delegate Func<in T, out TResult>
that returning the top type: object
is compatible with returning any types other aka convariant, but wrapping it into a class Expression<Func<in T, out TResult>>
will losing the covariant of return type:
Variance in Expression<Func<T,bool>>
So we will have to manually casting the generated anonymous type to object
like sharplab.io:
protected override Expression VisitLambda<T>(Expression<T> node)
{
_parameters = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
- return Expression.Lambda(Visit(node.Body), _parameters);
+ return Expression.Lambda(Visit(node.Body.Type.IsAnonymous()
+ // https://stackoverflow.com/questions/38316519/replace-parameter-type-in-lambda-expression/78560844#78560844
+ ? Expression.Convert(node.Body, typeof(object))
+ : node.Body), _parameters);
}
with the extension method Type.IsAnonymous()
that copied from How To Test if a Type is Anonymous?
public static class Extensions {
public static bool IsAnonymous(this Type type)
{ // https://stackoverflow.com/questions/2483023/how-to-test-if-a-type-is-anonymous
if (type == null)
throw new ArgumentNullException("type");
return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
&& type.IsGenericType && type.Name.Contains("AnonymousType")
&& (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
&& type.Attributes.HasFlag(TypeAttributes.NotPublic);
}
}
And if you are using LinqToDB they've already provided it: https://github.com/linq2db/linq2db/blob/0cb767639517d54023165780ddcdf2492268b794/Source/LinqToDB/Extensions/ReflectionExtensions.cs#L1045
Upvotes: 0
Reputation: 595
The Eli's answer is great.
But in my case, I have a lambda which have another lambda inside it. So, it sets the '_parameter' twice, overriding the old.
ex:
Expression<Func<IObjectWithCompanyUnits, bool>> expr = e => e.CompanyUnits.Any(cu => cu.ID == GetCurrentCompanyUnitId());
The Visitor breaks the expression above.
So I tweaked the original answer to my case:
public class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
private Dictionary<int, ReadOnlyCollection<ParameterExpression>> _parameters = new();
private int currentLambdaIndex = -1;
protected override Expression VisitParameter(ParameterExpression node)
{
var prms = _parameters.Count > currentLambdaIndex ? _parameters[currentLambdaIndex] : null;
var p = prms?.FirstOrDefault(p => p.Name == node.Name);
if (p != null)
{
return p;
}
else
{
if (node.Type == typeof(TSource))
{
return Expression.Parameter(typeof(TTarget), node.Name);
}
else
{
return node;
}
}
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
currentLambdaIndex++;
try
{
_parameters[currentLambdaIndex] = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
return Expression.Lambda(Visit(node.Body), _parameters[currentLambdaIndex]);
}
finally
{
currentLambdaIndex--;
}
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Member.DeclaringType == typeof(TSource))
{
return Expression.Property(Visit(node.Expression), node.Member.Name);
}
return base.VisitMember(node);
}
}
Upvotes: 0
Reputation: 22739
You need to do a few things for this to work:
Expression.Lambda
and anywhere they appear in the body - and use the same instance for both.Here's the code, with added generics:
public static Func<TTarget, bool> Convert<TSource, TTarget>(Expression<Func<TSource, bool>> root)
{
var visitor = new ParameterTypeVisitor<TSource, TTarget>();
var expression = (Expression<Func<TTarget, bool>>)visitor.Visit(root);
return expression.Compile();
}
public class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
{
private ReadOnlyCollection<ParameterExpression> _parameters;
protected override Expression VisitParameter(ParameterExpression node)
{
return _parameters?.FirstOrDefault(p => p.Name == node.Name) ??
(node.Type == typeof(TSource) ? Expression.Parameter(typeof(TTarget), node.Name) : node);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
_parameters = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
return Expression.Lambda(Visit(node.Body), _parameters);
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Member.DeclaringType == typeof(TSource))
{
return Expression.Property(Visit(node.Expression), node.Member.Name);
}
return base.VisitMember(node);
}
}
Upvotes: 18
Reputation: 887215
Properties are defined separately for each type.
That error happens because you can't get the value of a property defined by DomainColour
from a value of type ColourDto
.
You need to visit every MemberExpression
that uses the parameter and return a new MemberExpression
that uses that property from the new type.
Upvotes: 1