Linq Group By - Unable to group on custom classes? - linq

I have noticed that when I do a LINQ - group by, it only seems to be working if I dont group on any of my custom classes.
I have a Product class (shown below) and I would like to group on Product.Id ,Product.Variant (object) and on Product.Options (ICollection) (because my source list contains multiple times the same Product but with different Variants and / or Options)
Product:
public class Product
{
public int Id { get; set; }
public Variant Variant { get; set; }
public ICollection<Option> Options{ get; set; }
public string Name { get; set; }
public int Amount { get; set; }
}
The code below will do a grouping, but only on my Product.ID, when I try to also group on Variant / Options, I get no grouping (well, not the grouping I am intending to make) as it will return just as much items as my source list.
IEnumerable<productAndSum> productsAndSums = unmappedProducts
.GroupBy(prod => new { Id = prod.Id})
.Select(group => new productAndSum()
{
Key = group.Key,
Sum = group.Sum(x => x.Amount)
});
If I am on the right track and the issue is related to the Objects, then it might be usefull to add that also Option and Variant have multiple objects and collections themselves, or is this too deep?
Extra information: I first started to just group by my Product class (not Id, Variant & Options separately), but this was unsuccessful. so I started eliminating properties and this is how I found out this issue. I think that solving this issue will result in killing two birds with one stone.
Warm regards

Related

Returning Most Recent Records Based on Serial Number

I have the following model
public class EngineComplianceMetric
{
public int Id { get; set; }
public DateTime BuildDate { get; set; }
public string SerialNumber { get; set; }
public bool Complied { get; set; }
}
SerialNumber isn't unique to the table, as the same item could have been repaired multiple times over the years.
I'm trying to return a list that only includes each Serial Numbers most recent record that's before some set date.
So far I have
previousCompliedList = _dbContext.ComplianceMetrics
.Where(e => e.BuildDate < startDate)
.Distinct().ToList();
But this is just returning everything prior to the startDate.
You have to group your items by serial numbers, then you have to take the most recent item in each group (by ordering them and taking the first).
previousCompliedList = _dbContext.ComplianceMetrics
.Where(cm => cm.BuildDate < startDate).ToList()
.GroupBy(cm => cm.SerialNumber)
.Select(cm => cm.OrderByDescending(c => c.BuildDate).First());
Based on your error, you have to add the .ToList() after the where (before the GroupBy). But doing this will fetch all the items from the DB where the date is smaller and then run additional manipulations on it in memory.
If you don't want to fetch all the items from the DB, you will have to implement a solution similar to Astrid's suggestion in the comments.
I would recommend googling your error messages: https://stackoverflow.com/a/60874316/7175057

Combining Linq Expressions for Dto Selector

We have a lot of Dto classes in our project and on various occasions SELECT them using Expressions from the entity framework context. This has the benefit, that EF can parse our request, and build a nice SQL statement out of it.
Unfortunatly, this has led to very big Expressions, because we have no way of combining them.
So if you have a class DtoA with 3 properties, and one of them is of class DtoB with 5 properties, and again one of those is of class DtoC with 10 properties, you would have to write one big selector.
public static Expression<Func<ClassA, DtoA>> ToDto =
from => new DtoA
{
Id = from.Id,
Name = from.Name,
Size = from.Size,
MyB = new DtoB
{
Id = from.MyB.Id,
...
MyCList = from.MyCList.Select(myC => new DtoC
{
Id = myC.Id,
...
}
}
};
Also, they cannot be reused. When you have DtoD, which also has a propertiy of class DtoB, you would have to paste in the desired code of DtoB and DtoC again.
public static Expression<Func<ClassD, DtoD>> ToDto =
from => new DtoD
{
Id = from.Id,
Length = from.Length,
MyB = new DtoB
{
Id = from.MyB.Id,
...
MyCList = from.MyCList.Select(myC => new DtoC
{
Id = myC.Id,
...
}
}
};
So this will escalate pretty fast. Please note that the mentioned code is just an example, but you get the idea.
I would like to define an expression for each class and then combine them as required, as well as EF still be able to parse it and generate the SQL statement so to not lose the performance improvement.
How can i achieve this?
Have you thought about using Automapper ? You can define your Dtos and create a mapping between the original entity and the Dto and/or vice versa, and using the projection, you don't need any select statements as Automapper will do it for you automatically and it will project only the dto's properties into SQL query.
for example, if you have a Person table with the following structure:
public class Person
{
public int Id { get; set; }
public string Title { get; set; }
public string FamilyName { get; set; }
public string GivenName { get; set; }
public string Initial { get; set; }
public string PreferredName { get; set; }
public string FormerTitle { get; set; }
public string FormerFamilyName { get; set; }
public string FormerGivenName { get; set; }
}
and your dto was like this :
public class PersonDto
{
public int Id { get; set; }
public string Title { get; set; }
public string FamilyName { get; set; }
public string GivenName { get; set; }
}
You can create a mapping between Person and PersonDto like this
Mapper.CreateMap<Person, PersonDto>()
and when you query the database using Entity Framework (for example), you can use something like this to get PersonDto columns only:
ctx.People.Where(p=> p.FamilyName.Contains("John"))
.Project()
.To<PersonDto>()
.ToList();
which will return a list of PersonDtos that has a family name contains "John", and if you run a sql profiler for example you will see that only the PersonDto columns were selected.
Automapper also supports hierachy, if your Person for example has an Address linked to it that you want to return AddressDto for it.
I think it worth to have a look and check it, it cleans a lot of the mess that manual mapping requires.
I thought about it a little, and I didn't come up with any "awesome" solution.
Essentially you have two general choices here,
Use placeholder and rewrite expression tree entirely.
Something like this,
public static Expression<Func<ClassA, DtoA>> DtoExpression{
get{
Expression<Func<ClassA, DtoA>> dtoExpression = classA => new DtoA(){
BDto = Magic.Swap(ClassB.DtoExpression),
};
// todo; here you have access to dtoExpression,
// you need to use expression transformers
// in order to find & replace the Magic.Swap(..) call with the
// actual Expression code(NewExpression),
// Rewriting the expression tree is no easy task,
// but EF will be able to understand it this way.
// the code will be quite tricky, but can be solved
// within ~50-100 lines of code, I expect.
// For that, see ExpressionVisitor.
// As ExpressionVisitor detects the usage of Magic.Swap,
// it has to check the actual expression(ClassB.DtoExpression),
// and rebuild it as MemberInitExpression & NewExpression,
// and the bindings have to be mapped to correct places.
return Magic.Rebuild(dtoExpression);
}
The other way is to start using only Expression class(ditching the LINQ). This way you can write the queries from zero, and reusability will be nice, however, things get harder & you lose type safety. Microsoft has nice reference about dynamic expressions. If you structure everything that way, you can reuse a lot of the functionality. Eg, you define NewExpression and then you can later reuse it, if needed.
The third way is to basically use lambda syntax: .Where, .Select etc.. This gives you definitely better "reusability" rate. It doesn't solve your problem 100%, but it can help you to compose queries a bit better. For example: from.MyCList.Select(dtoCSelector)

Using LINQ to combine similar rows of grouped data

I've got some data in a table that looks like so:
Recipe | Category | Email
What I'd like to do is pull this data back from the source and put it into something that looks like so:
public class RecipeItem
{
public long Recipe { get; set; }
public long Category { get; set; }
public List<string> Names {get; set; }
}
Grouping by the Recipe and Category ids and putting all the emails that into the list.
So, what I've tried is to do something like this:
var recipeItems =
from entry in list
group entry by new { entry.Recipe, entry.Category}
into aRecipe
select new RecipeItem()
{
Recipe = aRecipe.Key.Recipe,
Category = aRecipe.Key.Category,
// ? Not sure how to stick the list of names in here
};
list is the data pulled back via entity framework.
But this isn't quite right - I think I'm close here (maybe). What am I missing here on this?
Follow-up:
Thanks to Aducci for clearing this up. The answer is that you can do this:
Names = aRecipe.Select(x => x.Name)
and this will add all those Names which are in each group into the Names collection for that group. Pretty nifty.
I would modify your class to look like this
public class RecipeItem
{
public long Recipe { get; set; }
public long Category { get; set; }
public IEnumerable<string> Names {get; set; }
}
And your link to entities query to:
var recipeItems =
from entry in list
group entry by new { entry.Recipe, entry.Category}
into aRecipe
select new RecipeItem()
{
Recipe = aRecipe.Key.Recipe,
Category = aRecipe.Key.Category,
Names = aRecipe.Select(x => x.Name)
};

Include nested entities using LINQ

I'm messing around with LINQ for the first time, and I'm using EF 4.1 code first.
I have entities containing nested Lists of other entities, for example:
class Release
{
int ReleaseID { get; set; }
string Title { get; set; }
ICollection<OriginalTrack> OriginalTracks { get; set; }
}
class OriginalTrack
{
int OriginalTrackID { get; set; }
string Title { get; set; }
ICollection<Release> Releases { get; set; }
ICollection<OriginalArtist> OriginalArtists { get; set; }
}
class OriginalArtist
{
int OriginalArtistID { get; set; }
string Name { get; set; }
ICollection<OriginalTrack> OriginalTracks { get; set; }
}
I'm wondering what is the quickest way, in one LINQ query, to obtain all the information for where ReleaseID == some value.
I've done my homework, but have found solutions that require implicit rebuilding of an object (usually anonymous) with the required data. I want the data out of the database in the exact format that it is held within the database, i.e. pulling a Release object with relevant ReleaseID pulls and populates all the OriginalTrack and OriginalArtist data in the Lists.
I know about Include(), but am not sure how to apply it for multiple entities.
All help greatly appreciated.
Use Include. This is the purpose of Include, and there's no reason to write a bunch of nested select statements.
context.Releases.Include("OriginalTracks.OriginalArtist")
.Where(release => release.ReleaseID == id);
This is simpler to write, simpler to read, and preserves your existing data structure.
To use Include you need to specify the name of the property you want to return - this means the name as it exists in your code, not in the database. For example:
.Include("OriginalTracks") will include the OriginalTracks property on each Release
.Include("OriginalTracks.OriginalArtist") will include OriginalTracks property on each Release, and the OriginalArtist on each Track (note that it's not possible - syntactically or logically - to include an OriginalArtist within including the OriginalTrack)
.Include("OriginalTracks").Include("OtherProperty") will include the OriginalTracks and OtherProperty objects on each Release.
You can chain as many of these as you like, for example:
.Include("Tracks.Artist").Include("AnotherProperty")
.Include("ThirdProperty.SomeItems").Where(r => r.something);
is perfectly valid. The only requirement is that you put the Include on the EntitySet, not on a query - you can't .Where().Include().
Don't worry about using include here
just do something like the following
var query =
from release in ctx.Releases
select new {
release,
originalTracks = from track in release.OriginalTracks
select new {
track,
releases = track.Releases,
orignialArtist = from artist in track.OriginalArtists
select new {
artist,
artist.OriginalTracks
}
}
}
var Releases = query.Select(x => x.Release);
Should load all of your data
I worked with information from this post here.
http://blogs.msdn.com/b/alexj/archive/2009/10/13/tip-37-how-to-do-a-conditional-include.aspx
To include the nested entities without using string literals, use Select, like this:
context.Releases.Include(r => r.OriginalTracks.Select(t => t.OriginalArtist))
.Where(release => release.ReleaseID == id);

Linq expression for filtered collection of collections?

I'm hoping this will be a rather simple question for anyone who's good at Linq. I'm struggling to come up with the right Linq expression for the following. I'm able to hack something to get the results, but I'm sure there's a proper and simple Linq way to do it, I'm just not good enough at Linq yet...
I have a database accessed through Entity Framework. It has a number of Tasks. Each Task has a collection of TimeSegments. The TimeSegments have Date and Employee properties.
What I want is to be able to get the tasks for a certain employee and a certain month and the timesegments for each task for that same month and employee.
Again, the tasks do not in themselves have month nor date information, but they do by the TimeSegments associated with each task.
Very simplified it looks sort of like this:
public class Model //Simplified representation of the Entity Framework model
{
public List<Task> Tasks { get; set; }
}
public class Task
{
public int Id { get; set; }
public List<TimeSegment> TimeSegments { get; set; }
public Customer Customer { get; set; }
}
public class TimeSegment
{
public int Id { get; set; }
public string Date { get; set; }
public Employee Employee { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
So how do I do this as simply as possible with Linq? I.e. tasks and associated timesegments for a certain month and employee. I would also like to be able to get it by Customer BTW...
This is the simplest thing I could come up with:
var tasksWithSegments =
from segment in model.TimeSegments
where segment.Date.Month == month
where segment.Employee.Id == employeeId
group segment by segment.Task into result
select new
{
Task = result.Key,
TimeSegments = result.ToArray()
};
Please note that you might have to add some properties to your model, such as Model.TimeSegment and TimeSegment.Task.
The trick with LINQ queries often is to start at the right collection. In this case the ideal starting point is TimeSegments.
ps. I'm not sure whether Date.Month == month will actually work with EF, but I think it will (with EF 4.0 that is).
Update:
Could you show how to extend this
query and get the tasks for a
particular Customer as well?
I'm not sure what you mean, but you can for instance filter the previous queryable like this:
var tasksWithSegmentsForCustomers =
from taskWithSegments in tasksWithSegments
where taskWithSegments.Task.Customer.Id == customerId
select taskWithSegments;
Can I get the return type to be a list
of Tasks with a list of TimeSegments
if I have this in a method?
Again, not sure what you exactly want, but if you want two separate lists that have no relation, you can do this:
List<Task> tasks = (
from taskWithSegments in tasksWithSegments
select taskWithSegments.Task).ToList();
List<TimeSegments> segments = (
from taskWithSegments in tasksWithSegments
from segment in taskWithSegments.Segments
select segment).ToList();
Of course, if this is what you need, than it might be easier to rewrite the original query to something like this:
List<TimeSegment> segments = (
from segment in model.TimeSegments
where segment.Date.Month == month
where segment.Employee.Id == employeeId
select segment).ToList();
List<Task> allTasks =
segments.Select(s => s.Task).Distinct().ToList();
Once you got the hang of writing LINQ queries, there is no way you want to go back to writing SQL statements or old-fashion foreach statements.
Think LINQ!!!
What I want is to be able to get the
tasks for a certain employee and a
certain month and the timesegments for
each task for that same month and
employee.
This will select tasks from an instance of Model where the task has at least one time segment that in the requested month for the requested employee (untested):
Model model = new Model();
tasks = model.Tasks.Where(t => t.TimeSegments.Any(ts => ts.Employee.Id = requestedId && Convert.ToDate(ts.Date).Month == requestedMonth));

Resources