Select categories with subcategories [duplicate] - c#

I have the following entity:
public class Item
{
public int Id { get; set; }
public int? ParentId { get; set; }
public Item Parent { get; set; }
public List<Item> Children { get; set; }
public double PropertyA { get; set; }
public double PropertyB { get; set; }
...
}
Now I want to query the database and retrieve data of all the nested children.
I could achieve this by using Eager Loading with Include():
var allItems = dbContext.Items
.Include(x => Children)
.ToList();
But instead of Eager Loading, I want to do the following projection:
public class Projection
{
public int Id { get; set; }
public List<Projection> Children { get; set; }
public double PropertyA { get; set; }
}
Is it possible to retrieve only the desired data with a single select?
We are using Entity Framework 6.1.3.
Edit:
This is what I have tried so far.
I really don't know how to tell EF to map all child Projection the same way than their parents.
An unhandled exception of type 'System.NotSupportedException' occurred in EntityFramework.SqlServer.dll
Additional information: The type 'Projection' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order.
var allItems = dbContext.Items
.Select(x => new Projection
{
Id = x.Id,
PropertyA = x.PropertyA,
Children = x.Children.Select(c => new Projection()
{
Id = c.Id,
PropertyA = c.PropertyA,
Children = ???
})
})
.ToList();

Generally speaking, you can't load a recursive structure of unknown unlimited depth in a single SQL query, unless you bulk-load all potentially relevant data irregardless whether they belong to the requested structure.
So if you just want to limit the loaded columns (exclude PropertyB) but its ok to load all rows, the result could look something like the following:
var parentGroups = dbContext.Items.ToLookup(x => x.ParentId, x => new Projection
{
Id = x.Id,
PropertyA = x.PropertyA
});
// fix up children
foreach (var item in parentGroups.SelectMany(x => x))
{
item.Children = parentGroups[item.Id].ToList();
}
If you want to limit the number of loaded rows, you have to accept multiple db queries in order to load child entries. Loading a single child collection could look like this for example
entry.Children = dbContext.Items
.Where(x => x.ParentId == entry.Id)
.Select(... /* projection*/)
.ToList()

I see only a way with first mapping to anonymous type, like this:
var allItems = dbContext.Items
.Select(x => new {
Id = x.Id,
PropertyA = x.PropertyA,
Children = x.Children.Select(c => new {
Id = c.Id,
PropertyA = c.PropertyA,
})
})
.AsEnumerable()
.Select(x => new Projection() {
Id = x.Id,
PropertyA = x.PropertyA,
Children = x.Children.Select(c => new Projection {
Id = c.Id,
PropertyA = c.PropertyA
}).ToList()
}).ToList();
A bit more code but will get the desired result (in one database query).

Let's say we have the following self-referencing table:
public class Person
{
public Person()
{
Childern= new HashSet<Person>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int? ParentId { get; set; }
[StringLength(50)]
public string Name{ get; set; }
public virtual Person Parent { get; set; }
public virtual ICollection<Person> Children { get; set; }
}
And for some point of time you need to get all grandsons for specific persons.
So, first of all I will create stored procedure(using code-first migration) to get all persons in the hierarchy for those specific persons:
public override void Up()
{
Sql(#"CREATE TYPE IdsList AS TABLE
(
Id Int
)
GO
Create Procedure getChildIds(
#IdsList dbo.IdsList ReadOnly
)
As
Begin
WITH RecursiveCTE AS
(
SELECT Id
FROM dbo.Persons
WHERE ParentId in (Select * from #IdsList)
UNION ALL
SELECT t.Id
FROM dbo.Persons t
INNER JOIN RecursiveCTE cte ON t.ParentId = cte.Id
)
SELECT Id From RecursiveCTE
End");
}
public override void Down()
{
Sql(#" Drop Procedure getChildIds
Go
Drop Type IdsList
");
}
After that you can use Entity Framework to load the ids(you could modify stored procedure to return persons instead of only returning ids) of persons under the passed persons(ex grandfather) :
var dataTable = new DataTable();
dataTable.TableName = "idsList";
dataTable.Columns.Add("Id", typeof(int));
//here you add the ids of root persons you would like to get all persons under them
dataTable.Rows.Add(1);
dataTable.Rows.Add(2);
//here we are creating the input parameter(which is array of ids)
SqlParameter idsList = new SqlParameter("idsList", SqlDbType.Structured);
idsList.TypeName = dataTable.TableName;
idsList.Value = dataTable;
//executing stored procedure
var ids= dbContext.Database.SqlQuery<int>("exec getChildIds #idsList", idsList).ToList();
I hope my answer will help others to load hierarchical data for specific entities using entity framework.

Related

Linq method - Return rows only found in another table

I want to return the Tags from the Tag table that are only found in the TagRecipe table. How can I do this?
var dataTags = await _context.Tags
.Include(tc => tc.TagCategory)
.ToListAsync();
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<TagRecipe> TagRecipes { get; set; }
public int TagCategoryID { get; set; }
public TagCategory TagCategory { get; set; }
}
public class TagRecipe
{
public int TagId { get; set; }
public int RecipeId { get; set; }
public Tag Tag { get; set; }
public Recipe Recipe { get; set; }
}
Thank you
Try this
var dataTags = await _context.TagRecipe
.Include(tc => tc.Tag.TagCategory)
.Select(i=> i.Tag)
.ToListAsync();
or you can use this syntax if you like it more
var dataTags = await _context.TagRecipe
.Include(t => t.Tag)
.ThenInclude(tc => tc.TagCategory)
.Select(i=> i.Tag)
.ToListAsync();
An alternative starting at table Tags using Join that will return a result without duplicates.
var dataTags = db.Tags
.Join(db.TagRecipes, tag => tag.Id, tagRecipe => tagRecipe.TagId, (tag, tagRecipe) => tag)
.Include(tag => tag.TagCategory)
.ToLookup(tag => tag.Id) // client-side from here
.Select(grouping => grouping.First()) // to make distinct
.ToList();
Will generate a straight-forward SQL
SELECT "t"."Id", "t"."Name", "t"."TagCategoryId", "t1"."Id", "t1"."Name"
FROM "Tags" AS "t"
INNER JOIN "TagRecipes" AS "t0" ON "t"."Id" = "t0"."TagId"
INNER JOIN "TagCategories" AS "t1" ON "t"."TagCategoryId" = "t1"."Id"
It is possible to use .Distinct in the above expression for removing duplicates instead of using grouping, but that will create a more complex SQL.
Table TagRecipes seems to be a join table in a many-to-many between table Tags and table Recipes. The latter is not included in the question, but I added it during my tests.
Please note that in EF Core 5, many-to-many relations may be created without an entity class for the join table.

LINQ create generic List of nestet objects

How can I get a List<Type1> which includes another List<Type2> from another List<Type3>?
Here is the situation:
I have a List<DbStruncture>. Each entry includes a DatabaseStructure
public partial class DatabaseStructure
{
public string TableSchema { get; set; }
public string TableName { get; set; }
public string ColumnName { get; set; }
public bool? IsPrimaryKey { get; set; }
}
I also have
public class Table
{
public string Name { get; set; }
public string Schema { get; set; }
public List<Column> Columns { get; set; }
}
public class Column
{
public string Name { get; set; }
public bool? IsPrimaryKey { get; set; }
}
Now I want to fill the Data from the List<DatabaseStructure> into a List<Table> which includes a List<Column> with all the Columns of that Table.
I tried it with LINQ and this is how far I got:
var query =
from t in result
group t.TableName by t.TableName
into tn
select new
{
Table = tn.Key,
Schema = from s in result where s.TableName == tn.Key select s.TableSchema.First(),
Columns = from c in result where c.TableName == tn.Key select new Column
{
Name = c.ColumnName,
IsPrimaryKey = c.IsPrimaryKey
}
};
The problem with my solution is, that my query is not a generic List...
Can anybody point me into the right direction? Is LINW the right way here? If yes, how do I get the wanted result?
Thanks in advance
Preface: I prefer (and recommend) using Linq with the Extension Method syntax instead of using the from,group,into keywords because it's more expressive and if you need to do more advanced Linq operations you'll need to use Extension Methods anyway.
To begin with, your input is denormalized (I presume the output of running SELECT ... FROM INFORMATION_SCHEMA.COLUMNS) where each row contains repeated table information, so use GroupBy to group the rows together by their table identifier (don't forget to use both the Table Schema and Table Name to uniquely identify a table!)
Then convert each group (IGrouping<TKey: (TableSchema,TableName), TElement: DatabaseStructure>) into a Table object.
Then populate the Table.Columns list by performing an inner Select from the IGrouping group and then .ToList() to get a concrete List<Column> object.
My expression:
List<DatabaseStructure> input = ...
List<Table> tables = input
.GroupBy( dbs => new { dbs.TableSchema, dbs.TableName } )
.Select( grp => new Table()
{
Name = grp.Key.TableName,
Schema = grp.Key.TableSchema,
Columns = grp
.Select( col => new Column()
{
Name = col.Name,
IsPrimaryKey = col.IsPrimaryKey
} )
.ToList()
} )
.ToList()
OK, just found the answer myself.
Here it is:
var query =
(from t in result
group t.TableName by t.TableName
into tn
select new Table
{
Name = tn.Key,
Schema = (from s in result where s.TableName == tn.Key select s.TableSchema).First(),
Columns = (from c in result
where c.TableName == tn.Key
select new Column
{
Name = c.ColumnName,
IsPrimaryKey = c.IsPrimaryKey
}).ToList()
});

Prevent SELECT N+1 issue in Entity Framework query using Joins

I'm trying to query something from an indirectly related entity into a single-purpose view model. Here's a repro of my entities:
public class Team {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Member> Members { get; set; }
}
public class Member {
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class Pet {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public Member Member { get; set; }
}
Each class is in a DbSet<T> in my database context.
This is the view model I want to construct from a query:
public class PetViewModel {
public string Name { get; set; }
public string TeamItIndirectlyBelongsTo { get; set; }
}
I do so with this query:
public PetViewModel[] QueryPetViewModel_1(string pattern) {
using (var context = new MyDbContext(connectionString)) {
return context.Pets
.Where(p => p.Name.Contains(pattern))
.ToArray()
.Select(p => new PetViewModel {
Name = p.Name,
TeamItIndirectlyBelongsTo = "TODO",
})
.ToArray();
}
}
But obviously there's still a "TODO" in there.
Gotcha: I can not change the entities at this moment, so I can't just include a List<Pet> property or a Team property on Member to help out. I want to fix things inside the query at the moment.
Here's my current solution:
public PetViewModel[] QueryPetViewModel_2(string pattern) {
using (var context = new MyDbContext(connectionString)) {
var petInfos = context.Pets
.Where(p => p.Name.Contains(pattern))
.Join(context.Members,
p => p.Member.Id,
m => m.Id,
(p, m) => new { Pet = p, Member = m }
)
.ToArray();
var result = new List<PetViewModel>();
foreach (var info in petInfos) {
var team = context.Teams
.SingleOrDefault(t => t.Members.Any(m => m.Id == info.Member.Id));
result.Add(new PetViewModel {
Name = info.Pet.Name,
TeamItIndirectlyBelongsTo = team?.Name,
});
}
return result.ToArray();
}
}
However, this has a "SELECT N+1" issue in there.
Is there a way to create just one EF query to get the desired result, without changing the entities?
PS. If you prefer a "plug and play" repro containing the above, see this gist.
You've made the things quite harder by not providing the necessary navigation properties, which as #Evk mentioned in the comments do not affect your database structure, but allow EF to supply the necessary joins when you write something like pet.Member.Team.Name (what you need here).
The additional problem with your model is that you don't have a navigation path neither from Team to Pet nor from Pet to Team since the "joining" entity Member has no navigation properties.
Still it's possible to get the information needed with a single query in some not so intuitive way by using the existing navigation properties and unusual join operator like this:
var result = (
from team in context.Teams
from member in team.Members
join pet in context.Pets on member.Id equals pet.Member.Id
where pet.Name.Contains(pattern)
select new PetViewModel
{
Name = pet.Name,
TeamItIndirectlyBelongsTo = team.Name
}).ToArray();

ASP.NET MVC LINQ Entity Framework recursive

I'm not sure how to write LINQ query. I have these models:
class Category
{
ICollection<Thread> Threads {get;set;}
ICollection<Category> SubCategories {get;set;}
}
class Thread
{
Category Category {get;set;}
//Some Stuff
}
So, there could be categories linked like -
Category1
Category2
Category3
Category4
Category5
Category6
I want find all threads linked to Category2 and it SubCategories(3, 4, 5).
I thought about just take Category1 form db, and using C# recursive function build List of threads i need, but i feel it's bad idea.
Any ideas or links would be great. Thank you!
There code, but there is Topics(in Threads), i didnt mention it couse it's not rly matter(at least i think so)
public ActionResult ShowCategoryTopics(int id)
{
var category = db.Categories.Where(x => x.Id == id).FirstOrDefault();
var topics = GetTopics(category);
return View();
}
public List<Topic> GetTopics(Category category)
{
List<Topic> topics = new List<Topic>();
if (!category.IsDeleted && !category.IsHidden)
return null;
foreach (Thread thread in category.Threads)
{
topics.AddRange(thread.Topics.Where(x => !x.IsDeleted).ToList());
}
foreach(Category childCategory in category.SubCategories)
{
topics.AddRange(GetTopics(childCategory));
}
return topics;
}
While EF can load joined records lazily and transparently, it can't load recursive joined records cause it's too complicate.
So, first of all, remove the Category.Threads navigation property:
public class Category
{
public int Id { get; set; }
public int? ParentId { get; set; }
// you can remove the attribute
[ForeignKey(nameof(ParentId))]
public virtual Category Parent { get; set; }
public string Title { get; set; }
public virtual ICollection<Category> SubCategories { get; set; } = new HashSet<Category>();
}
public class Thread
{
public int Id { get; set; }
public int CategoryId { get; set; }
// you can remove the attribute
[ForeignKey(nameof(Category))]
public Category Category { get; set; }
public string Title { get; set; }
}
Now you can use Common Table Expressions to recursive query and Database.SqlQuery<TElement> method to load the result of the query.
This is the SQL query to get all Threads corresponded to the specified #CategoryId and all its subcategories:
WITH RecursiveCategories(Id, ParentId, Title)
AS
(
SELECT Id, ParentId
FROM dbo.Categories AS c1
WHERE Id = #CategoryId
UNION ALL
SELECT Id, ParentId
FROM dbo.Categories AS c2
INNER JOIN c1 ON c2.ParentId = c1.Id
)
SELECT th.*
FROM dbo.Threads AS th
WHERE th.CategoryId IN (SELECT Id FROM RecursiveCategories)
The method to load threads of specified category recursively:
public IEnumerable<Thread> GetAllRecursivelyByCategoryId(int categoryId)
{
var query = #"WITH RecursiveCategories(Id, ParentId, Title)
AS
(
SELECT Id, ParentId
FROM dbo.Categories AS c1
WHERE Id = #CategoryId
UNION ALL
SELECT Id, ParentId
FROM dbo.Categories AS c2
INNER JOIN c1 ON c2.ParentId = c1.Id
)
SELECT th.*
FROM dbo.Threads AS th
WHERE th.CategoryId IN (SELECT Id FROM RecursiveCategories)";
var parameter = new SqlParameter("CategoryId", categoryId);
return _dbContext.Database
.SqlQuery<Thread>(query, parameter)
.AsEnumerable();
}
This method runs the recursive query and maps the result to enumerable of threads. Here is only one request to the SQL server, and the response contains only necessary threads.
The way to do this all in database would be to use a recursive Common Table Expression (CTE) to extract all the category hierarchy. However this is a bit difficult to implement using Linq without resorting to direct SQL.
As you state there will only be about 100 or so categories it may me simpler to do the category extraction in the code rather than database.
I'm assuming you have the foreign key columns as wells as the navigation properties.
First a Helper function, converts a list of categories to an enumerable of nested ids;
static IEnumerable<int> GetCategoryIds(IList<Category> categories, int? targetId) {
if (!targetId.HasValue) {
yield break;
}
yield return targetId;
foreach (var id in categories.Where(x => x.ParentId==targetId).SelectMany(x => GetCategoryIds(x.Id))) {
yield return id;
}
}
Now your query
var ids = GetCategoryIds(db.Categories.ToList(), 2).ToList();
var threads = db.Threads.Where(x => ids.Contains(x.CategoryId));

Linq to Entities subquery to fill array?

New at linq to entities trying to figure this out. I have the following tables:
Customer: Cust_Id, Name
Orders: Order_Id
CustomerOrders: Cust_Id, Order_Id
I have a class like so:
public class Customers
{
public List<Row> Rows { get; set; }
public Customers()
{
Rows = new List<Row>();
}
public class Row
{
public int Key { get; set; }
public string Name { get; set; }
public List<string> Order_Ids { get; set; }
}
}
Linq query is like this:
var query = from c in context.Customer
select new Customers.Row
{
Key = c.Cust_Id,
Name = c.Name,
Order_IDs = List<string>( ?? )
};
foreach (var row in query)
{
Customers.Rows.Add(row);
}
var serializer = new JavaScriptSerializer();
return serializer.Serialize(Customers);
Where I have '??', can I use a subquery or something to get a list of Order_Id's from the CustomerOrders table?
Right Now, I can only think to loop through the Customers table once it is filled and then query the DB again to get each array of Order Id's for each Customer.
If it's not a requirement, drop the "Row" collection from the "Customer" object. This should suffice:
public class Customer
{
public int Key { get; set; }
public string Name { get; set; }
public List<string> Order_Ids { get; set; }
}
Then you can query like this:
var customers = from c in context.Customers
select new Customer
{
Key = c.Cust_Id,
Name = c.Name,
Order_IDs = c.Orders.Select(o => o.Order_Id).ToList()
};
It's better to deal in objects when writing C# and using EF, than to deal in terms of tables and rows -less confusing.
Try something like this:
var query = from c in context.Customer
select new Customers.Row
{
Key = c.Cust_Id,
Name = c.Name,
Order_Ids = c.Rows.Select(row => row.Key.ToString()).ToList()
};
Where you have .Select(row => row.Key.ToString()) you can set the property you need (Key, Name, etc...). Select method is an extension method to IEnumerable and it return a collection of type of property you have seted, in this case, a collection of strings because I converted it with ToString() method.

Categories

Resources