Using Where( Expression<Func<T, bool>> ) in IGrouping - c#

Consider the following Linq to Entities query:
return (from lead in db.Leads
join postcodeEnProvincie in postcodeEnProvincies
on lead.Postcode equals postcodeEnProvincie.Postcode
where (lead.CreationDate >= range.StartDate) && (lead.CreationDate <= range.EndDate)
group lead by postcodeEnProvincie.Provincie into g
select new Web.Models.GroupedLeads() {
GroupName = g.Key,
HotLeads = g.Count(l => l.Type == Data.LeadType.Hot),
Leads = g.Count(),
PriorityLeads = g.Count(l => l.Type == Data.LeadType.Priority),
Sales = g.Count(l => l.Sold),
ProductA = g.Count(l => l.Producten.Any(a => ((a.Name.Equals("productA", StringComparison.CurrentCultureIgnoreCase)) || (a.Parent.Name.Equals("productA", StringComparison.CurrentCultureIgnoreCase))))),
ProductB = g.Count(l => l.Producten.Any(a => ((a.Name.Equals("productB", StringComparison.CurrentCultureIgnoreCase)) || (a.Parent.Name.Equals("productB", StringComparison.CurrentCultureIgnoreCase))))),
ProductC = g.Count(l => l.Producten.Any(a => ((a.Name.Equals("productC", StringComparison.CurrentCultureIgnoreCase)) || (a.Parent.Name.Equals("productC", StringComparison.CurrentCultureIgnoreCase))))),
ProductC = g.Count(l => l.Producten.Any(a => ((a.Name.Equals("productD", StringComparison.CurrentCultureIgnoreCase)) || (a.Parent.Name.Equals("productD", StringComparison.CurrentCultureIgnoreCase)))))
}).ToList();
If you're anything like me, your toes curl at the repetition of the product selection logic. This pattern is repeated in another place as well. I first attempted to replace it by an extension method on IEnumerable, which of course does not work: Linq to Entities needs an Expression to parse and translate.
So I created this method:
public static System.Linq.Expressions.Expression<Func<Data.Lead, bool>> ContainingProductEx(string productName)
{
var ignoreCase = StringComparison.CurrentCultureIgnoreCase;
return (Data.Lead lead) =>
lead.Producten.Any(
(product =>
product.Name.Equals(productName, ignoreCase) ||
product.Parent.Name.Equals(productName, ignoreCase)
));
}
The following selection now works perfectly fine:
var test = db.Leads.Where(Extensions.ContainingProductEx("productA")).ToList();
However, this won't compile, because IGrouping does not contain an override of Where that accepts an Expression:
return (from lead in db.Leads
join postcodeEnProvincie in postcodeEnProvincies
on lead.Postcode equals postcodeEnProvincie.Postcode
where (lead.CreationDate >= range.StartDate) && (lead.CreationDate <= range.EndDate)
group lead by postcodeEnProvincie.Provincie into g
select new Web.Models.GroupedLeads()
{
GroupName = g.Key,
HotLeads = g
.Where(l => l.Type == Data.LeadType.Hot)
.Count(),
Leads = g.Count(),
PriorityLeads = g
.Where(l => l.Type == Data.LeadType.Priority)
.Count(),
Sales = g
.Where(l => l.Sold)
.Count(),
ProductA = g
.Where(Extensions.ContainingProductEx("productA"))
.Count(),
ProductB = g
.Where(Extensions.ContainingProductEx("productB"))
.Count(),
ProductC = g
.Where(Extensions.ContainingProductEx("productC"))
.Count(),
ProductD = g
.Where(Extensions.ContainingProductEx("productD"))
.Count()
}).ToList();
Casting g to IQueryable compiles, but then yields a "Internal .NET Framework Data Provider error 1025.".
Is there any way to wrap this logic in its own method?

This is a problem that can be solved using LINQKit. It allows expressions to be invoked from within other expressions, and it will inline the invoked expression within its caller. Sadly, it only supports a handful of very specific situations, so we'll need to adapt your expression generating method a bit.
Rather than passing the product name to the expression generating method, we'll have it be a parameter of the returned expression:
public static Expression<Func<Data.Lead, string, bool>> ContainingProductEx()
{
var ignoreCase = StringComparison.CurrentCultureIgnoreCase;
return (lead, productName) =>
lead.Producten.Any(
(product =>
product.Name.Equals(productName, ignoreCase) ||
product.Parent.Name.Equals(productName, ignoreCase)
));
}
Next we'll need to call the method before declaring the query:
var predicate = Extensions.ContainingProductEx();
Your query can can now be written as:
from lead in db.Leads.AsExpandable()
//...
ProductA = g
.Where(lead => predicate.Invoke(lead, "productA"))
.Count(),
ProductB = g
.Where(lead => predicate.Invoke(lead, "productB"))
.Count(),
ProductC = g
.Where(lead => predicate.Invoke(lead, "productC"))
.Count(),
ProductD = g
.Where(lead => predicate.Invoke(lead, "productD"))
.Count()

Instead of worrying about creating a function pointer/expression inside your query that you can reference (may not be possible), why not just create a separate private method that takes an IEnumerable<Lead>, a string, and returns an int and reference the method group in your query? I think your confusion is stemming from trying to create an extension method on the collection instead of creating a method that in the collection and the value you are looking for.
Something like:
ProductA = GetLeadsForProduct(g, "productA")
private int GetLeadsForProduct(IEnumerable<Lead> leads, string productType)
{
return leads.Count(l => l.Producten.Any(a => ((a.Name.Equals(productType, StringComparison.CurrentCultureIgnoreCase)) || (a.Parent.Name.Equals(productType, StringComparison.CurrentCultureIgnoreCase)))))
}

Related

How to groupby with inner join using linq lamba expression

I'm trying to convert a sql stored proc to linq. I'm having issues with the groupby and inner joins.
Here is what I've tried:
var r = _context.Table1
.GroupBy(x => new { x.OptionId, x.Years, x.Strike })
.Join(_context.Table2,
oc => oc.OptionId, o => o.OptionId, (oc, o) => new
{
OptionsCosts = oc,
Options = o
}).Where(x => x.Options.OptionType == 1
&& x.Options.QualifierId != null
&& x.Options.CreditingMethod != "xxx")
.Select(y => new DataModel.Table1()
{
Years = y.Select(a => a.OptionsCosts.Years).FirstOrDefault(),
Strike = y.Select(a => a.OptionsCosts.Strike).FirstOrDefault(),
Value = y.Select(a => a.OptionsCosts.Value).FirstOrDefault(),
ChangeUser = y.Select(a => a.OptionsCosts.ChangeUser).FirstOrDefault(),
ChangeDate = DateTime.Now,
OptionId = y.Select(a => a.OptionsCosts.OptionId).FirstOrDefault()
});
Here is the SQL that I'm trying to convert:
SELECT o2.OptionId, o2.Years, o2.Strike, SUM(d2.Weights) as 'TotalWeight', COUNT(*) as 'Counts'
FROM Table1 o2
INNER JOIN #Dates d2 --this is a temp table that just holds dates. I was thinking just a where statement could do it???
ON d2.EffectiveDate = o2.EffectiveDate
INNER JOIN Table2 od2
ON od2.OptionId = o2.OptionId
AND od2.OptionType = 1
AND od2.qualifierid is null
AND od2.CreditingMethod <> 'xxx' --28095
GROUP BY o2.OptionId,o2.Years, o2.Strike
My data is way off so I'm sure I'm doing something wrong.
var table1=_context.Table1
.groupBy(o2=> new{
o2.OptionId
, o2.Years
, o2.Strike
})
.select(s=> new{
s.key.OptionId
, s.key.Years
, s.key.Strike
,TotalWeight=s.sum(x=>x.Weights)
,Counts=o2.count(c=>c.OptionId)
}).tolist();
var result=table1
.Join(_context.Table2,oc => oc.OptionId, o => o.OptionId, (oc, o) => new{ OptionsCosts = oc, Options = o })
.Where(x => x.Options.OptionType == 1
&& x.Options.QualifierId != null
&& x.Options.CreditingMethod != "xxx")
.select(x=> new {
x.oc.OptionId, x.oc.Years, x.oc.Strike, x.oc.TotalWeight, x.oc.Counts
}).tolist();
Small advise, when you rewriting SQL queries, use LINQ Query syntax which is close to SQL and more effective to avoid errors.
var dates = new List<DateTime>() { DateTime.Now }; // fill list
var query =
from o2 in _context.Table1
where dates.Contains(o2.EffectiveDate)
from od2 in _context.Table1.Where(od2 => // another way to join
od2.OptionId == o2.OptionId
&& od2.OptionType == 1
&& od2.qualifierid == null
&& od2.CreditingMethod != "xxx")
group o2 by new { o2.OptionId, o2.Years, o2.Strike } into g
select new
{
g.Key.OptionId,
g.Key.Years,
g.Key.Strike,
Counts = g.Count()
// SUM(d2.Weights) as 'TotalWeight', -this one is not available because dates in memory
};
If you are on start and trying to rewrite procedures on LINQ - EF Core is bad idea. Too limited IQueryable support and usually you will fight for each complex LINQ query.
Try linq2db which has temporary tables support and your stored proc can be rewritten into identical LINQ queries. Or you can use linq2db.EntityFrameworkCore to extend EF Core functionality.
Disclaimer. I’m creator of this extension and one from linq2db creators.

How to make LINQ query with Unions more efficient

I inherited the LINQ query below and I feel that the query can be refactored for efficiency. The query currently takes about 6-8 seconds of processing time to return one record to the user on the front-end of the application. LINQ is not my strong suite, so any help would be greatly appreciated.
The query should ultimately produce a distinct list of CA_TASK_VW objects that are tied to a list of distinct CA_OBJECT_ID's obtained from the CA_OBJECT, CA_PEOPLE, and CA_CONTRACTOR tables.
var data = (from a in _db.CA_TASK_VW
where a.TASK_TYPE == "INSPECTION" && a.TASK_AVAILABLE_FLAG == "Y" && a.TARGET_END_DATE == null
select a).AsQueryable();
data = data.Join(_db.CA_OBJECT.Where(o => o.ENTERED_BY == _userId),
o => o.CA_OBJECT_ID, p => p.CA_OBJECT_ID,
(t, p) => t)
.Union(data.Join(_db.CA_PEOPLE.Where(p => p.EMAIL == _email),
t => t.CA_OBJECT_ID, p => p.CA_OBJECT_ID,
(t, p) => t))
.Union(data.Join(_db.CA_CONTRACTOR.Where(c => c.CONTRACTOR.EMAIL == _email),
t => t.CA_OBJECT_ID, c => c.CA_OBJECT_ID,
(t, c) => t));
The code seems to be using Join/Union to execute basically a where predicate on the list of CA_TASK_VW, filtering it step by step to the final result, so what happens if you just specify the where condition directly?
var data = from a in _db.CA_TASK_VW
where a.TASK_TYPE == "INSPECTION" && a.TASK_AVAILABLE_FLAG == "Y" && a.TARGET_END_DATE == null
select a;
data = data.Where(t => _db.CA_OBJECT.Where(o => o.ENTERED_BY == _userId).Select(o => o.CA_OBJECT_ID).Contains(t.CA_OBJECT_ID) ||
_db.CA_PEOPLE.Where(p => p.EMAIL == _email).Select(p => p.CA_OBJECT_ID).Contains(t.CA_OBJECT_ID) ||
_db.CA_CONTRACTOR.Where(c => c.CONTRACTOR.EMAIL == _email).Select(c => c.CA_OBJECT_ID).Contains(t.CA_OBJECT_ID));
You could try using UNION ALL if you don`t really care about duplicates in your query results as it works much faster than UNION

Using Having Sum with Linq to SQL

I have a SQL command that I am trying to convert to a LINQ to SQL command, but am having difficulties.
My SQL command follows:
SELECT purchs.iclientid, ifeatureid, AddnlOptionList FROM purchs
WHERE AddnlOptionList <> ''
GROUP BY purchs.iclientid, ifeatureid, AddnlOptionList
HAVING (SUM(noptions) > 0)
I've managed to get this far following examples:
var q =
from purchs in db.Purchases
group q by purchs.noptions into g
where purchs.AddnlOptionList != ""
&& g.Sum(x => x.noptions) > 0
select q;
However, I seem to be stuck on group q with the following two errors:
Cannot use local variable 'q' before it is declared
Cannot convert lambda expression to type 'System.Collections.Generic.IEqualityComparer<decimal?> because it is not a delegate type
An example from here says that this should work, although it uses a join, and I am not. Any help would be appreciated.
SOLUTION
I had to modify Xiaoy312's code a little bit to get what I wanted, so I figured I would post it here in hopes that it might help someone in the future. Thank you #Xiaoy312 for the help.
var updates = db.Purchases
.Where(p => p.AddnlOptionList != "")
.GroupBy(p => new { p.iclientid, p.ifeatureid, p.AddnlOptionList })
.Where(g => g.Sum(p => p.noptions) > 0)
.Select(g => g.Key);
You can't put both the WHERE and HAVING clause into a single where. I'm less familiar with the other syntax, so here is the method syntax one :
var results = db.Purchases
.Where(p => p.AddnlOptionList != "")
.GroupBy(p => new { p.notions, p.iclientid, p.ifeatureid })
.Where(g => g.Sum(p => p.notions) > 0)
.SelectMany(g => g)
EDIT: converted to Linq syntax.
var results = from p in db.Purchases
where p.AddnlOptionList != ""
group p by new { p.notions, p.iclientid, p.ifeatureid } into g
where g => g.Sum(p => p.notions) > 0
from p in g
select p;
EDIT: I've miss read the sql command. It meant to only pull the groups, not every item in each group.
// method syntax
db.Purchases
.Where(p => p.AddnlOptionList != "")
.GroupBy(p => new { p.notions, p.iclientid, p.ifeatureid })
.Where(g => g.Sum(p => p.notions) > 0)
.Select(g => g.Key)
// query syntax
from p in db.Purchases
where p.AddnlOptionList != ""
group p by new { p.notions, p.iclientid, p.ifeatureid } into g
where g.Sum(p => p.notions) > 0
select new { g.Key.notions, g.Key.iclientid, g.Key.ifeatureid };

Orchard Content Manager: how to apply an OR operator between different Part Records fields?

I need to filter data using an OR operator on fields from two different parts:
var result = ContentManager.Query("ContentType")
.Where<PartARecord, PartBRecord>(
(a, b) => a.FieldA.Contains(searchText) || b.FieldB.Contains(searchText)
).List();
Is it possible?
Fields values stories in database as Infoset and you can't query it by standard sql (IHqlQuery or IContentQuery). But if you enable Projection Part that Fields values will be also story in separate table and you will be able to query this.
You can try something like this (I'm not sure that it will be work. May be have to replace Any() or Contains() functions)
var result = ContentManager.Query("ContentType")
.Where<FieldIndexPartRecord>(p => p.StringFieldIndexRecords.Any(
f =>
(f.PropertyName == "ContentType.FieldA." && f.Value.Contains(searchText)) ||
(f.PropertyName == "ContentType.FieldB." && f.Value.Contains(searchText))
))
.List();
But if you will use IHqlQuery, that do
var result = ContentManager.HqlQuery().ForType(new string[] { "ContentType" })
.Where(
a => a.ContentPartRecord<FieldIndexPartRecord>().Property("StringFieldIndexRecords", "fieldAAndB"),
f => f.Or(
f1 => f1.And(
checkName => checkName.Eq("PropertyName", "ContentType.FieldA."),
checkValue => checkValue.Like("Value", searchText, HqlMatchMode.Anywhere)
),
f2 => f2.And(
checkName => checkName.Eq("PropertyName", "ContentType.FieldB."),
checkValue => checkValue.Like("Value", searchText, HqlMatchMode.Anywhere)
))
)
.List();

Linq-To-SQL Marc Gravell's InRange Extension

I'm tring to use the InRange extension Marc Gravell wrote here:
LINQ Expression to return Property value?
This is my code so far:
// Fetch list of visit Ids and distinct Ips that fall into the date range
var q = (from c in db.tblTrackerVisits where c.Date >= MinDate select new { c.ID, c.IPID });
List<int> VisitIDs = q.Select(c => c.ID).ToList();
List<int> DistinctIPs = q.Select(c => c.IPID).Distinct().ToList();
// List of all campaigns that have visitors
var Campaigns = db.tblTrackerVariables
.Where(c =>
c.TypeID == Settings.CampaignTrackerVariableTypeID
//&& db.tblTrackerVisitVariables.Any(d=>VisitIDs.Contains(d.VisitID) && d.VariableID == c.ID)
&& db.tblTrackerVisitVariables.InRange(x => x.VisitID, 1500, VisitIDs)
)
.Select(c => new { c.ID, c.Name }).OrderBy(c=>c.Name);
However this throws:
Cannot implicitly convert type 'System.Collections.Generic.IEnumerable<tblTrackerVisitVariable>' to 'bool'
I'm not sure if I'm using it correctly, can anyone give me some pointers? The commented out && above it:
//&& db.tblTrackerVisitVariables.Any(d=>VisitIDs.Contains(d.VisitID) && d.VariableID == c.ID)
Is the old working code (but it throws the too many params error so I have to resort to this extension method).
Mark's extension method returns an IEnumerable, not a bool, and therefore cannot be included in a logical expression. I you want to check if the result is non-empty try the Any method:
db.tblTrackerVisitVariables.InRange(x => x.VisitID, 1500, VisitIDs).Any()

Categories

Resources