Handling Entity Relations
by Mike Wasson
This section describes some details of how EF loads related entities, and how to handle circular navigation properties in your model classes. (This section provides background knowledge, and is not required to complete the tutorial. If you prefer, skip to Part 5..)
Eager Loading versus Lazy Loading
When using EF with a relational database, it’s important to understand how EF loads related data.
It’s also useful to see the SQL queries that EF generates. To trace the SQL, add the following line of code to the BookServiceContext
constructor:
[!code-csharpMain]
1: public BookServiceContext() : base("name=BookServiceContext")
2: {
3: // New code:
4: this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
5: }
If you send a GET request to /api/books, it returns JSON like the following:
[!code-consoleMain]
1: [
2: {
3: "BookId": 1,
4: "Title": "Pride and Prejudice",
5: "Year": 1813,
6: "Price": 9.99,
7: "Genre": "Comedy of manners",
8: "AuthorId": 1,
9: "Author": null
10: },
11: ...
You can see that the Author property is null, even though the book contains a valid AuthorId. That’s because EF is not loading the related Author entities. The trace log of the SQL query confirms this:
[!code-consoleMain]
1: SELECT
2: [Extent1].[BookId] AS [BookId],
3: [Extent1].[Title] AS [Title],
4: [Extent1].[Year] AS [Year],
5: [Extent1].[Price] AS [Price],
6: [Extent1].[Genre] AS [Genre],
7: [Extent1].[AuthorId] AS [AuthorId]
8: FROM [dbo].[Books] AS [Extent1]
The SELECT statement takes from the Books table, and does not reference the Author table.
For reference, here is the method in the BooksController
class that returns the list of books.
[!code-csharpMain]
1: public IQueryable<Book> GetBooks()
2: {
3: return db.Books;
4: }
Let’s see how we can return the Author as part of the JSON data. There are three ways to load related data in Entity Framework: eager loading, lazy loading, and explicit loading. There are trade-offs with each technique, so it’s important to understand how they work.
Eager Loading
With eager loading, EF loads related entities as part of the initial database query. To perform eager loading, use the System.Data.Entity.Include extension method.
[!code-csharpMain]
1: public IQueryable<Book> GetBooks()
2: {
3: return db.Books
4: // new code:
5: .Include(b => b.Author);
6: }
This tells EF to include the Author data in the query. If you make this change and run the app, now the JSON data looks like this:
[!code-consoleMain]
1: [
2: {
3: "BookId": 1,
4: "Title": "Pride and Prejudice",
5: "Year": 1813,
6: "Price": 9.99,
7: "Genre": "Comedy of manners",
8: "AuthorId": 1,
9: "Author": {
10: "AuthorId": 1,
11: "Name": "Jane Austen"
12: }
13: },
14: ...
The trace log shows that EF performed a join on the Book and Author tables.
[!code-consoleMain]
1: SELECT
2: [Extent1].[BookId] AS [BookId],
3: [Extent1].[Title] AS [Title],
4: [Extent1].[Year] AS [Year],
5: [Extent1].[Price] AS [Price],
6: [Extent1].[Genre] AS [Genre],
7: [Extent1].[AuthorId] AS [AuthorId],
8: [Extent2].[AuthorId] AS [AuthorId1],
9: [Extent2].[Name] AS [Name]
10: FROM [dbo].[Books] AS [Extent1]
11: INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[AuthorId]
Lazy Loading
With lazy loading, EF automatically loads a related entity when the navigation property for that entity is dereferenced. To enable lazy loading, make the navigation property virtual. For example, in the Book class:
[!code-csharpMain]
1: public class Book
2: {
3: // (Other properties)
4:
5: // Virtual navigation property
6: public virtual Author Author { get; set; }
7: }
Now consider the following code:
[!code-csharpMain]
1: var books = db.Books.ToList(); // Does not load authors
2: var author = books[0].Author; // Loads the author for books[0]
When lazy loading is enabled, accessing the Author
property on books[0]
causes EF to query the database for the author.
Lazy loading requires multiple database trips, because EF sends a query each time it retrieves a related entity. Generally, you want lazy loading disabled for objects that you serialize. The serializer has to read all of the properties on the model, which triggers loading the related entities. For example, here are the SQL queries when EF serializes the list of books with lazy loading enabled. You can see that EF makes three separate queries for the three authors.
[!code-consoleMain]
1: SELECT
2: [Extent1].[BookId] AS [BookId],
3: [Extent1].[Title] AS [Title],
4: [Extent1].[Year] AS [Year],
5: [Extent1].[Price] AS [Price],
6: [Extent1].[Genre] AS [Genre],
7: [Extent1].[AuthorId] AS [AuthorId]
8: FROM [dbo].[Books] AS [Extent1]
9:
10: SELECT
11: [Extent1].[AuthorId] AS [AuthorId],
12: [Extent1].[Name] AS [Name]
13: FROM [dbo].[Authors] AS [Extent1]
14: WHERE [Extent1].[AuthorId] = @EntityKeyValue1
15:
16: SELECT
17: [Extent1].[AuthorId] AS [AuthorId],
18: [Extent1].[Name] AS [Name]
19: FROM [dbo].[Authors] AS [Extent1]
20: WHERE [Extent1].[AuthorId] = @EntityKeyValue1
21:
22: SELECT
23: [Extent1].[AuthorId] AS [AuthorId],
24: [Extent1].[Name] AS [Name]
25: FROM [dbo].[Authors] AS [Extent1]
26: WHERE [Extent1].[AuthorId] = @EntityKeyValue1
There are still times when you might want to use lazy loading. Eager loading can cause EF to generate a very complex join. Or you might need related entities for a small subset of the data, and lazy loading would be more efficient.
One way to avoid serialization problems is to serialize data transfer objects (DTOs) instead of entity objects. I’ll show this approach later in the article.
Explicit Loading
Explicit loading is similar to lazy loading, except that you explicitly get the related data in code; it doesn’t happen automatically when you access a navigation property. Explicit loading gives you more control over when to load related data, but requires extra code. For more information about explicit loading, see Loading Related Entities.
Navigation Properties and Circular References
When I defined the Book and Author models, I defined a navigation property on the Book
class for the Book-Author relationship, but I did not define a navigation property in the other direction.
What happens if you add the corresponding navigation property to the Author
class?
[!code-csharpMain]
1: public class Author
2: {
3: public int AuthorId { get; set; }
4: [Required]
5: public string Name { get; set; }
6:
7: public ICollection<Book> Books { get; set; }
8: }
Unfortunately, this creates a problem when you serialize the models. If you load the related data, it creates a circular object graph.
When the JSON or XML formatter tries to serialize the graph, it will throw an exception. The two formatters throw different exception messages. Here is an example for the JSON formatter:
[!code-consoleMain]
1: {
2: "Message": "An error has occurred.",
3: "ExceptionMessage": "The 'ObjectContent`1' type failed to serialize the response body for content type
4: 'application/json; charset=utf-8'.",
5: "ExceptionType": "System.InvalidOperationException",
6: "StackTrace": null,
7: "InnerException": {
8: "Message": "An error has occurred.",
9: "ExceptionMessage": "Self referencing loop detected with type 'BookService.Models.Book'.
10: Path '[0].Author.Books'.",
11: "ExceptionType": "Newtonsoft.Json.JsonSerializationException",
12: "StackTrace": "..."
13: }
14: }
Here is the XML formatter:
[!code-xmlMain]
1: <Error>
2: <Message>An error has occurred.</Message>
3: <ExceptionMessage>The 'ObjectContent`1' type failed to serialize the response body for content type
4: 'application/xml; charset=utf-8'.</ExceptionMessage>
5: <ExceptionType>System.InvalidOperationException</ExceptionType>
6: <StackTrace />
7: <InnerException>
8: <Message>An error has occurred.</Message>
9: <ExceptionMessage>Object graph for type 'BookService.Models.Author' contains cycles and cannot be
10: serialized if reference tracking is disabled.</ExceptionMessage>
11: <ExceptionType>System.Runtime.Serialization.SerializationException</ExceptionType>
12: <StackTrace> ... </StackTrace>
13: </InnerException>
14: </Error>
One solution is to use DTOs, which I describe in the next section. Alternatively, you can configure the JSON and XML formatters to handle graph cycles. For more information, see Handling Circular Object References.
For this tutorial, you don’t need the Author.Book
navigation property, so you can leave it out.
|