EF Core 6 not loading child entities - entity-framework-core

I am working on a simple web API that is just supposed to parse a JSON tree, and save it to a database. I am working with EF Core 6.0.4 and my application shows a really weird behaviour: right after saving the tree, it loads from the context just fine. But when I call a different endpoint and load the data using a freshly initialized context, the children won't load. I believe it's due to the EF config, but I can't figure out how to load the children.
I tried using _context.Entity(returnValue).Collection(x => x.Children) but the x in the lambda only has ICollection extension methods, not the model fields like in the code examples I saw.
I also tried using .Include, but that seems to be a thing from regular EF.
Here's some of my code:
Controller
public class CategoryTreeManagerController : ControllerBase
{
private readonly CategoryTreeManagerService _service;
public CategoryTreeManagerController(CategoryTreeManagerService service)
{
_service = service;
}
[HttpGet]
public IEnumerable<CategoryTreeNode> GetTree()
{
return _service.GetTree(); //this only returns the root node without any children
}
[HttpPost]
public IEnumerable<CategoryTreeNode> SaveTree(CategoryTreeNode[] nodes)
{
_service.SaveTree(nodes[0]);
return _service.GetTree(); //this correctly returns the tree
}
}
Service
public class CategoryTreeManagerService
{
private readonly CategoryTreeManagerApiDbContext _context;
public CategoryTreeManagerService(CategoryTreeManagerApiDbContext context)
{
_context = context;
}
public IEnumerable<CategoryTreeNode> GetTree()
{
CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
.Where(x => x.Parent == null) //just return the root node
.ToArray(); //easier for frontend
return returnValue;
}
public void SaveTree(CategoryTreeNode node)
{
if (_context.CategoryTreeNodes.Any(x => x.Id == node.Id))
{
_context.Update(node);
}
else
{
_context.CategoryTreeNodes.Add(node);
}
_context.SaveChanges();
}
}
DbContext
public class CategoryTreeManagerApiDbContext : DbContext
{
public CategoryTreeManagerApiDbContext() : base ()
{
Database.EnsureCreated();
}
public CategoryTreeManagerApiDbContext(DbContextOptions<CategoryTreeManagerApiDbContext> options) : base(options)
{
Database.EnsureCreated();
}
public DbSet<CategoryTreeNode> CategoryTreeNodes { get; set; }
public DbSet<TreeNodeDetail> TreeNodeDetails { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CategoryTreeNode>(entity =>
{
entity.HasKey(x => x.Id);
entity.HasOne(x => x.Parent)
.WithMany(x => x.Children)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
});
}
Model classes
public class CategoryTreeNode
{
public int Id { get; set; }
public string Label { get; set; }
public string Story { get; set; }
public string Icon { get; set; }
public ICollection<TreeNodeDetail> Details { get; set; }
public ICollection<CategoryTreeNode> Children { get; set; }
[JsonIgnore]
public CategoryTreeNode? Parent { get; set; }
}
public class TreeNodeDetail
{
[JsonPropertyName("detailId")]
public string Id { get; set; }
[JsonPropertyName("detailTitle")]
public string Title { get; set; }
[JsonPropertyName("detailValue")]
public string Value { get; set; }
[JsonIgnore]
[ForeignKey("CategoryTreeNode")]
public int CategoryTreeNodeId { get; set; }
}

Include would work for one level, or more if you expand out the expression, but it's not an ideal solution for tree structures which could be variable depth. (I.e. child of a child of a child ...)
For instance:
CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
.Include(x => x.Children)
.Where(x => x.Parent == null) //just return the root node
.ToArray(); //easier for frontend
would load all parents and their first level children. To load 2nd level children:
CategoryTreeNode[] returnValue = _context.CategoryTreeNodes
.Include(x => x.Children)
.ThenInclude(x => x.Children)
.Where(x => x.Parent == null) //just return the root node
.ToArray(); //easier for frontend
The issue is knowing how many levels to load, and each level produces a Cartesian Product for EF to work through, exponentially increasing the amount of data being loaded to build a tree. Loading an entire table once quickly becomes a much more efficient solution.
If you have a Single tree structure where you expect only one top-level entry, or want to load an entire reasonable set of top-level nodes then loading all entries into memory will work since EF will be tracking all of the entities and it can resolve all of the various references as it builds the entity structure. This has to load the entire set even if you only want one specific parent out of several possible parents.
If you have several top level parent nodes and a sizeable overall table size to work through, and do want to be able to load a single parent and it's children then one option is to add a de-normalized top-level ID reference to the tree node.
public int Id { get; set; }
public int? TopLevelId { get; set; }
This would be a null-able FK but does not need a navigation property. The current Parent reference would continue to use a shadow property. (I.e. ParentId) In this way once you have an ID for the top-level parent you want to load a tree for:
_context.CategoryTreeNodes.Where(x => x.TopLevelId == topLevelId).ToList();
var topLevelNode = _context.CategoryTreeNodes.Single(x => x.Id == topLevelId);
The first statement will have the DBContext load and track all nodes under that top level tree node. Then when you call the DbContext to get that top level node, the tracked related entities will all get filled in.
The caveat of this approach is that it is a denormalization, in that there is no DB-level assertion that a TopLevelId is set, or remains set correctly. For instance if nodes can be moved between top-level entities and you forget to update this value, this node would not be loaded and associated under the new parent using the above load.

Related

EF Loading Related Entities in Extension Methods

With the below 1 to many relationship setup:
public partial class Folder
{
public Folder()
{
Files = new HashSet<File>();
}
public int FolderId { get; set; }
public virtual ICollection<File> Files { get; set; }
}
public partial class File
{
public int FileId { get; set; }
public int FolderId { get; set; }
public virtual Folder Folder { get; set; }
}
public class DataContext: DbContext
{
...
public virtual DbSet<Folder> Folders { get; set; }
public virtual DbSet<File> Files { get; set; }
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Folder>(entity =>
{
...
});
modelBuilder.Entity<File>(entity =>
{
...
entity.HasOne(e => e.Folder)
.WithMany(e => e.Files)
.HasForeignKey(e => e.FolderId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("...");
});
}
}
public SomeReturnType AFunction(IQueryable<Folder> folders) =>
folders.GroupJoin(
...,
(folder, other) => new
{
folder,
something = folder.Files.Any(...)
});
With AFunction, the related Files are loaded for folder.Files.Any(). But if I do the below:
public static class FolderExtensions
{
public static SomeReturnType BFunction(this Folder folder) =>
folder.Files.Any(...);
}
public SomeReturnType AFunction(IQueryable<Folder> folders) =>
folders.GroupJoin(
...,
(folder, other) => new
{
folder,
something = folder.BFunction()
});
BFunction will not load the related Files. Any ideas to get around this problem?
To explain what happens let's first reduce the query to the essentials:
folders.Select(folder => new
{
folder,
something = folder.BFunction()
});
The problem here is that the BFunction method is executed client-side. That is, the part IQueryable<Folder> folders is first materialized into memory (client-side) and then the projection (new { ... }) is applied to the materialized result. This is something EF core does if anything in the final projection (and only in the final projection) can't be translated into SQL. Since Files are not Include-ed and no lazy loading occurs, the materialized folders don't have files and BFunction evaluates to false.
EF doesn't try to expand CLR functions into expression trees itself. In this case it would be possible, but often it won't be. At any rate, EF doesn't even try.
You have to do the expansion yourself:
folders.Select(folder => new
{
folder,
something = folder.Files.Any()
});
Including the files would also work, but of course that's more expensive:
folders
.Include(f => f.Files)
.Select(folder => new
{
folder,
something = folder.BFunction()
});
Side-note: the GroupJoin in this form is presently not supported by EF core (6.0).

Entity Framework Core 5.0 - Many to many select query

I am trying to get a single User, with a list of Items, mapped with a many-to-many entity UserItems. However, I am unable to retrieve the mapped Items due to to an error that I'm unable to solve (error at bottom of question). Here is my code:
public class User
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
}
public class Item
{
public int Id { get; set; }
public ICollection<UserItem> UserItems { get; set; }
}
public class UserItem
{
public int Id { get; set; }
public int UserId { get; set; }
public User User { get; set; }
public int ItemId { get; set; }
public Item Item { get; set; }
public int Quantity { get; set; }
}
The UserItem class configuration has the following relationships defined:
builder.HasOne(x => x.User)
.WithMany(x => x.UserItems)
.HasForeignKey(x => x.UserId)
.OnDelete(DeleteBehavior.ClientCascade);
builder.HasOne(x => x.Item)
.WithMany(x => x.UserItems)
.HasForeignKey(x => x.ItemId)
.OnDelete(DeleteBehavior.ClientCascade);
I have the following generic repo with this method:
public class GenericRepository<T> : where T : class
{
private readonly DbContext _context;
public GenericRepository(DbContext context) => _context = context;
public T Get(Expression<Func<T, bool>> where, params Expression<Func<T, object>>[] navigationProperties)
{
IQueryable<T> query = _context.Set<T>();
query = navigationProperties.Aggregate(query, (current, property) => current.Include(property));
var entity = query.FirstOrDefault(where);
return entity;
}
}
However, when I try to run the code, I get an error on the Select(x => x.Item):
var user = repo.Get(x => x.Id == 1, x => x.UserItems.Select(y => y.Item));
Error:
System.InvalidOperationException: 'The expression 'x.UserItems.AsQueryable().Select(y => y.Item)' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.'
What am I doing wrong, this seems to work for my other projects?
This error Occurs because you are not passing in a navigation property (x.UserItems would be a navigation property) but rather something you want to do with the navigation property. UserItems.Select(y => y.Item) is not a property of x because Select() is a function and therefore it cannot be included.
What you are trying to do (I assume it is including UserItems and also the corresponding Items) is not going to work with your current implementation of the repository. To include navigation properties of navigation properties .ThenInclude() must be used instead of .Include() which works only for navigation properties directly defined on the Entity the DbSet is created for.
But apart from your question I would suggest not to use such an generic implementation of Repository. The main benefit from using reposiories is to separarte code related to loading and storing of entities from the rest of your code. In your case if the consumer of repository knows that navigation properties must be included and that he has to provide them - then what is the point of having a repository at all? Then the consumer again cares about database specific code which makes having a repository unneccessary. I would recommend just making a conrete "UserRepository" which can only be used to retrieve users and explicitly includes the needed properties.

How to deep clone/copy in EF Core

What I would like to do is duplicate/copy my School object and all of its children/associations in EF Core
I have something like the following:
var item = await _db.School
.AsNoTracking()
.Include(x => x.Students)
.Include(x => x.Teachers)
.Include(x => x.StudentClasses)
.ThenInclude(x => x.Class)
.FirstOrDefaultAsync(x => x.Id == schoolId);
I have been reading up on deep cloning and it seems that I should be able to do just add the entity...so pretty much the next line.
await _db.AddAsync(item);
Then EF should be smart enough to add that entity as a NEW entity. However, right off the bat I get a conflict that says "the id {schoolId} already exists" and will not insert. Even if I reset the Id of the new item I am trying to add, I still get conflicts with the Ids of the associations/children of the school iteam.
Is anyone familiar with this and what I might be doing wrong?
I had the same problem too, but in my case EF core was throwing exception "the id already exists".
Following the answer of #Irikos so I have created method which clones my objects.
Here's example
public class Parent
{
public int Id { get; set; }
public string SomeProperty { get; set; }
public virtual List<Child> Templates { get; set; }
public Parent Clone()
{
var output = new Parent() { SomeProperty = SomeProperty };
CloneTemplates(output);
return output;
}
private void CloneTemplates(Parent parentTo, Child oldTemplate = null, Child newTemplate = null)
{
//find old related Child elements
var templates = Templates.Where(c => c.Template == oldTemplate);
foreach (var template in templates)
{
var newEntity = new Child()
{
SomeChildProperty = template.SomeChildProperty,
Template = newTemplate,
Parent = parentTo
};
//find recursivly all related Child elements
CloneTemplates(parentTo, template, newEntity);
parentTo.Templates.Add(newEntity);
}
}
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public virtual Parent Parent { get; set; }
public int? TemplateId { get; set; }
public virtual Child Template { get; set; }
public string SomeChildProperty { get; set; }
}
Then I just call DbContext.Parents.Add(newEntity) and DbContext.SaveChanges()
That worked for me. Maybe this will be useful for someone.
I had the same problem, but in my case, ef core was smart enough save them as new entities even with existing id. However, before realising that, I just made a copy constructor for all the items, created a local task variable containing only the desired properties and returned the copy.
Remove certain properties from object upon query EF Core 2.1

Nested Tables Using a DTO

I need help getting my WebApi Controller to work.
I have a 3 table Models like this.
First Table
public class MainTable{
public int MainTableID { get; set; }
... Other Fields
public ICollection<SubTable> SubTables { get; set; }
}
Second Table
public class SubTable{
public int SubTableID { get; set; }
... Other Fields
public int MainTableID { get; set; }
[ForeignKey("MainTableID ")]
[JsonIgnore]
public virtual MainTable MainTable{ get; set; }
public ICollection<SubSubTable> SubSubTables { get; set; }
}
Third Table
public class SubSubTable{
public int SubSubTableID { get; set; }
... Other Fields
public int SubTableID { get; set; }
[ForeignKey("SubTableID")]
[JsonIgnore]
public virtual SubTable SubTable{ get; set; }
}
I need to flatten the first model because of other relationships not mentioned in this post so I am using a dto
DTO
public class TableDTO
{
public int MainTableID { get; set; }
... Other Fields (there is a lot of flattening happening here but I am going to skip it to keep this simple)
public ICollection<SubTable> SubTables { get; set; }
}
Now that I got all of that out of the way. To my question.. I am linking this all to a web api controller.
If I use the DTO and create a controller like this
Controller with DTO
public IQueryable<TableDTO> GetMainTable()
{
var mainTable = from b in db.MainTables
.Include(b => b.SubTable.Select(e => e.SubSubTable))
select new TableDTO()
{
MainTableID = b.MainTableID
eager mapping of all the fields,
SubTables = b.SubTables
};
return mainTable;
}
This works for everything except the SubSubTable which returns null. If I ditch the DTO and create a controller like this
Controller without DTO
public IQueryable<MainTable> GetMainTable()
{
return db.MainTables
.Include(c => c.SubTables)
.Include(c => c.SubTables.Select(b => b.SubSubTables));
}
This works perfect and the JSon returns everything I need, except that I lose the DTO which I desperately need for other aspects of my code. I have rewritten my code in every way I can think of but nothing works. I am pretty sure that this can be done with the DTO but I don't know what it would take to make it work, and as they say "You don't know what you don't know" so hopefully someone here knows.
In Entity Framework 6 (and lower), Include is always ignored when the query ends in a projection, because the Include path can't be applied to the end result. Stated differently, Include only works if it can be positioned at the very end of the LINQ statement. (EF-core is more versatile).
This doesn't help you, because you explicitly want to return DTOs. One way to achieve this is to do the projection after you materialize the entities into memory:
var mainTable = from b in db.MainTables
.Include(b => b.SubTable.Select(e => e.SubSubTable))
.AsEnumerable()
select new MessageDTO()
{
MainTableID = b.MainTableID ,
// eager mapping of all the fields,
SubTables = b.SubTables
};
The phrase, "eager mapping of all the fields" suggests that the projection isn't going to narrow down the SELECT clause anyway, so it won't make much of a difference.
Another way could be to load all SubSubTable objects into the context that you know will be in the MainTables you fetch from the database. EF will populate all SubTable.SubSubTables collections by relationship fixup.
If this works:
public IQueryable<MainTable> GetMainTable()
{
return db.MainTables
.Include(c => c.SubTables)
.Include(c => c.SubTables.Select(b => b.SubSubTables));
}
Then use this one and just add a Select() to the end with a ToList(). Note the IEnumerable in the return type:
public IEnumerable<MainTableDto> GetMainTable()
{
return db.MainTables
.Include(c => c.SubTables)
.Include(c => c.SubTables.Select(b => b.SubSubTables))
.Select(c=> new MainTableDto { SubTables=c.SubTables /* map your properties here */ })
.ToList();
}
Not sure about the types though (at one place you have MainTableDto, at another you mention MessageDto?).

Problem Saving with Entity Framework (Need conceptual help)

Problem Summary: I have a Master and Detail entities. When I initialize a Master (myMaster), it creates an instance of Details (myMaster.Detail) and both appear to persist in the database when myMaster is added. However, when I reload the context and access myMasterReloaded.detail its properties are not initialized. However, if I pull the detail from the context directly, then this magically seems to initialize myMasterReloaded.detail. I've distilled this down with a minimal unit test example below. Is this a "feature" or am I missing some important conceptual detail?
//DECLARE CLASSES
public class Master
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid MasterId { get; set; }
public Detail Detail { get; set; }
public Master() { Detail = new Detail(); }
}
public class Detail
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid DetailId { get; set; }
public Master MyMaster{ get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Master> Masters { get; set; }
public DbSet<Detail> Details { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithOptionalPrincipal(x => x.MyMaster)
.WillCascadeOnDelete(true);
}
}
//PERFORM UNIT TEST
[TestMethod]
public void UnitTestMethod()
{
//Start with fresh DB
var context = new MyDbContext();
context.Database.Delete();
context.Database.CreateIfNotExists();
//Create and save entities
var master = context.Masters.Create();
context.Masters.Add(master);
context.SaveChanges();
//Reload entity
var contextReloaded = new MyDbContext();
var masterReloaded = contextReloaded.Masters.First();
//This should NOT Pass but it does..
Assert.AreNotEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
//Let's say 'hi' to the instance of details in the db without using it.
contextReloaded.Details.First();
//By simply referencing the instance above, THIS now passes, contracting the first Assert....WTF??
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
}
(This is the sticking point for a more sophisticated entity set. I've simply distilled this down to its simplest case I can't simply replace details with a complex type).
Cheers,
Rob
I think it's because when you first reload the Master, you have not eager-loaded the Detail, so the Detail entity will not be in the Entity Framework "graph" (internal memory). The only thing in the graph will be a single Master entity.
But when you query the Detail ("Let's say hi"), it is loaded into the graph and the reference was resolved based on the FK association, therefore your final test passes as the Master is now related to the Detail.
I could be wrong though - but that's what it sounds like.
Do you have lazy-loading enabled? If not, you need to eager-load the relationships you need.
Instead of this:
var masterReloaded = contextReloaded.Masters.First();
Try this:
var masterReloaded = contextReloaded.Masters.Include(x => x.Detail).First();
Matt Hamilton was right (See above). The problem was:
The Detail property should not be instantiated within the constructor, nor through the getters/setters via a backing member. If it's convenient to instantiate a Entity containing Properties in your new instance, then it may be helpful to have a separate initialize method which will not be automatically executed by the Entity Framework as it reconstructs objects from the database.
The Detail property needs to be declared virtual in the Master class for this to work properly.
The following WILL PASS (As expected/hope)
public class Master
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid MasterId { get; set; }
//Key new step: Detail MUST be declared VIRTUAL
public virtual Detail Detail { get; set; }
}
public class Detail
{
[Key, DatabaseGenerated(DatabaseGenerationOption.Identity)]
public Guid DetailId { get; set; }
//Set this to be VIRTUAL as well
public virtual Master MyMaster { get; set; }
}
public class MyDbContext : DbContext
{
public DbSet<Master> Masters { get; set; }
public DbSet<Detail> Details { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//This sets up a BI-DIRECTIONAL relationship
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithOptionalPrincipal(x => x.MyMaster)
.WillCascadeOnDelete(true);
}
}
[TestMethod]
public void UnitTestMethod()
{
var context = new MyDbContext();
context.Database.Delete();
context.Database.CreateIfNotExists();
//Create and save entities
var master = context.Masters.Create();
//Key new step: Detail must be instantiated and set OUTSIDE of the constructor
master.Detail = new Detail();
context.Masters.Add(master);
context.SaveChanges();
//Reload entity
var contextReloaded = new MyDbContext();
var masterReloaded = contextReloaded.Masters.First();
//This NOW Passes, as it should
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
//This line is NO LONGER necessary
contextReloaded.Details.First();
//This shows that there is no change from accessing the Detail from the context
Assert.AreEqual(master.Detail.DetailId, masterReloaded.Detail.DetailId);
}
Finally, it is also not necessary to have a bidirectional relationship. The reference to "MyMaster" can be safely removed from the Detail class and the following mapping can be used instead:
modelBuilder.Entity<Master>()
.HasOptional(x => x.Detail)
.WithMany()
.IsIndependent();
With the above, performing context.Details.Remove(master.Detail), resulted in master.Detail == null being true (as you would expect/hope).
I think some of the confusion emerged from the X-to-many mapping where you can initialize a virtual list of entities in the constructor (For instance, calling myDetails = new List(); ), because you are not instantiating the entities themselves.
Incidentally, in case anyone is having some difficulties with a one-to-many unidirectional map from Master to a LIST of Details, the following worked for me:
modelBuilder.Entity<Master>()
.HasMany(x => x.Details)
.WithMany()
.Map((x) =>
{
x.MapLeftKey(m => m.MasterId, "MasterId");
x.MapRightKey(d => d.DetailId, "DetailId");
});
Cheers, Rob