Cascade delete in entity framework - c#

I am having problems setting up relationships in entity framework 6 and making cascade deletion work.
I have the following objects
public class Module {
public long Id { get; set; };
public virtual ICollection<Group> Groups { get; set; };
}
public class Group {
public long Id { get; set; };
public virtual ICollection<Field> Fields { get; set; };
}
public class Field {
public long Id { get; set; };
public virtual FieldLink Link { get; set; };
}
public class FieldLink {
public long Id { get; set; };
public virtual Module LinkToModule { get; set; };
}
Now a module has groups, a group has fields, a field MAY have a link. A link will have a LinkToModule, but this can be a different module then the one that the parent field/group belongs too.
I have setup my relationships like so
public ModuleConfig()
{
this.ToTable("Module");
}
public FieldGroupConfig()
{
this.ToTable("FieldGroup");
// relationships
this.HasRequired(e => e.Module)
.WithMany(e => e.Groups)
.HasForeignKey(e => e.ModuleId);
}
public FieldConfig()
{
this.ToTable("Field");
this.HasRequired(e => e.FieldGroup)
.WithMany(e => e.Fields)
.HasForeignKey(e => e.FieldGroupId);
this.HasOptional(e => e.Link)
.WithRequired(e => e.Field);
}
public FieldLinkConfig()
{
this.ToTable("FieldLink");
this.HasRequired(e => e.LinkToModule)
.WithMany()
.HasForeignKey(e => e.LinkToModuleId);
}
Now i am running my tests, and i get the following error
Test method ModuleServiceTests.ModuleService_DeleteAsync_ByEntity threw exception:
System.Data.SqlClient.SqlException: The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.FieldLink_dbo.Field_Id". The conflict occurred in database "TestDb", table "dbo.FieldLink", column 'Id'.
For if i check the relationship, it is between table Field.Id > FieldLink.Id and the DELETE rule is set as NO ACTION. Fine, so i guess i need to update that relationship and use WillCascadeOnDelete(true)
So i updated the code in FieldConfig from
this.HasOptional(e => e.Link)
.WithRequired(e => e.Field);
to
this.HasOptional(e => e.Link)
.WithRequired(e => e.Field)
.WillCascadeOnDelete(true);
But now when i try to run my test, the database isnt even created and i get the error saying
Initialization method Test.TestInitialize threw exception.
System.Data.SqlClient.SqlException: Introducing FOREIGN KEY constraint 'FK_dbo.FieldLink_dbo.Field_Id' on table 'FieldLink' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Can someone please help? Have i setup a relationship incorrectly, am i going the wrong way about this?

MS SQL Server does not supports cycles in the cascade deletes actions. You need to choose just one of the two directions to cascade deletes or find a workaround like in this answer (example of the trigger is here in Listing 6). This answer also contains some insights.

Related

Proper action for EF Core onDelete in followers functionality

I'm working on a practice app with .NET 6 and EF Core and I'm trying to implement a followers system. However I'm having issues with setting up the onDelete for the join-table:
This is my Profile class:
public class Profile {
public int Id {get; set;}
public string Email { get; set; }
public ICollection<ProfileFollowing> Followers { get; set; }
public ICollection<ProfileFollowing> Followings { get; set; }
}
And this Is my Join Table, which keeps records of the followings:
public class ProfileFollowing {
public int ObserverId { get; set; }
public Profile Observer { get; set; }
public int TargetId { get; set; }
public Profile Target { get; set; }
}
I'm setting this up in my model builder the following way:
modelBuilder.Entity<ProfileFollowing>(profileFollowing =>
{
profileFollowing
.HasOne(o => o.Observer)
.WithMany(p => p.Followings)
.HasForeignKey(o => o.ObserverId)
.OnDelete(DeleteBehavior.Cascade);
profileFollowing
.HasOne(t => t.Target)
.WithMany(p => p.Followers)
.HasForeignKey(t => t.TargetId)
.OnDelete(DeleteBehavior.Cascade);
profileFollowing.HasIndex(pf => new {pf.ObserverId, pf.TargetId}).IsUnique();
});
However, apparently SQL Server does not like when both on the onDelete are marked as cascades as this May Cause Cycles Or Multiple Cascade Paths. The idea of multiple cascades is that when I delete a Profile it may attempt to delete the same records in my ProfileFollowings table twice, right?
Well is there any workaround for this? I see the very same implementation (i.e. this one or a similar one), however nobody mentions the issues with deleting the records. I want to be sure that if I delete a Profile, the ProfileFollowings will update correctly and all of the other Profiles will have their followers/followings adjusted.
PS: I know one workaround would be to set one of the FK (TargetId/ObserverId) to null and set onDelete action to be SetNull, but this way my unique index on ObserverId & TargetId becomes useless.

EF Core multiple references to same table with circullar reference

I have looked through related posts but I still can not figure out what should be the right solution for my issue.
I am creating database structure for user messaging application.
My idea for the schema was as follows:
User has many Conversations and Messages
Conversation has 2 Users and many Messages
Message has 1 User and 1 Conversation
I have 3 tables:
public class UserEntity : IEntity
{
public Guid Id { get; set; }
public ICollection<ConversationEntity> Conversations { get; set; }
public ICollection<MessageEntity> Messages { get; set; }
}
public class MessageEntity : IEntity
{
public Guid Id { get; set; }
public string Content { get; set; }
public Guid UserId { get; set; }
public UserEntity User { get; set; }
public Guid ConversationId { get; set; }
public ConversationEntity Conversation { get; set; }
}
public class ConversationEntity : IEntity
{
public Guid Id { get; set; }
public Guid UserOneId { get; set; }
public UserEntity UserOne { get; set; }
public Guid UserTwoId { get; set; }
public UserEntity UserTwo { get; set; }
public ICollection<MessageEntity> Messages { get; set; }
}
And this is the way how I am configuring those tables:
public void Configure(EntityTypeBuilder<UserEntity> builder)
{
builder.ToTable("Users");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedOnAdd();
}
public void Configure(EntityTypeBuilder<MessageEntity> builder)
{
builder.ToTable("Messages");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedOnAdd();
builder.HasOne(x => x.Conversation)
.WithMany(x => x.Messages)
.HasForeignKey(x => x.ConversationId);
builder.HasOne(x => x.User)
.WithMany(x => x.Messages)
.HasForeignKey(x => x.UserId);
}
public void Configure(EntityTypeBuilder<ConversationEntity> builder)
{
builder.ToTable("Conversations");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedOnAdd();
builder.HasOne(x => x.UserOne)
.WithMany()
.HasForeignKey(x => x.UserOneId)
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(x => x.UserTwo)
.WithMany()
.HasForeignKey(x => x.UserTwoId)
.OnDelete(DeleteBehavior.NoAction);
}
Now I have two issues:
Introducing FOREIGN KEY constraint 'FK_Conversations_Users_UserTwoId' on table 'Conversations' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
How can I rollback the applied changes? Some of the has been applied to database (I have checked and the conversation table has been generated) and the relations also has been created in a very weird way:
Can I please receive help how to rollback this? I tried applying earlier migration but this did not work (Introducing FOREIGN KEY constraint 'FK_Conversations_Users_UserTwoId' on table 'Conversations' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.)
[EDIT]:
I have tested the solution and I have recreated the whole database, what is interesting this error is not happening any longer, however in the Keys folder of Conversations table i Can see this:
Why there is this "FK_Conversations_Users_UserEntityId" ??
You can simplify the relationship between the users and the conversations by having it as a regular many-to-many relationship. The downside is that you need to inforce the rule that every conversation must have two users.
In order to try to minimize the change, properties can be written to replace the old UserOne and UserTwo properties:
public UserEntity UserOne
{
get => Users[0];
set => Users[0] = value;
}
(Users is the new collection navigation property added to ConversationEntity)
For the second point if you are on .NET Frameework 4.x open Tools -> Nuget Package Manager -> Package Manager Console under project tab select the project that contains the migrations folder and in the command line type Update-Database -TargetMigration:"NameOfMigrationYouWantToRollbackTo" -Force. If you use ASP.NET CORE/.NET Core open Tools -> Nuget Package Manager -> Package Manager Console under the project tab select the project that contains the migrations folder and type Remove-Migration to remove the last migration and after that Update-Database to rollback the database. If you want to remove two or more migrations just repeat the process.

Mapping foreign key in HasOptional().WithOptionalDependent() relation in Entity Framework 6

I have the following data-model in Entity Framework 6.1.3:
using System.Data.Entity;
public class Student
{
public int Id { get; set; }
public virtual Contact Contact { get; set; }
}
public class Contact
{
public int Id { get; set; }
public virtual Student Student { get; set; }
}
public class MyContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder builder)
{
builder.Entity<Contact>()
.HasOptional(x => x.Student)
.WithOptionalDependent(x => x.Contact)
.WillCascadeOnDelete(true);
}
}
public static class Program
{
private static void Main()
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
using (var context = new MyContext())
context.Database.Initialize(force: true);
}
}
When I launch this code, I get exactly the right table structure I am aiming for:
dbo.Contacts
Id (PK)
Student_Id (FK, NULL, CASCADE ON DELETE)
dbo.Students
Id (PK)
However, now I would like to add the Student_Id property to be available in the Contact entity. So I can read the Student_Id without needing to join the other table through .Student.Id navigation.
If I add the property to the Contact entity, I end up either with two columns Student_Id and Student_Id1, or I end up with an error message saying Each property name in a type must be unique..
The column is already in the database, all I need is to have it in the entity as well, why is it so much trouble? Is there a solution?
I managed to get a response from the Entity Framework Program Manager after asking on GitHub.
Unfortunately this is a limitation of EF6. You can not have a foreign key property in a one-to-one relationship, unless it is also the primary key property. This is essentially because EF6 doesn't support alternate keys/unique indexes, so you can't enforce that a non-primary key property is unique. The fact that you can do it when the foreign key property isn't in the entity is a bit of a quirk... but obviously not something we would remove 😄.
BTW alternate keys (and therefore this scenario) is supported in EF Core.
– Rowan Miller #
https://github.com/aspnet/EntityFramework6/issues/159#issuecomment-274889438
If you want to declare the FK property in the dependent entity in an one to one relationship, I'm afraid you must use it as a PK too. EF Code First requires that PK of the dependent entity must be FK of the relationship too:
public class Contact
{
[Key,ForeignKey("Student")]
public int StudentId { get; set; }
public virtual Student Student { get; set; }
}
But I think this is not what you are looking for. So, I think you have three options here:
You preserve your current relationship configuration.
Create an authentic one to one relationship.
Create an one to many relationship
By my experience the last one is the most adjusted to what are you trying to achieve (but that is my opinion). In this case you can work with the Fk property as you want, the only is you need to change the Contact navigation property on Student by a collection (or omit this nav. property and create an unidirectional relationship):
public class Student
{
public int Id { get; set; }
public virtual ICollection<Contact> Contacts { get; set; }
}
The configuration would be this way:
builder.Entity<Contact>()
.HasOptional(x => x.Student)
.WithMany(x => x.Contacts)
.HasForeignKey(x => x.StudentId)
.WillCascadeOnDelete(true);
Update
A fourth option could be create two unidirectional relationships:
builder.Entity<Contact>()
.HasOptional(x => x.Student)
.WithMany()
.HasForeignKey(x => x.StudentId)
.WillCascadeOnDelete(true);
builder.Entity<Student>()
.HasOptional(x => x.Contact)
.WithMany()
.HasForeignKey(x => x.ContactId)
.WillCascadeOnDelete(true);
But this option breaks the real relation between the two tables.

Entity Framework referencing columns in foreign key differs from number of referenced columns

I'm using Entity Framework code first, and I'm trying to get my migrations/database working (using the -update-database command), but I keep getting this error:
System.Data.SqlClient.SqlException (0x80131904): Number of referencing columns in foreign key differs from number of referenced columns, table 'dbo.FileSections'.
Which happens directly after this SQL Command (which EF is magic'ing up):
ALTER TABLE [dbo].[FileSections] ADD CONSTRAINT [FK_dbo.FileSections_dbo.Sections_Section_Id] FOREIGN KEY ([Section_Id]) REFERENCES [dbo].[Sections] ([Id], [EventName]) ON DELETE CASCADE
The simplified version of my issue is that an Event (Primary Key of string "Name") can have many Sections, and a Section (Primary Key of integer "Id") can have many Files (File with a Primary Key of string "Id"). The simplified classes are:
public class Event {
public string Name { get; set; }
public List<Section> Sections { get; set; }
}
public class Section {
public int Id { get; set; }
public string EventName { get; set; }
public string Title { get; set; }
public List<File> Files { get; set; }
}
public class File {
public string Id { get; set; }
public int SectionId { get; set; }
public string FileName { get; set; }
}
I'm also using code first, and the setup for this is:
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
modelBuilder.Entity<Event>()
.HasKey(X => X.Name);
modelBuilder.Entity<Event>()
.HasMany(X => X.Sections)
.WithRequired(X => X.Event)
.WillCascadeOnDelete();
modelBuilder.Entity<Section>()
.HasKey(x => x.Id);
modelBuilder.Entity<Section>()
.HasRequired(x => x.Event)
.WithMany(x => x.Sections)
.HasForeignKey(x => x.EventName);
modelBuilder.Entity<Section>()
.HasMany(x => x.Files)
.WithRequired(x => x.Section)
.HasForeignKey(x => x.SectionId)
.WillCascadeOnDelete();
modelBuilder.Entity<File>()
.HasKey(x => x.Id);
modelBuilder.Entity<File>()
.HasRequired(x => x.Section)
.WithMany(x => x.Files)
.HasForeignKey(x => x.SectionId);
}
What am I doing wrong? I've also tried using composite primary keys (and therefore composite foreign keys), but this still gives me the same error. I really am at my wits end, surely it can't be this hard to have an entity containing entities that contain entities.
It turns out that Visual Studio 2013 was the issue - it was trying to apply the initial migration, not the actual latest one (despite the initial migration already being applied). I used this answer to fix the migration issues with VS.

DbContext: Dependent property / ReferentialConstraint Exception when using Composite Key

I'm using EntityFramework via DbContext and an Exisiting Database.
When I Add an Order entity to my context and call SaveChanges(), I'm encountering an exception of: "A dependent property in a ReferentialConstraint is mapped to a store-generated column. Column: OrderId".
I believe this is happening because of the composite key on my OrderAddress table and I'm hoping there is a way around it...I don't want to create an IDENTITY on that table.
Here are my entities, simplified...
// OrderId is an IDENTITY PK
public class Order
{
public int OrderId { get; set; }
public IList<OrderAddress> Addresses { get; set; }
public int Total { get; set; }
}
// PK on this table is composite key of OrderId and OrderAddressTypeId
public class OrderAddress
{
public int OrderId { get; set; }
public int OrderAddressTypeId { get; set; }
public string Address { get; set; }
}
Here is my Context, simplified...
public class StoreContext : DbContext
{
DbSet<Order> Orders { get; set; }
DbSet<OrderAddress> OrderAddresses { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Set the Identity for Order
modelBuilder.Entity<Order>()
.Property(x => x.OrderId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// Set composite key for Order Address
modelBuilder.Entity<OrderAddress>()
.HasKey(x => new { x.OrderId, x.OrderAddressTypeId });
}
}
NOTE: I've looked at the other SO questions that are similar and tried the solutions listed with no success. I've verified that my foreign keys are setup correctly in the database. What's different about my question is the use of the composite key.
Thanks in advance for the help.
UPDATE:
This ended up not being related to the composite key at all. There was an additional line in my Context OnModelCreating method that required a child entity, OrderSummary, which is based on a View in my database. The line looked like this...
modelBuilder.Entity<OrderSummary>().HasRequired(x => x.Order).WithRequiredPrincipal(x => x.OrderSummary);
I had never intended for OrderSummary to be a required principal of Order. Changing it to the following fixed the problem...
modelBuilder.Entity<OrderSummary>().HasRequired(x => x.Order);
Unfortunately, the error message from EF was not very specific and lead me on a wild good chase.
Thanks for looking.
This error says that some OrderId property (the exception should contain information about the entity or relation where this happens) is mapped as store generated = it has DatabaseGeneratedOption set to Identity or Computed. If the issue is related to OrderAddress entity try to add this to your mapping definition:
modelBuilder.Entity<OrderAddress>()
.Property(x => x.OrderId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

Categories