7 Configuring relationships
This chapter covers
- Configuring relationships using By Convention
- Configuring relationships using Data Annotations
- Configuring relationships using Fluent API
- Other ways to map entities to database tables
Chapter 6 described how to configure scalar, or nonrelational, properties. This chapter covers how to configure database relationships. I assume you’ve read at least the first part of chapter 6, because configuring relationships uses the same three approaches, By Convention and Data Annotations and the Fluent API, to map the database relationships.
This chapter covers how EF Core finds and configures relationships between entity classes, with pointers on how to configure each type of relationship—one-to-one, one-to-many, and many-to-many—and examples of each. EF Core’s By Convention relationship rules can quickly configure many relationships, but you’ll also learn about all the Data Annotations and Fluent API configuration options, which allow you to precisely define the way you want a relationship to behave. You’ll also look at features that allow you to enhance your relationships with extra keys and alternative table-mapping approaches.
7.1 Defining some relationship terms
This chapter refers to the various parts of a relationship, and you need clear terms so you know exactly what part of the relationship we’re talking about. Figure 7.1 shows those terms, using the Book
and Review
entity classes from our book app. I follow this figure with a more detailed description so the terms will make sense to you when I use them in this chapter.
Figure 7.1 The Book
and Review
entity classes show six of the terms used in this chapter to discuss relationships: principal entity, dependent entity, principal key, navigational property, foreign key, and required relationship. Not shown is the optional relationship, which is described in section 2.4.4.

To ensure that these terms are clear, here are detailed descriptions:
- Principal key—Anew term, taken from EF Core’s documentation, that refers to either the primary key, defined in part 1, or the new alternate key, which has a unique value per row and isn’t the primary key (see section 7.7.3).
Note
Figure 7.1 provides an example of an alternate key called UniqueISBN
, which represents a unique value per entity. (ISBNstands for International Standard Book Number, which is unique for every book.)
- Principal entity —The entity that contains the principal-key property(s), which the dependent relationship refers to via a foreign key(s) (covered in chapter 3).
- Dependent entity —The entity that contains the foreign-key property(s) that refers to the principal entity (covered in chapter 3).
- Navigational property —Anew term taken from EF Core’s documentation that refers to the property containing a single entity class, or collection of entity classes, which EF Core uses to link entity classes.
- Foreign key—Defined in section 2.1.3, this holds the principal-key value(s) of the database row it’s linked to (or could be
null
). - Required relationship —A relationship in which the foreign key is non-nullable; the principal entity must exist.
- Optional relationship —A relationship in which the foreign key is nullable; the principal entity can be missing.
Note
You’ll see in section 7.4 that EF Core can find and configure most relationships by convention. In some cases, EF Core needs help, but generally, EF Core can find and configure your navigational properties for you if you use the By Convention naming rules.
7.2 What navigational properties do you need?
Before I describe how to configure relationship types, I want to cover the software design decisions around how you model a relationship. This is about selecting the best arrangement of the navigational properties between the entity classes—what do you want to expose at the software level, and what do you want to hide?
In our book app, the Book
entity class has many Review
entity classes, and each Review
class is linked, via a foreign key, to one Book
. You therefore could have a navigational property of type ICollection<Review>
in the Book
class, and a navigational property of type Book
in the Review
class. In that case, you’d have a fully defined relationship: a relationship with navigational properties at both ends.
But do you need a fully defined relationship? From the software design point of view, there are two questions about the Book/Review
navigational relationships. The answers to these questions will define which of the navigational relationships you need to include:
- Does the
Book
entity class need to know about theReview
entity classes? I say yes, because we want to calculate the average review score. - Does the
Review
entity class need to know about theBook
entity class? I say no, because in this example application we don’t do anything with that relationship.
Our solution is therefore to have only the ICollection<Review>
navigational property in the Book
class, which is what figure 7.1 portrays.
7.3 Configuring relationships
In the same way as in chapter 6, which covered configuring nonrelational properties, EF Core has three ways to configure relationships. Here are the three approaches for configuring properties, but focused on relationships:
- By Convention —EF Core finds and configures relationships by looking for references to classes that have a primary key in them.
- Data Annotations —These can be used to mark foreign keys and relationship references.
- Fluent API —This provides the richest set of commands to fully configure any relationship.
The next three sections detail each of these in turn. As you’ll see, the By Convention approach can autoconfigure many relationships for you, if you follow its naming standards. At the other end of the scale, the Fluent API allows you to manually define every part of a relationship, which can be useful if you have a relationship that falls outside the By Convention approach.
7.4 Configuring relationships By Convention
The By Convention approach is a real time-saver when it comes to configuring relationships. In EF6.x, I used to laboriously define my relationships because I hadn’t fully understood the power of the By Convention approach when it comes to relationships. Now that I understand the conventions, I let EF Core set up most of my relationships, other than the few cases where By Convention doesn’t work (section 7.4.6 lists those exceptions).
The rules are straightforward, but the ways the property name, type, and nullability all work together to define a relationship takes a bit of time to absorb. Hopefully, reading this section will save you time when you’re developing your next application that uses EF Core.
7.4.1 What makes a class an entity class?
Chapter 2 defined the term entity class as a normal .NET class that has been mapped by EF Core to the database. Here I want to define how EF Core finds and identifies a class as an entity class by using the By Convention approach.
Figure 6.1 showed the three ways that EF Core configures itself. The following is a recap of that process, but now focused on finding the relationships and navigational properties:
- EF Core scans the application’s DbContext, looking for any public
DbSet<T>
properties. It assumes the classes,T
, in theDbSet<T>
properties are entity classes. - EF Core also looks at every public property in the classes found in step 1, and looks at properties that could be navigational properties. These are all classes that aren’t defined as being scalar properties by the current database provider (
string
is a class, but it’s defined as a scalar property). These classes may appear as a single link (for instance,public PriceOffer Promotion ( get; set; }
) or a type that implements theIEnumerable<T>
interface (for instance,public ICollection<Review> Reviews { get; set; }
).
Note
Backing fields and the Fluent API, covered later in this chapter, can also add entity classes. Section 6.6 shows how you can exclude a class from EF Core’s mapping.
- EF Core then checks that each of these entity classes has a primary key (chapter 6 shows how a primary key is defined). If the class doesn’t have a primary key, and the class isn’t excluded, then EF Core will throw an exception.
7.4.2 An example of an entity class with navigational properties
Listing 7.1 shows the entity class Book
, which is defined in the application’s DbContext. In this case, you have a public property of type DbSet<Book>
, which passed the “must have a valid primary key” test in that it has a public property called BookId
.
What you’re interested in is how EF Core’s By Convention configuration handles the three navigational properties at the bottom of the class. As you’ll see in this section, EF Core can work out which sort of relationship it is by the type of the navigational property and the foreign key in the class that the navigational property refers to.
Listing 7.1 The Book
entity class, with the relationships at the bottom
public class Book { public int BookId { get; set; } //other scalar properties removed as not relevant… public PriceOffer Promotion { get; set; } #1 public ICollection<BookAuthor> #2 AuthorsLink { get; set; } #2 public ICollection<Review> Reviews { get; set; } #3 }
If two navigational properties exist between the two entity classes, the relationship is known as fully defined, and EF Core can work out By Convention whether it’s a one-to-one or a one-to-many relationship. If only one navigational property exists, EF Core can’t be sure, and assumes a one-to-many relationship.
Certain one-to-one relationships may need configuration via the Fluent API if you have only one navigational property, or you want to change the default By Convention setting—for example, when deleting an entity class with a relationship.
7.4.3 How EF Core finds foreign keys By Convention
A foreign key must match the principal key (defined in section 7.1) in type and in name, but to handle a few scenarios, the foreign-key name matching has three options, shown in figure 7.2. The figure shows an example of all three options for a foreign-key name using the entity class Review
that reference the primary key, BookId
, in the entity class Book
.
Figure 7.2 Three options for a foreign key referring to the Book
entity class’s primary key. These allow you to use a unique name for your foreign key, from which EF Core can work out which primary key this relationship refers to.

Option 1 is used the most; I showed this in figure 7.1. Option 2 is for developers who use the short, By Convention primary-key name, Id
, as it makes the foreign key unique to the class it’s linking to.
Option 3 helps with specific cases in which you’d get duplicate named properties if you used option 1. The following listing shows an example of using option 3 to handle a hierarchical relationship.
Listing 7.2 A hierarchical relationship with an option 3 foreign key
public class Employee { public int EmployeeId { get; set; } public string Name { get; set; } //------------------------------ //Relationships public int? ManagerEmployeeId { get; set; } #1 public Employee Manager { get; set; } } /************************************************
The entity class called Employee
has a navigational property called Manager
that links to the employee’s manager, who is an employee as well. You can’t use a foreign key of EmployerId
(option 1) because that’s already used for the primary key. You therefore use option 3, and call the foreign key ManagerEmployeeId
by using the navigational property name at the start.
7.4.4 Nullability of foreign keys—required or optional relationships
The nullability of the foreign key defines whether the relationship is required (non-nullable foreign key) or optional (nullable foreign key). A required relationship ensures that relationships exist by ensuring that the foreign key is linked to a valid principal key. Section 7.6.1 describes an Attendee
entity that has a required relationship to a Ticket
entity class.
An optional relationship allows there to be no link between the principal entity and the dependent entity, by having the foreign-key value(s) set to null
. The Manager
navigational property in the Employee
entity class, shown in listing 7.2, is an example of an optional relationship, as someone at the top of the business hierarchy won’t have a boss.
The required or optional status of the relationship also affects what happens when the principal entity is deleted. The default setting of the OnDelete
action for each relationship type is as follows:
- For a required relationship, EF Core sets the
OnDelete
action toCascade
. If the principal entity is deleted, the dependent entity will be deleted too. - For an optional relationship, EF Core sets the
OnDelete
action toClientSetNull
. If the dependent entity is being tracked, the foreign key will be set tonull
when theprincipal entity is deleted. But if the dependent entity isn’t being tracked, the database settings take over, and the entity is set toRestrict
, so the delete will fail in the database, and an exception will be thrown.
Note
The ClientSetNull
delete behavior is rather unusual, and section 7.7.1 explains why. That section also describes how to configure the delete behavior of a relationship.
7.4.5 Foreign keys—what happens if you leave them out?
If EF Core finds a relationship via a navigational property, or through a relationship you configured via the Fluent API, it needs a foreign key to set up the relationship in the relational database. Including foreign keys in your entity classes is good practice. This gives you better control over the nullability of the foreign key, and access to foreign keys can be useful when handling relationships in a disconnected update (see section 3.3.1).
But if you do leave out a foreign key (on purpose or by accident), EF Core configuration will add a foreign key as a shadow property. Chapter 6 introduced shadow properties, hidden properties that can be accessed only via specific EF Core commands.
Figure 7.3 shows the By Convention naming of shadow foreign-key properties if added by EF Core. It’s useful to know the default names, as you can access the shadow foreign-key properties by using the EF.Property<T>(string)
method if you need to (see section 6.11.2 for more details on accessing shadow properties).
Figure 7.3 If you don’t provide a foreign key and EF Core deems that one is needed, EF Core will create a shadow foreign key and use the preceding rules to decide what name to give it. If you want to access the shadow foreign key yourself, you can find which rule applies and then use it in an EF.Property<T>
method.

The important point to note is that the shadow foreign-key property will be nullable, which has the effect described in section 7.4.4 on nullability of foreign keys. If this isn’t what you want, you can alter the shadow property’s nullability by using the Fluent API IsRequired
method, as described in section 7.7.2.
EF6
EF6.x uses a similar approach of adding foreign keys if you left them out of your entity classes, but in EF6.x you can’t configure the nullability or access the content. EF Core’s shadow properties make the approach of leaving out foreign keys more controllable.
7.4.6 When does By Convention configuration not work?
If you’re going to use the By Convention configuration approach, you need to know when it’s not going to work, so you can use other means to configure your relationship. Here’s my list of scenarios that won’t work, with the most common listed first:
- You have composite foreign keys (see section 7.6 or section 7.5.1).
- You want to create a one-to-one relationship without navigational links going both ways (see section 7.6.1).
- You want to override the default delete behavior setting (see section 7.7.1).
- You have two navigational properties going to the same class (see section 7.5.2).
- You want to define a specific database constraint (see section 7.7.4).
7.5 Configuring relationships by using Data Annotations
Only two Data Annotations relate to relationships, as most of the navigational configuration is done via the Fluent API. They’re the ForeignKey
and InverseProperty
annotations.
7.5.1 The ForeignKey Data Annotation
The ForeignKey
Data Annotation allows you to define the foreign key for a navigational property in the class. Taking the hierarchical example of the Employee
class, you can use this to define the foreign key for the Manager
navigational property. The following listing shows an updated Employee
entity class with a new, shorter foreign-key name for the Manager
navigational property that doesn’t fit the By Convention naming.
Listing 7.3 Using the ForeignKey
data annotation to set the foreign-key name
public class Employee { public int EmployeeId { get; set; } public string Name { get; set; } //------------------------------ //Relationships public int? ManagerId { get; set; } [ForeignKey(nameof(ManagerId))] #1 public Employee Manager { get; set; } }
Note
You’ve applied the ForeignKey
data annotation to the Manager
navigational property, giving the name of the foreign key, ManagerId
. But the ForeignKey
data annotation also works the other way around. You could’ve applied the ForeignKey
data annotation to the foreign-key property, ManagerId
, giving the name of navigational property, Manager
—for instance, [ForeignKey(nameof(Manager))]
.
The ForeignKey
data annotation takes one parameter, which is a string. This should hold the name of the foreign-key property. If the foreign key is a composite key (it has more than one property), these should be comma delimited—for instance, [ForeignKey("Property1,
Property2")].
Tip
7.5.2 The InverseProperty Data Annotation
The InverseProperty
Data Annotation is a rather specialized Data Annotation for use when you have two navigational properties going to the same class. At that point, EF Core can’t work out which foreign keys relate to which navigational property. This is best shown by code, and the following listing gives you an example of the Person
entity class having two lists: one for books owned by the librarian and one for Books
out on loan to a specific person.
Listing 7.4 LibraryBook
entity class with two relationships to Person
class
public class LibraryBook { public int LibraryBookId { get; set; } public string Title { get; set; } //----------------------------------- //Relationships public int LibrarianPersonId { get; set; } public Person Librarian { get; set; } public int? OnLoanToPersonId { get; set; } public Person OnLoanTo { get; set; } }
The Librarian
and the borrower of the book (OnLoanTo
navigational property) are both represented by the Person
entity class. The Librarian
navigational property and the OnLoanTo
navigational property both link to the same class, and EF Core can’t set up the navigational linking without help. The InverseProperty
Data Annotation shown in the following listing provides the information to EF Core when it’s configuring the navigational links.
Listing 7.5 The Person
entity class, which uses the InverseProperty
annotation
public class Person { public int PersonId { get; set; } public string Name { get; set; } //------------------------------ //relationships [InverseProperty("Librarian")] #1 public ICollection<LibraryBook> LibrarianBooks { get; set; } [InverseProperty("OnLoanTo")] #2 public ICollection<LibraryBook> BooksBorrowedByMe { get; set; } }
This is one of those configuration options that you rarely use, but if you have this situation, you must use this, or define the relationship using the Fluent API. Otherwise, EF Core will throw an exception when it starts, as it can’t work out how to configure the relationships.
Take our tour and find out more about liveBook's features:
- Search - full text search of all our books
- Discussions - ask questions and interact with other readers in the discussion forum.
- Highlight, annotate, or bookmark.
7.6 Fluent API relationship configuration commands
As I said in section 7.4, you can configure most of your relationships by using EF Core’s By Convention approach. But if you want to configure a relationship, the Fluent API has a well-designed set of commands that cover all the possible combinations of relationships. It also has extra commands to allow you to define other database constraints. The format for defining a relationship with the Fluent API is shown in figure 7.4. All Fluent API relationship configuration commands follow this pattern.
Figure 7.4 The Fluent API allows you to define a relationship between two entity classes. HasOne/HasMany
and WithOne/WithMany
are the two main parts, followed by other commands to specify other parts or set certain features.

EF6
EF Core’s Fluent API command names have changed from EF6 and, for me, they’re much clearer. Personally I found EF6’s WithRequired
and WithRequiredPrincipal/WithRequiredDependent
commands a bit confusing, whereas the EF Core Fluent API commands have a clearer HasOne
/HasMany
followed by WithOne/WithMany
syntax.
We’ll now define a one-to-one, one-to-many, and many-to-many relationship to illustrate the use of these Fluent API relationships.
7.6.1 Creating a one-to-one relationship
One-to-one relationships can get a little complicated because there are three ways to build them in a relational database. To understand these options, you’ll look at an example in which you have attendees (entity class Attendee
) at a software convention, and each attendee has a unique ticket (entity class Ticket
).
Chapter 3 showed how to create, update, and delete relationships. To recap, here’s a code snippet showing how to create a one-to-one relationship:
var attendee = new Attendee { Name = "Person1", Ticket = new Ticket{ TicketType = TicketTypes.VIP} }; context.Add(attendee); context.SaveChanges();
Figure 7.5 shows the three options for building this sort of one-to-one relationship. The principal entities are at the top of the diagram, and the dependent entities are at the bottom. Note that option 1 has the Attendee
as the dependent entity, whereas options 2 and 3 have the Ticket
at the dependent entity.
Figure 7.5 The three ways of defining a one-to-one relationship in a relational database; comments at the bottom indicate EF Core’s handling of each approach. The difference between option 1 and option 2 (and 3) is that the order of the two ends of the one-to-one relationship are swapped, which changes which part can be forced to exist. In option 1, the Attendee
must have a T
icket
, whereas in options 2 and 3, the T
icket
is optional for the Attendee
.

Each option has its own advantages and disadvantages. You should use the one that’s right for your business need.
Option 1 is the standard approach to building one-to-one relationships, because it allows you to define that the one-to-one dependent entity is required (it must be present). In our example, an exception will be thrown if you try to save an Attendee
entity instance without a unique Ticket
attached to it. Figure 7.6 shows option 1 in more detail.
Figure 7.6 The non-nullable foreign key ensures that the principal entity (in this case, Attendee
) must have a dependent, one-to-one entity, Ticket
. Also, configuring the relationship as one-to-one ensures that each dependent entity, Ticket
, is unique. Notice the Fluent API on the right has navigational properties going both ways—each entity has a navigational property going to the other.

With the option 1 one-to-one arrangement, you can make the dependent entity optional by making the foreign key nullable. Also, in figure 7.6, you can see that the WithOne
method has a parameter that picks out the Attendee
navigational property in the Ticket
entity class that links back to the Attendee
entity class. Because the Attendee
class is the dependent part of the relationship, then if you delete the Attendee
entity, the linked Ticket
won’t be deleted, because the Ticket
is the principal entity in the relationship.
Options 2 and 3 in figure 7.5 turn the principal/dependent relationship around, with the Attendee
becoming the principal entity in the relationship. This swaps the required/optional nature of the relationship—now the Attendee
can exist without the Ticket
, but the Ticket
can’t exist without the Attendee
. Figure 7.7 shows this relationship.
Figure 7.7 Option 2: The Ticket
entity holds the foreign key of the Attendee
entity. This changes which entity is the principal and dependent entity. In this case, the Attendee
is now the principal entity, and the Ticket
is the dependent entity.

Option 2 can be useful because optional one-to-one relationships, often referred to as one-to-zero-or-one relationships, are more common. All you’ve done here is think of the relationship in a different order.
Option 3 is another, more efficient, way to define option 2, with the primary key and the foreign key combined. I would’ve used this for the PriceOffer
entity class in the book app, but some limitations exist in EF Core 2.0 (see https://github.com/aspnet/EntityFramework/issues/7340). EF Core 2.1 has fixed those limitations.
7.6.2 Creating a one-to-many relationship
One-to-many relationships are simpler, because there’s one format: the “many” entities contain the foreign-key value. Most one-to-many relationships can be defined using the By Convention approach, but figure 7.8 shows the Fluent API code to create a “one Book
has many Review
s” relationship in the book app.
Figure 7.8 A one-to-many relationship, in which the foreign key must be in the dependent entity; in this case, the Review
entity class. You can see in the Fluent API on the right that the Book
has a collection navigational property, Reviews
, linked to the Review
entity classes, but Review
doesn’t have a navigational property back to Book
.

In this case, the Review
entity class doesn’t have a navigational link back to the Book
, so the WithOne
method has no parameter.
Note
Listing 3.16 shows how to add a Review
to the Book
’s one-to-many collection navigational property, Reviews
.
Collections have a couple of features that are worth knowing about. First, you can use any generic type for a collection that implements the IEnumerable<T>
interface, such as ICollection<T>
, Collection<T>
, HashSet<T>
, List<T>
, and so on. IEnumerable<T>
on its own is a special case, as you can’t add to that collection (but see section 8.1 for one place where this is useful). The point is, for performance reasons, you should use the simplest generic collection type so that EF Core can instantiate the collection quickly when using the Include
method. That’s why I tend to use ICollection<T>
.
Note
Second, although you typically define a collection navigational property with a getter and a setter (for instance, public ICollection<Review> Reviews { get; set; }
), that isn’t totally necessary. You can provide a getter only if you initialize the backing field with an empty collection. The following is also valid:
public ICollection<Review> Reviews { get; } = new List<Review>();
Personally, I don’t initialize a collection to an empty list, because the collection will be null
if you load an entity with a collection navigational property without an Include
method to load that collection. Then your code is likely to fail rather than deliver an empty list when you forget the Include
(this is defensive programming). The downside of doing this is you need to manually initialize a collection navigational property in a new entity before you can add entries to it.
7.6.3 Creating a many-to-many relationship
In EF Core, a many-to-many relationship is made up of two one-to-many relationships. The many-to-many relationship between a Book
entity class and its Author
entity classes consists of the following:
- A one-to-many relationship from the
Book
entity class to theBookAuthor
linking entity class - A one-to-many relationship from the
Author
entity class to theBookAuthor
linking entity class
This listing shows the Fluent API that configures the primary key and then the two one-to-many relationships for this many-to-many relationship.
Listing 7.6 Configuring a many-to-many relationship via two one-to-many relationships
public static void Configure (this EntityTypeBuilder<BookAuthor> entity) { entity.HasKey(p => new { p.BookId, p.AuthorId }); #1 //----------------------------- //Relationships entity.HasOne(pt => pt.Book) #2 .WithMany(p => p.AuthorsLink) #2 .HasForeignKey(pt => pt.BookId);#2 entity.HasOne(pt => pt.Author) #3 .WithMany(t => t.BooksLink) #3 .HasForeignKey(pt => pt.AuthorId);#3 }
Note that you don’t need to add the Fluent API to configure the two one-to-many relationships because they follow the By Convention naming and therefore don’t need the Fluent API. The key, however, does need configuring because it doesn’t follow the By Convention naming.
EF6
EF6.x users need to do a bit more work in EF Core to handle many-to-many relationships. EF6.x automatically creates the linking table and automates the adding or removal of entries in the linking table. EF Core may gain the same many-to-many features that EF6.x has, but there’s no timescale on that.
7.7 Additional methods available in Fluent API relationships
In addition to the Fluent API relationship commands, other methods can be added to the end of the Fluent API methods that define a relationship. In summary, they’re as follows:
OnDelete
—Changes the delete action of a dependent entity (section 7.7.1)IsRequired
—Defines the nullability of the foreign key (section 7.7.2)HasPrincipalKey
—Uses an alternate unique key (section 7.7.3)HasConstraintName
—Sets the foreign-key constraint name andMetaData
access to the relationship data (section 7.7.4)
7.7.1 OnDelete—changing the delete action of a dependent entity
Section 7.4.4 described the default action on the deletion of a principal entity, which is based on the nullability of the dependent’s foreign key(s). The OnDelete
Fluent API method allows you to alter what EF Core does when a deletion that affects a dependent entity happens.
You can add the OnDelete
method to the end of a Fluent API relationship configuration. This listing shows the code added in chapter 4 to stop a Book
entity from being deleted if it was referred to in a customer order, via the LineItem
entity class.
Listing 7.7 Changing the default OnDelete
action on a dependent entity
public static void Configure (this EntityTypeBuilder<LineItem> entity) { entity.HasOne(p => p.ChosenBook) .WithMany() .OnDelete(DeleteBehavior.Restrict); #1 }
This code causes an exception to be thrown if someone tries to delete a Book
entity that a LineItem
’s foreign key links to that Book
. You do this because you want a customer’s order to not be changed. Table 7.1 explains the possible DeleteBehavior
settings.
Table 7.1 Delete behaviors available in EF Core. The middle column highlights the delete behavior that will be used if you don’t apply the OnDelete
option.
Name | Effect the delete behavior has on the dependent entity | Default for |
|
The delete operation isn’t applied to dependent entities. The dependent entities remain unchanged. This may cause the delete to fail, either in EF Core or in the relational database. | |
|
The dependent entity isn’t deleted, but its foreign-key property is set to null . If any of the dependent entity foreign-key properties aren’t nullable, an exception is thrown when SaveChanges is called. |
|
|
If EF Core is tracking the dependent entity, its foreign key is set to null and the dependent entity isn’t deleted. But if EF Core isn’t tracking the dependent entity, the database rules will apply; in a database created by EF Core, this will be set to Restrict , which will cause the delete to fail with an exception. |
Optional relationships |
|
The dependent entity is deleted. | Required relationships |
The ClientSetNull
delete behavior is unusual, because it’s the only one in which the action EF Core takes in software is different from the foreign-key constraint EF Core sets in the database. Here are the two dissimilar actions that EF Core and the database take on deleting a principal entity with an optional dependent entity and a delete behavior of ClientSetNull
:
- EF Core sets the optional dependent-entity foreign key to
null
, but only if the optional dependent entity is loaded and being tracked. - The database, if created by EF Core, has a foreign-key constraint of
ON DELETE NO ACTION
(SQL Server). If the optional dependent entity isn’t loaded and EF Core hasn’t set its foreign key tonull
, the database will return a foreign-key constraint error.
EF Core sets a dissimilar database setting because the “correct” setting of ON DELETE SET NULL
(SQL Server) can cause a database error when EF Core tries to create the database (typically, when the database server spots possible cyclic delete paths).
Having a default setting causing an exception on database creation/migration isn’t that friendly for the developer, so, in EF Core 2.0, the team added the new ClientSetNull
delete behavior. With this behavior, you won’t get an unexpected exception when EF Core creates/migrates the database for you, but you need to be a bit more careful when you delete a principal entity that has an optional dependent entity. Listing 7.8 shows the correct way to delete a principal entity that has an optional dependent entity: by ensuring that the optional dependent entity is tracked.
Listing 7.8 Deleting a principal entity with an optional dependent entity
var entity = context.DeletePrincipals #1 .Include(p => p.DependentDefault) #2 .Single(p => p.DeletePrincipalId == 1); context.Remove(entity); #3 context.SaveChanges(); #4
Note that if you don’t include the Include
method or another way of loading the optional dependent entity, SaveChanges
would throw a DbUpdateException
because the database server will have reported a foreign-key constraint violation.
One way to align EF Core’s approach to an optional relationship with the database server’s approach is to set the delete behavior to SetNull
instead of the default ClientSetNull
. This sets the foreign-key constraint in the database to ON DELETE SET NULL
(SQL Server), which is in line with what EF Core does. Whether or not you load the optional dependent entity, the outcome of the called SaveChanges
will be the same; the foreign key on the optional dependent entity will be set to null
. Be aware that some database servers may return an error on database creation in some circumstances, such as an optional hierarchical relationship, as shown in listing 7.2. All the other delete behaviors (Restrict
, SetNull
, and Cascade
) produce a foreign-key constraint that has the same behavior as EF Core’s software.
Note
If you’re managing the database creation/migration outside EF Core, it’s important to ensure that the relational database foreign-key constraint is in line with EF Core’s OnDelete
setting. Otherwise, you’ll get inconsistent behavior, depending on whether the dependent entity is being tracked.
7.7.2 IsRequired—defining the nullability of the foreign key
Chapter 6 describes how the Fluent API method IsRequired
allows you to set the nullability of a scalar property, such as a string. In a relationship, the same command sets the nullability of the foreign key, which, as I’ve already said, defines whether the relationship is required or optional.
The IsRequired
method is most useful in shadow properties because EF Core, by default, makes shadow properties nullable, and the IsRequired
method can change them to non-nullable. Listing 7.9 shows you the Attendee
entity class used previously to show a one-to-one relationship, but showing two other one-to-one relationships that are using shadow properties for their foreign keys.
Listing 7.9 The Attendee
entity class showing all its relationships
public class Attendee { public int AttendeeId { get; set; } public string Name { get; set; } public int TicketId { get; set; } #1 public Ticket Ticket { get; set; }#2 public OptionalTrack Optional { get; set; } #3 public RequiredTrack Required { get; set; } #4 }
The Optional
navigational property, which uses a shadow property for its foreign key, is configured by convention, which means the shadow property is left as a nullable value. Therefore, it’s optional, and if the Attendee
entity is deleted, the OptionalTrack
entity isn’t deleted.
For the Required
navigational property, the following listing presents the Fluent API configuration. Here you use the IsRequired
method to make the Required
one-to-one navigational property as required; each Attendee
entity must have a RequiredTrack
entity assigned to the Required
property.
Listing 7.10 The Fluent API configuration of the Attendee
entity class
public void Configure (EntityTypeBuilder<Attendee> entity) { entity.HasOne(p => p.Ticket) #1 .WithOne(p => p.Attendee) .HasForeignKey<Attendee> (p => p.TicketId) #2 .IsRequired(); entity.HasOne(p => p.Required) #3 .WithOne(p => p.Attend) .HasForeignKey<Attendee>( "MyShadowFk") #4 .IsRequired(); #5 }
You could’ve left out the configuration of the Ticket
navigational property, as this would be correctly configured with the By Convention rules. You leave it in so you can compare it with the configuration of the Required
navigational property, which uses a shadow property for its foreign key.
The configuration of the Required
navigational property is necessary, because the IsRequired
method changes the shadow foreign-key property from nullable to non-nullable, which in turn makes the relationship as required.
Type and naming conventions for shadow property foreign keys
Notice how listing 7.10 refers to the shadow foreign-key property: you need to use the HasForeignKey<T>(string)
method. The <T>
class tells EF Core where to place the shadow foreign-key property, which can be either end of the relationship for one-to-one relationships, or the “many” entity class of a one-to-many relationship.
The string parameter of the HasForeignKey<T>(string)
method allows you to define the shadow foreign-key property name. You can use any name; you don’t need to stick with the By Convention name listed in figure 7.3. But you need to be careful not to use a name of any existing property in the entity class you’re targeting, because that can lead to strange behaviors. (There’s no warning if you do select an existing property, as you might be trying to define a nonshadow foreign key.)
7.7.3 HasPrincipalKey—using an alternate unique key
I mentioned the term alternate key at the beginning of this chapter, and said it was a unique value but isn’t the primary key. I gave an example of an alternate key called UniqueISBN
, which represents a unique key that isn’t the primary key. (Remember, ISBN stands for International Standard Book Number, which is a unique number for every book.)
Now let’s look at a different example. You may be aware that the ASP.NET authorization library uses the user’s email address as its UserId
, which is unique for each user. The following listing creates a Person
entity class, which uses a normal int
primary key, but you’ll use the UserId
as an alternate key when linking to the person’s contact information, shown in listing 7.12.
Listing 7.11 Person
class, with UserId
taken from ASP.NET authorization
public class Person { public int PersonId { get; set; } public string Name { get; set; } [MaxLength(256)] [Required] public string UserId { get; set; } #1 }
Listing 7.12 ContactInfo
class with EmailAddress
as a foreign key
public class ContactInfo { public int ContactInfoId { get; set; } public string MobileNumber { get; set; } public string LandlineNumber { get; set; } [MaxLength(256)] [Required] public string EmailAddress { get; set; } #1 }
Figure 7.9 shows the Fluent API configuration commands, which use the alternate key in the Person
entity class as a foreign key in the ContactInfo
entity class.
Figure 7.9 The Fluent API sets up a one-to-one relationship by using the UserId
property, which contains the person’s email address and is unique, as the foreign key to link to the ContactInfo
. The command HasPrincipalKey
both defines the UserId
property as an alternate key and makes the foreign-key constraint link between the EmailAddress
property in the ContactInfo
entity and the User
I
d
in the Person
entity.

Here are a few notes on alternate keys:
- You can have composite alternate keys—an alternate key made up of two or more properties. This is handled in the same way as composite keys, by using an anonymous class. For example,
HasPrincipalKey<MyClass>(c => new {c.Part1, c.Part2})
. - Unique keys (see section 6.6) and alternate keys are different, and you should choose the correct one for your business case. Here are some of the differences:
- Unique keys ensure that each entry is unique; they can’t be used in a foreign key.
- Unique keys can be
null
, but alternate keys can’t. - Unique key values can be updated, but alternate keys can’t. (See EF Core issue #4073 at https://github.com/aspnet/EntityFramework/issues/4073.)
- You can define a property as a standalone alternate key by using the Fluent API command
modelBuilder.Entity<Car>().HasAlternateKey(c => c.LicensePlate)
, but there isn’t any need to do that, because using theHasPrincipalKey
method to set up a relationship automatically registers the property as an alternate key.
7.7.4 Less-used options in Fluent API relationships
This section briefly mentions but doesn’t cover in detail two Fluent API commands that can be used when setting up relationships.
HasConstraintName—setting the foreign-key constraint name
The method HasConstraintName
allows you to set the name of the foreign-key constraint. This can be useful if you want to catch the exception on foreign-key errors and use the constraint name to form a more user-friendly error message. Section 10.7.3 shows an example of setting the constraint name so that you can produce user-friendly error messages out of SQL errors.
MetaData—access to the relationship information
The MetaData
property provides access to the relationship data, some of which is read/write. Much of what the MetaData
property exposes can be accessed via specific commands, such as IsRequired
, but some parts can be useful. I recommend an article by an EF Core team member Arthur Vickers on MetaData
: http://mng.bz/YfiT.
7.8 Alternative ways of mapping entities to database tables
Sometimes it’s useful to not have a one-to-one mapping from an entity class to a database table. Instead of having a relationship between two classes, you might want to combine both classes into one table. This allows you to load only part of the table when you use one of the entities, which will improve the query’s performance. EF Core provides three alternative ways to map classes to the database, each with its own features:
- Owned types —This allows a class to be merged into the entity class’s table. Useful for using normal classes to group data.
- Table per hierarchy —This allows a set of inherited classes to be saved into one table; for instance, classes called
Dog
,Cat
, andRabbit
that inherit from theAnimal
class. - Table splitting —This allows multiple entity classes to be mapped to the same table. Useful when some columns in a table are read more often than all the table columns.
7.8.1 Owned types—adding a normal class into an entity class
EF Core has owned types, which allow you to define a class that holds a common grouping of data, such as an address or audit data, that you want to use in multiple places in your database. The owned type class doesn’t have a primary key, so doesn’t have an identity of its own, but relies on the entity class that “owns” it for its identity. In DDD terms, owned types are known as value objects.
EF6
EF Core’s owned types are similar to EF6.x’s complex types. The biggest change is you must specifically configure an owned type, whereas EF6.x considers any class without a primary key to be a complex type (which could cause bugs). EF Core’s owned types have an extra feature over EF6.x’s implementation: the data in an owned type can be configured to be saved into a separate, hidden table.
Here are two ways of using owned types:
- The owned type data is held in the same table that the entity class is mapped to.
- The owned type data is held in a separate table from the entity class.
Owned type data is held in the same table as the entity class
As an example of an owned type, you’ll create an entity class called OrderInfo
that needs two addresses: BillingAddress
and DeliveryAddress
. These are provided by the Address
class, as shown in this listing. The Address
class is an owned type with no primary key, as shown at the bottom of the listing.
Listing 7.13 The Address
owned type, followed by the OrderInfo
entity class
public class OrderInfo #1 { public int OrderInfoId { get; set; } public string OrderNumber { get; set; } public Address BillingAddress { get; set; } #2 public Address DeliveryAddress { get; set; } #2 } public class Address #3 { public string NumberAndStreet { get; set; } public string City { get; set; } public string ZipPostCode { get; set; } public string CountryCodeIso2 { get; set; } }
You tell EF Core that the BillingAddress
and the DeliveryAddress
properties in the OrderInfo
entity class aren’t relationships, but owned types, through the Fluent API. Listing 7.14 shows the configuration commands to do that.
Listing 7.14 The Fluent API to configure the owned types within OrderInfo
public class SplitOwnDbContext: DbContext { public DbSet<OrderInfo> Orders { get; set; } //… other code removed for clarity protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBulder.Entity<OrderInfo>() #1 .OwnsOne(p => p.BillingAddress); #2 modelBulder.Entity<OrderInfo>() #3 .OwnsOne(p => p.DeliveryAddress);#3 } }
The result is a table containing the two scalar properties in the OrderInfo
entity class, followed by two sets for Address
class properties, one prefixed by BillingAddress_
and another prefixed by DeliveryAddress_
. The following listing shows part of the SQL Server CREATE TABLE
command that EF Core produces for the OrderInfo
entity class with the naming convention.
Listing 7.15 The SQL CREATE TABLE
command showing the column names
CREATE TABLE [Orders] ( [OrderInfoId] int NOT NULL IDENTITY, [OrderNumber] nvarchar(max) NULL, [BillingAddress_City] nvarchar(max) NULL, [BillingAddress_CountryCodeIso2] nvarchar(max) NULL, [BillingAddress_NumberAndStreet] nvarchar(max) NULL, [BillingAddress_ZipPostCode] nvarchar(max) NULL, [DeliveryAddress_City] nvarchar(max) NULL, [DeliveryAddress_CountryCodeIso2] nvarchar(max) NULL, [DeliveryAddress_NumberAndStreet] nvarchar(max) NULL, [DeliveryAddress_ZipPostCode] nvarchar(max) NULL, CONSTRAINT [PK_Orders] PRIMARY KEY ([OrderInfoId]) );
Using owned types like this can help organize your database. Any common groups of data can be turned into owned types and added to entity classes. Here are two final points on owned types held in an entity class:
- You must provide all the owned class instances when you create a new instance to write to the database (for instance,
BillingAddress = new Address{…etc
.). If you don’t,SaveChanges
will throw an exception. - The owned type properties, such as
BillingAddress
, are automatically created and filled with data when you read the entity. There’s no need for anInclude
method or any other form of relationship loading.
Owned type data is held in a separate table from the entity class
The other way that EF Core can save the data inside an owned type is into a separate table, rather than the entity class. In this example, you’ll create a User
entity class that has a property called HomeAddress
of type Address
. In this case, you add a ToTable
method after the OwnsOne
method in your configuration code.
Listing 7.16 Configuring the owned table data to be stored in a separate table
public class SplitOwnDbContext: DbContext { public DbSet<OrderInfo> Orders { get; set; } //… other code removed for clarity protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBulder.Entity<User>() .OwnsOne(p => p.HomeAddress); .ToTable("Addresses"); #1 } }
EF Core sets up a one-to-one relationship, in which the primary key is also the foreign key (see section 7.6.1, option 3). And the OnDelete
state is set to Cascade
so that the owned type entry of the primary entity, User
, is deleted. The database therefore has two tables, the Users table and the Addresses table.
Listing 7.17 The two tables, Users and Addresses, in the database
CREATE TABLE [Users] ( [UserId] int NOT NULL IDENTITY, [Name] nvarchar(max) NULL, CONSTRAINT [PK_Orders] PRIMARY KEY ([UserId]) ); CREATE TABLE [Addresses] ( [UserId] int NOT NULL IDENTITY, [City] nvarchar(max) NULL, [CountryCodeIso2] nvarchar(max) NULL, [NumberAndStreet] nvarchar(max) NULL, [ZipPostCode] nvarchar(max) NULL, CONSTRAINT [PK_Orders] PRIMARY KEY ([UserId]), CONSTRAINT "FK_Addresses_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users" ("UserId") ON DELETE CASCADE );
This use of owned types differs from the first usage, in which the data is stored in the entity class table, because you can save a User
entity instance without an address. But the same rules apply on querying—the HomeAddress
property will be read in on a query of the User
entity, without the need for an Include
method.
The Addresses table used to hold the HomeAddress
data is hidden; you can’t access it via EF Core. This could be a good thing or a bad thing, depending on your business needs. But if you want to access the Address
part, you can implement the same feature by using two entity classes with a one-to-one relationship between them.
7.8.2 Table per hierarchy—placing inherited classes into one table
Table per hierarchy (TPH) stores all the classes that inherit from each other in a single database table. For instance, if you want to save a payment in a shop, it could be cash (PaymentCash
) or credit card (PaymentCard
). Both contain the amount (say, $10), but the credit card option has extra information; an online transaction receipt for instance. In this case, TPH uses a single table to store all the versions of the inherited classes and return the correct entity type, PaymentCash
or PaymentCard,
depending on what was saved.
TPH can be configured by convention, which will then combine all the versions of the inherited classes into one table. This has the benefit of keeping common data in one table, but accessing that data is a little cumbersome because each inherited type has its own DbSet<T>
property. But by adding the Fluent API, all the inherited classes can be accessed via one DbSet<T>
property, which in our example makes the PaymentCash
/ PaymentCard
example much more useful.
Configuring TPH by convention
To apply the By Convention approach to the PaymentCash/PaymentCard
example, you create a class called PaymentCash
and then another class, PaymentCard
, which inherits from PaymentCash
classes, as shown in this listing. As you can see, PaymentCard
inherits from PaymentCash
and adds an extra ReceiptCode
property.
Listing 7.18 The two classes: PaymentCash
and PaymentCard
public class PaymentCash { [Key] public int PaymentId { get; set; } public decimal Amount { get; set; } } //PaymentCredit – inherits from PaymentCash public class PaymentCard : PaymentCash { public string ReceiptCode { get; set; } }
Listing 7.19, which uses the By Convention approach, shows your application’s DbContext with two DbSet<T>
properties, one for each of the two classes. Because you include both classes, and PaymentCard
inherits from PaymentCash,
EF Core will store both classes in one table.
Listing 7.19 The updated application’s DbContext with the two DbSet<T>
properties
public class Chapter07DbContext : DbContext { //… other DbSet<T> properties removed //Table-per-hierarchy public DbSet<PaymentCash> CashPayments { get; set; } public DbSet<PaymentCard> CreditPayments { get; set; } public Chapter07DbContext( DbContextOptions<Chapter07DbContext> options) : base(options) { } protected override void OnModelCreating (ModelBuilder modelBuilder) { //no configuration needed for PaymentCash or PaymentCard } }
Finally, this listing shows the code that EF Core produces to create the table that will store both the PaymentCash
and PaymentCard
entity classes.
Listing 7.20 The SQL produced by EF Core to build the CashPayment
table
CREATE TABLE [CashPayments] ( [PaymentId] int NOT NULL IDENTITY, [Amount] decimal(18, 2) NOT NULL, [Discriminator] nvarchar(max) NOT NULL, #1 [ReceiptCode] nvarchar(max), #2 CONSTRAINT [PK_CashPayments] PRIMARY KEY ([PaymentId]) );
As you can see, EF Core has added a Discriminator column, which it uses when returning data to create the correct type of class, PaymentCash
or PaymentCard
, based on what was saved. Also, the ReceiptCode column is filled/read only if the class type is PaymentCard
.
Using the Fluent API to improve our TPH example
Although the By Convention approach reduces the number of tables in the database, you have two separate DbSet<T>
properties, and you need to use the right one to find the payment that was used. Also, you don’t have a common Payment
class that you can use in any other entity classes. But by a bit of rearranging and adding some Fluent API configuration, you can make this solution much more useful.
Figure 7.10 shows the new arrangement. You create a common base class by having an abstract class called Payment
that the PaymentCash
and PaymentCard
inherit from. This allows you to use the Payment
class in another entity class called SoldIt
.
Figure 7.10 By using the Fluent API, you can create a more useful form of the TPH. Here an abstract class called Payment
is used as the base, and this class can be used inside another entity class. The actual class type placed in the SoldIt
payment property will be either PaymentCash
or PaymentCard
, depending on what was used when the SoldI
t
class was created.

This approach is much more useful because you can now place a Payment
abstract class in the SoldIt
entity class and get the amount and type of payment, regardless of whether it’s cash or a card. The PType
property tells you the type (the PType
property is of type PTypes
, which is an enum with values Cash
or Card
), and if you need the Receipt
property in the PaymentCard
, you can cast the Payment
class to the type PaymentCard
.
In addition to creating the entity classes shown in figure 7.10, you also need to change the application’s DbContext and add some Fluent API configuration to tell EF Core about your TPH classes, as they no longer fit the By Convention approach. This listing shows the application’s DbContext, with the configuration of the Discrimination column.
Listing 7.21 Changed application’s DbContext with Fluent API configuration added
public class Chapter07DbContext : DbContext { //… other DbSet<T> properties removed public DbSet<Payment> Payments { get; set; } #1 public DbSet<SoldIt> SoldThings { get; set; } #2 public Chapter07DbContext( DbContextOptions<Chapter07DbContext> options) : base(options) { } protected override void OnModelCreating (ModelBuilder modelBuilder) { //… other configuretions removed modelBuilder.Entity<Payment>() .HasDiscriminator(b => b.PType) #3 .HasValue<PaymentCash>(PTypes.Cash) #4 .HasValue<PaymentCard>(PTypes.Card); #5 } }
Note
This example uses an abstract class as the base class, but you don’t have to do that. You could just as well keep the original PaymentCash
, with the PaymentCard
inheriting from that. I wanted to show you that EF Core can handle an abstract base class.
Accessing TPH entities
Now that you’ve configured a TPH set of classes, let’s cover any differences in CRUD operations. Most EF database access commands are the same, but a few changes access the TPH parts of the entities. EF Core does a nice job (as EF6.x did) of handling TPH.
First, the creation of TPH entities is straightforward. You create an instance of the specific type you need. For instance, the following code snippet creates a PaymentCash
type entity to go with a sale:
var sold = new SoldIt() { WhatSold = "A hat", Payment = new PaymentCash {Amount = 12} }; context.Add(sold); context.SaveChanges();
EF Core then saves the correct version of data for that type, and sets the discriminator so it knows the TPH class type of the instance. When you read back the SoldIt
entity you just saved, with an Include
to load the Payment
navigational property, the type of the loaded Payment
instance will be the correct type (PaymentCash
or PaymentCard
), depending on what was used when you wrote it to the database. Also, in this example the Payment
’s property PType
, which you set as the discriminator, tells you the type of payment, Cash
or Card
.
When querying TPH data, the EF Core OfType<T>
method allows you to filter TPH data to find a specific class. The query context.Payments.OfType<PaymentCard>()
would return only the payments that used a card, for example.
Updating the data inside a TPH entity uses all the normal conventions. But changing the type of the entity (from PaymentCard
to PaymentCash
) is possible but difficult. You need to set the discriminator value in your code and configure the discriminator value’s AfterSaveBehavior
to PropertySaveBehavior.Save
.
Listing 7.22 The updated application’s DbContext with the two DbSet<T>
properties
public class Chapter07DbContext : DbContext { //… other code removed protected override void OnModelCreating (ModelBuilder modelBuilder) { //… other configuretions removed modelBuilder.Entity<Payment>() .HasDiscriminator(b => b.PType) .HasValue<PaymentCash>(PTypes.Cash) .HasValue<PaymentCard>(PTypes.Card); entity.Property(p => p.PType) #1 .Metadata.AfterSaveBehavior = #1 PropertySaveBehavior.Save; #1 } }
Note
EF Core 2.1 adds a further small, but useful improvement to THP handling. I list this in section B.2.4.
7.8.3 Table splitting—mapping multiple entity classes to the same table
The final feature, called table splitting, allows you to map multiple entities to the same table. This is useful if you have a large amount of data to store for one entity, but your normal queries to this entity need only a few columns. It’s like building a Select
query into an entity class; the query will be quicker because you’re loading only a subsection of the whole entity’s data.
This example has two entity classes, BookSummary
and BookDetail
, that both map to a database table called Books. Figure 7.11 shows the result of configuring these two entity classes as a table split.
Figure 7.11 The result of using the table-splitting feature in EF Core to map two entity classes, BookSummary
and BookDetail
, to one table, Books. You do this because a book needs a lot of information, but most queries need only the BookSummary
part. The effect is to build a preselected set of columns for faster querying.

Here’s the configuration code to achieve this.
Listing 7.23 Configuring a table split between BookSummary
and BookDetail
public class SplitOwnDbContext : DbContext { //… other code removed protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBuilder.Entity<BookSummary>() #1 .HasOne(e => e.Details) #1 .WithOne() #1 .HasForeignKey<BookDetail> #2 (e => e.BookDetailId); #2 modelBuilder.Entity<BookSummary>() #3 .ToTable("Books"); #3 modelBuilder.Entity<BookDetail>() #3 .ToTable("Books"); #3 } }
After you’ve configured the two entities as a table split, you can query the BookSummary
entity on its own and get the summary parts. To get the BookDetails
part, you can either query the BookSummary
entity and load the Details
relationship property at the same time (say, with an Include
method) or read just the BookDetails
part straight from the database.
A few points before leaving this topic:
- When you create a new entity that’s table-split to be added into the database, you must define all the parts of the table split. For example, in the case of the
BookSummary
and theBookDetails
case,BookSummary
must have an instance of theBookDetails
entity class assigned to itsDetails
property before you callSaveChanges
. - You can update an individual entity class in a table split individually; you don’t have to load all the entities involved in a table split to do an update.
- You’ve seen a table split to two entity classes, but you can table-split any number of entity classes.
Summary
- If you follow the By Convention naming rules for foreign keys, EF Core can find and configure most normal relationships.
- Two Data Annotations provide a solution to a couple of specific issues around foreign keys with names that don’t fit the By Convention naming rules.
- The Fluent API is the most comprehensive way to configure relationships, and some features, such as setting the action on deletion of the dependent entity, are available only via the Fluent API.
- EF Core provides three alternative ways to map entity classes to a database table: owned types, table per hierarchy, and table splitting.
For readers who are familiar with EF6:
- The basic process of configuring relationships in EF Core is the same as in EF6.x, but the Fluent API commands have changed significantly.
- EF6.x adds foreign keys if you forget to add them yourself, but they aren’t accessible via normal EF6.x commands. EF Core allows you to access them via shadow properties.
- EF6.x provides a many-to-many relationship directly, but EF Core doesn’t. You need to use two one-to-many relationships with a linking table in the middle.
- EF Core has introduced new features, such as access to shadow properties, alternate keys, and backing fields.
- EF Core’s feature called owned types provides similar features to EF6.x’s complextypes.
- EF Core’s table-per-hierarchy feature is similar to EF6.x’s table-per-hierarchy feature.
- EF Core’s table-splitting feature is similar to EF6.x’s table-splitting feature.