Object Relational Mappers (ORMs) allow us to define relationships between entities in our relational database using C# objects. Depending on which ORM we use, we can also get the library to generate the relationships’ schema. Entity Framework Core is one of those tools with a built-in SQL migration paradigm.

This post will explore most of the relationships found within a relational database and how we would model those relationships using a code-first approach. We’ll use SQLite, a local file-based database provider, but many of these modeling techniques will work across most relational databases and likely yield a similar schema.

We’ll keep our entity definitions to a minimum so we can see what’s necessary when modeling our relationship type. These definitions are starting points, so developers should feel free to experiment and make them their own.

The first type of relationship is the stand-alone kind. In this scenario, the defining entity has no connection with any other entity in our database context. These are straightforward to design and typically require a single identifier column.

public class NotRelated
{
    public int Id { get; set; }
}

Within our DbContext, we need to define the DbSet property.

public DbSet<NotRelated> NotRelateds { get; set; }

One-to-One Bidirectional Relationship

When defining one-to-one bidirectional relationships, we still need to specify which entity in the connection is first. The specification allows EF Core to insert one row and then execute additional queries to tie the two entities together. Let’s define our one-to-one models first.

public class OneToOneLeft
{
    public int Id { get; set; }
    public int OneToOneRightId { get; set; }
    public OneToOneRight Right { get; set; }
}

public class OneToOneRight
{
    public int Id { get; set; }
    public int OneToOneLeftId { get; set; }
    public OneToOneLeft Left { get; set; }
}

We’ll notice that both entities have a reference to their one-to-one related entity. In this case, we see OneToOneLeft has a reference to OneToOneRight and vice versa. To define the order, we’ll need to add some additional code into our OnModelCreating method within the DbContext.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 1 to 1 (bidirectional)
    modelBuilder.Entity<OneToOneLeft>()
        .HasOne<OneToOneRight>()
        .WithOne(r => r.Left)
        .HasForeignKey<OneToOneRight>(r => r.Id);
    
    base.OnModelCreating(modelBuilder);
}

Finally, we can add both DbSet entities to our DbContext.

// 1 to 1 (bidirectional)
public DbSet<OneToOneLeft> OneToOneLefts { get; set; }
public DbSet<OneToOneRight> OneToOneRights { get; set; }

One-to-one Owned Relationship

EF Core has a concept of owned entities. An owned entity is one that we can only access through its parent relationship. For example, take the relationship between OneToOneOwner and OneToOneOwned.

public class OneToOneOwner
{
    public int Id { get; set; }
    public OneToOneOwned Owned { get; set; } 
}

[Owned]
public class OneToOneOwned
{
    public string Value { get; set; }
}

As we may have noticed, the OneToOneOwned doesn’t have an identifier column. By default, EF Core will store one-to-one owned values in the same table as the owning entity. The optimization enhances query performance while still maintaining C# modeling intent. Let’s take a look at the SQL schema generated for our OneToOneOwners table.

create table OneToOneOwners
(
   Id INTEGER not null
      constraint PK_OneToOneOwners
         primary key autoincrement,
   Owned_Value TEXT
);

EF Core has table splitting capabilities if we still want to store owned-entity data in a separate table, but we’ll not delve into that for this post. To add a one-to-one owned relationship to the DbContext, we need to add a DbSet of the parent entity, which is OneToOneOwner.

// 1 to 1 (owned)
public DbSet<OneToOneOwner> OneToOneOwners { get; set; }

One-to-Many Relationships

The one-to-many relationship is one of the more versatile relationships when dealing with database modeling. With EF Core, we can use most collection types found in C#, but I would generally recommend List<T> as it has one of the more useful interface implementations with methods like Add and AddRange.

Let’s start by defining our parent entity, OneToMany, and its children entities OneToManyItem types.

public class OneToMany
{
    public int Id { get; set; }
    public List<OneToManyItem> Items { get; set; } 
}

public class OneToManyItem
{
    public int Id { get; set; }
    public int OneToManyId { get; set; }
    public OneToMany OneToMany { get; set; }
}

As you may notice, the Items property in OneToMany is a List<OneToManyItem>. We can also see that OneToManyItem has a navigation property back to the parent entity. The navigation property to the parent isn’t always necessary, but it can help when writing LINQ queries. Finally, we can add both entities to our DbContext definition. It’s not always necessary to add the children entity as a DbSet property, but again, it can help us when constructing LINQ queries.

// 1 to Many
public DbSet<OneToMany> OneToManys { get; set; }
public DbSet<OneToManyItem> OneToManyItems { get; set; }

One-to-Many Owned Relationships

Unlike the one-to-one owned relationships, one-to-many owned relationships will generate separate tables. In our case, we have OneToManyOwner and OneToManyOwnedItem entities.

public class OneToManyOwner
{
    public int Id { get; set; }
    public List<OneToManyOwnedItem> Items { get; set; }
}

[Owned]
public class OneToManyOwnedItem
{
    public string Name { get; set; }
}

Even though we didn’t define an Id property on our OneToManyOwnedItem entity, EF Core will create what is known as a shadow property for the primary key Id. We can see this in the generated SQL schema.

create table OneToManyOwnedItem
(
   OneToManyOwnerId INTEGER not null
      constraint FK_OneToManyOwnedItem_OneToManyOwners_OneToManyOwnerId
         references OneToManyOwners
            on delete cascade,
   Id INTEGER not null,
   Name TEXT,
   constraint PK_OneToManyOwnedItem
      primary key (OneToManyOwnerId, Id)
);

To add the OneToManyOwner entity to our DbContext, we only need to add one DbSet property.

// 1 to Many (owned)
public DbSet<OneToManyOwner> OneToManyOwners { get; set; }

Many To Many Transparent Relationship

Developers coming from Entity Framework 6 Code-First will be familiar with transparent many-to-many relationships. EF Core manages the relationship table that connects two entities, abstracting it away from the developer. The transparent many-to-many relationship is only available since EF Core 5. Let’s look at two entities of ManyToManyLeft and ManyToManyRight.

public class ManyToManyLeft
{
    public int Id { get; set; }
    public List<ManyToManyRight> Rights { get; set; }
}

public class ManyToManyRight
{
    public int Id { get; set; }
    public List<ManyToManyLeft> Lefts { get; set; }
}

As we can see, both entities have a collection of the other entity. In this case, EF Core manages a transparent table of ManyToManyLeftManyToManyRight in our database schema.

create table ManyToManyLeftManyToManyRight
(
   LeftsId INTEGER not null
      constraint FK_ManyToManyLeftManyToManyRight_ManyToManyLefts_LeftsId
         references ManyToManyLefts
            on delete cascade,
   RightsId INTEGER not null
      constraint FK_ManyToManyLeftManyToManyRight_ManyToManyRights_RightsId
         references ManyToManyRights
            on delete cascade,
   constraint PK_ManyToManyLeftManyToManyRight
      primary key (LeftsId, RightsId)
);

create index IX_ManyToManyLeftManyToManyRight_RightsId
   on ManyToManyLeftManyToManyRight (RightsId);

A transparent many-to-many relationship is ideal for scenarios where the connection between two entities is matter-of-fact, meaning the relationship itself has no distinguishing attributes. As we’ll see in the next section, we can also model a many-to-many with the relationship realized as an entity.

To finish the modeling, we only need to add both entities in the connection to our DbContext.

// Many To Many (Transparent)
public DbSet<ManyToManyLeft> ManyToManyLefts { get; set; }
public DbSet<ManyToManyRight> ManyToManyRights { get; set; }

Modeled Many-To-Many Relationship

Modeling a many-to-many relationship with an exposed connecting entity is useful for scenarios where the relationship itself has defining attributes. An example might be between an individual and a home, where the individual could either own, lease to own, or rent.

Let’s look at an example with the following entities of ManyToManyWithModeledLeft, ManyToManyWithModeledRight, and ManyToManyRelationship.

public class ManyToManyWithModeledLeft
{
    public int Id { get; set; }
    public ManyToManyRelationship Relationship { get; set; }
}

public class ManyToManyWithModeledRight
{
    public int Id { get; set; }
    public ManyToManyRelationship Relationship { get; set; }
}

public class ManyToManyRelationship
{
    public int Id { get; set; }
    public List<ManyToManyWithModeledLeft> Lefts { get; set; }
    public List<ManyToManyWithModeledRight> Rights { get; set; }
}

As we may have noticed, the modeled many-to-many is a combination of one-to-one relationships and one-to-many relationships. The types ManyToManyWithModeledLeft and ManyToManyWithModeledRight each have a one-to-one relationship to the ManyToManyRelationship entity. The ManyToManyRelationship has a navigation collection to both ManyToManyWithModeledLeft and ManyToManyWithModeledRight.

Again, this approach’s advantage is how we can attach additional data to the relationship, where it makes sense, rather than incorrectly adding that data to either entity included in the many-to-many scenario.

We’ll need to add all these relationships to our DbContext.

// Many To Many (Modeled Relationship)
public DbSet<ManyToManyWithModeledLeft> ManyToManyWithModeledLefts { get; set; }
public DbSet<ManyToManyWithModeledRight> ManyToManyWithModeledRights { get; set; }
public DbSet<ManyToManyRelationship> ManyToManyRelationships { get; set; }

Hierarchical Relationships

Hierarchical relationships are when rows within the same table reference another row, typically in a parent/child relationship. Let’s look at how to model this type of relationship, starting with some C#.

public class Hierarchical
{
    public int Id { get; set; }
    public int ParentId { get; set; }
    public Hierarchical Parent { get; set; }
    public List<Hierarchical> Children { get; set; }
}

We can see that we have a navigation property of Parent and a collection navigation property of Children. While this relationship is straightforward to model, it is relatively challenging to query using LINQ. Use this modeling pattern with extreme caution, as hierarchical queries can be expensive to execute even when using raw SQL constructs.

We can see the SQL schema when generating the EF Core migrations.

create table Hierarchicals
(
   Id INTEGER not null
      constraint PK_Hierarchicals
         primary key autoincrement,
   ParentId INTEGER not null
      constraint FK_Hierarchicals_Hierarchicals_ParentId
         references Hierarchicals
            on delete cascade
);

create index IX_Hierarchicals_ParentId
   on Hierarchicals (ParentId);

To add this relationship to our DbContext, we only need to add our Hierarchial entity as a single DbSet property.

// Hierarchical
public DbSet<Hierarchical> Hierarchicals { get; set; }

Conclusion

We explored non-related, one-to-many, many-to-many, and hierarchical relationships with EF Core. While there are many permutations of each relationship type, this post will help beginners have a starting point for defining relationships and serve as a refresher for even the most experienced EF Core developers. To see and use these relationships, I’ve created a GitHub repository where you can clone and experiment with all these relationships.