LINQ Left Outer join with group by count through extension methods - c#

I have two tables. Categories and Ads.
Every Category can have many ads in Ads table.
I want all the categories details including how many ads do they have each.
Following is my Linq expression it fetches only categories that have ads may be bacause FirstOrDefault()
I like to know how I can achieve the result given that the condition plus only categories that have location id (let say: lid) of "7".
Following in my expression
var x1 = context.Categories
.GroupJoin(
context.ads,
cat => cat.id,
ad => ad.catid,
(cat, ad) => new { cats = cat, ads = ad })
.SelectMany(
a => a.ads.DefaultIfEmpty(),
(a, y) => new { catss = a.cats, adss = y })
.GroupBy(w => w.adss,ww=>new { cat=ww.catss,count=ww.catss.ads.Count()})
.Where(s=>s.FirstOrDefault().cat.lid==7);

Apparently there is a one-to-many relation between Categories and Ads: every Category has zero or more Ads, and every Ad belongs to exactly one Category.
In proper Entity Framework this would be modelled as follows:
class Category
{
public int Id {get; set;}
// every Category has zero or more Adds:
public virtual ICollection<Ad> Ads {get; set;}
...
}
class Ad
{
public int Id {get; set;}
// every Ad belongs to exactly one Category, using foreign key CategoryId:
public int CategoryId {get; set;}
public Category Category {get; set;}
...
}
If your classes are modelled like this, entity framework will assume the proper one-to-many relationship between Category and Add. It might be that you have different names for certain properties, and that you use Attributes and / or fluent API, but the structure of the classes will be similar.
Having done this, your query is easier than you thought:
var result = dbContext.Categories.Select(category => new
{
Category = Category,
AdCount = Category.Adds.Count(),
};
In words: for each Category in the collection of categories, create a new object of anonymous class, with two properties:
Category contains the considered Category
AdCount contains the number of Ads that the Category has.
The model of Entity Framework will understand that a join and a count is needed to do this.

You could try something like the following:
var result = context.Categories
.Where(category => category.lid == 7)
.GroupJoin(
context.ads
, category => category.id
, ad => ad.catid
, (c,a) => new
{
Category = category,
NumberOfAds = a.DefaultIfEmpty().Count(x => x!=null)
});

Related

Select all columns from main table and only 1 column from JOINed table

I have two tables:
Table1
Id ArticleName ArticleTypeId
1 Blah Blah 3
2 Helo Blah 5
and
Table2
ArticleTypeId TypeName
3 Business
5 Construction
I'm trying to Join TableA and TableB on ArticleTypeId and basically return everything from Table1 and TypeName from Table2
Here's what I'm trying to do, but I'm not sure to to edit the SELECT in the statement to include the TypeName
var articles = (from s in _context.Articles
join b in _context.ArticleTypes on s.ArticleTypeId equals b.ArticleTypeId
select s).ToList();
Or is there an easier way to do this?
Goal:
Id ArticleName TypeName
1 Blah Blah Business
2 Helo Blah Construction
So you have two tables, a table with Articles (Table1), and a table with ArticleTypes (Table2). I decided to give your tables names that are meaningful, this makes discussion easier.
There is a one to many relationship between Articles and ArticleTypes: Every Article has exactly one ArticleType, namely the Article type that the foreign key ArticleTypeId refers to. Every ArticleType has zero or more Articles that refer to it.
You are using entity framework. If you've followed Entity Framework Coding Conventions, you'll have classes similar to the following.
class Article
{
public int Id {get; set;}
public string Name {get; set;}
// every Article has one ArticleType, namely the one that the foreign key refers to
public int ArticleTypeId {get; set;}
public virtual ArticleType ArticleType {get; set;}
}
class ArticleType
{
public int Id {get; set;}
public string TypeName {get; set;}
// every ArticleType has zero or more Articles referring to it (one-to-many)
public virtual ICollection<Article> Articles {get; set;}
}
In entity framework the non-virtual properties refer to columns of the tables; the virtual properties refer to the relations between the tables (one-to-many, many-to-many, ...)
The foreign key ArticleTypeId is a real column, so the property is non-virtual. Property ArticleType is virtual, because it represents the one-to-many relation.
For completeness your DbContext:
class MyWarehouse : DbContext
{
public DbSet<Article> Articles {get; set;}
public DbSet<ArticleType> ArticleTypes {get; set;}
}
I'm trying to Join TableA and TableB on ArticleTypeId and basically return everything from Table1 and TypeName from Table2
After you've defined your classes, your query is easy. The easiest method is using the virtual properties.
Use the virtual properties
Requirement Give me the Id and Name of all Articles, each Article with its TypeName.
using (var wareHouse = new MyWareHouse(...))
{
var requestedArticles = wareHouse.Articles.Select(article => new
{
// Select only the Article Properties that you plan to use
Id = article.Id,
Name = article.Name,
TypeName = article.ArticleType.TypeName,
});
// Process the requested Articles before disposing the wareHouse
}
In words: from every Article in the table of Articles fetch the Id, the Name and the one and only TypeName it has.
Entity Framework knows the relation between Articles and ArticleTypes. Because you use the virtual Property Article.ArticleType it knows which join to perform.
Using the virtual properties you can also get each ArticleType together with all Articles that have this ArticleTypes
var constructionArticles = wareHouse.ArticleTypes
.Where(articleType => articleType.TypeName == "construction")
.Select(articleType => new
{
Id = articleType.Id,
TypeName = articleType.TypeName,
// fetch all articles that have this TypeName
Articles = articleType.Articles.Select(article => new
{
Id = article.Id,
Name = article.Name,
// no need to fetch the foreign key, you already got this value
// ArticleTypeId = article.ArticleTypeId,
})
.ToList(),
})
.ToList();
Entity framework knows the relation and will do the proper (Group-)join for you.
Did you notice how natural using the virtual properties feel?
Do the Join yourself
Some people don't want to use the virtual properties, they prefer to do the (Group-)joins themselves.
Use the overload of method Join that has a parameter resultSelector, so you can specify the desired result.
// Join Articles with ArticleTypes
var requestedArticles = wareHouse.Articles.Join(wareHouse.ArticleTypes,
// from every Article take the foreign key
article => articleTypeId,
// from every ArticleType take the primary key
articleType => articleType.Id,
// parameter resultSelector:
// take each article and its one and only matching ArticleType to make one new
(article, articleType) => new
{
Id = article.Id,
Name = article.Name
TypeName = articleType.TypeName,
});
If you have a one-to-many relation, like Schools with their Students, Customers with their Orders, or ArticleTypes with their Articles, use GroupJoin and start at the 'one' side. If you want Students, each Student with the School he attends, use Join, and start at the 'many' side.
var schoolsWithTheirStudents = dbContext.Schools
.Where(school => school.City == ...) // if you don't want all Schools
.GroupJoin(dbContext.Students,
// from every School take the primary key
school => school.Id,
// from every Student take the foreign key to the School he attends
student => student.SchoolId,
// resultSelector: take each Schools with its matching Students to make one ned
(school, studentsWhoAttendThisSchool) => new
{
// Select only the School properties that you plan to use:
Id = school.Id,
Name = school.Name,
Address = school.Address,
...
// studentsWhoAttendThisSchool is a queryable sequence,
// so you can use LINQ on it:
Students = studentsWhoAttendThisSchool.Select(student => new
{
Id = student.Id,
Name = student.Name,
...
})
.ToList(),
});
})

Get a list of the latest items grouped by a different property in Entity Framework

I have an Entity Framework Entity Shoe that has a one to many relationship with another Entity Person. Each Person has many Shoes. I want to get a list of the latest shoes each person has.
public class Shoe {
public Long PersonId { get; set; }
public DateTime PurchaseDate { get; set; }
}
I tried this:
var latestShoesPerPerson = _dbContext.Shoe
.GroupBy(shoe => shoe.PersonId)
.Select(shoes => shoes.OrderByDescending(shoe => shoe.PurchaseDate).First());
But, I got this error
orderbydescending could not be translated
The result of a GroupBy is an implicit Key+Collection. It actually has a number of "flavours" to do subtly different things. The one you would be looking for to get the results like that would be GroupBy<TSource,TKey,TResult>(IEnumerable<TSource>, Func<TSource,TKey>, Func<TKey,IEnumerable<TSource>,TResult>)
Using this, the expression would look more like this:
var latestShoesPerPerson = _dbContext.Shoe
.GroupBy(shoe => shoe.PersonId, (personId, shoes) => shoes.OrderByDescending(s => s.PurchaseDate).FirstOrDefault())
.ToList(); // list of the earliest shoe for each person.

Only primitive types do I need to cast?

I am trying to use anonymous types in Entity Framework, but I am getting an error about
Unable to create a constant value
MinQty and MaxQty are int so I don't know if I need to add to Convert.ToInt32?
Unable to create a constant value of type 'Anonymous type'. Only primitive types or enumeration types are supported in this context.
This builds a list object
var listOfLicense = (from l in db.License
select new
{
l.ProductId,
l.MinLicense,
l.MaxLicense
}).tolist();
This is the larger EF object where I am getting the error am I missing a casting?
var ShoppingCart = (from sc in db.ShoppingCarts
Select new model.Shoppingchart{
ShoppingCartId= sc.Id,
MinQty = (int)listOfLicense
.Where(mt => (int)mt.ProductId == sc.ProductId)
.Select(mt => (int)mt.MinLicense)
.Min(mt => mt.Value),
MaxQty = (int)listOfLicense
.Where(mt => (int)mt.ProductId == p.ProductId)
.Select(mt =>(int) mt.MaxQty)
.Max(mt => mt.Value)}.tolist();
This builds a list object
var listOfLicense = (from l in db.License
select new
{
l.ProductId,
l.MinLicense,
l.MaxLicense
})
The above example does not build a list of objects. It builds a query to return objects of that anonymous type.
This builds an in-memory list of objects of that type:
var listOfLicense = (from l in db.License
select new
{
l.ProductId,
l.MinLicense,
l.MaxLicense
}).ToList();
Using .ToList() here will execute the query and return a materialized list of the anonymous types. From there, your code may work as expected without the exception. However, this is effectively loading the 3 columns from all rows in your database table, which may be a problem as the system matures and rows are added.
The error you are getting isn't a casting issue, it is a translation issue. Because your initial query is still just an EF Query, (IQueryable) any further querying against it will need to conform to EF limitations. EF has to be able to translate what your expressions are trying to select back into SQL. In your case, what your real code is trying to do is breaking those rules.
Generally it is better to let EF work with the IQueryable rather than materializing an entire list to memory. Though to accomplish that we'd need to either see the real code, or a minimum reproducible example.
This code:
MinQty = (int)listOfLicense
.Where(mt => (int)mt.ParentProductId == p.ProductId)
.Select(mt => (int)mt.MinLicense)
.Min(mt => mt.Value),
... does not fit with the above anonymous type as there is no correlation between what mt.ParentProductId is in relation to the anonymous type. (p seems to be associated with that type, not mt so there looks to be a lot of Query code missing from your example.)
Edit: based on your updated example:
var ShoppingCart = (from sc in db.ShoppingCarts
Select new model.Shoppingchart{
ShoppingCartId= sc.Id,
MinQty = (int)listOfLicense
.Where(mt => (int)mt.ProductId == sc.ProductId)
.Select(mt => (int)mt.MinLicense)
.Min(mt => mt.Value),
MaxQty = (int)listOfLicense
.Where(mt => (int)mt.ProductId == p.ProductId)
.Select(mt =>(int) mt.MaxQty)
.Max(mt => mt.Value)}.ToList();
It may be possible to build something like this into a single query expression depending on the relationships between ShoppingCart, Product, and Licence. It almost looks like "Licence" really refers to a "Product" which contains a min and max quantity that you're interested in.
Assuming a structure like:
public class Product
{
[Key]
public int ProductId { get; set; }
public int MinQuantity { get; set; }
public int MaxQuantity { get; set; }
// ...
}
// Here lies a question on how your shopping cart to product relationship is mapped. I've laid out a many-to-many relationship using ShoppingCartItems
public class ShoppingCart
{
[Key]
public int ShoppingCartId { get; set; }
// ...
public virtual ICollection<ShoppingCartItem> ShoppingCartItems { get; set; } = new List<ShoppingCartItem>();
}
public class ShoppingCartItem
{
[Key, Column(0), ForeignKey("ShoppingCart")]
public int ShoppingCartId { get; set; }
public virtual ShoppingCart ShoppingCart{ get; set; }
[Key, Column(1), ForeignKey("Product")]
public int ProductId { get; set; }
public virtual Product Product { get; set; }
}
With something like this, to get shopping carts with their product min and max quantities:
var shoppingCarts = db.ShoppingCarts
.Select(sc => new model.ShoppingCart
{
ShoppingCartId = sc.Id,
Products = sc.ShoppingCartItems
.Select(sci => new model.Product
{
ProductId = sci.ProductId,
MinQuantity = sci.MinQuantity,
MaxQuantity = sci.MaxQuantity
}).ToList()
}).ToList();
This would provide a list of Shopping Carts with each containing a list of products with their respective min/max quantities.
If you also wanted a Lowest min quantity and highest max quantity across all products in a cart:
var shoppingCarts = db.ShoppingCarts
.Select(sc => new model.ShoppingCart
{
ShoppingCartId = sc.Id,
Products = sc.ShoppingCartItems
.Select(sci => new model.Product
{
ProductId = sci.ProductId,
MinQuantity = sci.MinQuantity,
MaxQuantity = sci.MaxQuantity
}).ToList(),
OverallMinQuantity = sc.ShoppingCartItems
.Min(sci => sci.MinQuantity),
OverallMaxQuantity = sc.ShoppingCartItems
.Max(sci => sci.MaxQuantity),
}).ToList();
Though I'm not sure how practical a figure like that might be in relation to a shopping cart structure. In any case, with navigation properties set up for the relationship between your entities, EF should be perfectly capable of building an IQueryable query for the data you want to retrieve without resorting to pre-fetching lists. One issue with pre-fetching and re-introducing those lists into further queries is that there will be a maximum # of rows that EF can handle. Like with SQL IN clauses, there is a maximum # of items that can be parsed from a set.
In any case it sounds like it's provided you with some ideas to try and get to the figures you want.

How can I get value from .Include() using .ThenInclude()c

I have some users filter in my project and I want to show each user's friends here. UserFrom - who send friendship request, UserTo - who accept it. So I need To know the Id in the code below to choose the opposite, beacuse it will be his friend.
var users = await _context.User
.Where(u => userFilter.Gender != null ?
u.Gender == userFilter.Gender : true)
.Where(u => (userFilter.Languages != null &&
userFilter.Languages.Count() != 0) ?
userFilter.Languages.Any(fl => u.Languages.Any(
ul => ul.LanguageCode == fl &&
LevelInRange(ul, userFilter.MinLevel))) : true)
.Where(u => (userFilter.MaxDistance != null) ?
LocationHelper.GetDistanceBetween((double)u.Longitude, (double)u.Latitude,
longtitude, latitude) <= userFilter.MaxDistance : true)
.Where(u => (userFilter.MaxAge != null) ?
GetAge(u.Birthdate) <= userFilter.MaxAge : true)
.Where(u => (userFilter.MinAge != null) ?
GetAge(u.Birthdate) >= userFilter.MinAge : true)
.Include(u => u.Languages)
.ThenInclude(ul => ul.Language)
.Include(u => u.CreatedEvents)
.Include(u => u.Friends)
.ThenInclude(f => f.UserTo) //The problem is here. How can I get u.Id there
.Include(u => u.Credentials)
.Include(u => u.Hobbies)
.ThenInclude(h => h.Hobby)
.ToListAsync();
Database management systems are optimized for selecting data. One of the slower parts is the transport of the selected data to your process. Hence it is wise to transport only the data that you actually plan to use.
If you have a one-to-many relation, like Schools with their Students, and School 10 has 1000 Students, then every Student of this School will have a foreign key SchoolId with a value 10.
So if you fetch "School [10] with its Students", you already know that every Student of school [10] will have a property SchoolId with a value 10. This value (that you already know) will be transported 1000 times (1001 if you also count the school's primary key). What a waste of processing power!
If you query data using entity framework, always use Select. Only use Include if you want to update the fetched data (change, delete)
Using Select enables you to select only the properties that you want, in the format that you want.
Back to your problem
Alas you forgot to give us your classes. So we'll have to guess it. It seems that a User has zero or more Languages, CreatedEvents, Friends, Hobbies, etc. Some of them will be a one-to-many relation, probably most of them will be a many-to-many relation: a user knows zero or more languages. Every language is spoken by zero or more Users.
If you've followed the entity framework code first conventions, you probably have classes similar to:
class User
{
public int Id {get; set;}
public string Name {get; set;}
// every User has zero or more Hobbies (many-to-many)
public virtual ICollection<Hobby> Hobbies {get; set;}
// every Student has created zero or more events (one-to-many)
public virtual ICollection<CreatedEvent> CreatedEvents {get; set;}
...
}
class Hobby
{
public int Id {get; set;}
public string Name {get; set;}
...
// every Hobby is practised by zero or more Users (many-to-many)
public virtual ICollection<User> Users {get; set;}
}
class CreatedEvent
{
public int Id {get; set;}
public string Name {get; set;}
public DateTime Date {get; set;}
// every event is created by exactly one User (one-to-many, using foreign key)
public int UserId {get; set;}
public virtual User User {get; set;}
}
etc.
In entity framework, the columns of your tables are represented by non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)
Hence, a foreign key is non-virtual. The item that the foreign key points to is virtual. If two classes have a virtual ICollection<...> pointing towards each other, entity framework knows that there is a many-to-many relation; if one of the two classes has virtual ICollection<...> while the other has virtual ... then entity framework knows that you intended to design a one-to-many relation.
If you've created your classes properly, especially the virtual ICollections, a query using Select is fairly easy. You seldom have to do a (group-)join anymore. Because you use the virtual properties, entity framework knows that a (group-)join is needed.
var queryUsers = dbContext.User.Where(...).Where(...) ...
.Select(user => new
{
// select only the user properties you really plan to use
Id = user.Id,
BirthDay = user.BirthDay,
// Select the data in the format that you want, for example:
FullName = user.FirstName + user.MiddleName + user.LastName,
// SubCollections:
Languages = user.Hobbies
.Where(hobby => ...) // only if you don't want all this user's hobbies
.Select(hobby => new
{
// again, select only the hobby properties that you plan to use
Id = hobby.Id,
...
// not needed, you already know the value:
// I know, it is probably a many-to-many, but let's suppose it is one-to-many
// UserId = hobby.UserId,
})
.ToList(),
...
});
Now your problem is in property Friends, you can add it to your Select, just like you selected the Hobbies
Friends = user.Friends
.Where(friend => ...) // only if you don't want all Friends
.Select(friend => new
{
// select the Friend properties you actually plan to use:
Id = friend.Id,
Name = friend.Name,
...
})
.ToList(),
// continue the select
IIRC, You can Select() over children with Linq expressions like so for children using .Include().
return _context.User
.Include(a => a.Friends.Select(c => c.UserTo));

Implement a "join" with a variety of tables in Entity Framework

I have three tables:
Materials:
ID
Title
Content
Likes:
ID
MaterialID
UserID
IsLiked
Visitors:
ID
UserID
MaterialID
Date
ReadNow
I would like to get an object like:
Title
Content
CountLikes
CountVisitors
I tried to do the following:
from mat in ctx.materials
let visitors = mat.VisitorsCollection.Where(x=>x.ReadNow).Count()
let likes = mat.LikesCollection.Where(x=>x.IsLiked).Count()
let iliked = mat.LikesCollection.Where(x=>x.UserID == myID && x.IsLiked).Any()
select new {
Material = mat,
Visitors = visitors,
Likes = likes,
Liked = iliked
}
I get a selection of materials and separately the Entity Framework receives data on the number of visitors and so on.
I also tried the following:
from mat in ctx.materials
join lik in ctx.Likes.Where(x=>x.UserID == myID && x.IsLiked) on map.ID equals lik.MaterialID
select new {
Material = mat,
Liked = lik.Any()
}
but now an error occurs:
Microsoft.EntityFrameworkCore.Query:Warning: The LINQ expression 'Any()' could not be translated and will be evaluated locally.
If you are using entity framework, consider to use the ICollections, instead of performing the joins yourself.
You have a sequence of Materials where every Material has zero or more Likes and zero or more Visitors, both one-to-many relations, using a foreign key to Material.
If you've followed the entity framework code first conventions, you'll have classes similar to the following
class Material
{
public int Id {get; set;}
public string Title {get; set;}
public string Content {get; set;}
// every Material has zero or more Likes (one-to-many)
public virtual ICollection<Like> Likes {get; set;}
// every Material has zero or more Visitors (one-to-many)
public virtual ICollection<Visitor> Visitors {get; set;}
}
Likes and Visitors:
class Like
{
public int Id {get; set;}
public bool IsLiked {get; set;}
...
// every Like belongs to exactly one Material, using foreign key
public int MaterialId {get; set;}
public virtual Material Material {get; set;}
}
class Visitor
{
public int Id {get; set;}
...
// every Visitor belongs to exactly one Material, using foreign key
public int MaterialId {get; set;}
public virtual Material Material {get; set;}
}
This is all that entity framework needs to detect the one-to-many relationships. It might be that you want different table names, or different identifiers for your columns. In that case attributes or fluent API is needed
In entity framework the columns of the tables are represented by non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many, etc)
Once you've got your class definitions correctly, your query is simple and very intuitive:
Requirement:
From my collection of Materials, give me from every Material, the Title, the Content, the number of Likes it has and the number of Visitors it has:
var result = myDbContext.Materials
.Where(material => ...) // only if you don't want all Materials
.Select(material => new // from every Material make one new object
{ // containing the following properties
Title = material.Title,
Content = material.Content,
// if you want any information of the likes of this material, use property Likes
LikeCount = material.Likes
.Where(like => like.IsLiked) // optional, only if you don't want all likes
.Count(),
NrOfVisitors = material.Visitors
.Where(visitor => ...) // only if you don't want all visitors
.Count(),
});
In words: from my complete collection of Materials, keep only those Materials that ... From every remaining Material, make one new object:
Title is the title of the Material
Content is the content of the Material
LikeCount is the number of Likes of this material (that have a true IsLiked)
NrOfVisitors is the number of Visitors of this material (that are ...)
Entity framework knows your relations, and knows that a GroupJoin is needed.
Well if you have foreign keys in the database then the EF would generate links between the objects so all you need to do is:
var result = ctx.materials.Select(x =>
new SomeClass{
Material = x,
Visitors = x.Visitors.Where(v => v.ReadNow).Count(),
Likes = x.Likes.Where(y => y.IsLiked).Count(),
Liked = x.Likes.Where(z => z.IsLiked && z.UserID == myID).Count()
}).ToList();
The syntax maybe is not totally correct, but you get the point ...

Categories