2016-07-12 6 views
2

Я создаю динамический построитель отчетов, который позволяет пользователю выбирать поля из предопределенных классов (которые сопоставляются с таблицами базы данных через Entity Framework) в качестве фильтров для их данных. Чтобы создать запрос LINQ to Entities, я использую деревья выражений из-за динамического характера запросов. У меня он работает практически для всех нестандартных сценариев, но я действительно стараюсь заставить его работать для нескольких пользовательских сценариев.Expression Trees: Отфильтрованный подсчет по навигационной собственности

Сокращенный вариант моих моделей для одного из моих пользовательских запросов выглядит следующим образом:

public class Attendee { 
    public int ID { get; set; } 
    public DateTime? CancelledOn { get; set; } 

    [ForeignKey("Event")] 
    public int Event_ID { get; set; } 
    public virtual Event Event { get; set; }   
} 

public class Event { 
    public int ID { get; set; } 
    public virtual ICollection<Attendee> Attendees { get; set; }   
} 

Пример запроса, который пользователь хочет запустить, чтобы отфильтровать, чтобы показывать только те события, которые имеют более чем 10 Attendees, что не отменены. Если бы я писал это в нормальном IQueryable запроса, я хотел бы написать это как

db.Event.Where(s => s.Attendees.Count(a => a.CancelledOn == null) > 10); 

с каркасной Expression Tree Я настройка, я могу справиться с «> 10» частью уже, но я не могу понять, как динамически генерировать часть «s.Attendees.Count (a => a.CancelledOn == null)». Я прочитал сообщения SO о том, как делать Count или Sum на свойстве верхнего уровня, но я не смог взломать любое из этих решений для работы с фильтрованным навигационным свойством. Пример сообщения: Dynamic LINQ, Select function, works on Enumerable, but not Queryable

Снимок экрана ниже - это пример другого фильтра, построенного с помощью деревьев выражений, чтобы вы могли увидеть пример того, что у меня есть. «pe» - это выражение ParameterExpression для типа, который передается в «Событие». «выражение» - это то, что я пытаюсь создать и оценить. http://grab.by/RoYm

LINQ запрос выше работает в

db.Event.Where(s=> s.StartDate >= '1/1/2016 12:00 am') 

Любая помощь или направление на это было бы весьма признателен и, пожалуйста, дайте мне знать, если мне нужно, чтобы включить любые другие фрагменты кода.

+1

Какой проект это? Если это MVC/Web API, вы должны действительно рассмотреть ODATA (я сделал то же самое, что вы делали некоторое время назад, оказалось, что это больно поддерживать). –

+0

@SteveG, ASP.NET MVC. ODATA принимает строковые значения в качестве входов в фильтр, а также для выбора? Существуют сотни полей, которые пользователь может выбрать для фильтрации и отображения, а также могут создавать сложные фильтры с группировкой AND/OR'ing, всевозможные операторы (имеет значение null, пусто, равно, содержит, больше и т. Д. .). –

+1

@ ZachFloyd: Да, именно для этого и предназначен ODATA. Вы можете создавать довольно сложные фильтры и выбирать только через строку запроса http, а реализация OData Web API будет применять их к IQueryables для вас. – StriplingWarrior

ответ

1

Не знаете, какие входные параметры для метода вы ищете, но следующее должно дать вам отправную точку. Существенной частью является создание метода вызова метода Enumerable.Count(predicate).

static Expression<Func<TSource, bool>> MakeCountPredicate<TSource>(string collectionName, string itemName, ExpressionType itemComparison, string itemValue, ExpressionType countComparison, int countValue) 
{ 
    var source = Expression.Parameter(typeof(TSource), "s"); 
    var collection = Expression.Property(source, collectionName); 
    var itemType = collection.Type.GetInterfaces() 
     .Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 
     .GetGenericArguments()[0]; 
    var item = Expression.Parameter(itemType, "e"); 
    var itemProperty = Expression.Property(item, itemName); 
    var itemPredicate = Expression.Lambda(
     Expression.MakeBinary(itemComparison, itemProperty, Expression.Constant(
      string.IsNullOrEmpty(itemValue) || itemValue.Equals("null", StringComparison.OrdinalIgnoreCase) ? null : 
      Convert.ChangeType(itemValue, itemProperty.Type))), 
     item); 
    var itemCount = Expression.Call(
     typeof(Enumerable), "Count", new[] { itemType }, 
     collection, itemPredicate); 
    var predicate = Expression.Lambda<Func<TSource, bool>>(
     Expression.MakeBinary(countComparison, itemCount, Expression.Constant(countValue)), 
     source); 
    return predicate; 
} 

поэтому выражение предиката образец

Expression<Func<Event, bool>> predicate = 
    s => s.Attendees.Count(a => a.CancelledOn == null) > 10 

может построить динамически как этот

var predicate = MakeCountPredicate<Event>("Attendees", 
    "CancelledOn", ExpressionType.Equal, "null", ExpressionType.GreaterThan, 10); 
0

Для того, чтобы генерировать Expression дерево для Count, вы должны произвести Call к соответствующему Method.

Здесь первоначальный проект кодекса ...

Expression callExpr = Expression.Call(
    Expression.Constant(s.Attendees), 
    typeof(ICollection<Attendee>).GetMethod("get_Count")); // + 2 Arguments 

Конечно, вы должны разработать более и объединить это в основной программе.

Базовый пример использования будет

// Print out the expression. 
Debug.WriteLine(callExpr.ToString()); 

// The following statement first creates an expression tree, 
// then compiles it, and then executes it. 
Debug.WriteLine(Expression.Lambda<Func<int>>(callExpr).Compile()()); 

Наконец, графExpression (NodeType Call) должны будут содержать 2 аргумента (в качестве третьего параметра, не показано выше):

  • Сбор
  • Lambda для a => a.CancelledOn == null