Filter query by related table count - c#

I'm trying to get a list of people who have at least one family member associated in the database.
FamilyMembers is a related table, tied by a one-to-many foreign key.
[one] People.PersonId --> [0 or more] FamilyMembers.PersonId
I tried doing this, but Count() doesn't seem to work like I thought it would.
I get results containing both 0 and more FamilyMembers.
public IEnumerable<Person> GetPeopleWithFamilyMembers()
{
IQueryable<Person> query = Context.Persons;
query = query.OrderBy(x => x.FamilyMembers.Count());
query = query.Where(x => x.FamilyMembers.Count() > 0);
// Execute query and return result:
return query.Select(x => x);
}
I'm really not sure what to do here :-/

I struggled for a good bit on a elegant solution to this without using expanded query form.
The only way I could achieve this in SQL was using a Group By and a Join, but I have no idea if I translated it to LINQ correctly. Please feel free to point out my mistakes.
var query = Persons
.Join(FamilyMembers,
p => p.Id,
f => f.Id,
(p,f) => new { Person = p, FamilyMembers = f }
)
.GroupBy(g => g.FamilyMembers)
.Where(w => w.FamilyMembers.Count() > 0);
.Select(x => new {
Person = x.Person,
FamilyMembers = x.Count(y => y.FamilyMembers)
}
);

Related

EF Core Reuse subquery in different queries

I have a problem trying to reuse some subqueries. I have the following situation:
var rooms = dbContext.Rooms.Select(r => new
{
RoomId = r.Id,
Zones = r.Zones.Select(zr => zr.Zone),
Name = r.Name,
Levels = r.Levels.Select(lr => lr.Level),
IdealSetpoint = (double?)r.Group.Setpoints.First(sp => sp.ClimaticZoneId == dbContext.ClimaticZonesLogs.OrderByDescending(cz => cz.Timestamp).First().ClimaticZoneId).Setpoint??int.MinValue,
Devices = r.Devices.Select(rd => rd.Device)
}).ToList();
var tagsTypes = rooms.Select(r => r.Devices.Select(d => GetSetpointTagTypeId(d.DeviceTypeId))).ToList().SelectMany(x => x).Distinct().ToList();
predicate = predicate.And(pv => tagsTypes.Contains(pv.TagSettings.TagTypeId) &&
pv.ClimaticZoneId == dbContext.ClimaticZonesLogs.OrderByDescending(cz => cz.Timestamp).First().ClimaticZoneId);
var setpoints = valuesSubquery.Include(t=>t.TagSettings).Where(predicate).ToList();
This works fine, and generates the exact queries as wanted. The problem is that I want to have this subquery dbContext.ClimaticZonesLogs.OrderByDescending(cz => cz.Timestamp).First().ClimaticZoneId to be taken from a method and not repeat it every time I need it.
I've tested it with the database, where I have values in the corresponding tables, and I've tested the query with the database without any data in the corresponding tables. It works fine with no problems or exceptions.
But when I try to extract the repeating subquery in a separate method and execute it against empty database tables (no data) the .First() statement throws error. Here is the code:
protected long GetClimaticZoneId()
{
return dbContext.ClimaticZonesLogs.OrderByDescending(cz => cz.Timestamp).First().ClimaticZoneId;
}
and the query generation:
var rooms = dbContext.Rooms.Select(r => new
{
RoomId = r.Id,
Zones = r.Zones.Select(zr => zr.Zone),
Name = r.Name,
Levels = r.Levels.Select(lr => lr.Level),
IdealSetpoint = (double?)r.Group.Setpoints.First(sp => sp.ClimaticZoneId == GetClimaticZoneId()).Setpoint??int.MinValue,
Devices = r.Devices.Select(rd => rd.Device)
}).ToList();
var tagsTypes = rooms.Select(r => r.Devices.Select(d => GetSetpointTagTypeId(d.DeviceTypeId))).ToList().SelectMany(x => x).Distinct().ToList();
predicate = predicate.And(pv => tagsTypes.Contains(pv.TagSettings.TagTypeId) &&
pv.ClimaticZoneId == GetClimaticZoneId());
var setpoints = valuesSubquery.Include(t=>t.TagSettings).Where(predicate).ToList();
After execution I get InvalidOperationException "Sequence do not contain any elements" exception in the GetClimaticZoneId method:
I'm sure that I'm not doing something right.
Please help!
Regards,
Julian
As #Gert Arnold suggested, I used the GetClimaticZoneId() method to make a separate call to the database, get the Id and use it in the other queries. I gust modified the query to not generate exception when there is no data in the corresponding table:
protected long GetClimaticZoneId()
{
return dbContext.ClimaticZonesLogs.OrderByDescending(cz => cz.Timestamp).FirstOrDefault()?.ClimaticZoneId??0;
}

LINQ efficiency

Consider the following LINQ statements:
var model = getModel();
// apptId is passed in, not the order, so get the related order id
var order = (model.getMyData
.Where(x => x.ApptId == apptId)
.Select(y => y.OrderId));
var orderId = 0;
var orderId = order.LastOrDefault();
// see if more than one appt is associated to the order
var apptOrders = (model.getMyData
.Where(x => x.OrderId == orderId)
.Select(y => new { y.OrderId, y.AppointmentsId }));
This code works as expected, but I could not help but think that there is a more efficient way to accomplish the goal ( one call to the db ).
Is there a way to combine the two LINQ statements above into one? For this question please assume I need to use LINQ.
You can use GroupBy method to group all orders by OrderId. After applying LastOrDefault and ToList will give you the same result which you get from above code.
Here is a sample code:
var apptOrders = model.getMyData
.Where(x => x.ApptId == apptId)
.GroupBy(s => s.OrderId)
.LastOrDefault().ToList();
Entity Framework can't translate LastOrDefault, but it can handle Contains with sub-queries, so lookup the OrderId as a query and filter the orders by that:
// apptId is passed in, not the order, so get the related order id
var orderId = model.getMyData
.Where(x => x.ApptId == apptId)
.Select(y => y.OrderId);
// see if more than one appt is associated to the order
var apptOrders = model.getMyData
.Where(a => orderId.Contains(a.OrderId))
.Select(a => a.ApptId);
It seems like this is all you need:
var apptOrders =
model
.getMyData
.Where(x => x.ApptId == apptId)
.Select(y => new { y.OrderId, y.AppointmentsId });

EF Core 2.1 GROUP BY and select first item in each group

Let's imaging a forum having a list of topics and posts in them.
I want to get the list of topics and a title of last post (by date) for each topic.
Is there a way to achieve this using EF Core (2.1)?
In SQL it could be done like
SELECT Posts.Title, Posts.CreatedDate, Posts.TopicId FROM
(SELECT Max(CreatedDate), TopicId FROM Posts GROUP BY TopicId) lastPosts
JOIN Posts ON Posts.CreatedDate = lastPosts.CreatedDate AND Posts.TopicId = lastPosts.TopicId
In EFCore I can select LastDates
_context.Posts.GroupBy(x => x.TopicId, (x, y) => new
{
CreatedDate = y.Max(z => z.CreatedDate),
TopicId = x,
});
And if I run .ToList() the query is correctly translated to GROUP BY.
But I can't go further.
The following is executed in memory, not in SQL (resulting in SELECT * FROM Posts):
.GroupBy(...)
.Select(x => new
{
x.TopicId,
Post = x.Posts.Where(z => z.CreatedDate == x.CreatedDate)
//Post = x.Posts.FirstOrDefault(z => z.CreatedDate == x.CreatedDate)
})
Attempting to JOIN gives NotSupportedException (Could not parse expression):
.GroupBy(...)
.Join(_context.Posts,
(x, y) => x.TopicId == y.TopicId && x.CreatedDate == y.CreatedDate,
(x, post) => new
{
post.Title,
post.CreatedDate,
})
I know I can do it using SELECT N+1 (running a separate query per topic), but I'd like to avoid that.
I don't know since which version of EFCore it's possible, but there's a simpler single-query alternative now:
context.Topic
.SelectMany(topic => topic.Posts.OrderByDescending(z => z.CreatedDate).Take(1),
(topic, post) => new {topic.Id, topic.Title, post.Text, post.CreatedDate})
.OrderByDescending(x => x.CreatedDate)
.ToList();
Basically what I'm doing now is after running
var topics = _context.Posts.GroupBy(x => x.TopicId, (x, y) => new
{
CreatedDate = y.Max(z => z.CreatedDate),
TopicId = x,
}).ToList();
I build the following query:
Expression<Func<Post, bool>> lastPostsQuery = post => false;
foreach (var topic in topics)
{
lastPostsQuery = lastPostsQuery.Or(post => post.TopicId == topic.TopicId && post.CreatedDate = topic.CreatedDate); //.Or is implemented in PredicateBuilder
}
var lastPosts = _context.Posts.Where(lastPostsQuery).ToList();
Which results in one query (instead of N) like SELECT * FROM Posts WHERE (Posts.TopicId == 1 AND Posts.CreatedDate = '2017-08-01') OR (Posts.TopicId == 2 AND Posts.CreatedDate = '2017-08-02') OR ....
Not extremely efficient but since the number of topics per page is quite low it does the trick.
In EF Core 2.1 GroupBy LINQ operator only support translating to the SQL GROUP BY clause in most common cases. Aggregation function like sum, max ...
linq-groupby-translation
You can until full support group by in EF Core use Dapper
I am not sure about version of EFCore it's possible, but you can try something like this: It will first group by then will select max id and return max id record from each group.
var firstProducts = Context.Posts
.GroupBy(p => p.TopicId)
.Select(g => g.OrderByDescending(p => p.id).FirstOrDefault())
.ToList();

How to filter a sublist inside parent and return parent with sublist filtered

I want to create a linq to sql query that will return a list of objects with a sublist that has been filtered.
It sounds easy but I'm not sure how to make this to work
Here the SQL Query which returns what I want:
select * from Texts t inner join Translations tt on t.TranslationId = tt.Id
inner join Pages p on tt.Id = p.TranslationId and tt.NeutralText = p.TitleNeutralTextId
where t.LanguageId = 1
Now I have to write this with linq.
What I've done so far is:
var query = this.Queryable() // Page entity
.AsNoTracking()
.Include(x => x.TitleTranslation.Texts);
return (from m in query
from l in m.TitleTranslation.Texts
where m.TitleTranslation.Texts.Any(l => l.LanguageId == 1)
select m);
But it didn't work because I got the sublist with all languages instead of language with id #1 only.
Thanks for helping,
David
Any specific reason you are writing query? Either you can use Eager Loading of EF to load all the child tables, Or below Linq statement can fetch the required result
var result = texts.Join(translations, t => t.TranslationId, tt => tt.Id, (t, tt) => new {t, tt})
.Join(pages, ttt => new { Id = ttt.tt.Id, NeutralTextId = ttt.tt.NeutralText }, p => new { Id = p.TranslationId, NeutralTextId = p.TitleNeutralTextId }, (ttt, p) => new {ttt, p})
.Where(tttt => tttt.ttt.t.LanguageId == 1);
Here replace texts, translations and pages with actual dbContext entities collection property.
I think you must try lime this. this will work for you .
This will be similar to sql query
One way to do this .
var result = from m in Texts
join Translations on Texts.TranslationId = Translation.Id
Join Pages on Translations.NeutralText = Pages.NeutralText
where Texts.LanguageId = 1
select m
There an other way to do this using entity framework
var result =
this.Queryable().AsNoTracking().Include(x=>x.Translations).Where(x=>x.LanguageId= 1)
I found the solution I wanted thanks to Hasnain Bukhari.
The solution was to start from the text table, assign the filter, include the desired Entity (Page) and put the results into memory (ToList()). Then select pages. It will give the result I want in the order I have to.
var query = textService.Queryable()
.AsNoTracking()
.Include(x => x.Translation.Pages)
.Where(x => x.LanguageId == languageId).ToList();
return query.SelectMany(x => x.Translation.Pages);

LINQ Multiple GroupBy Query Performing several times slower than T-SQL

I'm totally new to LINQ.
I have an SQL GroupBy which runs in barely a few milliseconds. But when I try to achieve the same thing via LINQ, it just seems awfully slow.
What I'm trying to achieve is fetch an average monthly duration of a ceratin database update.
In SQL =>
select SUBSTRING(yyyyMMdd, 0,7),
AVG (duration)
from (select (CONVERT(CHAR(8), mmud.logDateTime, 112)) as yyyyMMdd,
DateDIFF(ms, min(mmud.logDateTime), max(mmud.logDateTime)) as duration
from mydb.mydbo.updateData mmud
left
join mydb.mydbo.updateDataKeyValue mmudkv
on mmud.updateDataid = mmudkv.updateDataId
left
join mydb.mydbo.updateDataDetailKey mmuddk
on mmudkv.updateDataDetailKeyid = mmuddk.Id
where dbname = 'MY_NEW_DB'
and mmudkv.value in ('start', 'finish')
group
by (CONVERT(CHAR(8), mmud.logDateTime, 112))
) as resultSet
group
by substring(yyyyMMdd, 0,7)
order
by substring(yyyyMMdd, 0,7)
in LINQ => I first fetch the record from a table that links information of the Database Name and UpdateData and then do filtering and groupby on the related information.
entry.updatedata.Where(
ue => ue.updatedataKeyValue.Any(
uedkv =>
uedkv.Value.ToLower() == "starting update" ||
uedkv.Value.ToLower() == "client release"))
.Select(
ue =>
new
{
logDateTimeyyyyMMdd = ue.logDateTime.Date,
logDateTime = ue.logDateTime
})
.GroupBy(
updateDataDetail => updateDataDetail.logDateTimeyyyyMMdd)
.Select(
groupedupdatedata => new
{
UpdateDateyyyyMM = groupedupdatedata.Key.ToString("yyyyMMdd"),
Duration =
(groupedupdatedata.Max(groupMember => groupMember.logDateTime) -
groupedupdatedata.Min(groupMember => groupMember.logDateTime)
)
.TotalMilliseconds
}
).
ToList();
var updatedataMonthlyDurations =
updatedataInDateRangeWithDescriptions.GroupBy(ue => ue.UpdateDateyyyyMM.Substring(0,6))
.Select(
group =>
new updatedataMonthlyAverageDuration
{
DbName = entry.DbName,
UpdateDateyyyyMM = group.Key.Substring(0,6),
Duration =
group.Average(
gmember =>
(gmember.Duration))
}
).ToList();
I know that GroupBy in LINQ isn't the same as GroupBy in T-SQL, but not sure what happens behind the scenes. Could anyone explain the difference and what happens in memory when I run the LINQ version? After I did the .ToList() after the first GroupBy things got a little faster. But even then this way of finding average duration is really slow.
What would be the best alternative and are there ways of improving a slow LINQ statement using Visual Studio 2012?
Your linq query is doing most of its work in linq-to-objects. You should be constructing a linq-to-entities/sql query that generates the complete query in one shot.
Your query seems to have a redundant group by clause, and I am not sure which table dbname comes from, but the following query should get you on the right track.
var query = from mmud in context.updateData
from mmudkv in context.updateDataKeyValue
.Where(x => mmud.updateDataid == x.updateDataId)
.DefaultIfEmpty()
from mmuddk in context.updateDataDetailKey
.Where(x => mmudkv.updateDataDetailKeyid == x.Id)
.DefaultIfEmpty()
where mmud.dbname == "MY_NEW_DB"
where mmudkv.value == "start" || mmudkv.value == "finish"
group mmud by mmud.logDateTime.Date into g
select new
{
Date = g.Key,
Average = EntityFunctions.DiffMilliseconds(g.Max(x => x.logDateTime), g.Min(x => x.logDateTime)),
};
var queryByMonth = from x in query
group x by new { x.Date.Year, x.Date.Month } into x
select new
{
Year = x.Key.Year,
Month = x.Key.Month,
Average = x.Average(y => y.Average)
};
// Single sql statement is to sent to your database
var result = queryByMonth.ToList();
If you are still having problems, we will need to know if you are using entityframework or linq-to-sql. And you will need to provide your context/model information

Categories

Resources