Query from a many to many relationship table - c#

I have two tables Documents and Group like below. I joined the two tables creating a DocumentsGroup using code first.
Documents Table:
public class Documents
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Group> Groups { get; set;
}
Groups Table:
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Documents> Documents { get; set; }
}
Here is the DocumentsGroup table. This is the junction table and it does not have a model rather just showing how its looking.
{
public int DocumentsId { get; set; }
public int GroupsId{ get; set; }
}
I am trying to get all the documents which belong to one group from the junction table. I have the Group ID so am trying to get all the documents which belong to that ID like below:
int groupId = 4;
var documents = _database.Groups.Where(d => d.Id == groupId).Include(i => i.Documents).ToList();
I tried that but am not getting all the documents belonging to that group. Is there anything am doing wrong?

Use the following query:
int groupId = 4;
var query =
from g in _database.Groups
from d in g.Documents
where g.Id == groupId
select d;
var documents = query.ToList();
Or via method chain syntax:
int groupId = 4;
var documents = _database.Groups
.Where(g => g.Id == groupId)
.SelectMany(g => g.Documents)
.ToList();

Try reach the the Documents through the mapping table
int groupId = 4;
var documents = _database.DocumentsGroup
.Where(x => x.GroupId == groupId)
.Include(x => x.Documents)
.Select(x => new Documents
{
Id = x.Documents.Id,
// add all props you need
})
.ToList();
But if you don't have the mapping table just create it or you can try:
int groupId = 4;
var documents = _database.Groups
.Include(x => x.Documents)
.Where(x => x.Id == groupId )
.ToList();

The query you have written will return a result set of groups, not the documents. If "Group" table's "Id" column is unique, you should write this as:
var group = dbContext.Groups.Include(g => g.Documents).FirstOrDefault(g => g.Id == 4); //Given group id is 4
if (group != null) {
var documents = group.Documents.ToList(); // Here you should get the desired Documents, given that the the tables are correctly configured
}

Related

How can I make EF Core 3 translate the group by foreign key the same way efcore7 does it?

I have the following entities and a database context
public class Item
{
public int Id { get; set; }
public int? ReceiptId { get; set; }
public int ItemTypeId { get; set; }
}
public class ItemType
{
public int Id { get; set; }
public string Name { get; set; }
public List<Item> Items { get; set; }
}
public class Receipt
{
public int Id { get; set; }
public string ReceiptInfo { get; set; }
public List<Item> Items { get; set; }
}
I'm trying to get a the list of receipts, but instead of containing the items they contain, I want them to have the itemType and the amount of items for each. I have written the following linq query, which works:
var result = _databaseContext.Receipts.Select(r => new
{
r.Id,
r.ReceiptInfo,
ItemInfo = r.Items.GroupBy(item => item.ItemTypeId)
.Select(group => new
{
IdItemType = group.Key,
AmountOfItems = group.Count(),
}).ToList()
});
With EF Core 7, it is translated to the following SQL query:
SELECT [r].[Id], [r].[ReceiptInfo], [t].[IdItemType], [t].[AmountOfItems]
FROM [Receipts] AS [r]
OUTER APPLY
(SELECT [i].[ItemTypeId] AS [IdItemType], COUNT(*) AS [AmountOfItems]
FROM [Items] AS [i]
WHERE [r].[Id] = [i].[ReceiptId]
GROUP BY [i].[ItemTypeId]) AS [t]
ORDER BY [r].[Id]
Yet, I need to do this in an older project which doesn't support a version older than 3.1 for EF Core.
There it translates the query differently and I get this error
Column 'Receipts.Id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause
In case of EF Core 3.1, you have to postprocess loaded detail items on the client side:
var rawData = _databaseContext.Receipts.Select(r => new
{
r.Id,
r.ReceiptInfo,
RawItemInfo = r.Items.Select(item => new
{
IdItemType = item.ItemTypeId
}).ToList()
})
.ToList();
var result = rawData
.Select(r => new
{
r.Id,
r.ReceiptInfo,
ItemInfo = r.RawItemInfo
.GroupBy(item => item.ItemTypeId)
.Select(group => new
{
IdItemType = group.Key,
AmountOfItems = group.Count(),
}).ToList()
});
As you see, GroupBy support has improved drastically in EFC 7. EFC 3 only supports GroupBy with aggregates at the query root.
Therefore, to make it run in EFC 3 you need to force the query into the supported shape. To get the same grouping level, the query starts at Items and groups + aggregates once over three elements instead of two:
var result = _databaseContext.Items
.GroupBy(item => new { item.ReceiptId, item.Receipt.ReceiptInfo, item.ItemTypeId })
.Select(group => new
{
Id = group.Key.ReceiptId,
ReceiptInfo = group.Key.ReceiptInfo,
IdItemType = group.Key.ItemTypeId,
NrOfItems = group.Count(),
})
That returns the same data as the original query and does the reduction of data (aggregate) in the database. To get the same result shape, it needs some post-processing in-memory (i.e. after calling AsEnumerable()):
.AsEnumerable()
.GroupBy(x => new { x.Id, x.Receipt.ReceiptInfo })
.Select(g => new
{
g.Key.Id,
g.Key.ReceiptInfo,
ItemInfo = g.Select(x => new { x.IdItemType, x.NrOfItems })
});
This requires adding a navigation property Item.Receipt.

Displaying equivalent to SQL pivot table using Linq against Entity Framework

I've been struggling for a few days to display something like Pivoting a dynamic table on SQL, using Linq. I don't actually want to create a pivoted table, I just want to display the results as if it was a pivoted table.
I have two entities:
Category:
public int Id { get; set; }
public string Name { get; set; }
public string Icon { get; set; }
public ICollection<StaticEvent> StaticEvents { get; set; }
StaticEvent:
public int Id { get; set; }
public int CategoryId {get; set; }
public Category Category { get; set; }
public DateTimeOffset Time {get; set; }
[MaxLength(500)]
public string Comment {get; set;}
I'm trying to generate a table showing the COUNT of STATIC EVENTS per CATEGORY PER YEARMONTH. So the rows would be the YEARMONTH, the columns would be the Categories and the values would be the COUNT.
I got to the point where I can count the STATIC EVENTS per YEARMONTH:
var query = _context.Categories
.SelectMany(c => c.StaticEvents )
.GroupBy(c =>
new {
Year = c.Time.Year,
Month = c.Time.Month
})
.Select( x=> new {YearMonth = x.Key, Count = x.Count()})
.ToList();
foreach (var x in query)
{Console.WriteLine($"Month = {x.YearMonth.Year},{x.YearMonth.Month}, , Count = " + x.Count);}
but I'm lost about what to do from here.
If Categories is unknown values which are stored in database, it is not possible to do that via LINQ without dynamic query creation.
There is query which do the job when you known categories on the compile time. Since you have not specified EF version, I have emulated Count with Sum:
var query = _context.Categories
.SelectMany(c => c.StaticEvents)
.GroupBy(c =>
new {
Year = c.Time.Year,
Month = c.Time.Month
})
.Select(x => new {
YearMonth = x.Key,
Cat1 = x.Sum(z => z.CategoryId == 1 ? 1 : 0),
Cat2 = x.Sum(z => z.CategoryId == 2 ? 1 : 0),
Cat3 = x.Sum(z => z.CategoryId == 3 ? 1 : 0),
})
.ToList();

Left outer join using LINQ Query Syntax EF Core C#

I have a question in regards with the below,
Left outer join of two tables who are not connected through Foreign Key.
Order by the results matched in second table.
I would like this to be done in LINQ Query method syntax as I am adding lots of conditions depending on the input provided along with skip and limit.
If we have below Product and Favorite tables
So the output that I would like to have is:
meaning with the favorites as part of first set and which are not favorites should be behind them. Below are the tries that I did.
I am able to join the tables get the output but not sure how I can make sure that in the first page I get all the favs.
This answer was very near to what I thought but it gets the result and then does the ordering which will not be possible in my case as I am doing pagination and using IQueryable to get less data.
Group Join and Orderby while maintaining previous query
Open to any solutions to achieve the same.
[Table("Product")]
public class ProductModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ProductId { get; set; }
public string ProductName {get; set;}
public bool IsFavorite { get; set; }
}
[Table("UserFavorite")]
public class UserFavoriteModel
{
[Required]
public Guid UserId { get; set; }
[Required]
public Guid Identifier { get; set; }
[Required]
public FavoriteType Type { get; set; }
}
// Gets products
private async Task<List<ProductModel>> GetProductsAsync(
Guid categoryId,
Guid subCategoryId,
int from,
int limit)
{
var query = _context.Products.AsQueryable();
if (!string.IsNullOrEmpty(categoryId))
query = query.Where(product => product.CategoryId == categoryId);
if (!string.IsNullOrEmpty(subCategoryId))
query = query.Where(product => product.SubCategoryId == subCategoryId);
query = query.Skip(from).Take(limit);
var products = await query.ToListAsync();
query = query.GroupJoin(
_context.Favorites.AsNoTracking()
.Where(favorite => favorite.Type == FavoriteType.FASHION)
// This user Id will come from context just adding for overall picture.
.Where(favorite => favorite.UserId == userId),
//This orderby if I add will not make any difference.
//.OrderByDescending(favorite => favorite.Identifier),
v => v.ProductId,
f => f.Identifier,
(product, fav) => new { product, fav }).
SelectMany(x => x.Fav.DefaultIfEmpty(),
(x, y) => SetFavorite(x.Project, y));
}
private static ProductModel SetFavorite(ProductModel v, UserFavoriteModel si)
{
v.IsFavorite = (si != null);
return v;
}
I would do something like this:
var query =
_context.Products.AsQueryable().Select(p => new ProductModel {
ProductId = p.ProductId,
ProductName = p.ProductName,
IsFavorite =
_context.Favorites.Any(f =>
f.Identifier = p.ProductId &&
f.Type == FavoriteType.FASHION &&
f.UserId == userId
)
}).OrderByDescending(favorite => favorite.Identifier);

Get Table A record if Table B has a match in a search

I have two tables
CREATE TABLE RetailGroup(
Id int IDENTITY(1,1),
GroupName nvarchar(50),
)
CREATE TABLE RetailStore(
Id int IDENTITY(1,1),
StoreName nvarchar(100),
RetailGroupId int
)
Where RetailGroupId in RetailStore is referencing RetailGroup ID. I am trying to create a search function where I can search for both RetailGroup and RetailsStores. If I get a matching RetailStore I want to return the Group it is tied to and the matching Store record. If I get a matching Group, I want the group record but no retail stores.
I tried to do it the following way:
public List<RetailGroup> SearchGroupsAndStores(string value)
{
return _context.RetailGroups.Where(group => group.GroupName.Contains(value)).Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value))).ToList();
}
But this is wrong because include should not be used for selection.
Here is my entity framework model for groups
public class RetailGroup
{
[Key]
public int Id { set; get; }
[MaxLength(50)]
public String GroupName { set; get; }
//Relations
public ICollection<RetailStore> RetailStores { set; get; }
}
And here is the one for the store
public class RetailStore
{
[Key]
public int Id { get; set; }
[MaxLength(100)]
public string StoreName { get; set; }
[ForeignKey("RetailGroup")]
public int RetailGroupId { get; set; }
//Relations
public RetailGroup RetailGroup { get; set; }
public ICollection<EGPLicense> Licenses { get; set; }
}
How do I create my LINQ to get the results I am looking for ?
The query returning the desired result with projection is not hard:
var dbQuery = _context.RetailGroups
.Select(rg => new
{
Group = rg,
Stores = rg.RetailStores.Where(rs => rs.StoreName.Contains(value))
})
.Where(r => r.Group.GroupName.Contains(value) || r.Stores.Any());
The problem is that you want the result to be contained in the entity class, and EF6 neither supports projecting to entity types nor filtered includes.
To overcome these limitations, you can switch to LINQ to Objects context by using AsEnumerable() method, which at that point will effectively execute the database query, and then use delegate block to extract the entity instance from the anonymous type projection, bind the filtered collection to it and return it:
var result = dbQuery
.AsEnumerable()
.Select(r =>
{
r.Group.RetailStores = r.Stores.ToList();
return r.Group;
})
.ToList();
Try using an OR condition to filter both the group name and the store name.
return _context.RetailGroups
.Where(group => group.GroupName.Contains(value) || group.RetailStores.Any(store => store.StoreName.Contains(value)))
.Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value)))
.ToList();
Another option would be doing 2 searches, one for the groups and other for the stores. The problem here would be geting a unique set of groups from both results.
List<RetailGroup> retailGroups = new List<RetailGroup>();
var groupSearchResults = _context.RetailGroups
.Where(group => group.GroupName.Contains(value))
.Include(group => group.RetailStores.Where(store => store.StoreName.Contains(value)))
.ToList();
var storeSearchResults = _context.RetailStores
.Where(store => store.StoreName.Contains(value))
.Select(store => store.RetailGroup)
.ToList();
retailGroups.AddRange(groupSearchResults);
retailGroups.AddRange(storeSearchResults);
// Remove duplicates by ID
retailGroups = retailGroups
.GroupBy(group => group.Id)
.Select(group => group.First());
Either use OR condition and have the search in one statement :
public List<RetailGroup> SearchGroupsAndStores(string value)
{
return _context.RetailGroups
.Where(rg => rg.GroupName.Contains(value) || rg.RetailStores.Any(rs => rs.StoreName.Contains(value)))
.Include(rg => rg.RetailStores.Where(rs => rs.StoreName.Contains(value)).ToList())
.ToList();
}
Or you can split the search, first look for RetailGroups then search RetailStores and return their RetailGroup :
public List<RetailGroup> SearchGroupsAndStores(string value)
{
List<RetailGroup> searchResults = new List<RetailGroup>();
searchResults.AddRange(_context.RetailGroups.Where(rg => rg.GroupName.Contains(value)).ToList());
searchResults.AddRange(_context.RetailStores.Where(rs => rs.StoreName.Contains(value)).Select(rs => rs.RetailGroup).ToList());
}

How can I group by a navigation property member in linq to sql?

I have three tables holding Users Groups and their association, UserGroups as laid out in this fiddle:
I am trying to obtain the maximum level among the users' groups as shown in the query in the fiddle using linq2sql.
However, EntityFramework obfuscates the join table, TblUserGroup and instead just gives me the navigation properties: TblGroups.Users or User.TblGroups
This is what I have put together thus far but Linqpad tells me it cannot execute:
var maxGroup = from ua in ctx.TblGroups
group ua by ua.TblUsers.Select(s=>s.UserId)
into g
select new
{
UserId= g.Key,
MaxLevel = g.Max(s => s.GroupLevel)
};
Seems you can do it like this:
var result = users.Select(u => new
{
UserId = u.Id,
MaxLevel = u.Groups.Max(g => g.GroupLevel)
});
Having:
class User
{
public int UserId { get; set; }
public string UserName { get;set; }
public List<Group> Groups { get; set; }
}
class Group
{
public int GroupId { get; set; }
public string GroupName { get; set; }
public int GroupLevel { get; set; }
public List<User> Users { get; set; }
}
Does it work for you?
var maxGroup = ctx.TblUsers
.Where(u => u.TblUserGroups != null)
.Select(u => new
{
UserId = u.UserId,
MaxGroupLevel = u.TblUserGroups.TblGroups.Max(g => g.GroupLevel)
}
);

Categories

Resources