I wanted to create a method extending IQueryable
where a user can specify in a string a property name by which he wants to distinct a collection. I want to use a logic with a HashSet
.
I basically want to emulate this code:
HashSet<TResult> set = new HashSet<TResult>();
foreach(var item in source)
{
var selectedValue = selector(item);
if (set.Add(selectedValue))
yield return item;
}
using expression trees.
This is where i got so far:
private Expression AssembleDistinctBlockExpression (IQueryable queryable, string propertyName)
{
var propInfo = queryable.ElementType.GetProperty(propertyName);
if ( propInfo == null )
throw new ArgumentException();
var loopVar = Expression.Parameter(queryable.ElementType, "");
var selectedValue = Expression.Variable(propInfo.PropertyType, "selectedValue");
var returnListType = typeof(List<>).MakeGenericType(queryable.ElementType);
var returnListVar = Expression.Variable(returnListType, "return");
var returnListAssign = Expression.Assign(returnListVar, Expression.Constant(Activator.CreateInstance(typeof(List<>).MakeGenericType(queryable.ElementType))));
var hashSetType = typeof(HashSet<>).MakeGenericType(propInfo.PropertyType);
var hashSetVar = Expression.Variable(hashSetType, "set");
var hashSetAssign = Expression.Assign(hashSetVar, Expression.Constant(Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(propInfo.PropertyType))));
var enumeratorVar = Expression.Variable(typeof(IEnumerator<>).MakeGenericType(queryable.ElementType), "enumerator");
var getEnumeratorCall = Expression.Call(queryable.Expression, queryable.GetType().GetTypeInfo().GetDeclaredMethod("GetEnumerator"));
var enumeratorAssign = Expression.Assign(enumeratorVar, getEnumeratorCall);
var moveNextCall = Expression.Call(enumeratorVar, typeof(IEnumerator).GetMethod("MoveNext"));
var breakLabel = Expression.Label("loopBreak");
var loopBlock = Expression.Block(
new [] { enumeratorVar, hashSetVar, returnListVar },
enumeratorAssign,
returnListAssign,
hashSetAssign,
Expression.TryFinally(
Expression.Block(
Expression.Loop(
Expression.IfThenElse(
Expression.Equal(moveNextCall, Expression.Constant(true)),
Expression.Block(
new[] { loopVar },
Expression.Assign(loopVar, Expression.Property(enumeratorVar, "Current")),
Expression.Assign(selectedValue, Expression.MakeMemberAccess(loopVar, propInfo)),
Expression.IfThen(
Expression.Call(typeof(HashSet<>), "Add", new Type[] { propInfo.PropertyType }, hashSetVar, selectedValue),
Expression.Call(typeof(List<>), "Add", new Type[] { queryable.ElementType }, returnListVar, loopVar)
)
),
Expression.Break(breakLabel)
),
breakLabel
),
Expression.Return(breakLabel, returnListVar)
),
Expression.Block(
Expression.Call(enumeratorVar, typeof(IDisposable).GetMethod("Dispose"))
)
)
);
return loopBlock;
}
I get an exception when Expression.Block
is called for a variable loopBlock
which goes like this:
No method 'Add' exists on type 'System.Collections.Generic.HashSet`1[T]'.
The Expression.Call
method overload that you are using is for static methods.
Quoting from the reference above:
Creates a MethodCallExpression that represents a call to a static (Shared in Visual Basic) method by calling the appropriate factory method.
What you need to do is to use an overload of that method that is for calling instance methods.
Here is how the relevant part of your code would look like:
Expression.IfThen(
Expression.Call(hashSetVar, "Add", new Type[] { }, selectedValue),
Expression.Call(returnListVar, "Add", new Type[] { }, loopVar))
Notice how now we pass the instance (expression) that we need to invoke in the first parameter of Expression.Call
.
Please note also that we pass an empty type parameter list. The reason for this is that the Add
method in this class does not have any type parameters. The type parameter T
in HashSet<T>
and List<T>
is defined on the class level, not on the method level.
You would need to specify the type parameters only if they are defined on the method itself like this:
void SomeMethod<T1>(...