Динамическая LINQ GroupBy Несколько столбцов

Мне нужно перевести следующий запрос LINQ в Dynamic LINQ, который принимает несколько столбцов группировки на основе ввода пользователем. В основном у меня есть группа dropdownlists, которые применяют группировки, и я не хочу перечислять каждую комбинацию групп. Если Dynamic LINQ терпит неудачу, мне, возможно, придется создать SQL-запрос вручную, и никто этого не хочет.

var grouping = ( from entry in ObjectContext.OmniturePageModules
    where entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate &&
        ( section == "Total" || section == "All" || entry.Section == section ) &&
        ( page == "Total" || page == "All" || entry.Page == page ) &&
        ( module == "Total" || module == "All" || entry.Module == module ) 
    group entry by new
    {
        entry.Page, // I want to be able to tell this anonymous type
        entry.Module, // which columns to group by
        entry.StartOfWeek // at runtime
    }
    into entryGroup
    select new
    {
        SeriesName = section + ":" + entryGroup.Key.Page + ":" + entryGroup.Key.Module,
        Week = entryGroup.Key.StartOfWeek,
        Clicks = entryGroup.Sum( p => p.Clicks )
    } );

Я понятия не имею, как это сделать, поскольку Dynamic LINQ полностью недокументирован вне "привет мир!". выберите/где/порядок. Я просто не могу понять синтаксис.

Что-то вроде: (?)

var grouping = ObjectContext.OmniturePageModules.Where(entry => entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate &&
                                           ( section == "Total" || section == "All" || entry.Section == section ) &&
                                           ( page == "Total" || page == "All" || entry.Page == page ) &&
                                           ( module == "Total" || module == "All" || entry.Module == module ))
                                           .GroupBy("new (StartOfWeek,Page,Module)", "it")
                                           .Select("new (Sum(Clicks) as Clicks, SeriesName = section + key.Page + Key.Module, Week = it.Key.StartOfWeek)");

Я использую класс DynamicQueryable в System.Linq.Dynamic. См.: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Последующий: В основном работало решение Enigmativity. По какой-то причине он не хочет группировать столбец "StartOfWeek" datetime - обходным путем является просто выполнение вторичной группировки:

var entries = ( from entry in ObjectContext.OmniturePageModules
                            where entry.StartOfWeek >= startDate
                                && entry.StartOfWeek <= endDate
                                && ( section == "Total" || section == "All" || entry.Section == section )
                                && ( page == "Total" || page == "All" || entry.Page == page )
                                && ( module == "Total" || module == "All" || entry.Module == module )
                            select entry ).ToArray(); // Force query execution

            var grouping = from entry in entries
                            let grouper = new EntryGrouper( entry, section, page, module )
                            group entry by grouper into entryGroup
                            select new
                            {
                                entryGroup.Key.SeriesName,
                                entryGroup.Key.Date, 
                                Clicks = entryGroup.Sum( p => p.Clicks ),
                            };

            var grouping2 = (from groups in grouping
                            group groups by new {groups.SeriesName, groups.Date } into entryGroup
                            select new
                            {
                               entryGroup.Key.SeriesName,
                               entryGroup.Key.Date,
                               Clicks = entryGroup.Sum( p => p.Clicks ),
                            } );

но это, похоже, серьезно ухудшает производительность... =/

Ответ 1

Если вы явно хотите использовать библиотеку динамических запросов LINQ, мой ответ не будет таким, какой вы хотите, но если вы хотите, чтобы ваше поведение было желательным, и вы счастливы использовать обычный LINQ, то я думаю, что могу помочь.

По существу, я создал класс EntryGrouper, который обрабатывает логику группировки по выбранным значениям в раскрывающихся списках, и я предположил, что переменные section, page и module сохраняют эти значения. Я также предположил, что ObjectContext.OmniturePageModules является перечислимым типа Entry.

Итак, теперь ваш запрос LINQ становится следующим:

var entries = (from entry in ObjectContext.OmniturePageModules
               where entry.StartOfWeek >= startDate
                   && entry.StartOfWeek <= endDate
                   && (section == "Total" || section == "All" || entry.Section == section)
                   && (page == "Total" || page == "All" || entry.Page == page)
                   && (module == "Total" || module == "All" || entry.Module == module)
               select entry).ToArray(); // Force query execution

var grouping = from entry in entries
               let grouper = new EntryGrouper(entry, section, page, module)
               group entry by grouper into entryGroup
               select new
               {
                   SeriesName = entryGroup.Key.SeriesName,
                   Week = entryGroup.Key.StartOfWeek,
                   Clicks = entryGroup.Sum(p => p.Clicks),
               };

Первый запрос используется для принудительного простого запроса запроса в базе данных и возврата только записей, которые вы хотите сгруппировать. Обычно запросы group by вызывают базу данных несколько раз, поэтому запрос таким образом обычно выполняется намного быстрее.

Второй запрос группирует результаты первого запроса, создавая экземпляры класса EntryGrouper в качестве ключа группировки.

Я включил свойство SeriesName в класс EntryGrouper, чтобы вся логика группировки была четко определена в одном месте.

Теперь класс EntryGrouper довольно велик, так как для обеспечения возможности группировки он должен иметь свойства для StartOfWeek, section, page и module и содержать перегрузки Equals и GetHashCode и реализовать интерфейс IEquatable<Entry>.

Вот он:

public class EntryGrouper : IEquatable<Entry>
{
    private Entry _entry;
    private string _section;
    private string _page;
    private string _module;

    public EntryGrouper(Entry entry, string section, string page, string module)
    {
        _entry = entry;
        _section = section;
        _page = page;
        _module = module;
    }

    public string SeriesName
    {
        get
        {
            return String.Format("{0}:{1}:{2}", this.Section, this.Page, this.Module);
        }
    }

    public DateTime StartOfWeek
    {
        get
        {
            return _entry.StartOfWeek;
        }
    }

    public string Section
    {
        get
        {
            if (_section == "Total" || _section == "All")
                return _section;
            return _entry.Section;
        }
    }

    public string Page
    {
        get
        {
            if (_page == "Total" || _page == "All")
                return _page;
            return _entry.Page;
        }
    }

    public string Module
    {
        get
        {
            if (_module == "Total" || _module == "All")
                return _module;
            return _entry.Module;
        }
    }

    public override bool Equals(object other)
    {
        if (other is Entry)
            return this.Equals((Entry)other);
        return false;
    }

    public bool Equals(Entry other)
    {
        if (other == null)
            return false;
        if (!EqualityComparer<DateTime>.Default.Equals(this.StartOfWeek, other.StartOfWeek))
            return false;
        if (!EqualityComparer<string>.Default.Equals(this.Section, other.Section))
            return false;
        if (!EqualityComparer<string>.Default.Equals(this.Page, other.Page))
            return false;
        if (!EqualityComparer<string>.Default.Equals(this.Module, other.Module))
            return false;
        return true;
    }

    public override int GetHashCode()
    {
        var hash = 0;
        hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.StartOfWeek);
        hash ^= EqualityComparer<string>.Default.GetHashCode(this.Section);
        hash ^= EqualityComparer<string>.Default.GetHashCode(this.Page);
        hash ^= EqualityComparer<string>.Default.GetHashCode(this.Module);
        return hash;
    }

    public override string ToString()
    {
        var template = "{{ StartOfWeek = {0}, Section = {1}, Page = {2}, Module = {3} }}";
        return String.Format(template, this.StartOfWeek, this.Section, this.Page, this.Module);
    }
}

Логика группировки этого класса выглядит просто так:

if (_page == "Total" || _page == "All")
    return _page;
return _entry.Page;

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

Если у вас больше выпадающих списков, которые вы группируете, вам нужно добавить дополнительные свойства в класс EntryGrouper. Не забудьте добавить эти новые свойства в методы Equals и GetHashCode.

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

Наслаждайтесь!

Ответ 2

Здесь он находится в динамическом LINQ - конечно, вы создаете строки GroupBy и Select во время выполнения:

var double_grouping = ( ObjectContext.OmniturePageModules.Where( entry => entry.StartOfWeek >= startDate
                     && entry.StartOfWeek <= endDate
                     && ( section == "Total" || section == "All" || entry.Section == section )
                     && ( page == "Total" || page == "All" || entry.Page == page )
                     && ( module == "Total" || module == "All" || entry.Module == module ) )
                     .GroupBy( "new ( it.Section, it.Page, it.StartOfWeek )", "it" ) )
                     .Select( "new ( Sum(Clicks) as Clicks, Key.Section as SeriesSection, Key.Page as SeriesPage, Key.StartOfWeek as Week )" );

И вот нормальный способ LINQ, который ускользнул от меня, пока коллега не указал его - это в основном решение Enigmativity без класса grouper:

var grouping = ( from entry in ObjectContext.OmniturePageModules
    where entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate &&
        ( section == "Total" || section == "All" || entry.Section == section ) &&
        ( page == "Total" || page == "All" || entry.Page == page ) &&
        ( module == "Total" || module == "All" || entry.Module == module )
    group entry by new
    {
        Section = section == "All" ? entry.Section : section,
        Page = page == "All" ? entry.Page : page,
        Module = module == "All" ? entry.Module : module,
        entry.StartOfWeek
    }
        into entryGroup
        select new
        {
            SeriesName =
            entryGroup.Key.Section + ":" + entryGroup.Key.Page + ":" + entryGroup.Key.Module,
            Week = entryGroup.Key.StartOfWeek,
            Clicks = entryGroup.Sum( p => p.Clicks )
        } );

Ответ 3

Я знаю, что прошло некоторое время, так как этот вопрос был опубликован, но мне недавно пришлось столкнуться с подобной проблемой (динамическая группировка по нескольким столбцам, выбранным пользователем во время выполнения), поэтому я беру на себя это.

  • Вспомогательная функция для создания группировки lambdas

    static Expression<Func<T, Object>> GetGroupBy<T>( string property )
    {
      var data = Expression.Parameter( typeof( T ), "data" );
      var dataProperty = Expression.PropertyOrField( data, property );
      var conversion = Expression.Convert( dataProperty, typeof( object ) );
      return Expression.Lambda<Func<T, Object>>( conversion, data );
    }
    
  • Функция для группировки в памяти. Возвращает группы.

    static IEnumerable<IEnumerable<T>> Group<T>( IEnumerable<T> ds, params Func<T, object>[] groupSelectors )
    {
      Func<IEnumerable<T>, Func<T, object>[], IEnumerable<IEnumerable<T>>> inner = null;
      inner = ( d, ss ) => {
        if ( null == ss || ss.Length == 0 ) {
          return new[] { d };
        } else {
          var s = ss.First();
          return d.GroupBy( s ).Select( g => inner( g.Select( x => x ), ss.Skip( 1 ).ToArray() ) ) .SelectMany( x => x );
        }
      };
      return inner( ds, groupSelectors );
    }
    
  • Как он будет использоваться:

    String[] columnsSelectedByUser = ... // contains names of grouping columns selected by user
    var entries = ... // Force query execution i.e. fetch all data
    var groupBys = columnsSelectedByUser.Select( x => GetGroupBy( x ).Compile()).ToArray();
    var grouping = Group(entries, groupBys); // enumerable containing groups of entries
    

Что касается унизительных характеристик, я не думаю, что это действительно (большая) проблема. Даже если вы построили группировку SQL динамически, запрос должен был бы возвращать то же количество строк, что и запрос без группировки. Поэтому, хотя в этом подходе группировка не выполняется базой данных, количество строк, возвращаемых принудительным выполнением запросов, такое же, как и для гипотетического SQL-запроса с критериями группировки. Конечно, база данных, вероятно, превосходит группировку в памяти, выполняемую кодом С#, но объем трафика зависит только от того, сколько строк (entries) необходимо сгруппировать.