Example EF Core Relationships and Deleting Related Data

In this article, we are going to see couple of examples of deleting the related entities. We will see one example from each type of relationships (i.e. Many to ManyOne to Many and One to One).

What is so special with DELETE ?

The obvious question is – why are we looking at only the DELETE operation. I think one of the main reason is, we already have seen how to CREATE / READ the data. The UPDATE is very similar to CREATE operation.

The DELETE operation is basically an UPDATE, in case you want to soft delete the entities. It is only about setting IsDeleted property of the record (and also to the related records if that’s applicable) to true. The actual record would still exist in the database only a property would be changed.

The DELETE becomes fun when the hard-delete needs to be implemented, meaning the records would be deleted from database table. The relationships can have DeleteBehavior specified, telling if the related data should be deleted once the master record is deleted.

In remaining part of the post, we are going to create 6 different entities, for demonstrating the idea. Below are the hypothetical entities and relationships that we are going to consider for this demo.

  • Many Countries can have many spoken languages
  • One Teacher record can be associated with multiple student records
  • One Book can have one and only one Author

We are considering 6 entities and every pair is completely independent of other two pairs – for the sake of keeping this demo simple. Let’s get started.

Many To Many Relationship

The code snippet given below shows the countries and languages entities. Each entity has a collection navigation property to the other entity it is related to.

// One country -> Many languages
public class Country
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Language> Languages { get; set; }
}

// One language -> Many Countries
public class Language
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Country> Countries { get; set; }
}

// This will lead to creation of CountryLanguage table 
// to hold foreign keys although entity has not been created

Starting from EF Core 5, it is sufficient to have a many to many relationship between two entities. This is managed by using property bag entity types. Although an explicit entity is not present, it would still create a type CountryLanguage (combining two entity names) to hold the combination of keys which represents many to many association.

Although these types are supported, it is not recommended to use them. As per documentation, this implementation may change in future to improve the performance. And that’s why, we are going to create an indirect many to many relationship, we are going to add another entity, explicitly, to represent the relationship and hold the references.

The below code example shows entities, the DbContext, and a driver program.

// --------------------------------------------------------
// Entities
// --------------------------------------------------------
public class Language
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<CountryLanguage> CountryLanguages { get; set; }
}

public class Country
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<CountryLanguage> CountryLanguages { get; set; }
}

// Additional Entity
public class CountryLanguage
{
    public int Id { get; set; }

    public int CountryId { get; set; }
    public Country AssociatedCountry { get; set; }

    public int LanguageId { get; set; }
    public Language AssociatedLanguage { get; set; }
}

// --------------------------------------------------------
// Context
// --------------------------------------------------------
public class UniversityContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder
            .Entity<CountryLanguage>()
            .HasKey(t => t.Id);

        modelBuilder
            .Entity<CountryLanguage>()
            .HasOne(c => c.AssociatedCountry)
            .WithMany(c => c.CountryLanguages)
            .HasForeignKey(cl => cl.CountryId);

        modelBuilder
            .Entity<CountryLanguage>()
            .HasOne(l => l.AssociatedLanguage)
            .WithMany(l => l.CountryLanguages)
            .HasForeignKey(cl => cl.LanguageId);
    }

    // Many to Many Relationship
    public DbSet<Country> Countries { get; set; }
    public DbSet<Language> Languages { get; set; }
    public DbSet<CountryLanguage> CountryLanguages { get; set; }
}

// --------------------------------------------------------
// Driver
// --------------------------------------------------------
static async Task Main(string[] args)
{
    using var context = BuildUniversityContext();
    SeedData(context);

    var languages = await context.Languages.ToListAsync();

    context.Languages.Remove(languages[0]);
    await context.SaveChangesAsync();
}

One To Many Relationship

Now, let’s define one to many relationship between the Teacher entity and Student entity. In addition to the fields specific to each entity, we are going to add:

Then just a DbSet property for each table in the context and similar driver program. This time we are going to specify a cascade delete behavior using entity configurations. Below is the code sample:

// ----------------------------------------------------------------------
// Entities - One to Many
// ----------------------------------------------------------------------
// Student.cs
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    // Placing this attribute and property is not nullable
    // this would by default apply cascaded delete behavior.
    // [ForeignKey(nameof(Teacher))]
    public int TeacherId { get; set; }
    public Teacher Teacher { get; set; }
}

// Teacher.cs
public class Teacher
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Student> Students { get; set; }
}

// ----------------------------------------------------------------------
// Context
// ----------------------------------------------------------------------
public class UniversityContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Instead of foreign key attribute or the EF core convetions,
        // We are going to use entity configurations here
        modelBuilder.Entity<Teacher>()
            .HasMany(t => t.Students)
            .WithOne(c => c.Teacher)
            .HasForeignKey(c => c.TeacherId)
            .OnDelete(DeleteBehavior.Cascade);
    }

    // One to Many relation ship
    public DbSet<Teacher> Teachers { get; set; }
    public DbSet<Student> Students { get; set; }
}

// ----------------------------------------------------------------------
// Driver
// ----------------------------------------------------------------------
static async Task Main(string[] args)
{
    using var context = BuildUniversityContext();
    SeedData(context);

    var teachers = await context.Teachers.Include(x=> x.Students).ToListAsync();
    context.Teachers.Remove(teachers[0]);
    await context.SaveChangesAsync();
}

One To One Relationship

Now, let’s start with the last part of our demonstration. One to one relationship meaning, on both ends of relationship, one and exact one record exists.

If you want cascade delete behavior you need to ensure that you choose the principal entity and dependent entity appropriately. Principal entity is the one which must be referred by the dependent entity. So the foreign key would always be in the dependent entity.

NOTE:

Cascade delete means whenever the record in principal entity is deleted, all the dependent records should be deleted.

While defining one-to-one relationship, this definition of cascaded delete may be missed and one may assume wrongly that if record from any of the entities is deleted, the associated record from the other end should also get deleted if cascaded delete is enabled. But that would never be the case. Cascaded delete is triggered only if you delete the record from principal entity side (i.e. the entity which does not contain any foreign key).

So, coming back to our example, let’s create Book and Author entities. In our case, we are going to make AuthorId as foregin key in Book entity. This would make Book entity as dependent entity, as it depends on Author.

  • So, if cascaded delete is enabled and if an Author record is deleted, it would also delete corresponding Book record.
  • But if a Book record is deleted, it would not delete the corresponding Author record as Author is a principal entity.

Below is the code sample for this demo:

// -----------------------------------------------------
// Entities
// -----------------------------------------------------

// Author - Principal Entity
public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Book Book { get; set; }
}

// Book - Dependent Entity
public class Book
{
    public int Id { get; set; }
    public string Name { get; set; }

    // As property is not nullable, the delete behavior is cascade delete
    [ForeignKey(nameof(Author))]
    public int AuthorId { get; set; }
    public Author Author { get; set; }
}

// -----------------------------------------------------
// Context
// -----------------------------------------------------
public class UniversityContext : DbContext
{
    public UniversityContext(DbContextOptions<UniversityContext> options)
        : base(options)
    {
    }

    public DbSet<Book> Books { get; set; }
    public DbSet<Author> Authors { get; set; }
}

// -----------------------------------------------------
// Driver
// -----------------------------------------------------
public static async Task Main(UniversityContext context)
{
    using var context = BuildUniversityContext();
    SeedData(context);

    var books = await context.Books.Include(x => x.Author).ToListAsync();
    context.Books.Remove(books[0]);
    await context.SaveChangesAsync();
}

Wrapping Up

I hope this article helped you to gain conceptual understanding of entity relationships can be modeled and how the delete behavior is for different kinds of relationships.

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *