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.
Non-Related Entities
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.
Within our DbContext
, we need to define the DbSet
property.
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.
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
.
Finally, we can add both DbSet
entities to our DbContext
.
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
.
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.
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
.
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.
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.
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.
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.
To add the OneToManyOwner
entity to our DbContext
, we only need to add one DbSet
property.
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
.
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.
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
.
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
.
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
.
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#.
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.
To add this relationship to our DbContext
, we only need to add our Hierarchial
entity as a single DbSet
property.
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.