Vennila
Vennila

Reputation: 31

Getting the values of the collection from Expression with Lambda - .Contains

Getting the values of the collection from Expression with Lambda - .Contains

I'm trying to get all values from the collection that contains a member in IQueryable. However, it seems that different collections behave differently.

Consider the following listing:

 var sampleData = new List<MyClass> { new MyClass { MyMember = "a" }, new MyClass { MyMember = "b" }, new MyClass { MyMember = "c" } };
 IEnumerable<string> samplEnumerable = new List<string> { "b", "a" };
 List<string> sampleList = new List<string> { "b", "a" };
 var resultTest1 = sampleData.AsQueryable().GetCalledMemberAndValidValues(x => (new[] { "b", "a" }).Contains(x.MyMember));
 var resultTest2 = sampleData.AsQueryable().GetCalledMemberAndValidValues(x => sampleList.Contains(x.MyMember));
 var resultTest3 = sampleData.AsQueryable().GetCalledMemberAndValidValues(x => samplEnumerable.Contains(x.MyMember));

Extension Method

public static class ExtensionMethods
{
   public static dynamic GetCalledMemberAndValidValues<T>(this IQueryable<T> query, Expression<Func<T, bool>> predicate)
    {
      string parameterName = string.Empty;
      ArrayList values = new ArrayList();
  MethodCallExpression node = (MethodCallExpression)predicate.Body;

  if (predicate.Body.NodeType == ExpressionType.Call)
  {
    if ((node.Method.Name == "Contains"))
    {
      Type valueType = null;
      foreach (var obj in node.Arguments)
      {
        if (obj.NodeType == ExpressionType.MemberAccess && ((MemberExpression)obj).Expression.NodeType == ExpressionType.Parameter)
        {
          parameterName = ((MemberExpression)obj).Member.Name;
        }

        // The below block is valid for array[];
        if (obj.NodeType == ExpressionType.NewArrayInit)
        {
          values.AddRange(((NewArrayExpression)obj).Expressions);
        }

        // The below block is valid for IEnumerable<T>;
        if (obj.NodeType == ExpressionType.MemberAccess && ((MemberExpression)obj).Expression.NodeType == ExpressionType.Constant)
        {
          var value = (((MemberExpression) obj).Member as FieldInfo).GetValue(((obj as MemberExpression).Expression as ConstantExpression).Value);

          values.AddRange((ICollection)value);
        }
      }

      // The below block is valid for List<T>;
      if ((predicate.Body as MethodCallExpression).Object != null)
      {
        var obj = (MemberExpression)(predicate.Body as MethodCallExpression).Object;
        var value = (obj.Member as FieldInfo).GetValue((obj.Expression as ConstantExpression).Value);
        values.AddRange((ICollection)value);
      }
    }
  }

  return new { parameterName, values };
}
}

MethodCallExpression object stores the property name MyMember in the field Arguments and the value gets stored in the field Object.

MethodCallExpression object stores both the property name MyMemberand its value in the field Arguments.

As you can see, all three examples produce the same result.

However, i am not happy with having to deal with each collection type differently.

How to generalise this and get the property name and value from the same field of the MethodCallExpression?

Upvotes: 3

Views: 1249

Answers (1)

Rob
Rob

Reputation: 27357

This will get it done:

public static dynamic GetCalledMemberAndValidValues<T>(this IQueryable<T> query, Expression<Func<T, bool>> predicate)
{
    var methodCall = predicate.Body as MethodCallExpression;
    Expression collectionExpression = null;
    MemberExpression memberExpression = null;
    if (methodCall != null && methodCall.Method.Name == "Contains")
    {
        if (methodCall.Method.DeclaringType == typeof(Enumerable))
        {
            collectionExpression = methodCall.Arguments[0];
            memberExpression = methodCall.Arguments[1] as MemberExpression;
        } else {
            collectionExpression = methodCall.Object;
            memberExpression = methodCall.Arguments[0] as MemberExpression;
        }
    }

    if (collectionExpression != null && memberExpression != null)
    {
        var lambda = Expression.Lambda<Func<object>>(collectionExpression, new ParameterExpression[0]);
        var value = lambda.Compile()();
        return new { parameterName = memberExpression.Member.Name, values = value };
    } 

    return null;
}

You can't make it completely generic. These two methods:
(new[] { "b", "a" }).Contains
samplEnumerable.Contains

Are actually calling Enumerable.Contains(source, x.Member).

sampleList.Contains(

calls sampleList.Contains(x.Member).

Note the number and order of arguments. So while they look the same when writing the code, you're actually referencing different methods. This will check which contains method is being called, and then figures out which argument is the collection, and which argument is the member expression.

Upvotes: 1

Related Questions