Application data
.NET .NET Core Angular C# Entity Framework Core Visual Studio

Best Practices with ORM Patterns and Entity Framework Core

Welcome to today’s post.

In today’s post I will investigate ways in which lazy loading can be beneficial in a full-stack application.

In previous posts I showed how to load entities from a data store using the Entity Framework Core ORM with the three ORM loading patterns, lazy loading, eager loading, and explicit loading.

From the Web API side, the default loading in Entity Framework Core is to disable lazy loading. The data context setup is done by registering a data context using the AddDbContext() extension method with Entity Framework Core:

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(Environment.GetEnvironmentVariable("DB_CONN_STR")));

To enable lazy loading, we can use the UseLazyLoadingProxies() extension method from DbContextOptions:

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseLazyLoadingProxies(true)                          
        .UseSqlServer(Environment.GetEnvironmentVariable("DB_CONN_STR")));

The full details of how this is done are in the above posts I mentioned.

I will go through some scenarios in a front-end web interface to show how to optimally load data from a Web API.

Interfaces with Master Detail Relationships

The first example is to load a structure from the Web API that consists of a collection of entity records, with each record containing loaded reference to an entity.

The example we have is a list of book reviews, with each review record containing a navigation reference property to the corresponding book entity.

The Web API method to obtain the review records is shown:

[HttpGet("api/[controller]/GetBookReviews/{id}")]
public async Task<IActionResult> GetBookReviews(int id)
{
    var bookReviews = await _reviewService.GetBookReviews(id);
    return Ok(bookReviews);
}

With the corresponding service method to retrieve review records is shown:

public async Task<IEnumerable<ReviewViewModel>> GetBookReviews(int bookid)
{
    return await _db.Reviews.Where(b => b.BookID == bookid).ToListAsync();
}

Our Review POCO model is shown:

public class ReviewViewModel
{
    public int ID { get; set; }
    public string Reviewer { get; set; }
    public string Heading { get; set; }
    public string Comment { get; set; }
    public int Rating { get; set; }
    public DateTime DateReviewed { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
    public int BookID { get; set; }
    public bool IsVisible { get; set; }
    public string Approver { get; set; }
    public virtual BookViewModel Book { get; set; }
}

When we configure our Web API service without lazy-loading or eager-loading the result of the API method returns a collection of review records. The Book navigational reference property is null when the JSON response is returned to the web client:

[
  {
    "id": 4,
    "reviewer": "andyh",
    "author": null,
    "title": null,
    "heading": "a very entertaining book",
    "comment": "excellent read!",
    "rating": 5,
    "dateReviewed": "0001-01-01T00:00:00",
    "dateCreated": "2021-03-15T22:01:40.1994501",
    "dateUpdated": "0001-01-01T00:00:00",
    "bookID": 1,
    "isVisible": false,
    "approver": "",
    "book": null
  },

Our front-end Angular interface component, which is shown below consists of a book details header, followed by a list of book reviews as shown:

When we are loading API data into our component, we make two calls to API services to retrieve the form data:

  1. Book review records for the currently book.
  2. Book details (for the header).

The typescript to obtain the data is shown below (code to assign data to internal controls omitted):

    let getBook$ = this.apiService.getBook(this.id);
    let getBookReviews$ = this.apiService.getBookReviews(this.id);

    this.subscription = combineLatest(
        [
            getBook$, 
            getBookReviews$
        ]
    )
    .subscribe(([book, reviews]) => {
        if (!book || !reviews)
            return of(null);
	…

When we enable lazy-loading or eager-loading, the JSON response from the API method is shown below (with large sections removed for brevity):

[
  {
    "book": {
      "loans": [
        {
		  ...
        },
	    ... 
      ],
      "reviews": [
	    ...
      ],
      "id": 1,
      "title": "The Lord of the Rings",
      "author": "J. R. R. Tolkien",
      "yearPublished": 1954,
      "genre": "fantasy",
      "edition": "0",
      "isbn": "654835",
      "location": "sydney",
      "dateCreated": "2019-11-05T00:00:00",
      "dateUpdated": "2021-02-17T12:16:39.7501682"
    },
    "id": 4,
    "reviewer": "andyh",
    "author": null,
    "title": null,
    "heading": "a very entertaining book",
    "comment": "excellent read!",
    "rating": 5,
    "dateReviewed": "0001-01-01T00:00:00",
    "dateCreated": "2021-03-15T22:01:40.1994501",
    "dateUpdated": "0001-01-01T00:00:00",
    "bookID": 1,
    "isVisible": false,
    "approver": ""
  },
  ...

From the book property reference, we could read the book details without running a separate Web API call to retrieve the book details from the Web API.

This can be done from the component loading as follows:

let getBookReviews$ = this.apiService.getBookReviews(this.id);

this.subscription = getBookReviews$
    .subscribe(reviews => {
  	    if (reviews.length === 0)
            return of(null);
        var book = review[0].book;	

        reviews.forEach(itm => 
        {
            bookReviewReport = { id: book.id,                     
                                 title: book.title,  
                                 author: book.author, 
                                 yearPublished: book.yearPublished,  
                                 genre: book.genre, 
                                 edition: book.edition, 
                                 isbn: book.isbn, 
                                 location: book.location, 
                                 reviews: reviews };                                     
            });
  	        …

We would use the same API method to retrieve the data for the form, however the only difference with the previous retrieval method would be the retrieval of the book header details from the reference within the response data from the single API request.

Where we have a form that contains detail records, then using eager loading would be beneficial as there would be one API request to retrieve the data.

This can be done from the API method as shown:

public async Task<List<ReviewViewModel>> GetBookReviews(int id)
{
    List<ReviewViewModel> result = _db.Reviews
       	.Where(r => r.BookID == id)
        .Include(b => b.Book)
        .ToList();
    return result;
}

Each review that is returned contains the book header. 

In the next section, I will show how to optimally load pages of search results that contain record headers with detail records.

Search Results or Screens with Pages of Records

With each page of records, the data loading strategy would depend on whether the displayed records required additional detailed data to be displayed inline on demand by the user or if the detailed data could be displayed through an additional hyperlink.

In the example below we have data pre-loaded and accessible in the same list as an inline collapsible view:

In the case of using inline detailed data, the use of either lazy or eager loaded data would be beneficial to populate detailed reference and collection data. This would be a performance benefit as the data would be requested the least number of times from an API that is configured to use a lazy loaded data context. Within the context of an API request the number of database requests would also be minimal with the pre-loading of reference and collection entities.

In the case of using an explicit linked detailed data, the use of eager or explicitly loaded data would be more beneficial as the detailed data would only be shown to the user through a detail screen that would load the detail record properties from an API data context that is configured to return data that is eagerly or explicitly loaded. This would improve query performance.

In an Angular template we would include flags in our data models to determine collapsing or expanding detailed and reference data. Below are the header records for our book list with the section with ellipsis a placeholder for template HTML for collapsible views:

<mat-list *ngFor="let book of bookView">
    <div style="display: flex; flex-wrap: wrap; 
        border-style: solid; border-width: 1px;">
        <div class="grid-row" style="flex: 0 5%;" 
   	        class="clickLink" (click)="expandBookRecord(book)">   
            {{book.isCollapsed ? '+' : '&ndash;'}}
        </div>
        <div class="grid-row" style="flex: 0 40%;">{{book.title}}</div>
        <div class="grid-row" style="flex: 0 35%;">{{book.author}}</div>
        <div class="grid-row" style="flex: 0 20%;" class="clickLink" 
            (click)="selectBookById(book.id)"><u>VIEW</u>
        </div>
    </div> 
    …
    …
</mat-list>

Our models include a class for the BookDetail class which is bound to the Angular template Material list.

import { LoanView } from "./LoanView";

export class BookDetail
{
    public id: number;
    public title: string;
    public author: string;
    public yearPublished: number;
    public genre: string;
    public edition: string;
    public isbn: string;
    public location: string;
    public dateCreated: Date;
    public dateUpdated: Date;
    public isCollapsed: boolean;
    public numberLoans: number;
    public isLoanCollapsed: boolean;
    public loans: LoanView[];
}

And a class for the book’s loan data:

import { Book } from "./Book";

export class LoanView
{
    public id: number;
    public loanedBy: string;
    public dateLoaned: Date
    public dateDue: Date;
    public dateReturn: Date; 
    public onShelf: boolean;
    public bookID: number;
    public book: Book;
    public returnMethod: string;
}

In our typescript without lazy loading, we load the book data and loans separately from subscriptions as shown:

ngOnInit() {
    this.subscription = this.api.getBooks().pipe(delay(this.apidelay))
        .subscribe(res => {
            this.hasLoaded$.next(true);
            this.books = res;
            this.bookView = this.books;
            this.bookView.map(b =>
            {
                b.isCollapsed = true;
                b.isLoanCollapsed = true;
                b.loans = [];
             });
             this.numberBooks = this.books.length;
             console.log("read books from API");
        });

    this.subscription2 = this.api.getGetTopBooksLoaned()
        .pipe(delay(this.apidelay)).subscribe(res => {
            this.topLoanedBooks = res;
        });
}

In typescript with lazy loading, we load the book data from a subscriber in one request as shown:

this.subscription = this.api.getBooks().pipe(delay(this.apidelay))
    .subscribe(res => {
  	    this.hasLoaded$.next(true);
        this.books = res;
        this.bookView = this.books;
        this.bookView.map(b =>
        {
            b.isCollapsed = true;
            b.isLoanCollapsed = true;
        });
        this.numberBooks = this.books.length;
        console.log("read books from API");
    });

Without lazy loading, we populate the above BookDetail class structure with a loan count property. The book view collapsible state is toggled.

expandBookRecord(event: any)
{
    event.isCollapsed = !event.isCollapsed;
    this.topLoanedBooks.forEach(l => 
    {
        if (event.id == l.bookDetails.id) 
        {
            event.numberLoans = l.count;
         }
    })
}

The loan view class LoanView is populated from a subscription then set to the loans array property of the BookDetail class when the loan view is expanded. The loan view collapsible state is toggled.

expandLoanLink(event: any)
{
    event.isLoanCollapsed = !event.isLoanCollapsed;

    this.api.getBookLoans(event.id)
 	    .pipe(delay(this.apidelay))
        .subscribe(res => {            
            this.bookView.forEach(l => 
            {
     	        if (event.id == l.id) 
                {
                    if (event.loans.length == 0)
                        event.loans = res;
                }
            })            
        });
}

Below is the HTML template ng container for the collapsible book details:

<ng-container *ngIf="!book.isCollapsed">
    <div style="display: flex; flex-wrap: wrap; 
          border-style: solid; border-width: 1px;">
        <div style="flex: 0 5%;">
 	        <u><strong>&nbsp;</strong></u>
        </div> 
        <div style="flex: 0 5%;">&nbsp;</div>
        <div style="flex: 0 10%;">
            <u><strong>AUTHOR</strong></u>
        </div> 
        <div style="flex: 0 35%;">
            {{book.author}}
        </div>
        <div style="flex: 0 10%;">
            <u><strong>GENRE</strong></u>
        </div> 
 	    <div style="flex: 0 35%;">{{book.genre}}</div>
    </div>
    <div style="display: flex; flex-wrap: wrap; 
          border-style: solid; border-width: 1px;">
        <div style="flex: 0 5%;">
            <u><strong>&nbsp;</strong></u>
        </div> 
        <div style="flex: 0 5%;">&nbsp;</div>
        <div style="flex: 0 10%;">
 	        <u><strong>TITLE</strong></u>
        </div> 
        <div style="flex: 0 35%;">
            {{book.title}}
        </div>
        <div style="flex: 0 10%;">
            <u><strong>ISBN</strong></u>
        </div>
        <div style="flex: 0 35%;">
            {{book.isbn}}
        </div>
    </div>
    <div style="display: flex; flex-wrap: wrap; 
          border-style: solid; border-width: 1px;">    
        <div style="flex: 0 5%;">
            <u><strong>&nbsp;</strong></u>
        </div> 
        <div style="flex: 0 5%;">&nbsp;</div>
        <div style="flex: 0 10%;"><u><strong>YEAR</strong></u></div>
        <div style="flex: 0 35%;">{{book.yearPublished}}</div>
        <div style="flex: 0 10%;" class="clickLink" 
            (click)="expandLoanLink(book)"><u>
            {{book.isLoanCollapsed ? '+' : '&ndash;'}}
 			<strong>#LOANS</strong></u>
        </div>
  		<div style="flex: 0 35%;">{{book.numberLoans}}</div>
    </div>
</ng-container>  

Below is the HTML template ng container for the collapsible loan details:

<ng-container *ngIf="!book.isLoanCollapsed">
    <div *ngFor="let loan of book.loans" style="display: flex; 
          flex-wrap: wrap; border-style: solid; 
          border-width: 1px;">   
        <div style="flex: 0 10%;">
            <u><strong>&nbsp;</strong></u>
        </div> 
 		<div style="flex: 0 10%;">&nbsp;</div>
        <div style="flex: 0 15%;">
            <u><strong>DATE LOANED</strong></u>
        </div> 
        <div style="flex: 0 25%;">{{loan.dateLoaned}}</div>
        <div style="flex: 0 15%;">
            <u><strong>LOANED BY</strong></u>
        </div> 
        <div style="flex: 0 25%;">
            {{loan.loanedBy}}
        </div>
    </div>
</ng-container>

With the collapsible grid data, with lazy loading we would load data from our API for each BookDetail class structure with the loans array property pre-loaded. In this case the expand method for the loan data would be simplified as shown:

expandLoanLink(event: any)
{
    event.isLoanCollapsed = !event.isLoanCollapsed;
    event.numberLoans = event.loans.length;
}

With the above simplification, the book details expand link event would be further simplified:

expandBookRecord(event: any)
{
    event.isCollapsed = !event.isCollapsed;
}

In the example below we have loaded the header data into the list, with the detail records accessible in the same list as a hyperlink labelled VIEW:

If data changes through a CRUD operation between each data request call to a Web API, the changed data will be reflected in data that is returned from the API as the lifetime of the data context is scoped to the request. Each request would re-query the data, which includes committed changed data.

Using Charting and Analysis Based Applications

Another use for lazy loading that would be the most beneficial is when the client application is a dedicated analysis tool such as charting and decision support. In such an application we would be using minimal data CRUD operations, and mostly data reads that are used to pre-populate in-memory structures or DTOs with historical data and lookup tables that would be ideally used for reports that extensively use graphing components to display data with grouping and aggregations (averaging, grouping, counts etc.). One increasingly popular tool that is being used to load and persist data in front end web applications is NgRx store, which I discussed in a previous post

Choosing a data loading strategy for read-only based reporting vs real-time based reporting

In this case, pre-loaded data would allow graphical based reports to load much faster than reports that use eager loaded data. The only disadvantage with this approach would be that the data would not be real-time and would require a periodic polled notification from the API to refresh out of date data.

The benefits of having faster graphical reports would outweigh the need for real-time data in most of these types of analytical applications. The only case where pre-loaded data would be a disadvantage would be real-time based graphical reports that require data to be updated much more frequently. In this case, there is a need to use a separate API that returns data that is using a data context that disables lazy loading. In this case, the data would be retrieved by the client faster than a lazy loaded API. 

With an application that requires real-time data, the best approach would be to mix lazy loading and explicit loading and utilize the loading approach based on the performance need for the part of the application. So initial loading of data would be better served using lazy loading, and real-time charting would be better served using explicit data loading.  

To summarize, the use of loading strategy should not be decided in isolation; it really depends on the requirements of each interface on how often the end user is required to access detailed and reference data from an entity.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial