Select top n rows in each group in EntityFramework - c#

I'm trying to fetch recent contents of each type, currently I'm using something similar to the following code to fetch n records for each type
int n = 10;
var contents = Entities.OrderByDescending(i => i.Date);
IQueryable<Content> query = null;
for (int i = 1; i<=5; i++)
{
if (query == null)
{
query = contents.Where(c => c.ContentTypeIndex == i).Take(n);
}
else
{
query = query.Concat(contents.Where(c => c.ContentTypeIndex == i).Take(n));
}
}
One other solution can be creating an SP, but is it possible to do it by grouping in EF? If not, any cleaner solution?

contents.Where(c => c.ContentTypeIndex >= 1 && c.ContentTypeIndex <= 5)
.GroupBy(c => c.ContentTypeIndex)
.SelectMany(g => g.Take(n));
Note: if you want to select all types of indexes, then you don't need where filter here.

Related

How to call a local method from the linq query

In my web api i need to execute a method from linq query itself. My linq query code snippet belongs to method is shown below which calls local method to get required data.
var onlineData = (from od in db.RTLS_ONLINEPERSONSTATUS
let zoneIds = db.RTLS_PERSONSTATUS_HISTORY.Where(p => p.person_id == od.PERSONID).OrderByDescending(z => z.stime > startOfThisDay && z.stime < DateTime.Now).Select(z => z.zone_id).ToList()
let zoneIdsArray = this.getZoneList((zoneIds.ToArray()))
let fzones = zoneIdsArray.Select(z => z).Take(5)
select new OnlineDataInfoDTO
{
P_ID = od.PERSONID,
T_ID = (int)od.TAGID,
LOCS = fzones.ToList()
}
public int[] getZoneList(decimal[] zoneIdsArray)
{
int[] zoneIds = Array.ConvertAll(zoneIdsArray, x => (int)x);
List<int> list = zoneIds.ToList();
for (int c = 1; c < zoneIdsArray.Count(); c++)
{
if (zoneIdsArray[c] == zoneIdsArray[c - 1])
{
list.Remove((int)zoneIdsArray[c]);
}
}
return list.ToArray();
}
I am getting exception at let zoneIdsArray = this.getZoneList((zoneIds.ToArray())), is there any way to solve this problem. I got logic to solve my problem from this link(Linq query to get person visited zones of current day ), the given logic is absolutely fine for my requirement but i am facing problem while executing it.
One way to achieve that would be to perform the projection on the client instead of the underlying LINQ provider. This can be done by separating your query into 2 steps:
var peopleStatus =
from od in db.RTLS_ONLINEPERSONSTATUS
let zoneIds = db.RTLS_PERSONSTATUS_HISTORY
.Where(p => p.person_id == od.PERSONID)
.OrderByDescending(z => z.stime > startOfThisDay && z.stime < DateTime.Now)
.Select(z => z.zone_id)
.ToList()
select new
{
Person = od,
ZoneIds = zoneIds,
};
var onlineData =
from od in peopleStatus.ToList()
let zoneIdsArray = this.getZoneList((od.ZoneIds.ToArray()))
let fzones = zoneIdsArray.Select(z => z).Take(5)
select new OnlineDataInfoDTO
{
P_ID = od.Person.PERSONID,
T_ID = (int)od.Person.TAGID,
LOCS = fzones.ToList()
};

How to take last records from list

I have list as follows
static List<MessageDetail> CurrentMessage = new List<MessageDetail>();
Dynamically, values assigned to this list for example:
CurrentMessage.Add(new MessageDetail { UserName = 123,GroupName = somegrp, Message = somemsg });
Here, I want to take last 5 or so records.
// this returns first 5 result, dont want to user orderby clause either
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.Take(5).ToList();
Is there a way to implement TakeLast() attribute? Or any kind of help will be appreciated. Thanks for your time.
Use skip:
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName).Skip(Math.Max(0, CurrentMessage.Count() - 5)).ToList();
EDIT: I also find this that I think it is more easier to use (MoreLinq):
using MoreLinq;
var CurrentMessagesForGroup2 = CurrentMessage.TakeLast(5);
Use an OrderBy (ASC or DESC) to get the records lined up correctly for your Take operation.
Ascending:
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.OrderBy(c => c.GroupName)
.Take(5)
.ToList();
or Descending:
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.OrderByDescending(c => c.GroupName)
.Take(5)
.ToList();
If anyone using DotNet Core 2 or above or DotNet Standard 2.1 or above then you can use Linq's built in .TakeLast()
Reference: Microsoft Documentation here
You could use Reverse(), which is slightly perverse.
CurrentMessagesForGroup = CurrentMessage
.Where(c => c.GroupName == groupName)
.Reverse()
.Take(5).ToList();
I use an extension method for this.
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int numElements)
{
return source.Skip(Math.Max(0, source.Count() - numElements));
}
And to use it:
CurrentMessagesForGroup = CurrentMessage.Where(c => c.GroupName == groupName).TakeLast(5).ToList();
Edit: Credit to Using Linq to get the last N elements of a collection?
I like this implementation, it uses a circular buffer.
public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> input, int n)
{
if (n == 0)
yield break;
int tail = 0;
int head = 0;
int count = 0;
T[] buffer = new T[n];
foreach (T item in input)
{
buffer[tail] = item;
tail = (tail + 1) % n;
if (count < n)
count++;
else
head = (head + 1) % n;
}
for (int i = 0; i < count; i++)
yield return buffer[(head + i) % n];
}
If the MessageDetails class has numeric Id or Created date time we can use
var lastRecords= CurrentMessage.OrderByDescending(i=>i.Id).Where(p=>p.GroupName==groupName).Take(5).ToList();

LINQ to Entities does not recognize the method

This is my Code where I am fetching data.
var list = (from u in _dbContext.Users
where u.IsActive
&& u.IsVisible
&& u.IsPuller.HasValue
&& u.IsPuller.Value
select new PartsPullerUsers
{
AvatarCroppedAbsolutePath = u.AvatarCroppedAbsolutePath,
Bio = u.Bio,
CreateDateTime = u.CreationDate,
Id = u.Id,
ModifieDateTime = u.LastModificationDate,
ReviewCount = u.ReviewsReceived.Count(review => review.IsActive && review.IsVisible),
UserName = u.UserName,
Locations = (from ul in _dbContext.UserLocationRelationships
join l in _dbContext.Locations on ul.LocationId equals l.Id
where ul.IsActive && ul.UserId == u.Id
select new PartsPullerLocation
{
LocationId = ul.LocationId,
Name = ul.Location.Name
}),
Rating = u.GetPullerRating()
});
Now Here is my Extension.
public static int GetPullerRating(this User source)
{
var reviewCount = source.ReviewsReceived.Count(r => r.IsActive && r.IsVisible);
if (reviewCount == 0)
return 0;
var totalSum = source.ReviewsReceived.Where(r => r.IsActive && r.IsVisible).Sum(r => r.Rating);
var averageRating = totalSum / reviewCount;
return averageRating;
}
I have check this Post LINQ to Entities does not recognize the method
And I come to know I need to use
public System.Linq.Expressions.Expression<Func<Row52.Data.Entities.User, int>> GetPullerRatingtest
But how ?
Thanks
You can use conditionals inside LINQ to Entity queries:
AverageRating = u.ReviewsReceived.Count(r => r.IsActive && r.IsVisible) > 0 ?
u.ReviewsReceived.Where(r => r.IsActive && r.IsVisible).Sum(r => r.Rating) /
u.ReviewsReceived.Count(r => r.IsActive && r.IsVisible)
: 0
This will be calculated by the server, and returned as part of your list. Although with 10 million rows like you said, I would do some serious filtering before executing this.
Code within LINQ (to Entities) query is executed within database, so you can't put random C# code there. So you should either use user.GetPullerRating() after it is retrieved or create a property if you don't want to do the calculation every time.
You can also do:
foreach (var u in list)
u.Rating = u.GetPullerRating()
By the way, why is it extension method.

neo4jclient Expression type Add is not supported

I have two Nodes - Phone and Users. 10000 Phones and 10000 users. I want connect it with relation.Its show me error neo4jclient Expression type Add is not supported
for (int k=1;k<10000;k++)
{
client.Cypher
.Match("(user1:User)", "(user2:Phone)")
.Where((Users user1) => user1.Fio == "Radzhab"+k)
.AndWhere((Phone user2) => user2.Name == "33-333"+k)
.Create("user1-[:HAVE_PHONE]->user2")
.ExecuteWithoutResults();
}
MATCH (user1:User), (user2:Phone) WHERE user1.Fio = "Radzhab1" AND user2.Name = "33-3331" CREATE user1-[:HAVE_PHONE]->user2; its work correct in console
This looks like a bug in Neo4jClient.
As a workaround, try this:
for (var k = 1; k < 10000; k++)
{
var fio = "Radzhab"+k;
var name = "33-333"+k;
client.Cypher
.Match("(user1:User)", "(user2:Phone)")
.Where((Users user1) => user1.Fio == fio)
.AndWhere((Phone user2) => user2.Name == name)
.Create("user1-[:HAVE_PHONE]->user2")
.ExecuteWithoutResults();
}

Do I have to leave off the skip call in my query

I have a big table in which I want to clear old records. The records have a field 'FilePath'. The 'clear' herein means mark the 'FilePath' as null. The question is because the table has millions of records, one time updating it is impossible. It blows up the memory. So my strategy is each time to fetch 2000 rows and update them then continue work on the next block.
My query:
int pageNumber = 0;
int pageSize = 2000;
bool hasHitEnd = false;
while (!hasHitEnd)
{
var size = pageNumber * pageSize;
var query = cdrContext.Mytable.Where(c => c.FacilityID == facilityID && c.FilePath != null && c.TimeStationOffHook < oldDate)
.OrderBy(c => c.TimeStationOffHook)
.Skip(size)
.Take(pageSize)
.Select(c => new { c.FilePath, c.FileName })
.ToList();
var q = cdrContext.Mytable.Where(c => c.FacilityID == facilityID && c.FilePath != null && c.TimeStationOffHook < oldDate)
.OrderBy(c => c.TimeStationOffHook)
.Skip(size)
.Take(pageSize)
.ToList();
foreach (var y in q)
{
y.FilePath = null;
}
cdrContext.SaveChanges();
if (query.Count() < pageSize)
{
hasHitEnd = true;
}
pageNumber++;
I am not confident the code. Because after updating the data, the FilePath is null. Then in the next run, it may not point to the right block as I skip one block.
Do I need to remove skip part?
You don't need to skip records, because after update next page will become first page (updated items will not match you query filter on next call).
// define query, but don't execute it
var query = cdrContext.Mytable.Where(c => c.FacilityID == facilityID &&
c.FilePath != null &&
c.TimeStationOffHook < oldDate)
.OrderBy(c => c.TimeStationOffHook)
.Take(pageSize);
List<Foo> itemsToUpdate = query.ToList(); // get first N items
while(itemsToUpdate.Any()) // all items updated
{
// update items
cdrContext.SaveChanges();
itemsToUpdate = query.ToList(); // get first N items
}
No need to skip records, subsequent page will be your first page. Also you dont need to query the db twice I see you use query and q which is not necessary. Just use q it will help a lot in perfomance. You can remove following code
if (query.Count() < pageSize)
{
hasHitEnd = true;
}
Replace that with count of records in q if q.Count() == 0 then you can break the loop or set hasHitEnd = true;

Categories

Resources