GroupBy SQL to EF Lambda syntax - entity-framework

I'm currently defeated in my attempts to map the following sql to an EF lambda style query:
sql:
SELECT ResellerId, Name, Month, SUM(MonthTotal) AS MonthTotal
FROM dbo.View_ResellerYearMonthBase
WHERE (Year = 2015) AND (EventId > 0) AND (ValidationResponse IS NOT NULL)
GROUP BY ResellerId, Month, Name
I've tried
public JsonResult GetResellerAnnualReportData(int year, bool includeUnValidated, bool includeUnbooked)
{
var qry = _reportsDal.View_ResellerYearMonthBase.AsQueryable();
qry = qry.Where(x => x.Year == year);
if (!includeUnValidated) { qry = qry.Where(x => x.ValidationResponse.Length > 0); }
if (!includeUnbooked) { qry = qry.Where(x => x.EventId > 0); }
qry = qry.GroupBy(x => new { x.ResellerId, x.Month, x.Name }).Select(y => new ResellerAnnualReportDto
{
ResellerId = y.Key.ResellerId,
Month = y.Key.Month.Value,
Name = y.Key.Name,
SumMonthTotal = y.Sum(z => z.MonthTotal.Value)
});
throw new NotImplementedException();//keep the compiler happy for now
}
How should I go about achieving the SQL Query with the function parameters (year, includeUnValidated etc)

.GroupBy(key => new { key.ResellerId, key.Month, key.Name},
el => el.MonthTotal,
(key, el) => new ResellerAnnualReportDto
{
ResellerId = key.ResellerId,
Month = key.Month,
Name = key.Name,
MonthTotal = el.Sum(s => s.MonthTotal)
}).ToList();
This uses the overload with keyselector, elementselector and resultselector. This way you avoid making the IGrouping<key,value> and get the results you want immediately. Couldn't test though.

Here is how to do this:
var result = qry.GroupBy(x => new { x.ResellerId, x.Month, x.Name }).
Select(y => new {
y.Key.ResellerId,
y.Key.Month,
y.Key.Name,
SumMonthTotal = y.Sum(z => z.MonthTotal)
}).ToList();

Related

Use FirstOrDefaultAsync in query when creating instance

I have the following Entity Framework query:
IQueryable<Unit> units = context.Units;
Product product = new Product {
Conversions = model.Conversions.Select(y => new Conversion {
UnitId = units
.Where(z => z.Name == y.Unit)
.Select(z => z.Unit.Id)
.FirstOrDefault(),
Value = y.Value
}).ToList(),
Name = model.Name
}
I tried to use await before units.Where( ... as:
Product product = new Product {
Conversions = model.Conversions.Select(y => new Conversion {
UnitId = await units
.Where(z => z.Name == y.Unit)
.Select(z => z.Unit.Id)
.FirstOrDefaultAsync(),
Value = y.Value
}).ToList(),
Name = model.Name
}
This is not allowed as it is inside new Conversion ...
Shouldn't I use await? How can I use it in this query?
You can use this approach :
unitId = await Task.Factory.Start<int>(()=>{
return units.Where(z => z.Name == y.Unit)
.Select(z => z.Unit.Id)
.FirstOrDefault();
})
change int to your return type.

How can I achieve to implement the below query without the if statement (SUM/AVG)?

public class DailyAggregator : Aggregator
{
public override Dictionary<string, int?> Aggregate<T>(IQueryable<T> query, Expression<Func<T, DateTime>> groupByProperty, Expression<Func<T, double>> operationProperty = null)
{
if (operationProperty == null) // property of sum/avg (null = count)
operationProperty = x => 1;
if (_operationType.Equals(ReportAggregationOperation.Sum))
{
return query
.GroupBy(g => new
{
Day = TestableDbFunctions.TruncateTime(groupByProperty.Invoke(g))
})
.Select(x => new
{
Key = x.Key.Day.ToString().Substring(0, 10),
Value = (int?) x.Sum(operationProperty.Compile()),
})
.ToDictionary(t => t.Key, t => t.Value);
}
else
{
return query
.GroupBy(g => new
{
Day = DbFunctions.TruncateTime(groupByProperty.Invoke(g))
})
.Select(
x => new
{
Key = x.Key.Day.ToString().Substring(0, 10),
Value = (int?) x.Average(operationProperty.Compile()),
}).ToDictionary(t => t.Key, t => t.Value);
}
}
}
I'm using an IOC container for creating instances like dailyaggreagtor/monthlyaggregator...
But I wasn't able to build this group by expression, or to apply the right design pattern to eliminate the above if statement.
The compile and invoke functions comes from the LinqKit extension.
The classes using this aggregator(s) is querying the DB and collecting data for callcenter reports (ex. TotalCdrRecords report, ReachedCdrRecords report, CdrTalkTime report, CdrCallAfterworkTime etc., a CDR is a call data record, practically holding all information about a call). The type of the query (T) is specified in the report classes, but basically is an IReportableEntity, but of course the operationProperty it can be any property of the DB Entity which we can perfom the count/sum/avg operations, the groupByProperty is always a DateTime column.
It generates the following sql query:
SELECT
1 AS [C1],
SUBSTRING(CASE WHEN ([GroupBy1].[K1] IS NULL) THEN N'' ELSE CAST( [GroupBy1].[K1] AS nvarchar(max)) END, 0 + 1, 10) AS [C2],
CAST( [GroupBy1].[A1] AS int) AS [C3]
FROM ( SELECT
[Filter1].[K1] AS [K1],
SUM([Filter1].[A1]) AS [A1]
FROM ( SELECT
convert (datetime2, convert(varchar(255), [Extent1].[CreatedOn], 102) , 102) AS [K1],
cast(1 as float(53)) AS [A1]
FROM [dbo].[VCCCdr] AS [Extent1]
WHERE ([Extent1].[CreatedOn] >= #p__linq__0) AND ([Extent1].[CreatedOn] <= #p__linq__1) AND ([Extent1].[CompanyId] = #p__linq__2)
) AS [Filter1]
GROUP BY [K1]
) AS [GroupBy1]
Since you are using LINQKit, you can extract an expression for calling Sum / Average part and Invoke it inside the query.
For instance:
var aggregateFunc = _operationType.Equals(ReportAggregationOperation.Sum) ?
Linq.Expr((IEnumerable<double> source) => source.Sum()) :
Linq.Expr((IEnumerable<double> source) => source.Average());
return query
.GroupBy(g => new
{
Day = DbFunctions.TruncateTime(groupByProperty.Invoke(g))
})
.Select(x => new
{
Key = x.Key.Day.ToString().Substring(0, 10),
Value = (int?)aggregateFunc.Invoke(x.Select(operationProperty.Compile())),
})
.ToDictionary(t => t.Key, t => t.Value);

Prevent sort result of union in entity framework after select and distinct

Before I asked Prevent sort result of union in entity framework
I got my answer but now I have new problem with this issue. I have this code:
var productExactlyTitle = products.Where(x => x.Title == keyword);
var productStartWithPhrase = products.Where(x => x.Title.StartsWith(keyword));
var productStartWithWord = products.Where(x => x.Title.StartsWith(keyword + " "));
var productContainsWord = products.Where(x => x.Title.Contains(" " + keyword + " "));
var productContainsPhrase = products.Where(x => x.Title.Contains(keyword)
|| x.Title.Contains(keyword)
|| x.SubTitle.Contains(keyword)
|| x.OtherName.Contains(keyword));
var splitWords = keyword.Split(' ');
var productSplitWordSearch = splitWords.Aggregate(products, (current, word) => current.Where(x => x.Title.Contains(word.Trim())));
var p1 = productExactlyTitle.Select(x => new { Item = x, Order = 1 });
var p2 = productStartWithWord.Select(x => new { Item = x, Order = 2 });
var p3 = productStartWithPhrase.Select(x => new { Item = x, Order = 3 });
var p4 = productContainsWord.Select(x => new { Item = x, Order = 4 });
var p5 = productContainsPhrase.Select(x => new { Item = x, Order = 5 });
var p6 = productSplitWordSearch.Select(x => new { Item = x, Order = 6 });
var productList = p1
.Union(p2)
.Union(p3)
.Union(p4)
.Union(p5)
.Union(p6)
.OrderBy(x => x.Order)
.Take(21)
.AsEnumerable()
.Select(x => new ProductItemViewModel()
{
Id = x.Item.Id,
Title = x.Item.Title,
Price = DiscountController.ApplyDiscountToPrice(x.Item).ToPrice(),
Image = x.Item.Images.FirstOrDefault(y => y.IsCoverPhoto)?.ImageUrl
});
Result of above code have duplicate records and I have to use select and distinct to remove duplicate records. so I change my code like this:
var productList = p1
.Union(p2)
.Union(p3)
.Union(p4)
.Union(p5)
.Union(p6)
.OrderBy(x => x.Order)
.Select(x => x.Item)
.Distinct()
.Take(21)
.AsEnumerable()
.Select(x => new ProductItemViewModel()
{
Id = x.Id,
Title = x.Title,
Price = DiscountController.ApplyDiscountToPrice(x).ToPrice(),
Image = x.Images.FirstOrDefault(y => y.IsCoverPhoto)?.ImageUrl
});
But after that my result is sorted with Id column again.
How can I solved this?
First, since adding Order field to each query makes the record unique, using Union (which is supposed to remove duplicates) doesn't make sense, so simply use Concat instead.
Second, to remove duplicates and not lose the Order field needed for later ordering, you need to group by Item and take the minimum Order for each group. The rest is the same as in the original solution.
var productList = p1
.Concat(p2)
.Concat(p3)
.Concat(p4)
.Concat(p5)
.Concat(p6)
.GroupBy(e => e.Item)
.Select(g => new { Item = g.Key, Order = g.Min(e => e.Order) })
.OrderBy(e => e.Order)
.Select(e => e.Item)
.Take(21)
.AsEnumerable()
...

Prevent sort result of union in entity framework

In SQL server union, result is sorted based on primary key column. I want to prevent this behavior in entity framework.
In this post, #praveen has explained how to do this in pure sql. But I want to do this in entity framework.
My code:
public virtual ActionResult Search(string keyword)
{
var products = _db.Products
.Where(x => x.IsActive)
.AsQueryable();
var productExactlyTitle = products.Where(x => x.Title == keyword);
var productStartTitle = products.Where(x => x.Title.StartsWith(keyword));
var productContainsTitle = products.Where(x => x.Title.Contains(keyword)
|| x.Title.Contains(keyword)
|| x.SubTitle.Contains(keyword)
|| x.OtherName.Contains(keyword));
var productList = productExactlyTitle.Union(productStartTitle)
.Union(productContainsTitle)
.Take(10)
.AsEnumerable()
.Select(x => new ProductItemViewModel()
{
Id = x.Id,
Title = x.Title,
Price = x.Price.ToPrice(),
Image = x.Images.FirstOrDefault(y => y.IsCoverPhoto)?.ImageUrl
});
// some code ...
}
I want to show records with below order:
First: records of productExactlyTitle
Second: records of productStartTitle
Third: records of productContainsTitle
But result is sorted with Id column! and I don't want this.
Is there a way for do this?
In SQL all queries without an order by explicitly set is considered unordered. (and EF queries a translated into SQL). So if you want a specific order after your union just specify it.
var result = q1.Union(q2).OrderBy(x => x.?);
For your specific case:
var p1 = productExactlyTitle.Select(x => new { Item = x, Order = 1 });
var p2 = productStartTitle.Select(x => new { Item = x, Order = 2 });
var p3 = productContainsTitle.Select(x => new { Item = x, Order = 3 });
var productList = p1.Union(p2)
.Union(p3)
.OrderBy(x => x.Order)
.Select(x => x.Item)
.Take(10);

EntityFramework. SelectMany with Anonymous Type and Projection

I have a Banner with multiple Packs. Each pack has multiple files.
I have the following query:
List<BannerModel> models = context.Banners
.Select(x => x.Packs
.SelectMany(p => p.Files, (p, f) => new {
Id = p.Id,
Flag = p.Flag,
File = new { Id = f.Id, Flag = f.Flag, Key = f.Key, Mime = f.Mime }
})
.Where(a => a.File.Flag == "Img_200")
.Select(a => new BannerModel { PackId = a.Id, ImageKey = a.File.Key })
).ToList();
1) I get the error on "ToList()".
Cannot implicitly convert type 'System.Collections.Generic.List>' to 'System.Collections.Generic.List'
2) Then I removed the ToList and added "var models = ..."
I know there are 10 records where 5 of them satisfy the criteria:
.Where(a => a.File.Flag == "Img_200")
What is strange is that I get 10 items, 5 with data and 5 with no data.
Where I should only get a list of 5 items. The one that satisfy the criteria.
Could someone help me solving this problem?
Thank you,
Miguel
Should this be:
List<BannerModel> models = context.Banners
.SelectMany(x => x.Packs
.SelectMany(p => p.Files, (p, f) => new {
Id = p.Id,
Flag = p.Flag,
File = new { Id = f.Id, Flag = f.Flag, Key = f.Key, Mime = f.Mime }
})
.Where(a => a.File.Flag == "Img_200")
.Select(a => new BannerModel { PackId = a.Id, ImageKey = a.File.Key })
).ToList();