Expression.Call and Count

2019-06-26 07:28发布

问题:

I'm looking for a way to do following dynamically:

var q = context.Subscription
               .Include("Client")
               .Include("Invoices")
                Where(s=>s.Client.Invoices.Count(i=>i.InvoiceID == SomeInt) > 0);

I would like to build expression dynamically for the left side:

Expression left = s => s.Client.Invoices.Count(i => i.InvoiceID == iSomeVar); //!
Expression right = Expression.Constant(0);
var binary = Expression.GreaterThan(left, right);

Thanks!

UPDATED NOTES:

Please note: The end result must be

Expression<Func<T, bool>>

Simple version:

// To give clear idea, all what I want to achieve is to determine 
// whether specific record exists in reference table using known Path. 
// Ultimately I want to extend following function (which works great by 
// the way, but for simple operations)

static Expression CreateExpression<T>(string propertyPath, 
                                      object propertyValue, 
                                      ParameterExpression parameterExpression)
{
     PropertyInfo property = typeof(T).GetProperty(propertyName);
     MemberExpression left = Expression.Property(parameterExpression, property);
     ConstantExpression right = Expression.Constant(0);
     BinaryExpression binary = Expression.GreaterThan(left, right);

     return binary;
}

// And I want to call this function and get result exactly as shown below:

Expression result = 
           CreateExpression<Subscription>("Client.Invoices.InvoiceID", 
                                          theID,
                                          valueSelector.Parameters.Single());

// Where result will be: 
//       t => t.Client.Invoices.Count(i => i.InvoiceID == theID) > 0;

Extended version:

// 1) I'm using Silverlight 4, EF, RIA.

// 2) At the server side I have a function GetSubscriptionsByCriteria
//   that looks about it:

public IQueryable<Subscription> GetSubscriptionsByCriteria(...)
{
      var query = this.ObjectContext.Subscriptions.Include("Client")
                                                  .Include("Client.Invoices");
      var criteria = BuildCriteria(...);
      return query.Where(criteria)
}

// 3) BuildCriteria(...) function gathers Expressions and 
//    aggregates it into the single Expression with different 
//    AND/OR conditions, something like that:

public Expression<Func<Subscription, bool>> BuildCriteria(
                      List<SearchFilter> filters,
                      Expression<Func<Subscription, bool>> valueSelector)
{
    List<Expression> filterExpressions = new List<Expression>();
    ...
    Expression expr = CreateExpression<Subscription>(
                                   sfItem.DBPropertyName, 
                                   sfItem.DBPropertyValue, 
                                   paramExpression, 
                                   sf.SearchCondition);
    filterExpressions.Add(expr);
    ...

    var filterBody = 
        filterExpressions.Aggregate<Expression>(
                (accumulate, equal) => Expression.And(accumulate, equal));
   return Expression
           .Lambda<Func<Subscription, bool>>(filterBody, paramExpression);
}

// 4) Here is the simplified version of CreateExpression function:

 static Expression CreateExpression<T>(string propertyName, 
                                       object propertyValue, 
                                       ParameterExpression paramExpression)
 {
        PropertyInfo property = typeof(T).GetProperty(propertyName);
        ConstantExpression right = Expression.Constant(0);
        MemberExpression left = Expression.Property(paramExpression, property);

        return binary = Expression.Equals(left, right);
 }

So, I hope it's clear now why do I need Expression for the left side in my original post. Trying to make this as DRY as possible.

P.S. Not to make it too confusing here is why I think I need to do ёExpression.Call(...)ё: When I run following code and break it to see DebugView I notice this:

Expression<Func<Subscription, bool>> predicate = 
           t => t.Client.Invoices.Count(i => i.InvoiceID == 5) > 0;
BinaryExpression eq = (BinaryExpression)predicate.Body;
var left = eq.Left; // <-- See DEBUG VIEW
var right = eq.Right;   

// DEBUG VIEW:
// Arguments: Count = 2
//            [0] = {t.Client.Invoices}
//            [1] = {i => (i.InvoiceID == 5)}
// DebugView: ".Call System.Linq.Enumerable.Count(
//               ($t.Client).ClientInvoices,
//               .Lambda#Lambda1<System.Func`2[SLApp.Web.Invoice,System.Boolean]>)
//               .Lambda#Lambda1<System.Func`2[SLApp.Web.Invoice,System.Boolean]>
//               (SLApp.Web.ClientInvoice $i){ $i.ClientInvoiceID == 5 }"

回答1:

Here's a working program that does what I think you'd like. It defines a function that takes a path to an integer property inside a collection, and an integer value. It then checks whether or not that collection has Count > 0 of that value.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections;

namespace Test_Console
{
    public class Subscription
    {
        public int Id { get; set; }
        public Client Client { get; set; }
    }

    public class Client
    {
        public ICollection<Invoice> Invoices { get; set; }
    }

    public class Invoice
    {
        public int Id { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var subscriptions = new[]
            {
                new Subscription { Id = 1, Client = new Client { Invoices = new [] {
                    new Invoice { Id = 1 },
                    new Invoice { Id = 2 },
                    new Invoice { Id = 5 }
                } } },
                new Subscription { Id = 2, Client = new Client { Invoices = new [] {
                    new Invoice { Id = 4 },
                    new Invoice { Id = 5 },
                    new Invoice { Id = 5 }
                } } },
                new Subscription { Id = 3, Client = new Client { Invoices = new Invoice[] {
                } } },
            };

            var propertyPath = "Client.Invoices.Id";
            Console.WriteLine("What Id would you like to check " + propertyPath + " for?");
            var propertyValue = int.Parse(Console.ReadLine());
            var whereNumberOne = makeWhere<Subscription>(propertyPath, propertyValue);

            Console.WriteLine("The following Subscription objects match:");
            foreach (var s in subscriptions.Where(whereNumberOne).ToList())
            {
                Console.WriteLine("Id: " + s.Id);
            }
        }

        private static Func<T, bool> makeWhere<T>(string propertyPath, int propertyValue)
        {
            string[] navigateProperties = propertyPath.Split('.');

            var currentType = typeof(T);
            var functoidChain = new List<Func<object, object>>();
            functoidChain.Add(x => x);  // identity function starts the chain
            foreach (var nextProperty in navigateProperties)
            {
                // must be inside loop so the closer on the functoids works properly
                PropertyInfo nextPropertyInfo;

                if (currentType.IsGenericType
                 && currentType.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable)))
                {
                    nextPropertyInfo = currentType.GetGenericArguments()[0].GetProperty(nextProperty);
                    functoidChain.Add(x =>
                        ((IEnumerable<object>)x)
                        .Count(y => (int)nextPropertyInfo.GetValue(y, null) == propertyValue)
                    );
                }
                else
                {
                    nextPropertyInfo = currentType.GetProperty(nextProperty);
                    functoidChain.Add(x => nextPropertyInfo.GetValue(x, null));
                }
                currentType = nextPropertyInfo.PropertyType;
            }
            // compose the functions together
            var composedFunctoidChain = functoidChain.Aggregate((f, g) => x => g(f(x)));
            var leftSide = new Func<T, int>(x => (int)composedFunctoidChain(x));
            return new Func<T, bool>(r => leftSide(r) > 0);
        }
    }
}


回答2:

I think this should get you closer to what you're going for:

static Expression<Func<T, bool>> CreateAnyExpression<T, T2>(string propertyPath, 
                                    Expression<Func<T2, bool>> matchExpression)
{
    var type = typeof(T);
    var parameterExpression = Expression.Parameter(type, "s");
    var propertyNames = propertyPath.Split('.');
    Expression propBase = parameterExpression;
    foreach(var propertyName in propertyNames)
    {
        PropertyInfo property = type.GetProperty(propertyName);
        propBase = Expression.Property(propBase, property);
        type = propBase.Type;
    }
    var itemType = type.GetGenericArguments()[0];
    // .Any(...) is better than .Count(...) > 0
    var anyMethod = typeof(Enumerable).GetMethods()
        .Single(m => m.Name == "Any" && m.GetParameters().Length == 2)
        .MakeGenericMethod(itemType);
    var callToAny = Expression.Call(anyMethod, propBase, matchExpression);
    return Expression.Lambda<Func<T, bool>>(callToAny, parameterExpression);
}

Calling it like this:

CreateAnyExpression<Subscription, Invoice>("Client.Invoices", i => i.InvoiceID == 1)

... yields the following Expression<Func<Subscription,bool>>:

s => s.Client.Invoices.Any(i => (i.InvoiceID == 1)) 


回答3:

Here's a working program building Linq Expression

{(x.Children.Count(y => y.SomeID == SomeVar) > 0)}
using System;
using System.Linq;
using System.Linq.Expressions;

namespace ExpressionTree
{
    class Program
    {
        static void Main(string[] args)
        {
            ParameterExpression foundX = Expression.Parameter(typeof(Parent), "x");
            Guid[] guids = new Guid[1] { Guid.NewGuid() };

            Expression expression = GetCountWithPredicateExpression(guids, foundX);
        }

        private static Expression GetCountWithPredicateExpression(Guid[] idsToFilter, ParameterExpression foundX)
        {
            System.Reflection.PropertyInfo childIDPropertyInfo = typeof(Child).GetProperty(nameof(Child.SomeID));
            ParameterExpression foundY = Expression.Parameter(typeof(Child), "y");

            Expression childIDLeft = Expression.Property(foundY, childIDPropertyInfo);
            Expression conditionExpression = Expression.Constant(false, typeof(bool));

            foreach (Guid id in idsToFilter)
                conditionExpression = Expression.Or(conditionExpression, Expression.Equal(childIDLeft, Expression.Constant(id)));

            Expression<Func<Child, bool>> idLambda = Expression.Lambda<Func<Child, bool>>(conditionExpression, foundY);

            var countMethod = typeof(Enumerable).GetMethods()
                .First(method => method.Name == "Count" && method.GetParameters().Length == 2)
                .MakeGenericMethod(typeof(Child));

            System.Reflection.PropertyInfo childrenPropertyInfo = typeof(Parent).GetProperty("Children");
            Expression childrenLeft = Expression.Property(foundX, childrenPropertyInfo);

            Expression ret = Expression.GreaterThan(Expression.Call(countMethod, childrenLeft, idLambda), Expression.Constant(0));

            return ret;
        }
    }

    public class Parent
    {
        public Child[] Children { get; set; }
    }

    public class Child
    {
        public int ID { get; set; }
        public Guid SomeID { get; set; }
    }
}