LINQ - Entity framework code first - Grouped results to custom class - c#

I have a web application using Entity Framework code first and I want to be able to hook up my example data below into various Entity data aware controls in ASP.NET.
By using the ItemType property of the control to specify my custom cencapsulation class and the SelectMethod property of the control to call the LINQ queryI need to return an IQueryable collection which the control can use and work with automatic pagination etc.
Lets assume we have an entity to start with :-
public class MyEntity
{
[Key]
public int MyObjectId { get; set; }
public string Name { get; set;}
}
So this LINQ query will work fine
public IQueryable<MyObject> GetMyObjects()
{
SiteContext db = new SiteContext();
var myObjects = (from m in db.MyObjects
select m);
return myObjects;
}
Now using the following sample data :-
MyObjectId Name
1 Apple
2 Orange
3 Apple
4 Apple
5 Pear
6 Orange
7 Apple
8 Grapes
9 Apple
10 Orange
What I want to do is group the data and provide a count like this:-
Name Count
Apple 5
Orange 3
Pear 1
Grapes 1
So my first step is to create a class to encapsulate the Entity and provide the additional count column :-
public class MyObjectWithCount
{
public MyObject MyObject { get; set; }
public int Count { get; set; }
}
Now I want to get some results with a LINQ query
public IQueryable<MyObjectWithCount> GetMyObjectsWithCount()
{
SiteContext db = new SiteContext();
var myObjects = (from m in db.MyObjects
group m by m.Name into grouped
select new MyObjectWithCount()
{
MyObject = grouped,
Count = ?what goes here?
});
return myObjects;
}
So I have 2 problems with my select new MyObjectWithCount()
1) How do I get the count for each grouped item
2) How do I convert grouped into Myobject. As it stands, I get the design time error of cannot implicitly convert type system.linq.grouping to MyObject.
If I did not perform grouping in the LINQ query, then I am able to successfully return an IQueryable to my control and it all hooks up fine (but obviously the count is missing at this stage)
Also to clarify, this is an oversimplied example of my Entity/class, I do need to have access to the full MyObject entity as in the real world there will be many fields, and not just a name field as in the example above.
Thanks everyone for any help you can offer

You can use this linq query:
var myObjects = (from m in db.MyObjects
group m by m.Name into grouped
select new MyObjectWithCount()
{
MyObject = grouped.FirstOrDefault(),
Count = grouped.Count()
});
In this case your MyObjectProperty will have MyObjectId and other properties of first element in group.
If you have same other then MyObjectId properties for all objects with same Name, you should group by all properties excluding MyObjectId:
var myObjects = (from m in db.MyObjects
group m by new {m.Name, m.AnyOtherProperty, ...} into grouped
select new MyObjectWithCount()
{
MyObject = grouped.FirstOrDefault(),
Count = grouped.Count()
});
It will protect you from collapsing of group of different objects in one object.

Your Select is slightly off.
Also, as your return type is IQueryable<MyObjectWithCount> add AsQueryable.
EDIT: From your comments, you want to return the items in your class too, in that case you'd have to do the following:
Add another property to MyObjectWithCount to hold the matching properties and a string to contain the Name searched on:
public string Name { get; set; }
public List<MyObject> MatchingObjects { get; set; }
Then your Count can just work off that:
public int Count
{
get
{
return MatchingObjects.Count;
}
}
Then your LINQ will be:
var myObjects = (from m in db.MyObjects
group m by m.Name into grouped
select new MyObjectWithCount
{
Name = grouped.Key,
MatchingObjects = grouped.ToList()
}).AsQueryable();

Related

Exception when using LINQ orderby: "Failed to compare two elements in the array"

Here is sample code to reproduce the exception:
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.EntityFrameworkCore;
namespace Demo
{
[Keyless]
public class Contact
{
public string Name { get; set; } = default!;
public string? Address { get; set; } = default!;
public int? CCode { get; set; } = 0;
public int OtherValue { get; set; } = 0;
}
public class Foo
{
public static void Main()
{
List<Contact> raw = new();
raw.Add(new Contact { CCode = 1, Name = "Foo", Address = "Bar" });
raw.Add(new Contact { CCode = 2, Name = "Foo", Address = "Bar" });
ProcessRawResults(raw);
}
public static void ProcessRawResults(List<Contact> raw)
{
var q = from i in raw
group i by new { i.CCode, i.Name, i.Address } into g
orderby g.Key
select g;
foreach (var group in q)
{
}
}
}
}
When executing this program, an exception is thrown when execution reaches foreach (var group in q):
System.InvalidOperationException: 'Failed to compare two elements in the array.'
Inner Exception
ArgumentException: At least one object must implement IComparable
I have looked at other SO questions about this error message occurring when trying to Sort a List; but in this code I'm not sure which operation needs the comparator. It seems like the orderby g.Key operation might need to compare the anonymous class in the group, but then shouldn't the anon class have a default comparator? Or if it doesn't, I'm not sure where to put the code to implement this.
Confusingly, if I take i.CCode out of the group i by new line, then the exception doesn't happen any more.
Background: My real project is a Blazor app using EFCore 6 , and am receiving a List<Contact> from a Stored Procedure result, so it has to be [Keyless]. I have to work with the existing Stored Procedure unmodified, so am performing a transformation of the result in my code. I hope to collapse the set of results so that all entries with the same (CCode, Name, Address) result in a single row, and I'll concatenate the OtherValue into a list within that single row.
I guess it's because int? is actually Nullable<int> and Nullable<T> doesn't implement IComparable. I just tested your code but changed the grouping to this:
group i by new { CCode = i.CCode.HasValue ? i.CCode.Value : (int?)null, i.Name, i.Address } into g
and it seemed to work. It didn't throw that exception, at least.
Anonymous type do not have comparator, specify oder by properties:
var q = from i in raw
group i by new { i.CCode, i.Name, i.Address } into g
orderby g.Key.CCode, g.Key.Name, g.Key.Address
select g;

Populating a list within a list in C# not using foreach loop. better way?

I have a list of objects within a list of objects (List-ParentClass) that has as one of its objects a nested list (List-ChildClass). To populate List-ChildClass I have used a foreach loop as shown below. I have also nested a linq query as show below.
At this point I am having some performance issues and I feel like there is a better way to do this that I am just not finding.
Question: How could I do this better/faster?
Note - This is a Web based .net MVC application written in C#. I use EF back to a SQL database.
public class ParentClass
{
public int pcid { get; set; }
public List<ChildClass> ChildClassList { get; set; }
}
public class ChildClass
{
public int pcid { get; set; }
public int ccid { get; set; }
}
public class DoWork
{
public void ExampleMethodForEach()
{
List<ParentClass> ParentClassList = new List<ParentClass>();
foreach(ParentClass a in ParentClassList)
{
a.ChildClassList = EFDatabase2.where(b => b.pcid == a.pcid).select(b => b.ccid).ToList();
}
}
public void ExampleMethodLinq()
{
var ParentClassList = (from a in EFDatabase
select new ParentClass
{
ccid = a.ccid,
pcid = (from b in EFDatabase2
where b.pcid == a.pcid
select b.ccid).ToList()
//something like this were I nest a query
}).ToList();
}
}
The best way when working with relational databases and LINQ is to use joins to correlate data. In your case, the most appropriate is group join:
var ParentClassList =
(from p in EFDatabase
join c in EFDatabase2 on p.pcid equals c.pcid into children
select new ParentClass
{
pcid = p.pcid,
ChildClassList =
(from c in children
select new ChildClass
{
pcid = c.pcid,
ccid = c.ccid
}).ToList()
}).ToList();
which should give you a nice fast single database query.
P.S. Hope your EFDatabase and EFDatabase2 variables refer to two tables inside one and the same database.
You are hitting your database multiple times. You have a N+1 issue.
What I suggest is to query all parents first, but excluding the children data. Then get the ID of all parents that you retrieved and put it inside an array. We will use that array to create a IN clause in SQL.
After loading all the children using the array of parent IDs, map them to a Lookup using ToLookup using the parent ID as the key and use a foreach to assign the list of children to the parent.
var parents = EFDatabase2.Parents.Where(...).Select(p => new ParentClass { pcid = p.pcid }).ToList();
var ids = parents.Select(p => p.pcid).ToArray();
var children = EFDatabase2.Children.Where(c => ids.Contains(c.ccid)).Select(c => new ChildClass { pcid = c.pcid, ccid = c.ccid }).ToLookup(c => c.pcid);
foreach (var parent in parents)
{
parent.Children = children[parent.pcid];
}
In this case, you will only do two queries to your database.

Succinct LINQ filter expression

I have an MVC controller that will filter a product list based on a category.
Products = repository.Products.Where(p => category == null || p.Category1 == "category1" );
If I wanted to let the user filter the product with two categories, I would have to add in another if statement that contains Category1 and Category2. I can imagine if I have more categories, and the user can choose category 1,3,5 and so on, the permutation will get crazily large.
Is there a proper way of doing this?
I am assuming that your object model is defined along the lines of:
public class Product
{
// ...
public Category Category1 { get; set; }
public Category Category2 { get; set; }
public Category Category3 { get; set; }
// ...
}
(where you might be using strings instead of having a category class)
If the object model is within your control, then I would recommend changing it so that a product has a collection of categories rather than several named properties for Category1, Category2, Category3 etc, so more like this:
public class Product
{
// ...
public IList<Category> Categories { get; set; }
// ...
}
If the product class is fixed and it already has multiple individual category properties, I would recommend writing an extension method for your product class that returns a list of categories that are non-null. That way you can write a where expression more succinctly.
For example:
public static class ProductExtension
{
public static IList<Category> GetCategories(this Product product)
{
List<Category> categories = new List<Category>();
if (product.Category1 != null)
{
categories.Add(product.Category1);
}
if (product.Category2 != null)
{
categories.Add(product.Category2);
}
// etc.
return categories;
}
}
...which could then be used along the lines of
repository.Products.Where(p => p.GetCategories().Contains("category1"));
Another option is to create a ProductFilter object to do the filtering for you.
Give the ProductFilter class a field for every category that is possible to filter on, which each store predicates, and a PassesFilter(Product p) method which determines whether p passes the predicate for all categories where a predicate has been set, e.g.
method PassesFilter(Product p):
if Category1Filter is not null:
if p does not pass Category1Filter:
return false
if Category2Filter is not null:
if p does not pass Category2Filter:
return false
return true
(Excuse the pseudo-code, I don't do C# and it's late)
So you could use it like so:
ProductFilter pf = new ProductFilter();
...
/*build up your filters for all categories that apply in this search...*/
pf.ColourFilter = (Product p) => { return p.Colour == "Red"; };
pf.PriceFilter = (Product p) => { return p.Price > 100.00; };
...
Products = repository.Products.Where(p => category == null || pf.PassesFilter(p) );
You could also easily implement the PassesFilter method differently to handle OR instead of AND (or create a class for each implementation).
I know that using predicates in the way I described would allow you to put a price predicate in the colour predicate field, but I just thought I'd throw this example out there to illustrate the concept of using an object to do the work of lambdas :-)
1.You may use Expression to constructor condition expression
2.Use expression in the linq.

Hydrate property via joining another List<Property> on unique Id

I have two lists, one which is a list of Equipment and one which is a list of WorkflowItems with an Equipment property. The Equipment property within the List<WorkflowItem> list only has one of it's values hydrated, ProcessId. The List<Equipment> has two properties hydrated, ProcessId and Name. I want to hydrate the List<WorkflowItem>.Equipment.Name with the value from the single Equipment record in the List<Equipment>
This LINQ query below will select a generic item out doing basically what I'm looking for, but I would rather just fill in the original list.
var list = from item in workflowItems
join equipment in barcodeEquipmentList on
item.Equipment.ProcessId equals equipment.ProcessId
select new
{
ProcessId = item.Equipment.ProcessId,
EquipmentName = equipment.Name
};
Edit
The list is going to be relatively small, even doing something like this would be fine (aside from the fact that this does not work)
workflowItems.ForEach(x => x.Equipment = from e in barcodeEquipmentList
where e.Process.Id == x.Equipment.Process.Id
select e
);
...final edit
but this does work:
workflowItems.ForEach(x => x.Equipment = barcodeEquipmentList
.Where(e => e.Process.Id == x.Equipment.Process.Id)
.FirstOrDefault());
This piece of code should match your needs:
public class Equipment {
public int ProcessId { get; set; }
public string Name { get; set; }
}
public class WorkflowItem {
public Equipment { get; set; }
public void LoadEquipmentFrom(IEnumerable<Equipment> cache){
var equipment = cache.FirstOrDefault(e => e.ProcessId == Equipment.ProcessId);
if(equipment != null)
Equipment.Name = equipment.Name;
}
}
You could also assign the instance from cache to the existing one, it wouldn't matter since both must have the same Identifier Equipment = equipment;. That would be easier if you have more properties to set. To optimize further use an IDictionary<int, Equipment> instead of that IEnumerable<Equipment>, because you'll be reading that collection very often.
I'm guessing you are implementing a kind of ORM, in this case I can give you a good advice: "There is already something out there that'll fit your needs.".
Since the dataset was not very big (less than 20 records), I was able to this as below without a hit to performance
workflowItems.ForEach(x => x.Equipment = barcodeEquipmentList
.Where(e => e.Process.Id == x.Equipment.Process.Id)
.FirstOrDefault());

Computed Columns in EF using LINQ

With the following code;
using (var context = new FINSAT613Entities())
{
gridControl1.ForceInitialize();
DateTime endtime= new DateTime(2013, 03, 29, 15, 49, 54);
Text = "endtime:"+endtime.ToShortDateString();
var query =
from A in context.A
join B in context.B on A.ID equals B.ID
join C in context.C on A.ID2 equals C.ID2
where A.endtime> endtime && A.Chk.StartsWith("320")
select new
{
A.ID,B.FOO,C.BAR etc...
};
BindingSource.DataSource = query;
gridControl1.DataSource = BindingSource;
}
How can i add computed columns to it?(multiples a.bar with b.foo for example)
Tried using partial class but no luck with it.
public partial class A
{
public decimal Calculated
{
get { return 15; }
}
}
The exact error i get is :
{"The specified type member 'Computed' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported."}
You can create class, wich has got all the fields you need: id, foo, bar, etc. and add to it Calculated field, then just modify your query:
var query =
from A in context.A
join B in context.B on A.ID equals B.ID
join C in context.C on A.ID2 equals C.ID2
where A.endtime> endtime && A.Chk.StartsWith("320")
select new YourNewClass
{
Foo = A.foo,
Bar = B.bar,
... ,
Calculated = A.foo * B.bar
};
EDITED: if you have got a lot of fields, you can use automapper
Here is an example. Say, you have got class User from your DB and you have got class UserModel, in which you added a field FullName which constist from fields Name and Surname. So you want to create an object UserModel from object User but not to copy all the fields exclicitly. That's how you can do it:
class Program
{
static void Main(string[] args)
{
User u = new User { Name = "My", Surname = "Name" };
Mapper.CreateMap<User, UserModel>().ForMember(dest => dest.FullName, o => o.MapFrom(src => string.Format("{0} {1}", src.Name, src.Surname)));
UserModel um = Mapper.Map<User, UserModel>(u);
Console.WriteLine(um.FullName);
}
}
public class User
{
public string Name { get; set; }
public string Surname { get; set; }
}
public class UserModel
{
public string Name { get; set; }
public string Surname { get; set; }
public string FullName { get; set; }
}
Did you solve this?
I've been looking for a neater way myself,
I don't like using ViewModels when all I want to add to an existing model is only one or two pieces of additional info.
What I tend to do is create have a non mapped field in my model, get all the info I need from the LINQ into a temporary model and then perform any required remapping in another loop before I send it back to the client.
So for example;
public partial class A
{
[NotMapped]
public decimal Calculated {get;set;}
}
then in my Linq Query
var query =
from A in context.A
join B in context.B on A.ID equals B.ID
join C in context.C on A.ID2 equals C.ID2
where A.endtime> endtime && A.Chk.StartsWith("320")
select new
{
A = A,
B = B,
C = C
};
foreach(item i in query)
{
A.Calculated = A.Foo * B.Bar;
}
return query
OK, it means another loop, but at least it's only one DB call.
Another option would be to do it all in T-SQL and issues the T-SQL directly (or via a stored proc)
this link explains how - as an added bonus this method is much faster with more complex queries, but it always feels like a bit of a hack to me.
http://weblogs.asp.net/scottgu/archive/2007/08/16/linq-to-sql-part-6-retrieving-data-using-stored-procedures.aspx
I accomplished this by putting my data into a List first. This is probably terrible (especially if you have a large amount of results) but it does work and is easy to understand.
There's a loop involved going through the whole list. I'm not sure if other solutions require this but I also am currently having a hard time understanding them, so I need something that works for what I was trying to do, which was to populate a DropDownList.
Again I really don't think this solution is ideal:
Let's say I have an entity YourEntities with a table Registration
using (var context = new YourEntities())
{
var query = from x in context.Registrations
select x; //Just an example, selecting everything.
List<Registration> MyList = query.ToList();
foreach (Registration reg in MyList) //Can also be "var reg in MyList"
{
MyDropDownList.Items.Add(new ListItem(reg.LastName + ", " + reg.FirstName, reg.ID));
//Or do whatever you want to each List Item
}
}

Categories