Linq .GroupBy .Where .Select new Grouping item - c#

I'm trying to build a change log query that pulls logs since last revision.
I now need to consolidate the logs based on some rules.
Here's my Changelog Model:
public abstract class ChangeLogObject : TrackedObject {
/// <summary>
/// Type of transaction that occured
/// </summary>
public ChangeType ChangeType { get; set; }
/// <summary>
/// Original Id of the stateful object this changelog was created against
/// </summary>
public long EntityId { get; set; }
}
public enum ChangeType {
Created,
Updated,
Deleted
}
So the steps I need to follow are:
Get where Id > last revision
Group By EntityId
Don't bring back any groups where the entity has ChangeType.Created and ChangeType.Deleted
Don't bring back ChangeType.Updated records if there's a ChangeType.Deleted but no ChangeType.Created (remove updated records from the group)
Only bring back the last ChangeType.Updated record if there are multiples
If the group contains ChangeType.Created and ChangeType.Updated then only bring back the last ChangeType.Updated but change it to ChangeType.Created
This is how far I've got but I don't know how to 'bring back part of a group':
var newChanges =
// 1. Get where greater than last revision
srcSet.Where(x => x.Id > lastRevision)
// 2. Group by EntityId
.GroupBy(x => x.EntityId)
// 3. Don't bring back created and deleted (it's come and gone)
.Where(x => !(x.Any(y => y.ChangeType == ChangeType.Created) &&
x.Any(y => y.ChangeType == ChangeType.Deleted)))
// 4. Only bring back Deleted if no created (don't edit something you're about to delete)
.Where(x => (!x.Any(y => y.ChangeType == ChangeType.Created) &&
x.Any(y => y.ChangeType == ChangeType.Deleted)))
//create new grouping, but only use the delete record????
//convert back to individual change logs
.Select(x => x.ToList()
//maybe order by ID?
.OrderBy(y => y.Id));
For 4. How do I turn x into a new group with only the records/values that are ChangeType.Deleted if the group itself doesn't contain a ChangeType.Created but may contain ChangeType.Updated?

Some assumptions I've made:
srcSet is a a set of ChangeLogObject items.
As per requirement 5 in your list, to enable the retrieval of the "last" item, you should be able to sort the list by some value. For this I've introduced a ChangeDate field, which is the date on which the change occurred.
In my sample, TrackedObject looks like the below:
public abstract class TrackedObject
{
public int Id { get; set; }
public DateTime ChangeDate { get; set; }
}
I've broken down each step of the query and numbered them according to the requirement:
var lastRevision = 2;
var srcSet = new List<ChangeLogObject>();
// 1. & 2.
var entityGroupsByEntityId = srcSet
.Where(m => m.Id > lastRevision) // greater than last revision
//.OrderByDescending(m => m.ChangeDate)
.GroupBy(m => m.EntityId)
.Select(group => new
{
EntityId = group.Key,
ChangeCount = group.Count(),
Changes = group.ToList().OrderByDescending(m => m.ChangeDate)
});
// 3.
var entityGroupsWithNoDeleteAndCreate = entityGroupsByEntityId
.Where(group => !(group.Changes.Any(m => m.ChangeType == ChangeType.Created)
&& group.Changes.Any(m => m.ChangeType == ChangeType.Deleted))); // it doesn't contain both creates and deletes
// 4.
var entityGroupsWithoutDeletedAndNoCreate = entityGroupsWithNoDeleteAndCreate
.Where(group => !(group.Changes.Any(m => m.ChangeType == ChangeType.Deleted)
&& (group.Changes.Count(m => m.ChangeType == ChangeType.Created) < 1))); // it doesn't contain a delete without a create
// 5.
var entityGroupsWithUpdatedButNoCreated = entityGroupsWithoutDeletedAndNoCreate
.Where(group => group.Changes.Any(m => m.ChangeType == ChangeType.Updated)
&& !group.Changes.Any(m => m.ChangeType == ChangeType.Created)) // it updated but not created
.Select(group => new ChangeLogObject
{
EntityId = group.EntityId,
ChangeDate = group.Changes.First().ChangeDate,
ChangeType = group.Changes.First().ChangeType // don't change the type
});
// 6.
var entityGroupsWithCreatedAndUpdatedOnly = entityGroupsWithoutDeletedAndNoCreate
.Where(group => group.Changes.Any(m => m.ChangeType == ChangeType.Created)
&& group.Changes.Any(m => m.ChangeType == ChangeType.Updated)) // it contains both created and updated
.Select(group => new ChangeLogObject
{
EntityId = group.EntityId,
ChangeDate = group.Changes.First(m => m.ChangeType == ChangeType.Updated).ChangeDate, // bring back the first updated record
ChangeType = ChangeType.Created // change the type to created
});
// Join the 2 sets of Updated and Created changes
var finalResult = entityGroupsWithUpdatedButNoCreated.Union(entityGroupsWithCreatedAndUpdatedOnly).ToList();
Caveat:
The linq statements are not optimised for performance as these are not wrapped in IQueryable to ensure that these queries are evaluated on the database and not In-memory.
This should be enough to get you started on breaking down each part of your requirement into a query.

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;
}

EF core get data from entity per day by limit

This is an entity
public class ViewModel
{
public string Id { get; set; }
public DateTime Start { get; set; }
public string Name { get; set; }
}
This is my context query,works with ef core in dbcontext.
var list = _Service.GetDataByMonth(start,end).ToList();
// it returns all entity between giving start, end param.
// start and end is Datetime property comes from ajax. This code works fine it return almost 1k objectlist with type of ViewModel
like
[0] Id="1",Title="sample",Start:""15.12.2020"
[1] Id="2",Title="sample2",Start:""15.12.2020"
[2] Id="3",Title="sample3",Start:""16.12.2020"
[3] Id="4",Title="sample4",Start:""16.12.2020"
As shows above we got almost 20+ entity per day.
I can get count per day like
var listt = _Service.GetDataByMonth(start,end).GroupBy(x => x.Start.Date).Select(grp => new { Date = grp.Key, Count = grp.Count() });
[0] Key:""15.12.2020",Count:20
[1] Key:""16.12.2020",Count:25
[2] Key:""17.12.2020",Count:44
it returns like this.
So what i want is giving start and end param a funciton then get 3 values per day between giving datetime object
NEW
var list1= _Service.GetDataByMonth(start,end).GroupBy(x => x.StartDate.Date)
.Select(grp => grp.Take(3)).ToList();
//this type List<Ienumerable<Viewmodel>>
var list2 = _Service.GetDataByMonth(start,end).GroupBy(x => x.StartDate.Date).Select(grp => grp.Take(3).ToList()).ToList();
// this type List<List<Viewmodel>>
My want it List<Viewmodel>
service
...
return entity.Where(x =>
x.IsDeleted == false &&
(x.StartDate.Date >= start.Date && x.StartDate.Date <=end.Date)
).OrderBy(x => x.FinishDate).ToList();
// it work with this way but bad way
var lis = list.SelectMany(d => d).ToList();
Yes I figuredout using selectmany instead of select works fine. Thank you again
You can use .Take() to only take 3 items of each group:
_Service.GetDataByMonth(start,end)
.GroupBy(x => x.Start.Date)
.Select(grp => new { Data = grp.Take(3).ToList(), Date = grp.Key, Count = grp.Count() })
.ToList();
var list1= _Service.GetDataByMonth(start,end).GroupBy(x => x.StartDate.Date)
.Select(grp => grp.Take(3)).ToList();
It returns List<List> maybe it will help some one bu my need is comes from with this code
This code makes list of list each element is list and contians 3 object.
var list1= _Service.GetDataByMonth(start,end).GroupBy(x => x.StartDate.Date)
.SelectMany(grp => grp.Take(3)).ToList();
Many makes just list. It mades list with ordered objects

How do I to use Where, Group By, Select and OrderBy at same query linq?

I'm trying to make a linq using where, group by and select at same time but I cannot do it works and it always throws an exception.
How could I do it works ?
Linq
public ActionResult getParceiros(){
//return all partners
IList<ViewParceirosModel> lista = new List<ViewParceirosModel>();
lista = context.usuarios.Where(u => u.perfil == RoleType.Parceiro)
.Select(x => new ViewParceirosModel
{
id = x.id,
nomeParceiro = x.nome,
emailAcesso = x.email
})
.GroupBy(x => x.id)
.OrderBy(x => x.nomeParceiro)
.ToList();
return View(lista);
}
Exception
Your program doesn't do what you want. Alas you forgot to tell you what you want, you only showed us what you didn't want. We'll have to guess.
So you have a sequence of Usarios.
IQueryable<Usario> usarios = ...
I don't need to know what a Usario is, all I need to know is that it has certain properties.
Your first step is throwing away some Usarios using Where: you only want to keep thos usarios that have a Perfil equal to RoleType.Parceirdo:
// keep only the Usarios with the Perfil equal to RoleType.Parceirdo:
var result = usarios.Where(usario => usario.Perfil == RoleType.Parceirdo)
in words: from the sequence of Usarios keep only those Usarios that have a Perfil equal to RoleTyoe.Parceirdo.
The result is a subset of Usarios, it is a sequence of Usarios.
From every Usario in this result, you want to Select some properties and put them into one ViewParceirosModel:
var result = usarios.Where(usario => usario.Perfil == RoleType.Parceirdo)
.Select(usario => new ViewParceirosModel
{
Id = x.id,
NomeParceiro = x.nome,
EmailAcesso = x.email,
})
In words: from every Usario that was kept after your Where, take the Id, the Nome and the Email to make one new ViewParceirosModel.
The result is a sequence of ViewParceirosModels. If you add ToList(), you can assign the result to your variable lists.
However your GroupBy spoils the fun
I don't know what you planned to do, but your GroupBy, changes your sequence of ViewParceirosModels into a sequence of "groups of ViewParceirosModels" Every ViewParceirosModel in one group has the same Id, the value of this Id is in the Key.
So if after the GroupBy you have a group of ViewParceirosModel with a Key == 1, then you know that every ViewParceirosModel in this group will have an Id equal to 1.
Similarly all ViewParceirosModel in the group with Key 17, will have an Id equal to 17.
I think Id is your primary key, so there will only be one element in each group. Group 1 will have the one and only ViewParceirosModel with Id == 1, and Group 17 will have the one and only ViewParceirosModel with Id == 17.
If Id is unique, then GroupBy is useless.
After the GroupBy you want to Order your sequence of ViewParceirosModels in ascending NomeParceiro.
Requirement
I have a sequence of Usarios. I only want to keep those Usarios with a Perfil value equal to RoleType.Parceirdo. From the remaining Usarios, I want to use the values of properties Id / Nome / Email to make ViewParceirosModels. The remaining sequence of ViewParceirosModels should be ordered by NomeParceiro, and the result should be put in a List.
List<ViewParceirosModel> viewParceiroModels = Usarios
.Where(usario => usario.Perfil == RoleType.Parceirdo)
.Select(usario => new ViewParceirosModel
{
Id = x.id,
NomeParceiro = x.nome,
EmailAcesso = x.email,
}
.OrderBy(viewParceirosModel => viewParceirosModel.NomeParceiro)
.ToList();
When you create a LINQ query with group by clause, you receive as result a grouped query.
It is a kind of dictionary that has as key the field you chose to group and as value a list of records of this group.
So, you cannot order by "nomeParceiro" because this field is inside the group.
If you detail how you expect the result I can show you a code example for this.
You can find more details in this section of the doc: https://learn.microsoft.com/pt-br/dotnet/csharp/linq/group-query-results
Let's say ViewParceirosModel look like
public class ViewParceirosModel
{
public int id {get; set;}
public List<string> nomeParceiro {get; set;}
public List<string> emailAcesso {get; set;}
}
After that, you can Groupby then select combine with Orderby like below
IList<ViewParceirosModel> lista = new List<ViewParceirosModel>();
lista = context.usuarios.Where(u => u.perfil == RoleType.Parceiro)
.Select(x => new ViewParceirosModel
{
id = x.id,
nomeParceiro = x.nome,
emailAcesso = x.email
})
.GroupBy(x => x.id)
.Select(g => new ViewParceirosModel
{
id = g.Key,
nomeParceiro = g.Select(p => p.nomeParceiro).OrderBy(x => x.nomeParceiro).ToList()
nomeParceiro = g.Select(p => p.emailAcesso).ToList()
})
.ToList();
You can use the following code.
IList<ViewParceirosModel> lista = new List<ViewParceirosModel>();
lista = context.usuarios.Where(u => u.perfil == RoleType.Parceiro)
.Select(x => new ViewParceirosModel
{
id = x.id,
nomeParceiro = x.nome,
emailAcesso = x.email
})
.OrderBy(x => x.nomeParceiro)
.GroupBy(x => x.id)
.ToList();
or
List<List<ViewParceirosModel>> listb = context.usuarios
.Where(u => u.perfil == RoleType.Parceiro)
.GroupBy(g => g.id).OrderBy(g => g.Key)
.Select(g => g.OrderBy(x => x.nomeParceiro)).ToList();

Group by some columns depending on values in Entity Framework

I have the following simple statement in my Entity Framework code:
query = query
.Where(c => c.NotificationType == NotificationType.AppMessage)
.GroupBy(c => c.ConversationId)
.Select(d => d.OrderByDescending(p => p.DateCreated).FirstOrDefault());
It simply finds the latest Notification based on a group by with conversationId and select latest. Easy.
However, this is ONLY what I want if c.NotificationType == NotificationType.AppMessage. If the column is different than AppMessage (c.NotificationType <> NotificationType.AppMessage), I just want the column. What I truly Want to write is a magical statement such as:
query = query
.Where(c => (c.NotificationType <> NotificationType.AppMessage)
|| ((c.NotificationType == NotificationType.AppMessage)
.GroupBy(c => c.ConversationId)
.Select(d => d.OrderByDescending(p => p.DateCreated).FirstOrDefault()));
But this doesn't make sense because the GroupBy/Select is based on the first where statement.
How do I solve this?
The simplest way is to compose UNION ALL query using Concat at the end of your original query:
query = query
.Where(c => c.NotificationType == NotificationType.AppMessage)
.GroupBy(c => c.ConversationId)
.Select(d => d.OrderByDescending(p => p.DateCreated).FirstOrDefault())
.Concat(query.Where(c => c.NotificationType != NotificationType.AppMessage));
public class EntityClass
{
public int NotificationType { get; set; }
public int ConversationId { get; set; }
public DateTime Created { get; set; }
public static EntityClass GetLastNotification(int convId)
{
var list = new List<EntityClass>(); // Fill the values
list = list
.GroupBy(i => i.ConversationId) // Group by ConversationId.
.ToDictionary(i => i.Key, n => n.ToList()) // Create dictionary.
.Where(i => i.Key == convId) // Filter by ConversationId.
.SelectMany(i => i.Value) // Project multiple lists to ONLY one list.
.ToList(); // Create list.
// Now, you can filter it:
// 0 - NotificationType.AppMessage
// I didn't get what exactly you want to filter there, but this should give you an idea.
var lastNotification = list.OrderByDescending(i => i.Created).FirstOrDefault(i => i.NotificationType == 0);
return lastNotification;
}
}
you filter your list with "GroupBy" based on ConversationId. Next, create a dictionary from the result and make only one list (SelectMany). Then, you already have one list where should be only records with ConversationId you want.
Last part is for filtering this list - you wanted to last notification with certain NotificationType. Should be working :)

Linq OrderBy Sub List

I am trying to perform a fairly simple order by but seem to be struggling on how to go about doing it. Take for instance I have these two classes.
public class Method
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public List<Slot> Slots { get; set; }
}
public class Slot
{
public DateTime ExpectedDeliveryDate { get; set; }
}
Using the code below I want to order by the cheapest option and then by the quickest delivery date.
var methods = new List<Method>();
methods.Add(new Method { Id = 1, Name = "Standard", Price = 0M, Slots = new List<Slot> { new Slot { ExpectedDeliveryDate = DateTime.Now.AddDays(5).Date } } });
methods.Add(new Method { Id = 2, Name = "Super Fast Next Day", Price = 0M, Slots = new List<Slot> { new Slot { ExpectedDeliveryDate = DateTime.Now.AddDays(1).Date } } });
var b = methods.OrderBy(x => x.Price)
.ThenBy(y => y.Slots.OrderBy(t => t.ExpectedDeliveryDate.Date)
.ThenBy(t => t.ExpectedDeliveryDate.TimeOfDay))
.ToList();
The trouble I am getting here is that I am getting a runtime error stating "At least one object must implement IComparable".
Although I can fix this by implementing the IComparable interface, I was wondering if it was possible to do this. I imagine there is as if I had this code (see below) it works fine.
var slots = new List<Slot>();
slots.Add(new Slot { ExpectedDeliveryDate = DateTime.Now.AddDays(5).Date });
slots.Add(new Slot { ExpectedDeliveryDate = DateTime.Now.AddDays(1).Date });
slots.Add(new Slot { ExpectedDeliveryDate = DateTime.Now.AddDays(3).Date });
slots.Add(new Slot { ExpectedDeliveryDate = DateTime.Now.Date });
var d = slots.OrderBy(x => x.ExpectedDeliveryDate);
Cheers, DS.
Apologies for the naming of variables such as xyz in example above :) Code can be copied and pasted for manipulation pleasure.
EDIT
- Updated to simplify code example.
- Expectation of result would be after successful sorting
Input
ID Name Price Slot
1 Standard 0 DateTime.Now.AddDays(5).Date
2 Super Fast 0 DateTime.Now.Date
Output
2 Super Fast 0 DateTime.Now.Date
1 Standard 0 DateTime.Now.AddDays(5).Date
So my super fast option should be top due to it being the cheapest and of course has the quickest delivery date.
You can use Enumerable.Min() to pick out the slot with the earliest date, like so:
var query = deliveryMethods
.OrderBy(x => x.Slots.Min(s => s.ExpectedDeliveryDate).Year)
.ThenBy(x => x.Slots.Min(s => s.ExpectedDeliveryDate).Month)
.ThenBy(x => x.Slots.Min(s => s.ExpectedDeliveryDate).Date)
.ToList();
Or, just
var query = deliveryMethods
.OrderBy(x => x.Slots.Min(s => s.ExpectedDeliveryDate.Date))
.ToList();
Do be aware that Min() will throw an exception when the input sequence is empty and the type being minimized is a value type. If you want to avoid the exception, you could do this:
var query2 = deliveryMethods
.OrderBy(x => x.Slots.Min(s => (DateTime?)(s.ExpectedDeliveryDate.Date)))
.ToList();
By converting the DateTime to a nullable, Min() will return a null for an empty sequence, and Method objects with empty slot list will get sorted to the beginning.
I'd like to give an explanation of why the attempt you posted in your original post wasn't working:
var xyz = deliveryMethods
.OrderBy(x => x.Slots.OrderBy(y => y.ExpectedDeliveryDate.Year))
.ThenBy(x => x.Slots.OrderBy(y => y.ExpectedDeliveryDate.Month))
.ThenBy(x => x.Slots.OrderBy(y => y.ExpectedDeliveryDate.Date))
.ToList();
It was because you were nesting OrderBys inside OrderBys.
x.Slots.OrderBy(...) produces an IEnumerable<Slot>, so you were basically telling it "compare these IEnumerable<Slot>s against each other to decide the order of the delivery methods". But Linq doesn't know how to compare an IEnumerable<Slot> against another one and decide which comes before the other (IEnumerable<Slot> does not implement IComparable<T>), so you were getting an error.
The answer, as another user has pointed out, is to give it something that can be compared. As you have afterwards clarified, that would be the earliest slot for each delivery method:
var xyz = deliveryMethods
.OrderBy(x => x.Slots.Min(y => y.ExpectedDeliveryDate))
.ToList();
This will work under the assumption that each delivery method has at least one slot, but will throw a runtime exception if any of them has zero slots (or if Slots is null). I've asked you twice what it should do in that case, and I encourage you to clarify that.
One possible solution would be to only include delivery methods that have slots:
var xyz = deliveryMethods
.Where(x => x.Slots != null && x.Slots.Any())
.OrderBy(x => x.Slots.Min(y => y.ExpectedDeliveryDate))
.ToList();

Categories

Resources