LINQ group by date - включайте пустые дни БЕЗ использования join

Использование C#, NHibernate, NHibernate для LINQ. Используя NHibernate для LINQ, у меня нет функциональности JOIN. Я не могу использовать QueryOver либо.

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

var query3 = query2.AsEnumerable()
           .Where(x => x.Created <= toDate.Value && x.Created >= fromDate.Value)
           .GroupBy(x => x.Created.Date)
           .Select(g => new ReferrerChart {
               Date = g.Key.Date,
               LeadsCount = g.Count(x => !x.IsSale),
               SalesCount = g.Count(x => x.IsSale)
           });

но некоторых дат там нет (даты были 0 лидов и продаж).

Как я могу включить эти даты и позволить им установить LeadsCount = 0 и SalesCount = 0 БЕЗ использования join?

РЕДАКТИРОВАТЬ:

Конечный результат, который работает:

var selectedFromDate = fromDate.Value;
var selectedToDate = toDate.Value;

var selectedDates = Enumerable.Range(0, 1 + selectedToDate.Subtract(selectedFromDate).Days)
          .Select(offset => new ReferrerChart { 
                            Date = selectedFromDate.AddDays(offset),
                            LeadsCount = 0,
                            SalesCount = 0
                            })
          .ToList();

var query3 = Find(x => x.Source == "UserRef").AsEnumerable()
             .Where(x => x.Created <= toDate.Value && x.Created >= fromDate.Value)
             .GroupBy(x => x.Created.Date)
             .Select(g => new ReferrerChart {
                 Date = g.Key.Date,
                 LeadsCount = g.Count(x => !x.IsSale),
                 SalesCount = g.Count(x => x.IsSale)
             }).ToList();

var result = query3.Union(
                        selectedDates
                        .Where(e => !query3.Select(x => x.Date).Contains(e.Date)))
                        .OrderBy(x => x.Date);

3 ответа

Решение

Вы можете Union() данные из вашего запроса с фиктивными элементами, которые вы создаете в памяти. Например:

var query4 = query3.ToList(); // Prevent multiple execution.

var startDate = DateTime.Today.AddDays(-99);

var emptyData = Enumerable.Range(1,100).Select (i => 
    new ReferrerChart
        {
           Date = startDate.AddDays(i),
           LeadsCount = 0,
           SalesCount = 0
        });

var result = query4 .Union(
             emptyData
                .Where(e => !query4.Select(x => x.Date).Contains(e.Date)))
             .OrderBy(x => x.Date);

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

.Where(x => x.Created <= toDate.Value && x.Created >= fromDate.Value)

становится чем-то вроде

.Where(x => x.Created.Date == null || (x.Created <= toDate.Value && x.Created >= fromDate.Value))

Другим моим указателем будет то, что вашему.GroupBy требуется x.Created.Date, использование IsNull или аналогичной функции для установки значения здесь в значение независимо от того, является ли запись пустой

Например (x.Created == null ? "" : x.Created.ToString("yyyyMMdd"))

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

Есть отличный способ сделать это путем реализации метода расширения для IEnumerable<IGrouping<TKey, TElement>> (тип, который возвращается из GroupBy). По моему скромному мнению, это лучший подход по ряду причин:

  • Не пересчитывает заполненный результирующий набор (.ToList() также выполняет это, но ценой нетерпеливой оценки),
  • Сохраняет ленивую оценку (отложенное исполнение) GroupBy,
  • Дружественный к одной строке (может быть объединен в цепочку внутри быстрого запроса LINQ),
  • Универсальный, многоразовый,
  • Элегантный, легко читаемый.

Реализация

public static IEnumerable<IGrouping<TKey, TElement>> Fill<TKey, TElement>(this IEnumerable<IGrouping<TKey, TElement>> groups, IEnumerable<TKey> filling)
{
    List<TKey> keys = filling.ToList();

    foreach (var g in groups)
    {
        if(keys.Contains(g.Key))
            keys.Remove(g.Key);

        yield return g;
    }

    foreach(var k in keys)
        yield return new EmptyGrouping<TKey, TElement>(k);
}



class EmptyGrouping<TKey, TElement> : List<TElement>, IGrouping<TKey, TElement>
{
    public TKey Key { get; set; }

    public EmptyGrouping(TKey key)
    {
        this.Key = key;
    }
}

Пример использования

Random rand = new Random();

var results = Enumerable.Repeat(0, 5)                    // Give us five
    .Select(i => rand.Next(100))                         // Random numbers 0 - 99
    .GroupBy(r => r.Dump("Calculating group for:") / 10) // Group by tens (0, 10, 20, 30, 40...)
    .Fill(Enumerable.Range(0, 10))                       // Fill aby missing tens
    .OrderBy(g => g.Key);                                // Sort

@"Anything above = eager evaluations.
Anything below = lazy evaluations.".Dump();

results.Dump();

Образец вывода

(Пять целых чисел сверху печатаются изнутри запроса, когда он оценивается. Как видите, был только один проход вычисления).

Вывод реализации Fill Groups

Другие вопросы по тегам