7 Configuring relationships

published book

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.

join today to enjoy all our content. all the time.
 

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.
c07_01.png

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

A principal key and a foreign key can consist of more than one property/column. These are called composite keys. You’ve already seen one in section 3.4.4, as the BookAuthor many-to-many linking entity class has a composite primary key consisting of the BookId and the AuthorId.

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.

Get Entity Framework Core in Action
add to cart

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 the Review entity classes? I say yes, because we want to calculate the average review score.
  • Does the Review entity class need to know about the Book 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.

Sign in for more free preview time

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.

join today to enjoy all our content. all the time.
 

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:

  1. EF Core scans the application’s DbContext, looking for any public DbSet<T> properties. It assumes the classes, T, in the DbSet<T> properties are entity classes.
  2. 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 the IEnumerable<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.

  1. 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.
c07_02.png

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 to Cascade. If the principal entity is deleted, the dependent entity will be deleted too.
  • For an optional relationship, EF Core sets the OnDelete action to ClientSetNull. If the dependent entity is being tracked, the foreign key will be set to null when theprincipal entity is deleted. But if the dependent entity isn’t being tracked, the database settings take over, and the entity is set to Restrict, 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.
c07_03.png

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).
Sign in for more free preview time

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

I suggest you use the nameof keyword to provide the property name string. That’s safer, because if you change the name of the foreign-key property, nameof will either be updated at the same time, or throw a compile error if you forgot to change all the references.

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.

Tour livebook

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.
take the tour

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.
c07_04.png
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 Ticket, whereas in options 2 and 3, the Ticket is optional for the Attendee.
c07_05.png

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.
c07_06.png

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.
c07_07.png

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 Reviews” 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.
c07_08.png

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

Internally, EF Core uses HashSet<T> to hold a collection. In some specific cases with noninitialized backing field collections, you may need to use the HashSet<T> type as the collection type.

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 the BookAuthor linking entity class
  • A one-to-many relationship from the Author entity class to the BookAuthor 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.

join today to enjoy all our content. all the time.
 

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 and MetaData 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
Restrict
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.
SetNull
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.
ClientSetNull
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
Cascade
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 to null, 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 UserId in the Person entity.
c07_09.png

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 the HasPrincipalKey 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, and Rabbit that inherit from the Animal 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 an Include 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 SoldIt class was created.
c07_10.png

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.
c07_11.png

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 the BookDetails case, BookSummary must have an instance of the BookDetails entity class assigned to its Details property before you call SaveChanges.
  • 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.
sitemap

Unable to load book!

The book could not be loaded.

(try again in a couple of minutes)

manning.com homepage