Adding Asynchronous Processing to ASP.NET Core 3.1 Razor Pages Applications Built With the MVVM Design Pattern

July 12, 2020
Written by
AJ Saulsberry
Opinions expressed by Twilio contributors are their own

Adding Asynchronous Processing to ASP.NET Core 3.1 Razor Pages Applications Built With the MVVM Design Pattern

If you’re building web applications with ASP.NET Core you’re likely to be looking for ways to make them performant as they scale. Using the asynchronous features of C#, ASP.NET Core, and Entity Framework Core is a good way to do this.

Making hot code paths asynchronous is one of Microsoft’s ASP.NET Core Performance Best Practices. A hot code path is one that is “…frequently called and where much of the execution time occurs.”

There are two categories of hot code paths where it’s particularly important to use asynchronous processing in data-driven ASP.NET Core applications:

  1. Razor Pages controller actions – the entire call stack is asynchronous, so you can benefit from that by making your controller actions asynchronous.
  2. Data access actions – Entity Framework Core includes asynchronous features to improve the performance of code that relies on calls to a persistent data store.

Why is it important to make these components asynchronous? Web applications should be able to handle many requests simultaneously. To do this they need to allocate and use a limited resource, process threads, efficiently. Handling web requests synchronously ties up threads which can be used to process other requests, leading to thread pool starvation and decreased performance.

Data access code is often the slowest part of a web request because it can involve calling a remote machine and accessing a physical disk to retrieve data. Network latency, storage subsystem performance, and the load on the database server can all have a detrimental effect on performance, particularly if there’s a long queue of requests and your web server is waiting for them to be executed synchronously. That can tie up a lot of threads, compounding thread pool utilization problems.

This post will show you how to add asynchronous processing to an existing ASP.NET Core 3.1 Razor Pages application that’s been built according to the Model-View-ViewModel (MVVM) design pattern. You’ll see how to make controller actions and data repository calls asynchronous using the features of C#, ASP.NET Core, and Entity Framework Core.


You’ll need the following development resources to build and run the case study project:

.NET Core SDK (includes the APIs, runtime, and CLI)

Visual Studio 2019 (the Community edition is free) with the following options:

  • C#
  • ASP.NET and web development workload
  • .NET Core cross-platform development workload
  • SQL Server Express LocalDB
  • GitHub Extension for Visual Studio

In addition to these tools you should have a working knowledge of C#, and some experience with HTML, CSS, and JavaScript. You should be familiar with the Visual Studio user interface.

This is an introductory- to intermediate-level post. The code is limited to what’s necessary to demonstrate the topics discussed in this post, so don’t expect production-ready code that implements all the SOLID principles.

Understanding the case study

In this tutorial you’ll be working with an application you’ll clone from a GitHub repository. The repo contains a Visual Studio solution, RazorDrop, containing a C# ASP.NET Core 3.1 Razor Pages project, RazorDrop. The project structure is based on the default Razor Pages project template, and adds Entity Framework Core 3.1 with the SQL Server driver. The database structure is created and modified using EF Core Migrations in the code-first approach. The database runs in the default SQL Server instance installed with Visual Studio.

The application provides a simple user interface for creating new customer records and viewing a list of existing customers. The list of existing customers is the default Index page for the /Customers route, and customers are created with the Create page. When creating a customer, a user enters the customer name and selects the country and, optionally, the region in which the customer is located. A customer ID is generated automatically. The Index and Create pages are each bound to a view model.

The data model consists of entities for Customer, Country, and Region, with relationships between the entities defined in the model and applied to the database by EF Core. There are repositories for each entity, and the repos use the view models to provide and accept data from the Razor pages. An EF Core DbContext data context provides instructions for implementing the data model in the database and provides seed data for the Countries and Regions tables.

If you want to learn more about the case study, including how it implements the Model-View-ViewModel design paradigm, see the previous posts in this series, which also demonstrates how to build hierarchical dropdown lists, a user interface feature that’s widely used in web applications:

Setting up the Visual Studio solution

There are a few steps required to prepare the application before you can get started with this tutorial. They’ll just take a few minutes.

Open Visual Studio and clone the following GitHub repository to a local path where you’d like to keep the files:

When the cloning process is finished you should see the RazorDrop solution tree in the Solution Explorer panel.

Switch to the uncoupling branch and create a new branch named async-local.

Visual Studio should automatically restore the NuGet packages required by the RazorDrop project, as defined in the RazorDrop.csproj file, but if it doesn’t you can install the required NuGet packages by opening the Package Manager Console window and executing the following command:


Note that “package” is singular. Using this command without an argument will update all the packages to the latest version. You can also use the NuGet Package Manager UI to perform these tasks.

You’ll need to create the SQL Server database the application uses as a data persistence layer and seed it with data using Entity Framework Core. If you installed SQL Express 2016 LocalDB with Visual Studio 2019, which is part of a typical VS 2019 installation, you’ll have the necessary database engine. If not you can find it in the list of Individual components in the Visual Studio Installer (from the Visual Studio menu: Tools > Get Tools and Features).

Unless you’re already sure of it, you can verify the name of your instance(s) of LocalDB using the SqlLocalDB Utility. Open a PowerShell or Windows Command Prompt console window and execute the following command:

sqllocaldb i

The utility will return the names(s) of all the instances of LocalDB owned by the current user.

With the name of your instance of LocalDB, you can update the connection string Entity Framework Core needs to connect to the database.

Open the appsettings.json file in the RazorDrop project root and update the connection string set in the RazorDropContext element to point to the correct database server.

Open the Package Manager window and execute the following command-line instruction:


Visual Studio will build the solution and run the EF Core migrations, which will create the database in the default location for LocalDB, which is C:\Users\<users> on Windows machines. You should see the following output in the Package Manager Console if everything worked:

PM> update-database
Build started...
Build succeeded.

Verify that the database was created properly by creating a connection to the RazorDrop database in Visual Studio, SQL Server Management Studio, or LINQPad 6 and getting the list if countries with the following SQL query:

select * from countries order by countryid

You should see two records, for Canada and the USA.

Run the RazorDrop project. When the default ASP.NET Core template home page appears in your browser, go to https://localhost:44329/Customers. (Your port number may be different.) You should see the default page for the /Customers route, Pages/Customers/Index.cshtml in the RazorDrop project. An example is shown below:

Chrome browser screenshot of /Customers/Index.cshtml

Click Create a new customer and enter the data required to create a new customer. As you do so you can verify the values for the Country and State/Region dropdown lists are appearing, as shown below:

Chrome web browser screenshot of Create.cshtml

Return to the previous page. You should see the new customer in the list of customers. You’ve exercised all the functionality of the application from user interface to data persistence layer.

You can stop the application now.

Adding asynchronous processing to a data repository

Before implementing asynchronous functionality in a data repository using Entity Framework Core (EF Core) it’s important to understand that DbContext isn’t thread safe and to understand how to deal with that. It’s particularly important when you’re using dependency injection injection with DbContext.

Open the Startup.cs file in the project root, find the ConfigureServices method, and locate the statement that adds the RazorDropContext DbContext to the service collection:

services.AddDbContext<RazorDropContext>(options =>

This is a standard approach to setting up a SqlServer DbContext for dependency injection. By default, the DbContext is configured with a Scoped lifetime. This means an instance of RazorDropContext is created once per client connection request.

As long as you await the results of an asynchronous action that uses an instance of the DbContext before starting another action using the same DbContext instance, the call will be thread safe. If you fail to do this and make another asynchronous request using the same DbContext your application may blow up and your data may be corrupted. Yikes!

Implementing parallel execution of async queries requires different techniques. Since this tutorial focuses on asynchronous processing, all the repository calls will use single scoped instances of the DbContext provided through the default configuration of the DbContext through dependency injection.

Adding asynchronous method calls to a data repository

To make data context calls asynchronous in a data repository you need to do three things:

  1. Add the namespace that provides asynchronous functionality in .NET Core.
  2. Make the repository method calls asynchronous.
  3. Use the asynchronous versions of the EF Core methods that return query results from a data provider such as a database and await them.

Open the CustomersRepository.cs file in the Data folder and add the following using directive to the existing directives:

using System.Threading.Tasks;

This makes the System.Threading.Tasks namespace available in the file, making the types used for asynchronous programming available for use in the code. The class you’ll be using from this namespace is one of the most widely used: Task<TResult>, which represents a single operation that will execute asynchronously.

The case study solution uses C# interfaces to uncouple the repository implementation from the classes like the controllers which make use of it. Adding asynchronous functionality to the implementation necessitates adding to the interface as well. It’s easy.

In the CustomersRepository.cs file, modify the ICustomersRepository interface so it looks like the following:

public interface ICustomersRepository
    public Task<List<CustomerDisplayViewModel>> GetCustomers();
    public Task<CustomerEditViewModel> CreateCustomer();
    public Task<bool> SaveCustomer(CustomerEditViewModel customeredit);

This wraps each of the return types of the repository’s methods in a Task<TResult> so the task can be awaited where it’s called in a non-blocking way. The implementation of the interface will need to be modified in the same way.

The Task<TResult> class can wrap generic collections, such as List<T>, as it does in GetCustomers(). It can also wrap complex types like CustomerEditViewModel and value types like bool.

In addition to concrete types, Task<TResult> can also wrap interfaces. Since this project uses view models with classes that derive from the Razor Pages PageModel class, the view model classes need to be concrete classes, a restriction that’s explained in the post:

Locate the GetCustomers method of the CustomersRepository class and modify the first line of the method so it looks like the following:

public async Task<List<CustomerDisplayViewModel>> GetCustomers()

This wraps the return type in a Task<TResult> just like in the interface, so the implementation in the concrete class matches the abstraction in the interface. It also add the C# async keyword to specify that the method is asynchronous.

Async methods can have a limited number of return types. The most frequently used are Task<TResult>, as it’s used here, and Task, which is used for asynchronous methods that don’t return a value.

To implement the third part of asynchronous processing you need to change the EF Core method that’s used to return the list of customers from its synchronous form to its asynchronous form. You also need to need to await the results.

In the GetCustomers() method, modify the first five lines of the method so they look like the following:

List<Customer> customers = new List<Customer>();
customers = await _context.Customers.AsNoTracking()
    .Include(x => x.Country)
    .Include(x => x.Region)

The revised code adds the await keyword before the invocation of the query and changes the .ToList() method to its asynchronous form, .ToListAsync(). It’s important to await each use of the DbContext in which an asynchronous method is used; this ensures the query using this instance of DbContext is complete before another is begun with the same instance.

Also note that the AsNoTracking() method is used in the query. Tracking in EF Core is used to maintain information about the state of objects so changes can be persisted using the .SaveChanges() method. In web apps that’s impractical and bad design, so this method gets rid of the unneeded state tracking and improves performance in the process.

Making Razor Pages action methods asynchronous

Now that you have an asynchronous repository method you can use it in a page model. The CustomersRepository.GetCustomers() method is used in just one place, so it will be easy to update all the action methods that use it.

In the Pages/Customers folder, open the Index.cshtml.cs file. You’ll see some lint in the OnGet() method indicating a type conversion error:

Visual Studio 2019 screenshot showing code error

The CustomersRepostory.GetCustomers() method now returns a Task<TResult> type, but that’s not the same type as CustomerDisplayList.

You need to “await” the results of the asynchronous method call.

Add the await C# keyword to the repository call so the OnGet() method looks like this:

public IActionResult OnGet()
    CustomersDisplayList = await _customersRepo.GetCustomers();
    return Page();

You’ll still see lint.

If you roll over the await keyword with your cursor you’ll see that the return type is now correct, a List<CustomerDisplayViewModel> type, but the await keyword can only be used in an async method:

Visual Studio 2019 tool tip screenshot

Fortunately, the solution to this problem will take you in the direction you want to go: implementing asynchronous functionality throughout the Razor Pages call stack. It’s easy.

Add the following using directive to the existing directives in the Index.cshtml.cs file:

using System.Threading.Tasks;

As in CustomersRepository.cs, you’ll want to add this namespace wherever you want to use asynchronous classes and methods.

Modify the OnGet() action method so it looks like this:

public async Task<IActionResult> OnGetAsync()
    CustomersDisplayList = await _customersRepo.GetCustomers();
    return Page();

This adds the async C# keyword to pair with the await used to call _customersRepo.GetCustomers() asynchronously.

It also changes the return type of the method to the Task<TResult> type, freeing the Razor Pages middleware to handle this call asynchronously, managing the thread pool used to handle HTTP requests more efficiently.

Adding “Async” to “OnGet” to produce the method name OnGetAsync doesn’t do anything magically — or even “automagically” — it’s just good programming practice, a convention to denote an asynchronous method. The “OnGet” part is automagic and determines the default behavior for HTTP GET requests through Razor Pages named handler methods.

The type of Task<TResult> in Task<IActionResult> is a class that implements the IActionResult interface: a Microsoft.AspNetCore.Mvc.RazorPages.PageResult object. It’s returned by the Page() method of the PageModel class, the base class for IndexModel.

Looking at the definition of the PageResult class in metadata, you can see that PageResult also uses the System.Threading.Tasks namespace and performs actions asynchronously:

#region Assembly Microsoft.AspNetCore.Mvc.RazorPages, Version=, Culture=neutral, PublicKeyToken=adb9793829ddae60
// C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\3.1.3\ref\netcoreapp3.1\Microsoft.AspNetCore.Mvc.RazorPages.dll

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace Microsoft.AspNetCore.Mvc.RazorPages
    // Summary:
    //     An Microsoft.AspNetCore.Mvc.ActionResult that renders a Razor Page.
    public class PageResult : ActionResult
        public PageResult();

        // Summary:
        //     Gets or sets the Content-Type header for the response.
        public string ContentType { get; set; }
        // Summary:
        //     Gets the page model.
        public object Model { get; }
        // Summary:
        //     Gets or sets the Microsoft.AspNetCore.Mvc.RazorPages.PageBase to be executed.
        public PageBase Page { get; set; }
        // Summary:
        //     Gets or sets the HTTP status code.
        public int? StatusCode { get; set; }
        // Summary:
        //     Gets or sets the Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary for
        //     the page to be executed.
        public ViewDataDictionary ViewData { get; set; }

        public override Task ExecuteResultAsync(ActionContext context);

As you can see, adding your Razor Pages handler methods to the asynchronous call stack doesn’t depend on magic or automagic: it’s the result of integrating the asynchronous features of C#, .NET Core, and EF Core.

Some web pages can be served fast with static OnGet handler methods, but pages that need to be populated with data from a database when they’re served can be made more performant with asynchronous processing.

Completing the asynchronous repository implementation

You’ve completed the three steps required to convert an MVVM Razor Pages web app to asynchronous processing, but the other repositories and page models still need to be converted. There are a few additional details worth pointing out along the way, so you can either follow along with the coding tutorial or get the completed code from the companion repository.

To get the completed code, clone the companion repository and switch to the async branch.

To continue building the project, return to the CustomersRepository.cs file. Modify the CreateCustomer() method so it looks like the following:

public async Task<CustomerEditViewModel> CreateCustomer()
    var customer = new CustomerEditViewModel()
    CustomerId = Guid.NewGuid().ToString(),
    Countries = await _countriesRepo.GetCountries(),
    Regions = _regionsRepo.GetRegions()
    return customer;

You’ll see lint in the sixth line.

The CreateCustomer() method is used to create an empty CustomerEditViewModel object that’s bound in the CreateModel PageModel class and used to collect user input on the Create.cshtml Razor Page. The CustomerEditViewModel class includes complex data types for Countries and Regions so the Customers/Create page can be pre-populated with a list of Country entities from which the user can select a Country.

Once the user has selected a Country, the list of appropriate Region entities can be populated using an Ajax call. In the meantime, the list of regions will contain a single record containing a data entry tip.

Open the CountriesRepository.cs file in the Data folder.

Add the following using directive to the existing list:

using System.Threading.Tasks;

Modify the ICountriesRepository interface so it looks like this:

public interface ICountriesRepository
   public Task<IEnumerable<SelectListItem>> GetCountries();

This wraps the return value in a Task.

Modify the GetCountries() method to make it asynchronous, return a Task, and await the asynchronous version of the .ToList() extension method:

public async Task<IEnumerable<SelectListItem>> GetCountries()
    List<SelectListItem> countries = await _context.Countries.AsNoTracking()
        .OrderBy(n => n.CountryNameEnglish)
        .Select(n =>
            new SelectListItem
                Value = n.CountryId.ToString(),
                Text = n.CountryNameEnglish

The remainder of the method, which adds a data entry tip, is unaffected. As with the methods in the CustomersRepository class, the await-ing happens in the same statement as the asynchronous method call.

You’re finished with the CountriesRepository class.

Open the RegionsRepository.cs file in the Data folder and add the following using directive to the existing list:

using System.Threading.Tasks;

Modify the IRegionsRepository interface so it looks like the following:

public interface IRegionsRepository
    public IEnumerable<SelectListItem> GetRegions();
    public Task<IEnumerable<SelectListItem>> GetRegions(string countryId);

Note that only the second signature of the GetRegions method is being made asynchronous. Since the parameterless method GetRegions() only returns a user input tip, it doesn’t make a call to the data context, so there’s nothing to asynchronize: all the action happens in code.

When a value is supplied for the countryId parameter, the GetRegions(string countryId) method returns only the Region entities appropriate for the specified value.

Replace the existing GetRegions(string countryId) method with the following C# code:

public async Task<IEnumerable<SelectListItem>> GetRegions(string countryId)
    if (!String.IsNullOrWhiteSpace(countryId))
        IEnumerable<SelectListItem> regions = await _context.Regions.AsNoTracking()
            .OrderBy(n => n.RegionNameEnglish)
            .Where(n => n.CountryId == countryId)
            .Select(n =>
                new SelectListItem
                    Value = n.RegionId,
                    Text = n.RegionNameEnglish
        return new SelectList(regions, "Value", "Text");
    return null;

As before, the statement making an asynchronous call to the data context is awaited. While this statement is more complex, containing .OrderBy, .Where, and .Select extension methods, it’s only the .ToListAsync() method that actually makes the data context call: the other methods are used to compose the query.

Filtering and ordering recordsets is more efficient when it’s done as part of the query, rather than in code. This is true not only because the query will have to return fewer records, but also because the composition of the recordset is done as part of the asynchronous action.

You’re finished with the RegionsRepository.cs file.

Return to the CustomersRepository.cs file and replace the existing SaveCustomer method with the following code:

public async Task<bool> SaveCustomer(CustomerEditViewModel customeredit)
    if (customeredit != null)
    if (Guid.TryParse(customeredit.CustomerId, out Guid newGuid))
        var customer = new Customer()
            CustomerId = newGuid,
            CustomerName = customeredit.CustomerName,
            CountryId = customeredit.SelectedCountryId,
            RegionId = customeredit.SelectedRegionId
        // The next two lines will execute sequentially.
        customer.Country = await _context.Countries.FindAsync(customeredit.SelectedCountryId);
        customer.Region = await _context.Regions.FindAsync(customeredit.SelectedRegionId);

        // The next two lines will, and must, execute sequentially.
        await _context.Customers.AddAsync(customer);
        await _context.SaveChangesAsync();
        return true;
    // Return false if customeredit == null or CustomerID is not a Guid.
    return false;

When this method is called to save a Customer entity record it has to create a referential link to the appropriate Country and Region entities. It does this by executing queries to find the entities in the Countries and Regions tables that correspond to the values in the SelectedCountryId and SelectedRegionId of the CustomerEditViewModel object that’s passed to the method as an argument.

It makes sense to make these calls asynchronously, so the asynchronous variant of the EF Core .Find method is used for locating each entity. These are two separate uses of the data context, so each call is await-ed. Because code execution waits for the first call to return before proceeding, these calls occur asynchronously and sequentially.

Because these lookup calls aren’t related or dependent on each other, they could be conducted in parallel. But to do so in a thread safe manner requires an additional coding technique covered in another post.

For now, the important thing to remember is not to be tempted to implement parallelism through shortcuts. Always await your data context calls immediately.

It’s also worth noting that you didn’t have to do anything to the Country or Region classes to be able to search those database entities asynchronously. The functionality is a standard part of EF Core.

Implementing asynchronous actions in data-bound PageModel classes

When you modified the IndexModel class you saw how to use asynchronous action methods to display data in response to an HTTP GET request. Now that you’ve made all the necessary changes to the data repository interfaces and implementations you can make use of the  asynchronous methods used to create new Customer entity records with the Customers/Create page.

Open the Create.cshtml.cs file in the Pages/Customers folder and add the necessary using directive to the existing list of directives:

using System.Threading.Tasks;

Replace the existing synchronous OnGet() method with it’s asynchronous counterpart:

public async Task<IActionResult> OnGetAsync()
    CustomerEditViewModel = await _customersRepo.CreateCustomer();
    return Page();

This implementation follows the same pattern you used in modifying the OnGet() method of the IndexModel class. The CustomersRepository.CreateCustomers() method returns an instance of the CustomerEditViewModel class populated with a list of Countries.

Replace the OnPost() method with its asynchronous implementation:

public async Task<IActionResult> OnPostAsync()
        if (ModelState.IsValid)
            bool saved = await _customersRepo.SaveCustomer(CustomerEditViewModel);
            if (saved)
                return RedirectToPage("Index");
        // Handling model state errors is beyond the scope of the demo, so just throwing an ApplicationException when the ModelState is invalid
        // and rethrowing it in the catch block.
        throw new ApplicationException("Invalid model");
    catch (ApplicationException ex)
        throw ex;

This is a place where the separation of concerns in Model-View-ViewModel really shines. You only need to make the action method asynchronous and await the call to the SaveCustomer method of the repository.

Calling asynchronous PageModel action methods from Ajax

There’s one more method needing revision. The user interface of the Create.cshtml Razor Page implements cascading dropdown lists. When a user selects a Country an Ajax function makes an HTTP POST request to the OnPostRegions() method of the CreateModel class.

The action method reads the value of selectedCountry from the body of the HTTP request using the CopyToAsync static method of the Stream class and uses the value to get the appropriate list of Regions by calling the RegionsRepository.GetRegions(countryId) method, which is now asynchronous. That has a few implications:

  1. The call to GetRegions will have to be await-ed because it’s asynchronous
  2. The OnPostRegions action method will have to be async and the return value wrapped in a task
  3. The Request.Body.CopyToAsync(stream); statement will have to be awaited because it’s asynchronous

The last point is one you should watch out for as you implement asynchronous functionality: you’ll need to await calls to asynchronous methods that you didn’t need to await when the containing method was synchronous.

To accomplish these tasks, replace the OnPostRegions() action method with the following code:

public async Task<IActionResult> OnPostRegionsAsync()
    MemoryStream stream = new MemoryStream();
    await Request.Body.CopyToAsync(stream);
    stream.Position = 0;
    using StreamReader reader = new StreamReader(stream);
    string requestBody = reader.ReadToEnd();
    if (requestBody.Length > 0)
        IEnumerable<SelectListItem> regions = await _regionsRepo.GetRegions(requestBody);
        return new JsonResult(regions);
    return null;

A note on naming conventions: As you’ve seen in a number of places, appending “Async” to the names of asynchronous methods is a good practice. Arguably, when you’re converting your data repository methods to asynchronous functionality it’s a good idea to rename them as well. The code in this tutorial has kept the repository method names unchanged so you can more easily identify the required changes, but when you’re writing your own code you should consider following the convention.

Testing the completed application

Build and run the application and go https://localhost:44329/Customers. (Your port number may be different.) You should see the default page for the /Customers route, Pages/Customers/Index.cshtml in the RazorDrop project.

Click Add a new customer and try adding a new customer. You should be returned to the list of customers when you click Create.

Adding asynchronous functionality shouldn’t alter the perceived operation of the application. It’s also unlikely that you’ll see a change in the app’s speed.

If you need to debug, you can check your code against the async branch of the companion repository on GitHub. It contains the complete source code for this tutorial.

Understanding asynchronous performance

Adding asynchronous features actually imposes a bit of overhead, so it’s possible you’ll see the application running very slightly slower as you’re testing. The effect will hold true in other low-volume operations.

But for hot code paths, those that are used frequently by multiple users, you should see a performance improvement. Just as importantly, web applications that leverage asynchronous features should be able to sustain a greater number of concurrent users and transactions because they make more efficient use of threads.

Potential enhancements

There are a number of ways you can take this case study project and make it more suitable for a production environment. There are some additional parts of the application that should be completed to make it fully functional, and the existing parts can be extended to make them more complete.

Add parallel processing – As you saw in the SaveCustomer(CustomerEditViewModel) method of the CustomersRepository class, there are places where running asynchronous operations in parallel can improve performance. It’s important to do this in a thread safe way.

Add caching – It can be advantageous to cache frequently-used data to the web server’s memory rather than making a call to the data context to retrieve the data from a persistent data store, such as a database on a separate server. An example in the tutorial project would be the list of countries: it’s used every time the Customers/Create page is loaded. And although there is, as of the date of this post, some instability in the world, it’s unlikely the list of countries is going to change much.

Add error handling – There are known errors, like the referential integrity error that the SQL Server data provider will return when a user tries to delete a Country that has linked Customers, and there are unknown errors, like bugs in the code. The repositories should be able to handle SQL errors and the application should be able to present the user with meaningful information about those errors and offer ways to respond to them. The code should also be able to fail gracefully and report its problems. One way to stay on top of errors from running apps is to integrate


This post showed you how to add asynchronous functionality to an ASP.NET Core 3.1 Razor Pages web application built with Model-View-ViewModel design. You saw how to change data repository methods to use the asynchronous versions of Entity Framework Core extension methods and how to properly await the results of those calls. You also saw how to wrap the methods’ return values in Task<TResult>. In the PageModel class implementations, you saw how to call asynchronous repository methods and how to make PageModel action methods asynchronous to take advantage of the asynchronous features of ASP.NET Core. Along with those programming exercises you learned a bit about how the ASP.NET Core middleware works and picked up some advice on how to avoid the pitfalls of asynchronous functionality.

Additional resources

If you want to brush up on your MVVM implementation skills or learn more about this case study project, take a look at the previous posts in this series:

To delve deeper into the topics covered in this post, consult the following references:

Asynchronous code – introduction to asynchronous code in ASP.NET Core 3.1.

Asynchronous Queries – introduction to asynchronous queries in Entity Framework Core.

Tracking vs. No-Tracking Queries – explanation of change tracking in Entity Framework Core.

ASP.NET Core Performance Best Practices – guidelines for making your ASP.NET Core web applications run fast.

Optimization Techniques in EF – A useful 3rd-party blog post on how to optimize Entity Framework calls from ASP.NET Core. Undated and not original source material, so exercise caution in using these techniques.

Reading request body in ASP.NET Core – A quick explanation of how to read the HTTP request body in ASP.NET Core apps by Twilio Voices contributor and MVP Gunnar Peipman.

Creating and configuring a model – information on building and using data models with Entity Framework Core.

TwilioQuest – Learn more about Twilio products and sharpen your programming skills in a variety of languages while playing an exciting video game! Join TwilioQuest and help defeat the forces of legacy systems!

Twilio trial account – Sign up for a free Twilio account using this link and receive an additional $10 credit on your account.

AJ Saulsberry is a Technical Editor @Twilio. Get in touch with him if you’ve “been there and back again” with a .NET development task and would like to get paid to write about it for the Twilio blog.