Reputation: 1005
Expressions
are not really identic, but should be. They differ in one slight details. I am quite new to Expressions
, but I think this could be confusing even for quite experienced player. I refactored the code which processes some data to make Expression
used as parameter for IQueryable.Where()
. It is functionally equivalent as far as I can see.
I have here former code, which worked well, and generates perfectly functional Expression:
private Expression<Func<T, bool>> StringPropertyContains<T>(string propertyName, string value)
{
if (string.IsNullOrWhiteSpace(propertyName))
{
throw new ArgumentNullException(nameof(propertyName));
}
var param = Expression.Parameter(typeof(T));
MemberExpression member = null;
if (propertyName.Contains('/'))
{
var splittedPropertyName = propertyName.Split('/');
var propertyInfo = this.GetPropertyInfo(typeof(T), splittedPropertyName.First());
member = Expression.MakeMemberAccess(param, propertyInfo);
for (int i = 1; i < splittedPropertyName.Length; i++)
{
if (propertyInfo.PropertyType.IsInterface)
{
//specifically for IActorWithExtraDetails -> reason to refactor
if (typeof(IActor).IsAssignableFrom(propertyInfo.PropertyType) && typeof(IActor).GetProperties().FirstOrDefault(pi => pi.Name.Equals(splittedPropertyName[i], StringComparison.OrdinalIgnoreCase)) != null)
{
propertyInfo = this.GetPropertyInfo(typeof(IActor), splittedPropertyName[i]);
}
else
{
propertyInfo = this.GetPropertyInfo(propertyInfo.PropertyType, splittedPropertyName[i]);
}
}
else
{
propertyInfo = this.GetPropertyInfo(propertyInfo.PropertyType, splittedPropertyName[i]);
}
}
member = Expression.MakeMemberAccess(member, propertyInfo);
}
else
{
var propertyInfo = this.GetPropertyInfo(typeof(T), propertyName);
member = Expression.MakeMemberAccess(param, propertyInfo);
}
var constant = Expression.Constant(value, typeof(string));
var methodInfo = typeof(string).GetMethod("Contains", new Type[] { typeof(string) });
var body = Expression.Call(member, methodInfo, constant);
return Expression.Lambda<Func<T, bool>>(body, param);
}
This is how it looks in DebugView
property of the IQueryable
:
.Lambda #Lambda2<System.Func`2[AccessManagement.Model.Application,System.Boolean]>(AccessManagement.Model.Application $var1)
{
.Call ($var1.Name).Contains("hive")
}
Here is new, refactored code moved to own method:
private Expression<Func<T, bool>> StringPropertyContains<T>(string propertyName, string value)
{
if (string.IsNullOrWhiteSpace(propertyName))
{
throw new ArgumentNullException(nameof(propertyName));
}
var param = Expression.Parameter(typeof(T));
MemberExpression member = this.GetMemberExpression(typeof(T), propertyName.Trim('/').Split('/'));
var constant = Expression.Constant(value, typeof(string));
var methodInfo = typeof(string).GetMethod("Contains", new Type[] { typeof(string) });
var body = Expression.Call(member, methodInfo, constant);
return Expression.Lambda<Func<T, bool>>(body, param);
}
private MemberExpression GetMemberExpression(Type baseType, string[] path)
{
MemberExpression result = null;
Type type = baseType;
PropertyInfo propertyInfo = null;
foreach (string segment in path)
{
//if type is interface, just spray and pray
if (type.IsInterface)
{
propertyInfo = this.GetDescendantProperties(type)
.FirstOrDefault(pi => pi.Name.Equals(segment, StringComparison.OrdinalIgnoreCase));
}
else
{
propertyInfo = this.GetPropertyInfo(type, segment);
}
if (propertyInfo == null)
{
throw new ArgumentNullException(nameof(propertyInfo));
}
result =
result == null ?
Expression.MakeMemberAccess(Expression.Parameter(baseType), propertyInfo) :
Expression.MakeMemberAccess(result, propertyInfo);
}
return result;
}
This is how expression from refactored method looks like in DebugView
:
.Lambda #Lambda2<System.Func`2[AccessManagement.Model.Application,System.Boolean]>(AccessManagement.Model.Application $var1)
{
.Call ($var2.Name).Contains("hive")
}
There is only one difference. As you can see, there is $var2
in second case, not $var1
. This variable doesn't exist in whole expression tree. I have no idea why, but I'd bet that is the issue, because everything else remains same. Only other difference is that within second case Expression
processing is something cachced in member.RuntimeMethodInfo.base.m_cachedData
(debug view path).
Upvotes: 3
Views: 267
Reputation: 51330
In your first code snippet, you're declaring a parameter:
var param = Expression.Parameter(typeof(T));
This is used as the lambda parameter, and is used in the code here:
Expression.MakeMemberAccess(param, propertyInfo);
(this is called twice actually). So the code makes use of the parameter passed to the lambda.
In your second code snippet, you're still using param
as the parameter to the lambda, but then you don't use it anywhere in the lambda body.
You're calling this instead:
Expression.MakeMemberAccess(Expression.Parameter(baseType), propertyInfo)
That Expression.Parameter(baseType)
creates a second and unrelated variable, which never gets an actual value assigned to it. You should have used the param
reference here.
That's where $var2
comes from. $var1
is the param
reference.
Consider using the Expression.Parameter(Type, string)
overload next time, which lets you name your parameters for debugging purposes. It'll be easier to reason about.
Upvotes: 2