Search code examples
c#code-firstasp.net-core-2.2entity-framework-core-2.2

Code First: EntityFramework - Migration Error on One-To-Many relation


It's a .NET Core app 2.2. With EF Core. Code First. I am kinda junior experimenting with a bit more heavier project for my own learning.

To simplify the model structure see: model-structure. Basically there is an abstract default user - which is inherited by Doctor and Patient.

Both Doctor and Patient have many Appointments. Doctor with many Patients, Patient with many Doctors. (or with the same Pat/Doc). The reference to Appointments in both class looks like:

public virtual ICollection<Appointment> Appointments { get; set; }

In the Appointment both Doctor and Patient are Required. As no Appointment is expected to happen without any of them.

[Required]
    public Doctor Doctor { get; set; }

[Required]
    public Patient Patient { get; set; }

When they align on certain meeting the timing, location etc are created. Yet the Appointment shall hold a reference for Doctor and Patient as well.

When certain user is deleted I need the related Appointments to be deleted as well. But not the related other-user, location, etc.

Add-Migration is created. Error occurs when I want to Update-Database.

The part of the migration file. What is weird that DoctorID is added as FK once with Restrict and once with Cascade. Question 1) Not sure if this is correct and if I can touch it directly?

migrationBuilder.CreateTable(
name: "Appointment",
columns: table => new
{
    Id = table.Column<Guid>(nullable: false),
    LocationId = table.Column<Guid>(nullable: false),
    CreatedDate = table.Column<DateTime>(nullable: false),
    StartTime = table.Column<DateTime>(nullable: false),
    Duration = table.Column<TimeSpan>(nullable: false),
    FollowUpAppointmentId = table.Column<Guid>(nullable: true),
    DoctorId = table.Column<Guid>(nullable: true)
},
constraints: table =>
{
    table.PrimaryKey("PK_Appointment", x => x.Id);
    table.ForeignKey(
        name: "FK_Appointment_Doctors_DoctorId",
        column: x => x.DoctorId,
        principalTable: "Doctors",
        principalColumn: "Id",
        onDelete: ReferentialAction.Restrict);
    table.ForeignKey(
        name: "FK_Appointment_Doctors_Id",
        column: x => x.Id,
        principalTable: "Doctors",
        principalColumn: "Id",
        onDelete: ReferentialAction.Cascade);
    table.ForeignKey(
        name: "FK_Appointment_Patients_Id",
        column: x => x.Id,
        principalTable: "Patients",
        principalColumn: "Id",
        onDelete: ReferentialAction.Cascade);
}};

In the ApplicationDbContext I used Fluent API to configure relations.

builder.Entity<Doctor>()
    .HasMany<Appointment>(ap => ap.Appointments)
    .WithOne(d => d.Doctor)
    .HasForeignKey(fk => fk.Id)
    .OnDelete(DeleteBehavior.Cascade);

builder.Entity<Patient>()
    .HasMany<Appointment>(ap => ap.Appointments)
    .WithOne(p => p.Patient)
    .HasForeignKey(fk => fk.Id)
    .OnDelete(DeleteBehavior.Cascade);

The error message refers to an issue to cycling references but I can't see it to be honest - unless in case of ie.: Doctor is deleted --> triggers deletion of Appointment --> triggering deletion of Patient (which is clearly not the intent)

Error: Introducing FOREIGN KEY constraint 'FK_Appointment_Patients_Id' on table 'Appointment' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.

I am unsure if this is a design issue or a coding issue.

I tried to get some help from MS Docs, from EFCore.com, browsed several SO posts yet I fail to get it narrowed down..

Steps I have made in the past five days or so..:

  1. I have tried to remove the Required tag from Doctor and Patient in Appointment class. It results in :

Unable to determine the relationship represented by navigation property 'Doctor.Appointments' of type 'ICollection'

  1. Removed the navigation property in Doctor/Patient classes solved the issue yet this is clearly not the intent. If I understand it would mean that I need to scan each record in Appointment table and filter for Doctor equals for certain Doctor.

  2. I tried to add a new foreign key to Appointment class with DoctorId and PatientId, but it seems that it is done automatically by the migration (in the migration file the FK key is created accordingly). Anyway it resulted in the same error.

    public Guid DoctorId { get; set; } public Guid PatientId { get; set; }

Question 2: Might be dumb but my understanding is that Appointment has a one-to-many relationship with Doctor and Patient, so not sure if a Join Table class would help? But in that case how cascading deletion works?

ie.: I create a ComboDocApp class, which has reference for one Doctor and one Appointment. And Doctor has an ICollection and Appointment has also an ICollection. Yet this seems to be strange as for Appointment only one ComboDocApp is needed.

EDIT: Ok, so after the comments I have created a new class called ComboDocPatAppLoc.

    public class ComboDocPatAppLoc
{
    [Key]
    public Guid Id { get; set; }

    [Required]
    public Doctor Doctor { get; set; }
    public Guid DoctorId { get; set; }

    [Required]
    public Patient Patient { get; set; }
    public Guid PatientId { get; set; }

    [Required]
    public Appointment Appointment { get; set; }
    public Guid AppointmentId { get; set; }

    [Required]
    public Location Location { get; set; }
    public Guid LocationId { get; set; }
}

In the ApplicationDbContext I have created the relations as the following:

builder.Entity<ComboDocPatAppLoc>()
    .HasKey(key => new { key.DoctorId, key.PatientId, key.AppointmentId, key.LocationId });

builder.Entity<ComboDocPatAppLoc>()
    .HasOne(d => d.Doctor)
    .WithMany(cdpa => cdpa.ComboDocPatAppLoc)
    .HasForeignKey(fk => fk.Id);

builder.Entity<ComboDocPatAppLoc>()
    .HasOne(p => p.Patient)
    .WithMany(cdpa => cdpa.ComboDocPatAppLoc)
    .HasForeignKey(fk => fk.Id);

builder.Entity<ComboDocPatAppLoc>()
    .HasOne(l => l.Location);

builder.Entity<ComboDocPatAppLoc>()
    .HasOne(app => app.Appointment)
    .WithOne(cdpa => cdpa.ComboDocPatAppLoc);

Yet the error remains:

Introducing FOREIGN KEY constraint 'FK_ComboDocPatAppLoc_Patients_Id' on table 'ComboDocPatAppLoc' may cause cycles or multiple cascade paths

I feel like missing the tiny bits on the ModelBuilder settings, but might be wrong.. I believe the issue is how cascading is chained.

  • If I remove a Doctor/Patient --> I need to have the Appointment and ComboDocPatAppLoc removed.
  • If I remove an Appointment --> I need to have a ComboDocPatAppLoc removed.
  • If I remove a Location --> I need to have an Appointment
    removed.

UDPATE:

Ok, eventually I found it to be a design issue rather and reworking it helped to solve it. Doctor and Patient classes had a reference to Appointments and Appointments had a reference to Doctors and Patients.

Why it was an issue: In case a Patient is deleted the cascading deletion would trigger deletion of the Appointment with all references to Doctor Location, etc. As Doctor had also a reference to Location the certain Location entry would have been deleted twice by cascading sequence.

Solution: Rely on Discovery and don't try to create back-and-forth references (navigation properties all along).

Patient has a navigation property to: Appointments --> which has navigation property to Doctor --> which has navigation property to Location. This made the navigation more of a straight line than circle.

Thanks in advance for your help!


Solution

  • I have updated the question to flag that issue got resolved via redesigning the Data structure and the navigation properties. Keeping it there thus to help if anyone encounters similar concerns.

    Salute