Circular dependency on delete (EF core) - c#

I have two classes in my DB that reference each other, like in the example below.
Parent can have any number of Child objects, and I set a foreign key constraint to have Child.ParentID reference Parent.ID; setting DeleteBehavior.Cascade for the relation ensures that when a Parent is deleted all Child objects are deleted as well.
The problem is that I also need a reference to one of the Child objects in the Parent class, called PreferredChild in the example below. I was expecting that creating a constraint between Parent.PreferredChildId and Child.ID would work if I set DeleteBehavior.SetNull, but what actually happens is that when I delete a Parent object, if PreferredChildID is set, I get this exception:
System.InvalidOperationException: 'Unable to save changes because a circular dependency was detected in the data to be saved: 'Parent [Deleted] PreferredChild PreferredParent { 'PreferredChildID' } <- Child [Deleted] Parent Children { 'ParentID' } <- Parent [Deleted]'.'
Is there a way to model this so that I can delete a Parent object without unsetting the PreferredChildID first?
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
public class Parent {
public int? ID { get; set; }
public int? PreferredChildID { get; set; }
public virtual Child PreferredChild { get; set; }
public virtual ICollection<Child> Children { get; set; }
}
public class Child {
public int? ID { get; set; }
public int? ParentID { get; set; }
public virtual Parent Parent { get; set; }
public virtual Parent PreferringParent { get; set; }
}
public class TestContext : DbContext {
public DbSet<Parent> Parents { get; set; }
public DbSet<Child> Children { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) {
options.UseSqlite("Data Source=test.db");
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
// I would like to configure the model so that when a Parent is deleted,
// all Children are deleted. Using SetNull on PreferredChildID to avoid object
// being deleted twice
modelBuilder
.Entity<Child>()
.HasOne(c => c.PreferringParent)
.WithOne(p => p.PreferredChild)
.HasForeignKey<Parent>(p => p.PreferredChildID)
.OnDelete(DeleteBehavior.SetNull);
modelBuilder
.Entity<Child>()
.HasOne(c => c.Parent)
.WithMany(p => p.Children)
.HasForeignKey(c => c.ParentID)
.OnDelete(DeleteBehavior.Cascade);
}
}
class Program {
static void Main(string[] args) {
using (var context = new TestContext()) {
var parent = new Parent();
context.Parents.Add(parent);
context.SaveChanges();
var child1 = new Child { ParentID = parent.ID };
var child2 = new Child { ParentID = parent.ID };
context.Children.AddRange(child1, child2);
context.SaveChanges();
parent.PreferredChildID = child2.ID;
context.SaveChanges();
// This explodes
context.Parents.Remove(parent);
context.SaveChanges();
}
}
}

Why not just do the following?
public class Parent
{
public int? ID { get; set; }
public int? LastChildID => LastChild?.ID;
public virtual Child LastChild => Children?.LastOrDefault();
public virtual ICollection<Child> Children { get; set; }
}
Edit: After OP has edited his question, it's more clear what he wants.
I suggest adding a third table like this
CREATE TABLE [dbo].[PreferredChilds] (
[ParentId] INT NOT NULL,
[PreferredChild] INT NOT NULL,
PRIMARY KEY CLUSTERED ([ParentId] ASC, [PreferredChild] ASC),
FOREIGN KEY ([ParentId]) REFERENCES [dbo].[Parents] ([Id]),
FOREIGN KEY ([PreferredChild]) REFERENCES [dbo].[Children] ([Id])
);
This way you don't have any problem with circular dependencies.
If you delete the parent, the entry in the preferredchilds and children table will also gets deleted.

Related

How to delete a foreign key record with Entity Framework?

I've got two models - Parent and Child. When I delete the Parent, I was hoping the Child would get deleted as it has a ForeignKey attribute however that's not the case. I could add logic within the Parent repository's Delete method as shown in the commented out code below but I was wondering if that's required of if there's a simpler way to go about this?
public record Parent
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
}
public record Child
{
[Key]
public Guid Id { get; set; }
[ForeignKey("Parent")]
public Guid ParentId { get; set; }
public string Name { get; set;}
}
public void Delete(Guid id)
{
/*
var children = _dbContext.Childs.Where(c => c.ParentId == id);
if (children != null)
{
foreach (var child in children)
{
_dbContext.Childs.Remove(child);
}
}
*/
var parent = _dbContext.Parents.FirstOrDefault(p => p.Id == id);
if (parent != null)
{
_dbContext.Parents.Remove(parent);
}
_dbContext.SaveChanges();
}
Include the Child when querying the parent as the following:
var parent = _dbContext.Parents
.include(p => p.Child)
.FirstOrDefault(p => p.Id == id);
Then call remove as you already doing.
Also, you may want to consider the OnDelete behavior to define what happens to Child when you delete the Parent. If you want the Child to be deleted when the parent is deleted use "Cascade" delete behavior. This can be done at database level in Child table definition similar to the following:
CONSTRAINT [FK_Childs_Parents_ParentId] FOREIGN KEY ([ParentId]) REFERENCES [Parent] ON DELETE CASCADE
or at EF context level as the following:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Parent>()
.HasMany(e => e.Childs)
.WithOne(e => e.Parent)
.OnDelete(DeleteBehavior.ClientCascade);
}
Further related details from official Microsoft documentation here
If the model in the databaseContext snapshot has ".OnDelete(DeleteBehavior.Cascade)" you could just include the table and the child data will be removed.
var parent = _dbContext.Parents.Include(parent => parent.Children).FirstOrDefault(p => p.Id == id);
For this to work you should also make a list of Children in the parent model:
public record Parent
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public IEnumerable<Child> Children { get; set; }
}
This is the link to the Microsoft documentation

How to properly setup ApplicationUser and FriendRequest entities to allow for cascade deleting

I have the traditional ApplicationUser (IdentityUser), and that user can send a friend request to another ApplicationUser. I currently have the following general entity classes:
public class ApplicationUser : IdentityUser
{
public virtual List<DeviceToken> DeviceTokens { get; set; } = new List<DeviceToken>();
public string DisplayName { get; set; }
}
public class FriendRequest
{
public int Id { get; set; }
public DateTime DateRequested { get; set; }
public ApplicationUser Requester { get; set; }
public ApplicationUser Receiver { get; set; }
}
I have ran database-update etc and this is working fine. However when I go into my SQLServer to try to delete an ApplicationUser, it tells me that The DELETE statement conflicted with the REFERENCE constraint "FK_FriendRequest_AspNetUsers_RequesterId".
So I have decided to implement a cascade delete flow from the ApplicationUser to the friend requests that they are part of.
I have tried the resource on here by Microsoft on configuring cascade delete but I cannot figure out how to apply it to my case:
builder.Entity<ApplicationUser>()
.HasMany(e => e.FriendRequests)//No such property, no idea how to address
.OnDelete(DeleteBehavior.ClientCascade);
How do I set up this cascade delete scenario?
Also how do I add a property to ApplicationUser that refers to all the FriendRequests they are part of, and make sure EFCore knows I am referring to that existing FriendRequest entity/table?
Update
Following the suggested approach of adding a virtual property to ApplicationUser, would this be way forward:
public class ApplicationUser : IdentityUser
{
public virtual List<DeviceToken> DeviceTokens { get; set; } = new List<DeviceToken>();
public string DisplayName { get; set; }
public ICollection<FriendRequest> FriendRequests { get; }
}
builder.Entity<ApplicationUser>()
.HasMany(u => u.FriendRequests)
.WithOne(u => u.Requester)
.OnDelete(DeleteBehavior.ClientCascade); //not sure about this
builder.Entity<ApplicationUser>()
.HasMany(u => u.FriendRequests)
.WithOne(u => u.Requester)
.OnDelete(DeleteBehavior.ClientCascade); //not sure about this
Your ApplicationUser needs 2 virtual ICollections.
public class ApplicationUser
{
public int Id { get; set; }
public string DisplayName { get; set; }
public virtual ICollection<FriendRequest> FriendRequestsAsRequestor { get; set; }
public virtual ICollection<FriendRequest> FriendRequestsAsReceiver { get; set; }
}
public class FriendRequest
{
public int Id { get; set; }
public DateTime DateRequested { get; set; }
public int RequestorId { get; set; }
public ApplicationUser Requestor { get; set; }
public int ReceiverId { get; set; }
public ApplicationUser Receiver { get; set; }
}
public class ApplicationUserConfig : IEntityTypeConfiguration<ApplicationUser>
{
public void Configure(EntityTypeBuilder<ApplicationUser> builder)
{
builder.HasMany(au => au.FriendRequestsAsRequestor)
.WithOne(fr => fr.Requestor)
.HasForeignKey(fr => fr.RequestorId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(au => au.FriendRequestsAsReceiver)
.WithOne(fr => fr.Receiver)
.HasForeignKey(fr => fr.ReceiverId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Use:
void AddFriendRequest(int requestorId, int receiverId)
{
var ctxt = new DbContext();
FriendRequest fr = new FriendRequest
{
RequestorId = requestorId;
ReceiverId = receiverId;
DateRequested = DateTime.Now;
}
ctxt.FriendRequests.Add(fr);
ctxt.SaveChanges();
}
List<FriendRequest> GetFriendRequests()
{
var ctxt = new DbContext();
return ctxt.FriendRequests
.Include(fr => fr.Requestor)
.Include(fr => fr.Receiver)
.ToList();
}
ApplicationUser GetUserWithFriendRequests(int id)
{
var ctxt = new DbContext();
return ctxt.ApplicationUser
.Include(au => au.FriendRequestsAsRequestor)
.Include(au => au.FriendRequestsAsReceiver)
.SingleOrDefault(au => au.Id == id);
}
I have tried the resource on here by Microsoft on configuring cascade delete but I cannot figure out how to apply it to my case:
builder.Entity<ApplicationUser>()
.HasMany(e => e.FriendRequests)//No such property, no idea how to address
.OnDelete(DeleteBehavior.ClientCascade);
From the doc of DeleteBehavior :
ClientCascade : For entities being tracked by the DbContext, dependent entities will be deleted when the related principal is deleted. If the database has been created from the model using Entity Framework Migrations or the EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated.
In this case, it's the client (the .NET app) and not the DB that ensure the cascade delete. If the client fail to do the cascade delete (related entity not tracked), the db will generate the error you see.
Maybe the DeleteBehavior.Cascade is more appropriate to your code first scenario :
Cascade : For entities being tracked by the DbContext, dependent entities will be deleted when the related principal is deleted. If the database has been created from the model using Entity Framework Migrations or the EnsureCreated() method, then the behavior in the database is the same as is described above for tracked entities. Keep in mind that some databases cannot easily support this behavior, especially if there are cycles in relationships, in which case it may be better to use ClientCascade which will allow EF to perform cascade deletes on loaded entities even if the database does not support this. This is the default for required relationships. That is, for relationships that have non-nullable foreign keys.
If you try this, you go with this SQL script migration (I assume the SGBDR is SQL Server) :
CREATE TABLE [ApplicationUser] (
[Id] int NOT NULL IDENTITY,
[DisplayName] nvarchar(max) NULL,
CONSTRAINT [PK_ApplicationUser] PRIMARY KEY ([Id])
);
GO
CREATE TABLE [FriendRequests] (
[Id] int NOT NULL IDENTITY,
[DateRequested] datetime2 NOT NULL,
[RequesterId] int NULL,
[ReceiverId] int NULL,
CONSTRAINT [PK_FriendRequests] PRIMARY KEY ([Id]),
CONSTRAINT [FK_FriendRequests_ApplicationUser_ReceiverId] FOREIGN KEY ([ReceiverId]) REFERENCES [ApplicationUser] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_FriendRequests_ApplicationUser_RequesterId] FOREIGN KEY ([RequesterId]) REFERENCES [ApplicationUser] ([Id]) ON DELETE CASCADE
);
GO
And when it's apply, this produce this error :
Introducing FOREIGN KEY constraint 'FK_FriendRequests_ApplicationUser_RequesterId' on table 'FriendRequests' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
First time I see this error, then I will refer to this question with #onedaywhen's answer :
SQL Server does simple counting of cascade paths and, rather than trying to work out whether any cycles actually exist, it assumes the worst and refuses to create the referential actions (CASCADE)...
A no perfect solution is to use DeleteBehavior.Cascade and ensure all related entities are tracked before the delete :
public class ApplicationUser
{
public int Id { get; set; }
public string DisplayName { get; set; }
public ICollection<FriendRequest> RequestedRequests { get; set; }
public ICollection<FriendRequest> RecevedRequests { get; set; }
}
public class FriendRequest
{
public int Id { get; set; }
public DateTime DateRequested { get; set; }
public ApplicationUser Requester { get; set; }
public ApplicationUser Receiver { get; set; }
}
public class MyDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlServer("***");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<FriendRequest>()
.HasOne(r => r.Requester)
.WithMany(u => u.RequestedRequests)
.OnDelete(DeleteBehavior.ClientCascade);
modelBuilder.Entity<FriendRequest>()
.HasOne(r => r.Receiver)
.WithMany(u => u.RecevedRequests)
.OnDelete(DeleteBehavior.ClientCascade);
}
public DbSet<ApplicationUser> Users { get; set; }
public DbSet<FriendRequest> FriendRequests { get; set; }
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
PrepareUserToDeleting();
return base.SaveChangesAsync();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
PrepareUserToDeleting();
return base.SaveChanges();
}
private void PrepareUserToDeleting()
{
// For each deleted user entity
foreach(var entry in ChangeTracker.Entries<ApplicationUser>().Where(e => e.State == EntityState.Deleted))
{
var user = entry.Entity;
// If RecevedRequests isn't loaded
if (user.RecevedRequests == null)
{
//Then load RecevedRequests
entry.Collection(u => u.RecevedRequests).Load();
}
// Idem with RequestedRequests
if (user.RequestedRequests == null)
{
entry.Collection(u => u.RequestedRequests).Load();
}
}
}
}

Parent/Child with EF6 child collection always null

When ever I pull a MyList object via EF, the parent is associated correctly but the Children collection is always null. Not sure what I'm doing wrong, pretty much every article shows to do it this way.
Database
CREATE TABLE [dbo].[MyList] (
[MyListId] BIGINT IDENTITY (1, 1) NOT NULL,
[ParentMyListId] BIGINT NULL,
CONSTRAINT [PK_MyList] PRIMARY KEY CLUSTERED ([MyListId] ASC) WITH (FILLFACTOR = 90),
CONSTRAINT [FK_MyList_MyList_MyListId] FOREIGN KEY (ParentMyListId) REFERENCES MyList(MyListId)
);
Model
public class MyList
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long MyListId { get; set; }
public long? ParentMyListId { get; set; }
[ForeignKey("ParentMyListId")]
public virtual List MyListParent { get; set; }
public virtual ICollection<MyList> MyListChildren { get; set; }
}
DBContext
public class MyContext : DbContext
{
public MyContext() : base(Properties.Settings.Default.DbContext)
{
Configuration.LazyLoadingEnabled = false;
}
public DbSet<MyList> MyLists { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<MyList>()
.ToTable("MyList", "dbo")
.HasOptional(x => x.MyListParent)
.WithMany(x => x.MyListChildren)
.HasForeignKey(x => x.ParentMyListId)
.WillCascadeOnDelete(false);
}
base.OnModelCreating(modelBuilder);
}
I tried with same structure in EF 6.1.3 version and it worked like charm. I added image of output and data present in db. The only thing that might stopped working if you disable loading in configuration. I hope it work for you please try my sample code.
// Your entity class I added name property to show you the results
public class MyList
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long MyListId { get; set; }
public long? ParentMyListId { get; set; }
public string Name { get; set; }
[ForeignKey("ParentMyListId")]
public virtual MyList MyListParent { get; set; }
public virtual ICollection<MyList> MyListChildren { get; set; }
}
// DBContext please note no configuration properties set just default constructor
// you need t check here if you have set soemthing here
public class TestContext : DbContext
{
public TestContext()
: base("name=TestConnection")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<MyList>()
.ToTable("MyList", "dbo")
.HasOptional(x => x.MyListParent)
.WithMany(x => x.MyListChildren)
.HasForeignKey(x => x.ParentMyListId)
.WillCascadeOnDelete(false);
}
public virtual DbSet<MyList> Lists { get; set; }
}
The console app to show result:
static void Main(string[] args)
{
using (var ctx = new TestContext())
{
// for testing to see al working
//this is important to read the entity first .
var parent = ctx.Lists.ToList();
foreach (var p in parent)
{
foreach (var child in p.MyListChildren)
{
Console.WriteLine(string.Format(#"Parent Name {0} has child with name {1}", p.Name, child.Name));
}
}
}
Console.ReadLine();
}
Output of app and data in database ...

Entity Framework One To Many Relationship mapping foreignkey causing null on navigation properties

I'm trying to build relationship while wanted to specify child and parent tables foreign key in addition to navigation property, model looks like this
public class Parent
{
public Parent()
{
this.Childern = new HashSet<Child>();
}
public Guid Id { get; set; }
public string Name { get; set; }
//public Guid Child_Id { get; set; }
public ICollection<Child> Childern { get; set; }
}
public class Child
{
public Child()
{
this.Parent = new Parent();
}
public Guid Id { get; set; }
public string Name { get; set; }
public Guid Parent_Id { get; set; }
public Parent Parent { get; set; }
}
With only above model when i build and run project, i get expected results on navigation properties as navigation properties are populated with expected data.
But i get additional un-wanted column in database as below
Id
Name
Parent_Id
Parent_Id1 (FK)
I further configured relationship in OnModelCreating like below
modelBuilder.Entity<Child>()
.HasRequired(p => p.Parent)
.WithMany(p => p.Childern)
.HasForeignKey(k => k.Parent_Id);
This time I got desired result on table structure now table looks like this
Id
Name
Parent_Id (FK)
But I'm getting null on navigation properties, please note that I'm trying Eagerly Loading
public static void TestParent()
{
using (var context = new ILDBContext())
{
var parents = context.Parents
.Include(p => p.Childern)
.ToList();
foreach (var parent in parents)
{
Console.WriteLine("Parent: {0} Childern: {1}", parent.Name, parent.Childern.Count());
foreach (var child in parent.Childern)
{
Console.WriteLine("Child: {0}", child.Name);
}
}
}
}
In addition I will be thankful if anyone can advise how should i configure relationship if i need FK in both models like, Parent_Id and Child_Id
Thanks.
You need to remove the Parent assignment in your Child constructor:
public Child()
{
this.Parent = new Parent();
}
has to be
public Child() {}
or you don't define it at all. For the ICollection<Child> in your Parent it doesn't really make a difference.

Fluent NHibernate: ManyToMany Self-referencing mapping

I need help in creating the correct fluent nh mapping for this kind of scenario:
A category can be a child of one or more categories. Thus, resulting to this entity:
public class Category : Entity, IAggregateRoot
{
[EntitySignature]
public virtual string Name { get; set; }
public virtual IList<Category> Parents { get; set; }
public virtual IList<Category> Children { get; set; }
public virtual IList<ProductCategory> Products { get; set; }
public Category()
{
Parents = new List<Category>();
Children = new List<Category>();
Products = new List<ProductCategory>();
}
public virtual void AddCategoryAsParent(Category parent)
{
if (parent != this && !parent.Parents.Contains(this) && !Parents.Contains(parent))
{
Parents.Add(parent);
parent.AddCategoryAsChild(this);
}
}
public virtual void RemoveCategoryAsParent(Category parent)
{
if (Parents.Contains(parent))
{
Parents.Remove(parent);
parent.RemoveCategoryAsChild(this);
}
}
public virtual void AddCategoryAsChild(Category child)
{
if(child != this && !child.Children.Contains(this) && !Children.Contains(child))
{
Children.Add(child);
child.AddCategoryAsParent(this);
}
}
public virtual void RemoveCategoryAsChild(Category child)
{
if(Children.Contains(child))
{
Children.Remove(child);
child.RemoveCategoryAsParent(this);
}
}
}
My initial mapping is this:
public class CategoryMap : ClassMap<Category>
{
public CategoryMap()
{
Id(p => p.Id).GeneratedBy.Identity();
HasManyToMany(x => x.Parents)
.Table("CategoryParents")
.ParentKeyColumn("CategoryId")
.ChildKeyColumn("ParentCategoryId")
.Cascade.SaveUpdate()
.LazyLoad()
.AsBag();
HasManyToMany(x => x.Children)
.Table("CategoryParents")
.ParentKeyColumn("ParentCategoryId")
.ChildKeyColumn("CategoryId")
.Cascade.SaveUpdate()
.Inverse()
.LazyLoad()
.AsBag();
}
}
The problem with this mapping is whenever I remove a category as a parent or as a child of another category, the resulting SQL statement is this:
NHibernate: DELETE FROM CategoryParents WHERE CategoryId = #p0;#p0 = 2
NHibernate: INSERT INTO CategoryParents (CategoryId, ParentCategoryId) VALUES (#p0, #p1);#p0 = 2, #p1 = 3
It deletes all the mapping first, then insert the remaining mapping. The proper way is just delete the category parent mapping which this kind of statement:
DELETE FROM CategoryParents WHERE CategoryId = #p0 AND ParentCategoryId = #p1;#p0 = 2, #p1=1
Any ideas?
after looking at your mappings I think you are wanting to change your Cascade options. Here is an article which outlines a parent - child relationship between entities. Although it comes from the point of view of having orphaned entities I think you will find the blog helpful. Good luck...
http://ayende.com/Blog/archive/2006/12/02/nhibernatecascadesthedifferentbetweenallalldeleteorphansandsaveupdate.aspx

Categories

Resources