EF Core 6 Database First Parent Child Relation issue - c#

We are building an application using .NET 6 and EF Core 6 with an existing SQL Server database. We are using the database first approach and running the Scaffold-DbContext tool we were able to generate the dbcontex class. Everything works fine, a part for a parent child relation between two tables:
The scaffold tool, for the above tables generated the following two classes:
public partial class TreeNode
{
public TreeNode()
{
TreeNodeHierarchyChildren = new HashSet<TreeNodeHierarchy>();
TreeNodeHierarchyParents = new HashSet<TreeNodeHierarchy>();
}
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public bool IsLeaf { get; set; }
public int? OrganisationId { get; set; }
public bool IsDeleted { get; set; }
public virtual ICollection<TreeNodeHierarchy> TreeNodeHierarchyChildren { get; set; }
public virtual ICollection<TreeNodeHierarchy> TreeNodeHierarchyParents { get; set; }
}
public partial class TreeNodeHierarchy
{
public int Id { get; set; }
public int ParentId { get; set; }
public int ChildId { get; set; }
public virtual TreeNode Child { get; set; }
public virtual TreeNode Parent { get; set; }
}
And in the dbcontext class the following mapping:
modelBuilder.Entity<TreeNode>(entity =>
{
entity.ToTable("TreeNode");
entity.Property(e => e.Code).HasMaxLength(100);
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(255);
});
modelBuilder.Entity<TreeNodeHierarchy>(entity =>
{
entity.ToTable("TreeNodeHierarchy");
entity.HasOne(d => d.Child)
.WithMany(p => p.TreeNodeHierarchyChildren)
.HasForeignKey(d => d.ChildId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Child");
entity.HasOne(d => d.Parent)
.WithMany(p => p.TreeNodeHierarchyParents)
.HasForeignKey(d => d.ParentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Parent");
});
Here is the issue, when I write the following:
var nodes = _context.TreeNodes.Include(th => th.TreeNodeHierarchyChildren)
.Where(tn => tn.IsLeaf)
.....
it loads the child but not the parent.
This relation works properly in the current application (.net 4.7) using LINQ to SQL.
Am I missing something?
Updated
as suggested from #SpruceMoose, I included also the TreeNodeHierarchyParents property in the query but it didn't fix the issue.
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
Updated #2
I applied the mapping suggested from #Dave which in my opinion it makes sense (at the end the relation is like the Windows folders/files system).
Anyway there is still something that's not working properly. When I debug the following code:
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
.ToList();
I still see that the parent has not been loaded
Updated #3
I applied the change to the query as suggested from #Moho
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.ThenInclude(tnhc => tnhc.Child)
.Include(th => th.TreeNodeHierarchyParents)
.ThenInclude(tnhp => tnhp.Parent)
.Where(tn => tn.IsLeaf)
.ToList();
and finally we got the Parent value
Now we are missing the last step, the parents of a parent

You need to explicitly (eagerly) load the Parent elements by using an Include() on the TreeNodeHierarchyParents navigation property (as you are currently for the TreeNodeHierarchyChildren navigation property).
Change your linq query to the following:
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.Include(th => th.TreeNodeHierarchyParents)
.Where(tn => tn.IsLeaf)
.....

I think your relationship mapping is wrong. You say one child has many children and one parent has many parents. It should be one child has many parents, and one parent has many children.
I think it's also a good idea to define these kinds of relationships on both sides, so that if you get something wrong it shows up as an error faster. Note also that I think some of these statements would already be the default.
Also, important, note that I think you need to use Nullable Reference Types to indicate nullability. Anything that is supposed to be nullable should have a ? on its type name in the entity types. Though I think it's possible you should cascade delete, not set null. It depends how your model works.
Something like this, though I can't guarantee compilation:
modelBuilder.Entity<TreeNode>(tnb => {
tnb.ToTable("TreeNode");
tnb.Property(tn => tn.Code).HasMaxLength(100);
tnb.Property(tn => tn.Name).IsRequired().HasMaxLength(255);
tnb
.HasMany(tn => tn.TreeNodeHierarchyParents)
.WithOne(tnh => tnh.Child);
tnb
.HasMany(tn => tn.TreeNodeHierarchyChildren)
.WithOne(tnh => tnh.Parent);
});
modelBuilder.Entity<TreeNodeHierarchy>(tnhb => {
tnhb.ToTable("TreeNodeHierarchy");
tnhb
.HasOne(tnh => tnh.Child)
.WithMany(tn => tn.TreeNodeHierarchyParents)
.HasForeignKey(tnh => tnh.ChildId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Child");
tnhb
.HasOne(tnh => tnh.Parent)
.WithMany(tn => tn.TreeNodeHierarchyChildren)
.HasForeignKey(tnh => tnh.ParentId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_TreeNodeHierarchy_TreeNode_Parent");
});
One thing you can do to try to make sure your model definition is correct is to create an empty second database with it and compare its model against the real one, and then keep fine tuning it until you get it right.

Regarding Update #2:
You’re eager loading the relationship entities of type TreeNodeHierarchy but you are not eager loading the TreeNode entities they reference. You need to add .ThenInclude calls to do so.
var nodes = _context.TreeNodes
.Include(th => th.TreeNodeHierarchyChildren)
.ThenInclude(tnhc => tnhc.Child)
.Include(th => th.TreeNodeHierarchyParents)
.ThenInclude(tnhp => tnhp.Parent)
.Where(tn => tn.IsLeaf)
The Child property in your example is populated in your current query because the TreeNode entity is loaded by your base query and EF Core will automatically hook it up to relevant navigation properties in other tracked entities. Thus, any “parent” that is not IsLeaf (nor any “child” that is not IsLeaf) is not loaded without the additional ThenInclude.
Another (not recommended) alternative is to enable lazy loading.

Related

Problem with master/detail tables and Entity Framework

I have a typical master/detail (User / Settings table) table schema (SQL Server) and setup Entity Framework using Fluent API to work with those tables.
I define this as an independent association, so the UserProfileSetting class doesn't include the UserId property, but I understand is correctly mapped in the configuration.
Well, my problem is that when one item of Settings is updated for a profile, at the database level that settings is updated for all users. Basically USER_ID is not considered.
The SQL query produced is this:
UPDATE [dbo].[T_USERPROFILE_SETTING]
SET [VALUE] = #0
WHERE ([KEY] = #1)
Any idea what could be wrong? I guess that if I finally add the UserId property to UserProfileSettings, that will fix the problem, but I wanted to try to fix this without it.
Current code below...
Code updating the data
var entry = profile.Settings.Where(s => s.Key == key).SingleOrDefault();
if (entry != null)
{
entry.Value = value;
} else {
var setting = /* Here create a new setting */
profile.Settings.Add(setting);
}
DataContext.SaveChanges();
Entities:
public partial class UserProfile
{
[Key]
public string UserId { get; set; }
public DateTimeOffset LastLogin { get; set; }
public ICollection<UserProfileSetting> Settings { get; set; }
}
public class UserProfileSetting
{
public UserProfileSetting() { }
public string Key { get; set; }
public string Value { get; set; }
}
Entity configuration:
public class UserProfileConfiguration : EntityTypeConfiguration<UserProfile>
{
public UserProfileConfiguration()
{
ToTable("T_USERPROFILE");
HasKey<string>(p => p.UserId);
Property(p => p.UserId)
.HasColumnName("USER_ID")
.HasMaxLength(50)
.IsUnicode()
.IsRequired();
Property(p => p.LastLogin)
.HasColumnName("LAST_LOGIN_AT")
.IsRequired();
HasMany<UserProfileSetting>(p => p.Settings)
.WithOptional()
.Map(m => m.MapKey("USER_ID"));
}
}
public class UserProfileSettingConfiguration : EntityTypeConfiguration<UserProfileSetting>
{
public UserProfileSettingConfiguration()
{
ToTable("T_USERPROFILE_SETTING");
HasKey(p => p.Key );
Property(p => p.Key)
.HasColumnName("KEY")
.HasMaxLength(50)
.IsUnicode()
.IsRequired();
Property(p => p.Value)
.HasColumnName("VALUE")
.IsUnicode()
.IsRequired();
}
}
From EF documentation...
When foreign key columns are not included in the model, the association information is managed as an independent object. Relationships are tracked through object references instead of foreign key properties. This type of association is called an independent association. The most common way to modify an independent association is to modify the navigation properties that are generated for each entity that participates in the association.
So, I was wrong. In my code, UserProfile should include UserProfileSetting either as a FK (Just the ID) or as an independent Object.
In the 1st case a UserId should be mapped into UserProfileSetting and the navigation property in UserProfile should be changed to...
HasMany<UserProfileSetting>(p => p.Settings)
.WithOptional()
.HasForeignKey(s => s.UserId);
In the 2nd case, (this is what is called an Independent Association) a new navigation property should be added into UserProfileSetting for UserProfile.
Entity framework maps to relational database and so it must stick with some of it concepts. The main thing here is, that each entity is mapped to a table containing all the records of that entity and it needs some data to distinguish the relation.
Therefore you need to add USER_ID to tell which record is for which user (to define the relation). In other words you need to have it in table and also in C# entity.
I don’t think it is possible in code first to not have the relation property on entity. On the other hand, you can create some extra DTO layer to hide it.

EF Core Include on multiple sub-level collections

Consider this aggregate root...
class Contact
{
ICollection<ContactAddress> Addresses { get; set; }
ICollection<ContactItem> Items { get; set; }
ICollection<ContactEvent> Events { get; set; }
}
...which I have used like so...
class Person
{
Contact ContactDetails { get; set; }
}
How do I eager load all of the collections with the contact?
I tried this...
Context
.Set<Person>()
.Include(o => o.ContactDetails)
.ThenInclude(o => o.Addresses)
.ThenInclude(????)
. ...
I've also tried this...
Context
.Set<Business>()
.Include(o => o.ContactDetails.Addresses)
.Include(o => o.ContactDetails.Events)
.Include(o => o.ContactDetails.Items)
On a somewhat related note, is it possible to express what should be returned as part of an aggregate root using fluent configuration?
The ThenInclude pattern allows you to specify a path from the root to a single leaf, hence in order to specify a path to another leaf, you need to restart the process from the root by using the Include method and repeat that for each leaf.
For your sample it would be like this:
Context.Set<Person>()
.Include(o => o.ContactDetails).ThenInclude(o => o.Addresses) // ContactDetails.Addresses
.Include(o => o.ContactDetails).ThenInclude(o => o.Items) // ContactDetails.Items
.Include(o => o.ContactDetails).ThenInclude(o => o.Events) // ContactDetails.Events
...
Reference: Loading Related Data - Including multiple levels

Automapper projection (EF) with encapsulated child collections

I use Automapper to map from EF entities to view models.
I now have this entity
public class MenuGroup : IEntity
{
public int MenuGroupId { get; set; }
protected ICollection<MenuGroupItem> _menuGroupItems { get; set; }
public IEnumerable<MenuGroupItem> MenuGroupItems { get { return _menuGroupItems; } }
public void AddMenuItem(MenuGroupItem menuGroupItem)
{
_menuGroupItems.Add(menuGroupItem);
}
}
That is an encapsulated collection, I followed instructions here to make this work: http://lostechies.com/jimmybogard/2014/05/09/missing-ef-feature-workarounds-encapsulated-collections/
So I configure it like so this.HasMany(x => x.MenuGroupItems).WithRequired(x => x.BelongsTo).WillCascadeOnDelete(true);
Now the problem I get is when I try to use automapper to map my MenuGroup into a viewmodel.
I run this code: menuGroup = _context.MenuGroups.Project().To<MenuGroupEditModel>().Single(x => x.UniqueUrlFriendlyName == request.UniqueUrlFriendlyName);
and get this error: The specified type member 'MenuGroupItems' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
Now I can work with the collection, it saves correctly to the database and all is well there it's only when i want to user automapper here that it fails.
If I replace the protected ICollection and public IEnumerable with simply: public ICollection<MenuGroupItem> MenuGroupItems { get; set; } it works right away so the problem lies in automapping with my encapsulated collection.
Update: I also tried this menuGroup = _context.MenuGroups.Include(x => x.MenuGroupItems).Where(x => x.UniqueUrlFriendlyName == request.UniqueUrlFriendlyName).Project().ToSingleOrDefault<MenuGroupEditModel>(); with no difference other than that it errored in the ToSingleOrDefault instead.
Your problem is that Automapper can't modify MenuGroupItems because there is no public setter.
Your solution is changing it to this:
public IEnumerable<MenuGroupItem> MenuGroupItems { get; set; }
public void AddMenuItem(MenuGroupItem menuGroupItem)
{
MenuGroupItems.Add(menuGroupItem);
}
After some more debugging I figured out the Config file looking like this
public MenuGroupConfiguration()
{
this.HasMany(x => x.MenuGroupAssigments).WithRequired(x => x.BelongTo).WillCascadeOnDelete(true);
this.HasMany(x => x.MenuGroupItems).WithRequired(x => x.BelongsTo).WillCascadeOnDelete(true);
}
had not been included leading to that error that now makes sense.
I can add as a general tip that if you don't use auto-mapper for a query but still use your encapsulated collection remember that you have to call decompile for it to work.
like so
var menuGroupsWithType =
_context.MenuGroups.Include(x => x.MenuGroupItems).Include(x => x.MenuGroupAssigments).Where(x => x.MenuGroupAssigments.Any(y => y.AssignToAll == selectedStructureType))
.OrderBy(x => x.Name).Decompile().ToList();

Include causes OutOfMemoryException

I have a very strange problem when using Entity Framework with Code First approach.
I have disabled lazy loading and are using include statements when I need.
Unfortunately I cannot show you the code, but will try to explain as detailed I can.
I have two tables where one is referring to the other:
TableA (ID, Name)
TableB (ID, ForeignKeyTableA)
The corresponding classes:
public class A
{
public int ID { get; set; }
public string Name { get; set; }
}
public class B
{
public int ID { get; set; }
public A ClassA { get; set; }
}
I can load A and B by themselves without and problem, but when I write for example:
Include(p => p.A) when loading B and finally calling ToList() it will cause an OutOfMemoryException after a while.
The tables aren't that big. Table A have around 600 records and table B 5 records.
The mapping for class A looks like this:
this.ToTable("TableA");
this.HasKey(p => p.ID);
this.Property(p => p.ID).HasColumnName("ID");
this.Property(p => p.ID).HasColumnName("Name");
The mapping for class B looks like this:
this.ToTable("TableB");
this.HasKey(p => p.ID);
this.Property(p => p.ID).HasColumnName("ID");
HasRequired(p => p.A).WithOptional()
.Map(m => m.MapKey("ForeignKeyTableA"));
I need to map them because the column and table names aren't by convention. Include can be used on other objects without any problems.
The weirdest thing is that when I am using Include() the database will never be hit.
Anyone got suggestions? Because I am really running out of ideas now.

Representing paths in SQL Server database (ASP.NET MVC)

I am writing a small bookmarking tool in ASP.NET MVC, and each bookmark is stored in a folder. I am currently representing this by simply storing a path as a string in the database, but when it comes to beginning to display items by folder and list folders this seems like an efficient way to do it.
Is there any built-in way to represent and then manipulate paths in a database? Or an accepted best practice?
If you are talking paths like in filesystem paths, then you might want to start thinking about about a trees and a hierachical system.
In essence, you will have an additional table in a form named like Category or something, which is self referencing with a ParentId. With this you can iterate along your caegories until, there is no parent or child item, depending on the direction of your query and build a path with the result of your query.
This of course can get ugly, if your bookmarkings have more then 1 category. You will want to get some additional validation into this.
The following code is acutally from an article on the web, which I used myself, but I can't find the article again, to quote it as the source.
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public int? ParentId { get; set; }
public virtual Category Parent { get; set; }
public virtual ICollection<Category> Children { get; set; }
}
public class CategoryMapping : EntityTypeConfiguration<Category>
{
public CategoryMapping()
{
HasKey(x => x.CategoryId);
Property(x => x.CategoryId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(x => x.Name)
.IsRequired()
.HasMaxLength(255);
//Parent is optional, because root node has no parent
HasOptional(x => x.Parent)
.WithMany(x => x.Children)
.HasForeignKey(x => x.ParentId)
.WillCascadeOnDelete(false);
}
}
References:
http://sqllessons.com/categories.html
http://www.sitepoint.com/hierarchical-data-database/
Entity Framework Code First Self Referencing Parent Child with Payload

Categories

Resources