Recently, I have been working on a project in which I need to integrate EF Core. I'm trying to implement a chat application, but when I attempt to run the migration command, I encounter the following error. 

Introducing FOREIGN KEY constraint 'FK_UserChatHistory_Users_SenderUserId' on table 'UserChatHistory' 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 or index. See previous errors.

Here is my DbContext:

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace AspCoreMvcSingnalR.DatabaseEntity
{
    public class ChatDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<UserChatHistory> UserChatHistory { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Data Source=DESKTOP-MFLCOI2;Initial Catalog=ChatDb;User ID=sa;Password=adk@1234;Encrypt=false;");
        }
    }
    public class User
    {
        public Guid UserId { get; set; }
        public string FullName { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
    }
    [Table("UserChatHistory", Schema = "dbo")]
    public class UserChatHistory
    {
        public Guid Id { get; set; }
        public virtual User SenderUser { get; set; }
        public Guid SenderUserId { get; set; }
        public virtual User ReceiverUser { get; set; }

        public Guid ReceiverUserId { get; set; }
        public string Message { get; set; }
        public DateTime CreatedAt { get; set; }
    }
}

I've been grappling with this issue for a while and haven't been able to figure out what's happening. However, after a struggle, I've managed to find a generic solution to this error. Let's discuss various available solutions to this error in detail.

Specify ON DELETE NO ACTION or ON UPDATE NO ACTION: 

This means that when a related record is deleted or updated, no action will be taken on the dependent records. You can specify this behavior for the foreign key constraint causing the issue.

Open your migration file and review the constraints. Change the onDelete ReferentialAction from Cascade to NoAction.



constraints: table =>
                {
                    table.PrimaryKey("PK_UserChatHistory", x => x.Id);
                    table.ForeignKey(
                        name: "FK_UserChatHistory_Users_ReceiverUserId",
                        column: x => x.ReceiverUserId,
                        principalTable: "Users",
                        principalColumn: "UserId",
                        onDelete: ReferentialAction.NoAction);
                    table.ForeignKey(
                        name: "FK_UserChatHistory_Users_SenderUserId",
                        column: x => x.SenderUserId,
                        principalTable: "Users",
                        principalColumn: "UserId",
                        onDelete: ReferentialAction.NoAction);
                });

Configure the model for the database context

Copy the code below into your DbContext class. Afterward, delete all migration files and then recreate them. Finally, apply the migrations again.


protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            if (modelBuilder == null)
                throw new ArgumentNullException("modelBuilder");

            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                entityType.SetTableName(entityType.DisplayName());

                entityType.GetForeignKeys()
                    .Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade)
                    .ToList()
                    .ForEach(fk => fk.DeleteBehavior = DeleteBehavior.Restrict);
            }

            base.OnModelCreating(modelBuilder);
        }
Final DbContext Code:
namespace AspCoreMvcSingnalR.DatabaseEntity
{
    public class ChatDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<UserChatHistory> UserChatHistory { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Data Source=DESKTOP-MFLCOI2;Initial Catalog=ChatDb;User ID=sa;Password=adk@1234;Encrypt=false;");
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            if (modelBuilder == null)
                throw new ArgumentNullException("modelBuilder");

            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                entityType.SetTableName(entityType.DisplayName());

                entityType.GetForeignKeys()
                    .Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade)
                    .ToList()
                    .ForEach(fk => fk.DeleteBehavior = DeleteBehavior.Restrict);
            }

            base.OnModelCreating(modelBuilder);
        }
    }
    public class User
    {
        public Guid UserId { get; set; }
        public string FullName { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
    }
    [Table("UserChatHistory", Schema = "dbo")]
    public class UserChatHistory
    {
        public Guid Id { get; set; }
        public virtual User SenderUser { get; set; }
        public Guid SenderUserId { get; set; }
        public virtual User ReceiverUser { get; set; }

        public Guid ReceiverUserId { get; set; }
        public string Message { get; set; }
        public DateTime CreatedAt { get; set; }
    }

This method starts with a null check for the modelBuilder parameter to ensure it's not null. If it is null, an ArgumentNullException is thrown then iterates over each entity type in the model using modelBuilder.Model.GetEntityTypes().For each entity type, the table name is set to be the same as the display name of the entity type. This can be useful for maintaining consistency between entity names and table names in the database.

Modify Delete Behavior: 
Iterates over the foreign keys of each entity type. For foreign keys that are not part of an ownership relationship and have a delete behavior of Cascade, the delete behavior is modified to Restrict. To ensures that when a record with a foreign key relationship is deleted, it cannot cause cascading deletes of related records. Instead, it restricts the delete operation if there are dependent records, enforcing referential integrity.

This method customizes the behavior of foreign key constraints in the database context by setting the delete behavior to Restrict for foreign keys involved in relationships where cascading deletes could potentially cause issues.