EF Linq group by ICollection of objects - c#

All,
I have a Linq query which fetches a list of events which works great. The problem I'm facing is that Events contains a ICollection of Artists called headliners and in the list I only want 1 event per,set of, Artist(s).
The query underneath works fine but: I require a top 10 of Events but only one Event per, set of, artist(s) for sorting the popularity of the artist with highest popularity can be used - not what i want.
Context.Events
.Where(x => x.Stage.Venue.AreaId == 1 && x.StartDateTimeUtc > DateTime.UtcNow && x.IsVerified)
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity))
.Take(10)
.ToList();
How can I adjust the query above that I only get one Event per Artist. I would need to do some sort of grouping to see if the event is performed by same (set of) Artist(s).
I'm looking into using the Artist's primary key but because it is an collection i cannot get it to work. I already tried the String.Join to get a single unique key for the headliners. This is however not support in entity framework.
Is this something that can (gracefully) be supported by Linq to EF?
The following SQL query does almost what i want expect that it won't work with multiple artist for the same event
SELECT MAX(E.EventId), MAX(E.Name)
FROM [dbo].[Events] E
INNER JOIN [dbo].[Stages] S ON E.StageId = S.StageId
INNER JOIN [dbo].[Venues] V ON S.VenueId = V.VenueId
INNER JOIN [dbo].[Areas] A ON V.AreaId = A.AreaId
INNER JOIN [dbo].[Headliners] H ON E.EventId = H.EventId
INNER JOIN [dbo].[Artists] A2 ON A2.ArtistId = H.ArtistId
WHERE E.IsVerified = 1 AND E.StartDateTimeUtc>GETDATE() AND A.AreaId = 1
GROUP BY A2.ArtistId, A2.Name, A2.EchoNestHotttnesss
ORDER BY A2.EchoNestHotttnesss desc

Challenging task, but here it is:
var availableEvents = db.MusicEvents.Where(e =>
e.Stage.Venue.AreaId == 1 && e.StartDateTimeUtc > DateTime.UtcNow && e.IsVerified);
var topEvents =
(from e1 in availableEvents
where e1.Headliners.Any() &&
!availableEvents.Any(e2 => e2.StartDateTimeUtc < e1.StartDateTimeUtc &&
!e2.Headliners.Any(a2 => !e1.Headliners.Any(a1 => a1.Id == a2.Id)) &&
!e1.Headliners.Any(a1 => !e2.Headliners.Any(a2 => a2.Id == a1.Id)))
orderby e1.Headliners.Max(a => a.Popularity) descending
select e1)
.Take(10)
.ToList();
The first subquery (availableEvents) is just for reusing the "availability" filter inside the main query. It does not execute separately.
The critical part is the condition
!availableEvents.Any(e2 => e2.StartDateTimeUtc < e1.StartDateTimeUtc &&
!e2.Headliners.Any(a2 => !e1.Headliners.Any(a1 => a1.Id == a2.Id)) &&
!e1.Headliners.Any(a1 => !e2.Headliners.Any(a2 => a2.Id == a1.Id)))
The idea is to exclude the later events for the same set of headliners. It should be read this way:
Exclude the event if there is another available event starting earlier and there is no at least one artist from either event that is not headliner of the other event (i.e. they have the same headliner set).

Edit:
A pretty decent partial-LINQ lazily executed solution could be done in this way:
Firstly, get your query up to the ordered events based on popularity:
var evArtists = Context.Events
.Where(x => x.Stage.Venue.AreaId == 1 && x.StartDateTimeUtc > DateTime.UtcNow && x.IsVerified)
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity));
Secondly, since a ICollection<Artist> can be unordered yet forming equal set, creates an intermediate function to check if two ICollection<Artist> are of identical members:
private bool areArtistsEqual(ICollection<Artist> arts1, ICollection<Artist> arts2) {
return arts1.Count == arts2.Count && //have the same amount of artists
arts1.Select(x => x.ArtistId)
.Except(arts2.Select(y => y.ArtistId))
.ToList().Count == 0; //when excepted, returns 0
}
Thirdly, use the above method to get the unique artists set in the query results, put the results in a List, and fill the List with the number of elements you need (say, 10 elements):
List<Events> topEvList = new List<Events>();
foreach (var ev in evArtists) {
if (topEvList.Count == 0 || !topEvList.Any(te => areArtistsEqual(te.Headliners, ev.Headliners)))
topEvList.Add(ev);
if (topEvList.Count >= 10) //you have had enough events
break;
}
Your result is in the topEvList.
Benefits:
The solution above is lazily executed and is also pretty decent in the sense that you can really break down the logic and check your execution piece by piece without breaking the performance.
Note that using the method above you do not need to refer to the evArtists (which is your large query) other than by its individual element ev. Using full-LINQ solution is possible, yet you may need to refer to evArtists.Any to find the duplicates set of artists (as you do have have memory of what sets has been chosen before) from the original ordered query itself (rather than by simply using its element (ev) one by one).
This is possible because you create a temporary memory topEvList which records what sets have been chosen before and only need to check if the next element (ev) is not among the already selected set of artists. Thus, you do not impair your performance by checking you set of artists against the whole ordered query every time.
Original:
You are almost there actually. What you further need are LINQ GroupBy and First, and put your Take(10) the last:
var query = Context.Events
.Where(x => x.Stage.Venue.AreaId == 1 && x.StartDateTimeUtc > DateTime.UtcNow && x.IsVerified)
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity))
.GroupBy(a => a.ArtistId)
.Select(e => e.First())
.Take(10);
Since in by this query you have sorted your headliner artist:
.OrderByDescending(x => x.Headliners.Max(y => y.Popularity))
Then you only need to group your headliners by ArtistId:
.GroupBy(a => a.ArtistId)
Thus each artist would be having one group. Then next, you only want the first element in the group (supposedly the most popular Event per Artist):
.Select(e => e.First())
And thus you will get all the most popular events per artist. And lastly, among these most popular events per artist, you only want to take 10 of them, thus:
.Take(10);
And you are done!

Related

C# LINQ to Entities - Retrieve all records and position for waiting list records

I have a waitlist table (id, locationid, timeslotid, sessiondate, memberid, dateAdded) which contains a list of people on various waitlist for various appointments.
I am trying to retrieve a list of all waitlist records for a specific user but i also need to get the position of that record (ordered by dateAdded) so that i can see if they are in position 1, 2, 3 etc in the queue.
The following code is what i have atm for getting all user records, but it am struggling on how to join the count to this query.
db.WaitingLists.Where(x => x.MemberId == member.Id && x.LocationId == locationId && x.SessionDate >= currentLocationDate.Date).ToList();
Some suggestions would be welcomed on how to do this.
Thanks
============= UPDATE ==============
This is the SQL that provides the response I need. I am trying to prevent using a stored Proc and try and use linq to entities where possible.
select
(
SELECT count(*) RowNr
FROM (
SELECT
ROW_NUMBER() OVER (ORDER BY CreatedDate) AS RowNr,
MemberId
FROM waitinglist where LocationId = wl.LocationId and TimeSlotId = wl.TimeSlotId and [SessionDate] = wl.SessionDate
) sub
)
as Position, * from WaitingList as wl where memberid = '00000000-0000-0000-0000-000000000000'
I haven't tested this, but it should be pretty close. Filter first by the location and date, then sort it, then use the overload of Select that gives you an index, then filter by the member ID.
db.WaitingLists
.Where(x => x.Member.LocationId == locationId && x.Member.SessionDate >= currentLocationDate.Date)
.OrderBy(x => x.DateAdded)
.Select((x, i) => new { Position = i, Member = x })
.Where(x => x.Member.MemberId == member.Id)
.ToList();
This will give you a list of anonymous objects with two properties:
Position, which is the position in the waiting list, and
Member, which is the member details
I can't say what the SQL will actually look like, and if it will be efficient.
This might work for you. I'm assuming that the queue position is across all locations and session dates. If it isn't, insert another Where clause in between db.WaitingLists and the OrderBy.
var temp = db.WaitingLists
.Orderby(x => x.dateAdded)
.Select( (r, i) => new { Request = r, QueuePosition = i + 1)
.Where(x => x.Request.MemberId == member.Id &&
x.Request.LocationId == locationId &&
x.Request.SessionDate >= currentLocationDate.Date);
You now have a list of anonymous-type objects that have the Request (my name for your type; rename it if you want to) and a second property called QueuePosition, based on the position of the requests in the queue, as represented by the WaitingList sorted by dateAdded.
If you need to extract just the Request object, you can get it this way:
var selectedRequests = temp.Select(x => x.Request).ToList();
If you want to get any entries that were in the first 3 positions, you'd do this:
var selectedRequests = temp.Where(x => x.QueuePosition <= 3).Select(x => x.Request).ToList();
Disclaimer: done from memory, not actually tested.

Linq select a value based on where condition from other table

I have this query:
var result = (from game in db.Games
join gameevent in db.Events
on game.GameId equals gameevent.GameId into events
from _event in events
join _targetObjects in db.TargetObjects
on _event.TargetObject equals _targetObjects.TargetObjectId into targetss
where game.userId == userId
select new ProfileViewModel
{
record = events.Where(s => s.TargetObjectId == _event.TargetObject && _event.EventType == 35).Select(/* Here I want to select a value from targetss, field called TargetName */).ToList()
}).First();
As you can see, I want to get value based on where clause from other table. Is that possible in the select new part?
I want to get the name of the targetObject based on the targetObjectId which matches the targetObjectId in events table and also the event type should be 35.
If the query starts to become a too complex one then it is worth splitting it into parts.
In my example below I use the extension method syntax of LINQ instead of query keywords just because it is easier for me to use.
// First we collect the relevant games.
var games =
db
.Games
.Where(game => game.UserId == userId);
// Then we collect the events of the collected games that have the specified event type.
var events =
db
.Events
.Join(
games,
gameEvent => gameEvent.GameId,
game => game.GameId,
(gameEvent, game) => gameEvent
)
.Where(gameEvent => gameEvent.EventType == 35);
// Then we collect the target objects based on the collected events.
var targetObjects =
db
.TargetObjects
.Join(
events,
targetObject => targetObject.TargetObjectId,
gameEvent => gameEvent.TargetObjectId,
(targetObject, gameEvent) => targetObject
);
// Last we select the target name from the collected target objects.
var records =
targetObjects
.Select(targetObject => targetObject.TargetName)
.ToList(); // The query will be executed at this point.
If this is not what you are looking for, please clarify what data the ProfileViewModel should have exactly and from which set should it be selected as I am not very familiar with this syntax.

Using nested LINQ queries and collections of objects

I am using a two LINQ queries(nested)
Here's what I want my query to achieve:
In my inner query
Retrieve a collection of UserLocation objects using two conditions
In my outer query
Retrieve a filtered collection of User objects where User.UID matches the property UserLocation.UID of each UserLocation object from the collection in the inner query.
I'm almost there code-wise, I'm just missing the final step -- I don't know how to get the outer query to enumerate through the UserLocation collection and match the UID.
In my code I have two queries, the top one is a working example of getting the FullName property of a User object using the inner query and the conditions I need(as well as matching UID).
The second query is the one I am having trouble with. What am I missing?
ownerLiteral.Text =
Users.First(u => u.UID.Equals(
UserLocations.First(s => s.IsOwner == true && s.LID.Equals(building.LID)).UID)).FullName;
var propertyteam =
Users.Where(c => c.UID.Equals(
UserLocations.Where(x => x.IsPropertyTeam == true && x.LID.Equals(building.LID))));
Edit: Fixed the problem
I had forgotten that UserLocations was a member of Users -- I shortened down my query and used .Any to select the UserLocations members that fit my conditions, then just return the User.
In the first query I return the FullName for the User object.
In the second query I now return a collection of User objects that fit the conditions.
For those that are interested I bind the second query to a DataList and then evaluate for their FullName in the user control.
ownerLiteral.Text =
Users.First(
u => u.UserLocations.Any(
ul => ul.IsOwner == true && ul.LID.Equals(building.LID))).FullName;
var propertyteam =
Users.Where(
u => u.UserLocations.Any(
ul => ul.IsPropertyTeam && ul.LID.Equals(building.LID)));
Your class relationships are confusing me, but I think your problem is that you're trying to treat a collection of UserLocation objects (Where() returns an IEnumerable) as a single UserLocation
I think this might do it:
var propertyteam = LoggedInUser.ExtranetUser.Company.Users
.Where(c => c.UID.IsPropertyTeam == true && c.UID.LID.Equals(building.LID));
Edit, based on further information:
So maybe this is what you're looking for?
var uidsWhoArePartOfThePropertyTeamForThisBuilding
= UserLocations.Where(x => x.IsPropertyTeam && x.LID == building.LID)
.Select(x => x.UID);
Assuming the UID member of a UserLocation is a whole User object, and not just some int ID for a User.
To write a T-SQL type IN clause, in LINQ you would use Contains. This would be where I would start:
var userlocations = LoggedInUser.ExtranetUser.UserLocations.Select(ul => ul.UID);
var propertyteam = LoggedInUser.ExtranetUser.Company.Users.Where(userlocations.Contains(u => u.UID));

How to Group and Order in a LINQ Query

I would like to group & order by in a query builder expression. The following query gets me close to what i want but the order by does not appear to be working.
what i have is an object that has unique ids but some will have a common versionId. I would like to get the last edited item of the same versionId. So only one item per version id and i want it to be the last edited one.
IQueryable<Item> result = DataContext.Items.Where(x => (x.ItemName.Contains(searchKeyword) ||
x.ItemDescription.Contains(searchKeyword))
.GroupBy(y => y.VersionId)
.Select(z => z.OrderByDescending(item => item.LastModifiedDateTime).FirstOrDefault());
Edit: I don't really care about the order of the result set, i really just care about what item within the grouping is returned. I want to make sure that the last edited Item within a versionId group is return.
Your z parameter contains the individual group objects.
By calling OrderBy inside of Select, you're ordering the items in each group, but not the groups themselves.
You need to also call OrderBy after Select, like this:
.Select(z.OrderByDescending(item => item.LastModifiedDateTime).FirstOrDefault())
.Where(item => item != null)
.OrderByDescending(item => item.LastModifiedTime)

LINQ: Doing an order by!

i have some Linq to Entity code like so:
var tablearows = Context.TableB.Include("TableA").Where(c => c.TableBID == 1).Select(c => c.TableA).ToList();
So i'm returning the results of TableA with TableB.TableBID = 1
That's all good
Now how can I sort TableA by one of its column? There is a many to many relation ship between the two tables
I tried various ways with no look, for example
var tablearows = Context.TableB.Include("TableA").Where(c => c.TableBID == 1).Select(c => c.TableA).OrderBy(p => p.ColumnToSort).ToList();
In the above case when i type "p." i don't have access to the columns from TableA, presumably because it's a collection of TableA objects, not a single row
How about using SelectMany instead of Select :
var tablearows = Context.TableB.Include("TableB")
.Where(c => c.TableBID == 1)
.SelectMany(c => c.TableA)
.OrderBy(p => p.ColumnToSort)
.ToList();
EDIT :
The expression below returns collection of TableAs -every element of the collection is an instance of TableA collection not TableA instance- (that's why you can't get the properties of the TableA) :
var tablearows = Context.TableB.Include("TableB")
.Where(c => c.TableBID == 1)
.Select(c => c.TableA);
If we turn the Select to SelectMany, we get the result as one concatenated collection that includes elements :
var tablearows = Context.TableB.Include("TableB")
.Where(c => c.TableBID == 1)
.SelectMany(c => c.TableA);
Okay, so now I've taken on board that there's a many to many relationship, I think Canavar is right - you want a SelectMany.
Again, that's easier to see in a query expression:
var tableARows = from rowB in Context.TableB.Include("TableA")
where rowB.TableBID == 1
from rowA in rowB.TableA
orderby rowA.ColumnToSort
select rowA;
The reason it didn't work is that you've got a different result type. Previously, you were getting a type like:
List<EntitySet<TableA>>
(I don't know the exact type as I'm not a LINQ to Entities guy, but it would be something like that.)
Now we've flattened all those TableA rows into a single list:
List<TableA>
Now you can't order a sequence of sets by a single column within a row - but you can order a sequence of rows by a column. So basically your intuition in the question was right when you said "presumably because it's a collection of TableA objects, not a single row" - but it wasn't quite clear what you mean by "it".
Now, is that flattening actually appropriate for you? It means you no longer know which B contributed any particular A. Is there only actually one B involved here, so it doesn't matter? If so, there's another option which may even perform better (I really don't know, but you might like to look at the SQL generated in each case and profile it):
var tableARows = Context.TableB.Include("TableA")
.Where(b => b.TableBID == 1)
.Single()
.TableA.OrderBy(a => a.ColumnToSort)
.ToList();
Note that this will fail (or at least would in LINQ to Objects; I don't know exactly what will happen in entities) if there isn't a row in table B with an ID of 1. Basically it selects the single row, then selects all As associated with that row, and orders them.

Categories

Resources