This chapter is about building websites with a modern HTTP architecture on the server side using Microsoft ASP.NET Core MVC, including the startup configuration, authentication, authorization, routes, request and response pipeline, models, views, and controllers that make up an ASP.NET Core MVC project.
This chapter will cover the following topics:
ASP.NET Core Razor Pages are great for simple websites. For more complex websites, it would be better to have a more formal structure to manage that complexity.
This is where the Model-View-Controller (MVC) design pattern is useful. It uses technologies like Razor Pages, but allows a cleaner separation between technical concerns, as shown in the following list:
.cshtml files, that render data in view models into HTML web pages. Blazor uses the .razor file extension, but do not confuse them with Razor files!The best way to understand using the MVC design pattern for web development is to see a working example.
You will use a project template to create an ASP.NET Core MVC website project that has a database for authenticating and authorizing users. Visual Studio 2022 defaults to using SQL Server LocalDB for the accounts database. Visual Studio Code (or more accurately the dotnet tool) uses SQLite by default and you can specify a switch to use SQL Server LocalDB instead.
Let's see it in action:
mvcPracticalAppsNorthwind.Mvc--auth IndividualNorthwind.Mvc as the active OmniSharp project. Northwind.Mvc project.help switch to see other options for this project template, as shown in the following command:
dotnet new mvc --help
ASP.NET Core Web App (Model-View-Controller) (C#)
Author: Microsoft
Description: A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.
This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/6.0-third-party-notices for details.
There are many options, especially related to authentication, as shown in the following table:
|
Switches |
Description |
|
|
The type of authentication to use:
|
|
|
Whether to use SQL Server LocalDB instead of SQLite. This option only applies if |
|
|
Determines if the project is configured to use Razor runtime compilation in |
|
|
The target framework for the project. Values can be: |
If you created the MVC project using Visual Studio 2022, or you used dotnet new mvc with the -uld or --use-local-db switch, then the database for authentication and authorization will be stored in SQL Server LocalDB. But the database does not yet exist. Let's create it now.
At a command prompt or terminal, in the Northwind.Mvc folder, enter the command to run database migrations so that the database used to store credentials for authentication is created, as shown in the following command:
dotnet ef database update
If you created the MVC project using dotnet new, then the database for authentication and authorization will be stored in SQLite and the file has already been created named app.db.
The connection string for the authentication database is named DefaultConnection and it is stored in the appsettings.json file in the root folder for the MVC website project.
For SQL Server LocalDB (with a truncated connection string), see the following markup:
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Northwind.Mvc-...;Trusted_Connection=True;MultipleActiveResultSets=true"
},
For SQLite, see the following markup:
{
"ConnectionStrings": {
"DefaultConnection": "DataSource=app.db;Cache=Shared"
},
Let's review the behavior of the default ASP.NET Core MVC website project template:
Northwind.Mvc project, expand the Properties folder, open the launchSettings.json file, and note the random port numbers (yours will be different) configured for the project for HTTPS and HTTP, as shown in the following markup:
"profiles": {
"Northwind.Mvc": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7274;http://localhost:5274",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
5001 for HTTPS and 5000 for HTTP, as shown in the following markup:
"applicationUrl": "https://localhost:5001;http://localhost:5000",
launchSettings.json file.http://localhost:5000/ and note the following, as shown in Figure 15.1:5001.
Figure 15.1: The ASP.NET Core MVC project template website home page
By default, passwords must have at least one non-alphanumeric character, they must have at least one digit (0-9), and they must have at least one uppercase letter (A-Z). I use Pa$$w0rd in scenarios like this when I am just exploring.
The MVC project template follows best practice for double-opt-in (DOI), meaning that after filling in an email and password to register, an email is sent to the email address, and the visitor must click a link in that email to confirm that they want to register.
We have not yet configured an email provider to send that email, so we must simulate that step:
test@example.com and Pa$$w0rd.)In your code editor, in Visual Studio Solution Explorer (toggle on Show All Files) or in Visual Studio Code EXPLORER, review the structure of an MVC website project, as shown in Figure 15.2:

Figure 15.2: The default folder structure of an ASP.NET Core MVC project
We will look in more detail at some of these parts later, but for now, note the following:
Areas: This folder contains nested folders and a file needed to integrate your website project with ASP.NET Core Identity, which is used for authentication.bin, obj: These folders contain temporary files needed during the build process and the compiled assemblies for the project.Controllers: This folder contains C# classes that have methods (known as actions) that fetch a model and pass it to a view, for example, HomeController.cs.Data: This folder contains Entity Framework Core migration classes used by the ASP.NET Core Identity system to provide data storage for authentication and authorization, for example, ApplicationDbContext.cs.Models: This folder contains C# classes that represent all of the data gathered together by a controller and passed to a view, for example, ErrorViewModel.cs.Properties: This folder contains a configuration file for IIS or IIS Express on Windows and for launching the website during development named launchSettings.json. This file is only used on the local development machine and is not deployed to your production website.Views: This folder contains the .cshtml Razor files that combine HTML and C# code to dynamically generate HTML responses. The _ViewStart file sets the default layout and _ViewImports imports common namespaces used in all views like tag helpers:Home: This subfolder contains Razor files for the home and privacy pages.Shared: This subfolder contains Razor files for the shared layout, an error page, and two partial views for logging in and validation scripts.wwwroot: This folder contains static content used by the website, such as CSS for styling, libraries of JavaScript, JavaScript for this website project, and a favicon.ico file. You also put images and other static file resources like PDF documents in here. The project template includes Bootstrap and jQuery libraries.app.db: This is the SQLite database that stores registered visitors. (If you used SQL Server LocalDB, then it will not be needed.)appsettings.json and appsettings.Development.json: These files contain settings that your website can load at runtime, for example, the database connection string for the ASP.NET Core Identity system and logging levels.Northwind.Mvc.csproj: This file contains project settings like the use of the Web .NET SDK, an entry for SQLite to ensure that the app.db file is copied to the website's output folder, and a list of NuGet packages that your project requires, including:Microsoft.AspNetCore.Diagnostics.EntityFrameworkCoreMicrosoft.AspNetCore.Identity.EntityFrameworkCoreMicrosoft.AspNetCore.Identity.UIMicrosoft.EntityFrameworkCore.Sqlite or Microsoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.ToolsProgram.cs: This file defines a hidden Program class that contains the Main entry point. It builds a pipeline for processing incoming HTTP requests and hosts the website using default options like configuring the Kestrel web server and loading appsettings. It adds and configures services that your website needs, for example, ASP.NET Core Identity for authentication, SQLite or SQL Server for identity data storage, and so on, and routes for your application.Open appsettings.json to find the connection string used for the ASP.NET Core Identity database, as shown highlighted for SQL Server LocalDB in the following markup:
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Northwind.Mvc-2F6A1E12-F9CF-480C-987D-FEFB4827DE22;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
If you used SQL Server LocalDB for the identity data store, then you can use Server Explorer to connect to the database. You can copy and paste the connection string from the appsettings.json file (but remove the second backslash between (localdb) and mssqllocaldb).
If you installed an SQLite tool such as SQLiteStudio, then you can open the SQLite app.db database file.
You can then see the tables that the ASP.NET Core Identity system uses to register users and roles, including the AspNetUsers table used to store the registered visitor.
Good Practice: The ASP.NET Core MVC project template follows good practice by storing a hash of the password instead of the password itself, which you will learn more about in Chapter 20, Protecting Your Data and Applications.
Let's walk through the parts that make up a modern ASP.NET Core MVC website.
Appropriately enough, we will start by exploring the MVC website's default initialization and configuration:
Program.cs file and note that it uses the top-level program feature (so there is a hidden Program class with a Main method). This file can be considered to be divided into four important sections from top to bottom. .NET 5 and earlier ASP.NET Core project templates used a Startup class to separate these parts into separate methods but with .NET 6, Microsoft encourages putting everything in a single Program.cs file.
using Microsoft.AspNetCore.Identity; // IdentityUser
using Microsoft.EntityFrameworkCore; // UseSqlServer, UseSqlite
using Northwind.Mvc.Data; // ApplicationDbContext
Remember that by default, many other namespaces are imported using the implicit usings feature of .NET 6 and later. Build the project and then the globally imported namespaces can be found in the following path: obj\Debug\net6.0\Northwind.Mvc.GlobalUsings.g.cs.
appsettings.json file for its data storage, adds ASP.NET Core Identity for authentication and configures it to use the application database, and adds support for MVC controllers with views, as shown in the following code:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration
.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString)); // or UseSqlite
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddControllersWithViews();
The builder object has two commonly used objects: Configuration and Services:
Configuration contains merged values from all the places you could set configuration: appsettings.json, environment variables, command-line arguments, and so onServices is a collection of registered dependency servicesThe call to AddDbContext is an example of registering a dependency service. ASP.NET Core implements the dependency injection (DI) design pattern so that other components like controllers can request needed services through their constructors. Developers register those services in this section of Program.cs (or if using a Startup class then in its ConfigureServices method.)
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
We learned about most of these methods and features in Chapter 14, Building Websites Using ASP.NET Core Razor Pages.
Good Practice: What does the extension method UseMigrationsEndPoint do? You could read the official documentation, but it does not help much. For example, it does not tell us what relative URL path it defines by default: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.migrationsendpointextensions.usemigrationsendpoint. Luckily, ASP.NET Core is open source, so we can read the source code and discover what it does, at the following link: https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/Diagnostics.EntityFrameworkCore/src/MigrationsEndPointOptions.cs#L18. Get into the habit of exploring the source code for ASP.NET Core to understand how it works.
Apart from the UseAuthentication and UseAuthorization methods, the most important new method in this section of Program.cs is MapControllerRoute, which maps a default route for use by MVC. This route is very flexible because it will map to almost any incoming URL, as you will see in the next topic.
Although we will not create any Razor Pages in this chapter, we need to leave the method call that maps Razor Page support because our MVC website uses ASP.NET Core Identity for authentication and authorization, and it uses a Razor Class Library for its user interface components, like visitor registration and login.
app.Run(); // blocking call
The responsibility of a route is to discover the name of a controller class to instantiate and an action method to execute with an optional id parameter to pass into the method that will generate an HTTP response.
A default route is configured for MVC, as shown in the following code:
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
The route pattern has parts in curly brackets {} called segments, and they are like named parameters of a method. The value of these segments can be any string. Segments in URLs are not case-sensitive.
The route pattern looks at any URL path requested by the browser and matches it to extract the name of a controller, the name of an action, and an optional id value (the ? symbol makes it optional).
If the user hasn't entered these names, it uses defaults of Home for the controller and Index for the action (the = assignment sets a default for a named segment).
The following table contains example URLs and how the default route would work out the names of a controller and action:
|
URL |
Controller |
Action |
ID |
|
|
Home |
Index |
|
|
|
Muppet |
Index |
|
|
|
Muppet |
Kermit |
|
|
|
Muppet |
Kermit |
Green |
|
|
Products |
Index |
|
|
|
Products |
Detail |
|
|
|
Products |
Detail |
3 |
In MVC, the C stands for controller. From the route and an incoming URL, ASP.NET Core knows the name of the controller, so it will then look for a class that is decorated with the [Controller] attribute or derives from a class decorated with that attribute, for example, the Microsoft-provided class named ControllerBase, as shown in the following code:
namespace Microsoft.AspNetCore.Mvc
{
//
// Summary:
// A base class for an MVC controller without view support.
[Controller]
public abstract class ControllerBase
{
...
As you can see in the XML comment, ControllerBase does not support views. It is used for creating web services, as you will see in Chapter 16, Building and Consuming Web Services.
ControllerBase has many useful properties for working with the current HTTP context, as shown in the following table:
|
Property |
Description |
|
|
Just the HTTP request. For example, headers, query string parameters, the body of the request as a stream that you can read from, the content type and length, and cookies. |
|
|
Just the HTTP response. For example, headers, the body of the response as a stream that you can write to, the content type and length, status code, and cookies. There are also delegates like |
|
|
Everything about the current HTTP context including the request and response, information about the connection, a collection of features that have been enabled on the server with middleware, and a |
Microsoft provides another class named Controller that your classes can inherit from if they do need view support, as shown in the following code:
namespace Microsoft.AspNetCore.Mvc
{
//
// Summary:
// A base class for an MVC controller with view support.
public abstract class Controller : ControllerBase,
IActionFilter, IFilterMetadata, IAsyncActionFilter, IDisposable
{
...
Controller has many useful properties for working with views, as shown in the following table:
|
Property |
Description |
|
|
A dictionary that the controller can store key/value pairs in that is accessible in a view. The dictionary's lifetime is only for the current request/response. |
|
|
A dynamic object that wraps the |
|
|
A dictionary that the controller can store key/value pairs in that is accessible in a view. The dictionary's lifetime is for the current request/response and the next request/response for the same visitor session. This is useful for storing a value during an initial request, responding with a redirect, and then reading the stored value in the subsequent request. |
Controller has many useful methods for working with views, as shown in the following table:
|
Property |
Description |
|
|
Returns a |
|
|
Returns a |
|
|
Returns a |
|
|
Returns a |
The responsibilities of a controller are as follows:
Let's review the controller used to generate the home, privacy, and error pages:
Controllers folderHomeController.csHomeController that is set in a constructor.View and return the results as an IActionResult interface to the client.Error action method passes a view model into its view with a request ID used for tracing. The error response will not be cached:using Microsoft.AspNetCore.Mvc; // Controller, IActionResult
using Northwind.Mvc.Models; // ErrorViewModel
using System.Diagnostics; // Activity
namespace Northwind.Mvc.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0,
Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId =
Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
If the visitor navigates to a path of / or /Home, then it is the equivalent of /Home/Index because those were the default names for controller and action in the default route.
The Index and Privacy methods are identical in implementation, yet they return different web pages. This is because of conventions. The call to the View method looks in different paths for the Razor file to generate the web page.
Let's deliberately break one of the page names so that we can see the paths searched by default:
Northwind.Mvc project, expand the Views folder and then the Home folder.Privacy.cshtml file to Privacy2.cshtml.https://localhost:5001/, click Privacy, and note the paths that are searched for a view to render the web page (including in Shared folders for MVC views and Razor Pages), as shown in Figure 15.3:
Figure 15.3: An exception showing the default search path for views
Privacy2.cshtml file back to Privacy.cshtml. You have now seen the view search path convention, as shown in the following list:
/Views/{controller}/{action}.cshtml/Views/Shared/{action}.cshtml/Pages/Shared/{action}.cshtmlYou have just seen that some errors are caught and written to the console. You can write messages to the console in the same way by using the logger.
Controllers folder, in HomeController.cs, in the Index method, add statements to use the logger to write some messages of various levels to the console, as shown in the following code:
_logger.LogError("This is a serious error (not really!)");
_logger.LogWarning("This is your first warning!");
_logger.LogWarning("Second warning!");
_logger.LogInformation("I am in the Index method of the HomeController.");
Northwind.Mvc website project.fail: Northwind.Mvc.Controllers.HomeController[0]
This is a serious error (not really!)
warn: Northwind.Mvc.Controllers.HomeController[0]
This is your first warning!
warn: Northwind.Mvc.Controllers.HomeController[0]
Second warning!
info: Northwind.Mvc.Controllers.HomeController[0]
I am in the Index method of the HomeController.
When you need to add some functionality to multiple controllers and actions, you can use or define your own filters that are implemented as an attribute class.
Filters can be applied at the following levels:
Filters collection of the MvcOptions instance that can be used to configure MVC when calling the AddControllersWithViews method, as shown in the following code:
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(MyCustomFilter));
});
You might want to ensure that one particular action method of a controller class can only be called by members of certain security roles. You do this by decorating the method with the [Authorize] attribute, as described in the following list:
[Authorize]: Only allow authenticated (non-anonymous, logged-in) visitors to access this action method.[Authorize(Roles = "Sales,Marketing")]: Only allow visitors who are members of the specified role(s) to access this action method.Let's see an example:
HomeController.cs, import the Microsoft.AspNetCore.Authorization namespace.Privacy method to only allow access to logged-in users who are members of a group/role named Administrators, as shown highlighted in the following code:
[Authorize(Roles = "Administrators")]
public IActionResult Privacy()
By default, role management is not enabled in an ASP.NET Core MVC project, so we must first enable it before creating roles, and then we will create a controller that will programmatically create an Administrators role (if it does not already exist) and assign a test user to that role:
Program.cs, in the setup of ASP.NET Core Identity and its database, add a call to AddRoles to enable role management, as shown highlighted in the following code:
services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>() // enable role management
.AddEntityFrameworkStores<ApplicationDbContext>();
Controllers, add an empty controller class named RolesController.cs and modify its contents, as shown in the following code:
using Microsoft.AspNetCore.Identity; // RoleManager, UserManager
using Microsoft.AspNetCore.Mvc; // Controller, IActionResult
using static System.Console;
namespace Northwind.Mvc.Controllers;
public class RolesController : Controller
{
private string AdminRole = "Administrators";
private string UserEmail = "test@example.com";
private readonly RoleManager<IdentityRole> roleManager;
private readonly UserManager<IdentityUser> userManager;
public RolesController(RoleManager<IdentityRole> roleManager,
UserManager<IdentityUser> userManager)
{
this.roleManager = roleManager;
this.userManager = userManager;
}
public async Task<IActionResult> Index()
{
if (!(await roleManager.RoleExistsAsync(AdminRole)))
{
await roleManager.CreateAsync(new IdentityRole(AdminRole));
}
IdentityUser user = await userManager.FindByEmailAsync(UserEmail);
if (user == null)
{
user = new();
user.UserName = UserEmail;
user.Email = UserEmail;
IdentityResult result = await userManager.CreateAsync(
user, "Pa$$w0rd");
if (result.Succeeded)
{
WriteLine($"User {user.UserName} created successfully.");
}
else
{
foreach (IdentityError error in result.Errors)
{
WriteLine(error.Description);
}
}
}
if (!user.EmailConfirmed)
{
string token = await userManager
.GenerateEmailConfirmationTokenAsync(user);
IdentityResult result = await userManager
.ConfirmEmailAsync(user, token);
if (result.Succeeded)
{
WriteLine($"User {user.UserName} email confirmed successfully.");
}
else
{
foreach (IdentityError error in result.Errors)
{
WriteLine(error.Description);
}
}
}
if (!(await userManager.IsInRoleAsync(user, AdminRole)))
{
IdentityResult result = await userManager
.AddToRoleAsync(user, AdminRole);
if (result.Succeeded)
{
WriteLine($"User {user.UserName} added to {AdminRole} successfully.");
}
else
{
foreach (IdentityError error in result.Errors)
{
WriteLine(error.Description);
}
}
}
return Redirect("/");
}
}
Administrators role does not exist, we use the role manager to create it. Administrators role.mark@example.com.)roles as a relative URL path, as shown in the following link: https://localhost:5001/roles. User test@example.com created successfully.
User test@example.com email confirmed successfully.
User test@example.com added to Administrators successfully.
test@example.com, and their password, and then click Log in, and you should now have access.To improve response times and scalability, you might want to cache the HTTP response that is generated by an action method by decorating the method with the [ResponseCache] attribute.
You control where the response is cached and for how long by setting parameters, as shown in the following list:
Duration: In seconds. This sets the max-age HTTP response header measured in seconds. Common choices are one hour (3600 seconds) and one day (86400 seconds).Location: One of the ResponseCacheLocation values, Any, Client, or None. This sets the cache-control HTTP response header.NoStore: If true, this ignores Duration and Location and sets the cache-control HTTP response header to no-store.Let's see an example:
HomeController.cs, add an attribute to the Index method to cache the response for 10 seconds on the browser or any proxies between the server and browser, as shown highlighted in the following code:
[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any)]
public IActionResult Index()
Views, in Home, open Index.cshtml, and add a paragraph to output the current time in long format to include seconds, as shown in the following markup:
<p class="alert alert-primary">@DateTime.Now.ToLongTimeString()</p>
warn: Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery[8]
The 'Cache-Control' and 'Pragma' headers have been overridden and set to 'no-cache, no-store' and 'no-cache' respectively to prevent caching of this response. Any response that uses antiforgery should not be cached.
You might want to define a simplified route for an action method instead of using the default route.
For example, to show the privacy page currently requires the following URL path, which specifies both the controller and action:
https://localhost:5001/home/privacy
We could make the route simpler, as shown in the following link:
https://localhost:5001/private
Let's see how to do that:
HomeController.cs, add an attribute to the Privacy method to define a simplified route, as shown highlighted in the following code:
[Route("private")]
[Authorize(Roles = "Administrators")]
public IActionResult Privacy()
https://localhost:5001/private
In MVC, the M stands for model. Models represent the data required to respond to a request. There are two types of models commonly used: entity models and view models.
Entity models represent entities in a database like SQL Server or SQLite. Based on the request, one or more entities might need to be retrieved from data storage. Entity models are defined using classes since they might need to change and then be used to update the underlying data store.
All the data that we want to show in response to a request is the MVC model, sometimes called a view model, because it is a model that is passed into a view for rendering into a response format like HTML or JSON. View models should be immutable, so they are commonly defined using records.
For example, the following HTTP GET request might mean that the browser is asking for the product details page for product number 3:
http://www.example.com/products/details/3
The controller would need to use the ID route value 3 to retrieve the entity for that product and pass it to a view that can then turn the model into HTML for display in a browser.
Imagine that when a user comes to our website, we want to show them a carousel of categories, a list of products, and a count of the number of visitors we have had this month.
We will reference the Entity Framework Core entity data model for the Northwind database that you created in Chapter 13, Introducing Practical Applications of C# and .NET:
Northwind.Mvc project, add a project reference to Northwind.Common.DataContext for either SQLite or SQL Server, as shown in the following markup:
<ItemGroup>
<!-- change Sqlite to SqlServer if you prefer -->
<ProjectReference Include=
"..\Northwind.Common.DataContext.Sqlite\Northwind.Common.DataContext.Sqlite.csproj" />
</ItemGroup>
Northwind.Mvc project to compile its dependencies.appsettings.json, add a connection string for the Northwind database using SQL Server, as shown highlighted in the following markup:
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Northwind.Mvc-DC9C4FAF-DD84-4FC9-B925-69A61240EDA7;Trusted_Connection=True;MultipleActiveResultSets=true",
"NorthwindConnection": "Server=.;Database=Northwind;Trusted_Connection=True;MultipleActiveResultSets=true"
},
Program.cs, import the namespace to work with your entity model types, as shown in the following code:
using Packt.Shared; // AddNorthwindContext extension method
builder.Build method call, add statements to load the appropriate connection string and then to register the Northwind database context, as shown in the following code:
// if you are using SQL Server
string sqlServerConnection = builder.Configuration
.GetConnectionString("NorthwindConnection");
builder.Services.AddNorthwindContext(sqlServerConnection);
// if you are using SQLite default is ..\Northwind.db
builder.Services.AddNorthwindContext();
Models folder and name it HomeIndexViewModel.cs.Good Practice: Although the ErrorViewModel class created by the MVC project template does not follow this convention, I recommend that you use the naming convention {Controller}{Action}ViewModel for your view model classes.
using Packt.Shared; // Category, Product
namespace Northwind.Mvc.Models;
public record HomeIndexViewModel
(
int VisitorCount,
IList<Category> Categories,
IList<Product> Products
);
HomeController.cs, import the Packt.Shared namespace, as shown in the following code:
using Packt.Shared; // NorthwindContext
Northwind instance, and initialize it in the constructor, as shown highlighted in the following code:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly NorthwindContext db;
public HomeController(ILogger<HomeController> logger,
NorthwindContext injectedContext)
{
_logger = logger;
db = injectedContext;
}
...
ASP.NET Core will use constructor parameter injection to pass an instance of the NorthwindContext database context using the connection string you specified in Program.cs.
Index action method to create an instance of the view model for this method, simulating a visitor count using the Random class to generate a number between 1 and 1000, and using the Northwind database to get lists of categories and products, and then pass the model to the view, as shown highlighted in the following code:
[ResponseCache(Duration = 10, Location = ResponseCacheLocation.Any)]
public IActionResult Index()
{
_logger.LogError("This is a serious error (not really!)");
_logger.LogWarning("This is your first warning!");
_logger.LogWarning("Second warning!");
_logger.LogInformation("I am in the Index method of the HomeController.");
HomeIndexViewModel model = new
(
VisitorCount: (new Random()).Next(1, 1001),
Categories: db.Categories.ToList(),
Products: db.Products.ToList()
);
return View(model); // pass model to view
}
Remember the view search convention: when the View method is called in a controller's action method, ASP.NET Core MVC looks in the Views folder for a subfolder with the same name as the current controller, that is, Home. It then looks for a file with the same name as the current action, that is, Index.cshtml. It will also search for views that match the action method name in the Shared folder and for Razor Pages in the Pages folder.
In MVC, the V stands for view. The responsibility of a view is to transform a model into HTML or other formats.
There are multiple view engines that could be used to do this. The default view engine is called Razor, and it uses the @ symbol to indicate server-side code execution. The Razor Pages feature introduced with ASP.NET Core 2.0 uses the same view engine and so can use the same Razor syntax.
Let's modify the home page view to render the lists of categories and products:
Views folder, and then expand the Home folder.Index.cshtml file and note the block of C# code wrapped in @{ }. This will execute first and can be used to store data that needs to be passed into a shared layout file like the title of the web page, as shown in the following code:
@{
ViewData["Title"] = "Home Page";
}
<div> element that uses Bootstrap for styling.Good Practice: As well as defining your own styles, base your styles on a common library, such as Bootstrap, that implements responsive design.
Just as with Razor Pages, there is a file named _ViewStart.cshtml that gets executed by the View method. It is used to set defaults that apply to all views.
For example, it sets the Layout property of all views to a shared layout file, as shown in the following markup:
@{
Layout = "_Layout";
}
Views folder, open the _ViewImports.cshtml file and note that it imports some namespaces and then adds the ASP.NET Core tag helpers, as shown in the following code:
@using Northwind.Mvc
@using Northwind.Mvc.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Shared folder, open the _Layout.cshtml file.ViewData dictionary that was set earlier in the Index.cshtml view, as shown in the following markup:
<title>@ViewData["Title"] – Northwind.Mvc</title>
~ means the wwwroot folder, as shown in the following markup:
<link rel="stylesheet"
href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
<body>
<header>
<nav class="navbar ...">
<div> containing a partial view for logging in and hyperlinks to allow users to navigate between pages using ASP.NET Core tag helpers with attributes like asp-controller and asp-action, as shown in the following markup:
<div class=
"navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area=""
asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark"
asp-area="" asp-controller="Home"
asp-action="Privacy">Privacy</a>
</li>
</ul>
<partial name="_LoginPartial" />
</div>
The <a> elements use tag helper attributes named asp-controller and asp-action to specify the controller name and action name that will execute when the link is clicked on. If you want to navigate to a feature in a Razor Class Library, like the employees component that you created in the previous chapter, then you use asp-area to specify the feature name.
<main> element, as shown in the following markup:
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
The RenderBody method injects the contents of a specific Razor view for a page like the Index.cshtml file at that point in the shared layout.
<script> elements at the bottom of the page so that it does not slow down the display of the page and that you can add your own script blocks into an optional defined section named scripts, as shown in the following markup:
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js">
</script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("scripts", required: false)
When asp-append-version is specified with a true value in any element like <img> or <script> along with a src attribute, the Image Tag Helper is invoked (this helper is poorly named because it does not only affect images!).
It works by automatically appending a query string value named v that is generated from a hash of the referenced source file, as shown in the following example generated output:
<script src="~/js/site.js? v=Kl_dqr9NVtnMdsM2MUg4qthUnWZm5T1fCEimBPWDNgM"></script>
If even a single byte within the site.js file changes, then its hash value will be different, and therefore if a browser or CDN is caching the script file, then it will bust the cached copy and replace it with the new version.
Now that you've reviewed the structure of a basic MVC website, you will customize and extend it. You have already registered an EF Core model for the Northwind database, so the next task is to output some of that data on the home page.
The home page will show a list of the 77 products in the Northwind database. To make efficient use of space, we want to show the list in three columns. To do this, we need to customize the stylesheet for the website:
wwwroot\css folder, open the site.css file.product-columns ID, as shown in the following code:
#product-columns
{
column-count: 3;
}
The Northwind database includes a table of eight categories, but they do not have images, and websites look better with some colorful pictures:
wwwroot folder, create a folder named images.images folder, add eight image files named category1.jpeg, category2.jpeg, and so on, up to category8.jpeg.You can download images from the GitHub repository for this book at the following link: https://github.com/markjprice/cs10dotnet6/tree/master/Assets/Categories
Before we customize the home page view, let's review an example Razor file that has an initial Razor code block that instantiates an order with price and quantity and then outputs information about the order on the web page, as shown in the following markup:
@{
Order order = new()
{
OrderId = 123,
Product = "Sushi",
Price = 8.49M,
Quantity = 3
};
}
<div>Your order for @order.Quantity of @order.Product has a total cost of $@ order.Price * @order.Quantity</div>
The preceding Razor file would result in the following incorrect output:
Your order for 3 of Sushi has a total cost of $8.49 * 3
Although Razor markup can include the value of any single property using the @object.property syntax, you should wrap expressions in parentheses, as shown in the following markup:
<div>Your order for @order.Quantity of @order.Product has a total cost of $@ (order.Price * order.Quantity)</div>
The preceding Razor expression results in the following correct output:
Your order for 3 of Sushi has a total cost of $25.47
To improve the IntelliSense when writing a view, you can define what type the view can expect using an @model directive at the top:
Views\Home folder, open Index.cshtml.HomeIndexViewModel, as shown in the following code:
@model HomeIndexViewModel
Now, whenever we type Model in this view, your code editor will know the correct type for the model and will provide IntelliSense for it.
While entering code in a view, remember the following:
@model (with a lowercase m).@Model (with an uppercase M).Let's continue customizing the view for the home page.
string variable for the current item and under the existing <div> element add new markup to output categories in a carousel and products as an unordered list, as shown in the following markup:
@using Packt.Shared
@model HomeIndexViewModel
@{
ViewData["Title"] = "Home Page";
string currentItem = "";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
<p class="alert alert-primary">@DateTime.Now.ToLongTimeString()</p>
</div>
@if (Model is not null)
{
<div id="categories" class="carousel slide" data-ride="carousel"
data-interval="3000" data-keyboard="true">
<ol class="carousel-indicators">
@for (int c = 0; c < Model.Categories.Count; c++)
{
if (c == 0)
{
currentItem = "active";
}
else
{
currentItem = "";
}
<li data-target="#categories" data-slide-to="@c"
class="@currentItem"></li>
}
</ol>
<div class="carousel-inner">
@for (int c = 0; c < Model.Categories.Count; c++)
{
if (c == 0)
{
currentItem = "active";
}
else
{
currentItem = "";
}
<div class="carousel-item @currentItem">
<img class="d-block w-100" src=
"~/images/category@(Model.Categories[c].CategoryId).jpeg"
alt="@Model.Categories[c].CategoryName" />
<div class="carousel-caption d-none d-md-block">
<h2>@Model.Categories[c].CategoryName</h2>
<h3>@Model.Categories[c].Description</h3>
<p>
<a class="btn btn-primary"
href="/category/@Model.Categories[c].CategoryId">View</a>
</p>
</div>
</div>
}
</div>
<a class="carousel-control-prev" href="#categories"
role="button" data-slide="prev">
<span class="carousel-control-prev-icon"
aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#categories"
role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
}
<div class="row">
<div class="col-md-12">
<h1>Northwind</h1>
<p class="lead">
We have had @Model?.VisitorCount visitors this month.
</p>
@if (Model is not null)
{
<h2>Products</h2>
<div id="product-columns">
<ul>
@foreach (Product p in @Model.Products)
{
<li>
<a asp-controller="Home"
asp-action="ProductDetail"
asp-route-id="@p.ProductId">
@p.ProductName costs
@(p.UnitPrice is null ? "zero" : p.UnitPrice.Value.ToString("C"))
</a>
</li>
}
</ul>
</div>
}
</div>
</div>
While reviewing the preceding Razor markup, note the following:
<ul> and <li> with C# code to output the carousel of categories and the list of product names.<div> element with the id attribute of product-columns will use the custom style that we defined earlier, so all of the content in that element will display in three columns.<img> element for each category uses parentheses around a Razor expression to ensure that the compiler does not include the .jpeg as part of the expression, as shown in the following markup: "~/images/category@(Model.Categories[c].CategoryID).jpeg"<a> elements for the product links use tag helpers to generate URL paths. Clicks on these hyperlinks will be handled by the HomeController and its ProductDetail action method. This action method does not exist yet, but you will add it later in this chapter. The ID of the product is passed as a route segment named id, as shown in the following URL path for Ipoh Coffee: https://localhost:5001/Home/ProductDetail/43.Let's see the result of our customized home page:
Northwind.Mvc website project.
Figure 15.4: The updated Northwind MVC website home page
For now, clicking on any of the categories or product links gives 404 Not Found errors, so let's see how we can implement pages that use the passed parameters to see the details of a product or category.
One way to pass a simple parameter is to use the id segment defined in the default route:
HomeController class, add an action method named ProductDetail, as shown in the following code:
public IActionResult ProductDetail(int? id)
{
if (!id.HasValue)
{
return BadRequest("You must pass a product ID in the route, for example, /Home/ProductDetail/21");
}
Product? model = db.Products
.SingleOrDefault(p => p.ProductId == id);
if (model == null)
{
return NotFound($"ProductId {id} not found.");
}
return View(model); // pass model to view and then return result
}
Note the following:
id passed in the route to the parameter named id in the method.id does not have a value, and if so, we call the BadRequest method to return a 400 status code with a custom message explaining the correct URL path format.id value.NotFound method to return a 404 status code and a custom message explaining that a product with that ID was not found in the database.Views/Home folder, add a new file named ProductDetail.cshtml.@model Packt.Shared.Product
@{
ViewData["Title"] = "Product Detail - " + Model.ProductName;
}
<h2>Product Detail</h2>
<hr />
<div>
<dl class="dl-horizontal">
<dt>Product Id</dt>
<dd>@Model.ProductId</dd>
<dt>Product Name</dt>
<dd>@Model.ProductName</dd>
<dt>Category Id</dt>
<dd>@Model.CategoryId</dd>
<dt>Unit Price</dt>
<dd>@Model.UnitPrice.Value.ToString("C")</dd>
<dt>Units In Stock</dt>
<dd>@Model.UnitsInStock</dd>
</dl>
</div>
Northwind.Mvc project.
Figure 15.5: The product detail page for Chang
Model binders are powerful, and the default one does a lot for you. After the default route identifies a controller class to instantiate and an action method to call, if that method has parameters, then those parameters need to have values set.
Model binders do this by looking for parameter values passed in the HTTP request as any of the following types of parameters:
id as we did in the previous section, as shown in the following URL path: /Home/ProductDetail/2/Home/ProductDetail?id=2<form action="post" action="/Home/ProductDetail">
<input type="text" name="id" value="2" />
<input type="submit" />
</form>
Model binders can populate almost any type:
int, string, DateTime, and bool.class, record, or struct.Let's create a somewhat artificial example to illustrate what can be achieved using the default model binder:
Models folder, add a new file named Thing.cs.Id and a string named Color, as shown in the following code:
namespace Northwind.Mvc.Models;
public class Thing
{
public int? Id { get; set; }
public string? Color { get; set; }
}
HomeController, add two new action methods, one to show a page with a form and one to display a thing with a parameter using your new model type, as shown in the following code:
public IActionResult ModelBinding()
{
return View(); // the page with a form to submit
}
public IActionResult ModelBinding(Thing thing)
{
return View(thing); // show the model bound thing
}
Views\Home folder, add a new file named ModelBinding.cshtml.@model Thing
@{
ViewData["Title"] = "Model Binding Demo";
}
<h1>@ViewData["Title"]</h1>
<div>
Enter values for your thing in the following form:
</div>
<form method="POST" action="/home/modelbinding?id=3">
<input name="color" value="Red" />
<input type="submit" />
</form>
@if (Model != null)
{
<h2>Submitted Thing</h2>
<hr />
<div>
<dl class="dl-horizontal">
<dt>Model.Id</dt>
<dd>@Model.Id</dd>
<dt>Model.Color</dt>
<dd>@Model.Color</dd>
</dl>
</div>
}
Views/Home, open Index.cshtml, and in the first <div>, add a new paragraph with a link to the model binding page, as shown in the following markup:
<p><a asp-action="ModelBinding" asp-controller="Home">Binding</a></p>

Figure 15.6: An unhandled ambiguous action method mismatch exception
Although the C# compiler can differentiate between the two methods by noting that the signatures are different, from the routing of an HTTP request's point of view, both methods are potential matches. We need an HTTP-specific way to disambiguate the action methods.
We could do this by creating different names for the actions or by specifying that one method should be used for a specific HTTP verb, like GET, POST, or DELETE. That is how we will solve the problem:
HomeController, decorate the second ModelBinding action method to indicate that it should be used for processing HTTP POST requests, that is, when a form is submitted, as shown highlighted in the following code:
[HttpPost]
public IActionResult ModelBinding(Thing thing)
The other ModelBinding action method will implicitly be used for all other types of HTTP request, like GET, PUT, DELETE, and so on.
Id property is set from the query string parameter and the value for the color property is set from the form parameter, as shown in Figure 15.7:
Figure 15.7: The Model Binding Demo page
Now we will set the property using a route parameter:
2 as a route parameter, as shown highlighted in the following markup:
<form method="POST" action="/home/modelbinding/2?id=3">
Id property is set from the route parameter and the value for the Color property is set from the form parameter.Now we will set the property using a form parameter:
<form method="POST" action="/home/modelbinding/2?id=3">
<input name="id" value="1" />
<input name="color" value="Red" />
<input type="submit" />
</form>
Id and Color properties are both set from the form parameters.Good Practice: If you have multiple parameters with the same name, then remember that form parameters have the highest priority and query string parameters have the lowest priority for automatic model binding.
The process of model binding can cause errors, for example, data type conversions or validation errors if the model has been decorated with validation rules. What data has been bound and any binding or validation errors are stored in ControllerBase.ModelState.
Let's explore what we can do with model state by applying some validation rules to the bound model and then showing invalid data messages in the view:
Models folder, open Thing.cs.System.ComponentModel.DataAnnotations namespace.Id property with a validation attribute to limit the range of allowed numbers to 1 to 10, and one to ensure that the visitor supplies a color, and add a new Email property with a regular expression for validation, as shown highlighted in the following code:
public class Thing
{
[Range(1, 10)]
public int? Id { get; set; }
[Required]
public string? Color { get; set; }
[EmailAddress]
public string? Email { get; set; }
}
Models folder, add a new file named HomeModelBindingViewModel.cs.namespace Northwind.Mvc.Models;
public record HomeModelBindingViewModel
(
Thing Thing,
bool HasErrors,
IEnumerable<string> ValidationErrors
);
HomeController, in the ModelBinding method that handles HTTP POST, comment out the previous statement that passed the thing to the view, and instead add statements to create an instance of the view model. Validate the model and store an array of error messages, and then pass the view model to the view, as shown highlighted in the following code:
[HttpPost]
public IActionResult ModelBinding(Thing thing)
{
HomeModelBindingViewModel model = new(
thing,
!ModelState.IsValid,
ModelState.Values
.SelectMany(state => state.Errors)
.Select(error => error.ErrorMessage)
);
return View(model);
}
Views\Home, open ModelBinding.cshtml.@model Northwind.Mvc.Models.HomeModelBindingViewModel
<div> to show any model validation errors, and change the output of the thing's properties because the view model has changed, as shown highlighted in the following markup:
<form method="POST" action="/home/modelbinding/2?id=3">
<input name="id" value="1" />
<input name="color" value="Red" />
<input name="email" value="test@example.com" />
<input type="submit" />
</form>
@if (Model != null)
{
<h2>Submitted Thing</h2>
<hr />
<div>
<dl class="dl-horizontal">
<dt>Model.Thing.Id</dt>
<dd>@Model.Thing.Id</dd>
<dt>Model.Thing.Color</dt>
<dd>@Model.Thing.Color</dd>
<dt>Model.Thing.Email</dt>
<dd>@Model.Thing.Email</dd>
</dl>
</div>
@if (Model.HasErrors)
{
<div>
@foreach(string errorMessage in Model.ValidationErrors)
{
<div class="alert alert-danger" role="alert">@errorMessage</div>
}
</div>
}
}
1, Red, and test@example.com are valid values.Id of 13, clear the color textbox, delete the @ from the email address, click the Submit button, and note the error messages, as shown in Figure 15.8:
Figure 15.8: The Model Binding Demo page with field validations
Good Practice: What regular expression does Microsoft use for the implementation of the EmailAddress validation attribute? Find out at the following link: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs#L54
While creating a view for ASP.NET Core MVC, you can use the Html object and its methods to generate markup.
Some useful methods include the following:
ActionLink: Use this to generate an anchor <a> element that contains a URL path to the specified controller and action. For example, Html.ActionLink(linkText: "Binding", actionName: "ModelBinding", controllerName: "Home") would generate <a href="/home/modelbinding">Binding</a>. You can achieve the same result using the anchor tag helper: <a asp-action="ModelBinding" asp-controller="Home">Binding</a>.AntiForgeryToken: Use this inside a <form> to insert a <hidden> element containing an anti-forgery token that will be validated when the form is submitted.Display and DisplayFor: Use this to generate HTML markup for the expression relative to the current model using a display template. There are built-in display templates for .NET types and custom templates can be created in the DisplayTemplates folder. The folder name is case-sensitive on case-sensitive filesystems.DisplayForModel: Use this to generate HTML markup for an entire model instead of a single expression.Editor and EditorFor: Use this to generate HTML markup for the expression relative to the current model using an editor template. There are built-in editor templates for .NET types that use <label> and <input> elements, and custom templates can be created in the EditorTemplates folder. The folder name is case-sensitive on case-sensitive filesystems.EditorForModel: Use this to generate HTML markup for an entire model instead of a single expression.Encode: Use this to safely encode an object or string into HTML. For example, the string value "<script>" would be encoded as "<script>". This is not normally necessary since the Razor @ symbol encodes string values by default.Raw: Use this to render a string value without encoding as HTML.PartialAsync and RenderPartialAsync: Use these to generate HTML markup for a partial view. You can optionally pass a model and view data.Views/Home, open ModelBinding.cshtml.Email property to use DisplayFor, as shown in the following markup:
<dd>@Html.DisplayFor(model => model.Thing.Email)</dd>
Models/Thing.cs, comment out the [EmailAddress] attribute above the Email property.Models/Thing.cs, uncomment the [EmailAddress] attribute. It is the combination of decorating the Email property with the [EmailAddress] validation attribute and rendering it using DisplayFor that notifies ASP.NET Core to treat the value as an email address and therefore render it as a clickable link.
Let's create a new action method that can have a query string parameter passed to it and use that to query the Northwind database for products that cost more than a specified price.
In previous examples, we defined a view model that contained properties for every value that needed to be rendered in the view. In this example, there will be two values: a list of products and the price the visitor entered. To avoid having to define a class or record for the view model, we will pass the list of products as the model and store the maximum price in the ViewData collection.
Let's implement this feature:
HomeController, import the Microsoft.EntityFrameworkCore namespace. We need this to add the Include extension method so that we can include related entities, as you learned in Chapter 10, Working with Data Using Entity Framework Core.public IActionResult ProductsThatCostMoreThan(decimal? price)
{
if (!price.HasValue)
{
return BadRequest("You must pass a product price in the query string, for example, /Home/ProductsThatCostMoreThan?price=50");
}
IEnumerable<Product> model = db.Products
.Include(p => p.Category)
.Include(p => p.Supplier)
.Where(p => p.UnitPrice > price);
if (!model.Any())
{
return NotFound(
$"No products cost more than {price:C}.");
}
ViewData["MaxPrice"] = price.Value.ToString("C");
return View(model); // pass model to view
}
Views/Home folder, add a new file named ProductsThatCostMoreThan.cshtml.@using Packt.Shared
@model IEnumerable<Product>
@{
string title =
"Products That Cost More Than " + ViewData["MaxPrice"];
ViewData["Title"] = title;
}
<h2>@title</h2>
@if (Model is null)
{
<div>No products found.</div>
}
else
{
<table class="table">
<thead>
<tr>
<th>Category Name</th>
<th>Supplier's Company Name</th>
<th>Product Name</th>
<th>Unit Price</th>
<th>Units In Stock</th>
</tr>
</thead>
<tbody>
@foreach (Product p in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => p.Category.CategoryName)
</td>
<td>
@Html.DisplayFor(modelItem => p.Supplier.CompanyName)
</td>
<td>
@Html.DisplayFor(modelItem => p.ProductName)
</td>
<td>
@Html.DisplayFor(modelItem => p.UnitPrice)
</td>
<td>
@Html.DisplayFor(modelItem => p.UnitsInStock)
</td>
</tr>
}
<tbody>
</table>
}
Views/Home folder, open Index.cshtml.<h3>Query products by price</h3>
<form asp-action="ProductsThatCostMoreThan" method="GET">
<input name="price" placeholder="Enter a product price" />
<input type="submit" />
</form>
50, and then click on Submit.
Figure 15.9: A filtered list of products that cost more than £50
When building a desktop or mobile app, multiple tasks (and their underlying threads) can be used to improve responsiveness, because while one thread is busy with the task, another can handle interactions with the user.
Tasks and their threads can be useful on the server side too, especially with websites that work with files, or request data from a store or a web service that could take a while to respond. But they are detrimental to complex calculations that are CPU-bound, so leave these to be processed synchronously as normal.
When an HTTP request arrives at the web server, a thread from its pool is allocated to handle the request. But if that thread must wait for a resource, then it is blocked from handling any more incoming requests. If a website receives more simultaneous requests than it has threads in its pool, then some of those requests will respond with a server timeout error, 503 Service Unavailable.
The threads that are locked are not doing useful work. They could handle one of those other requests but only if we implement asynchronous code in our websites.
Whenever a thread is waiting for a resource it needs, it can return to the thread pool and handle a different incoming request, improving the scalability of the website, that is, increasing the number of simultaneous requests it can handle.
Why not just have a larger thread pool? In modern operating systems, every thread in the pool has a 1 MB stack. An asynchronous method uses a smaller amount of memory. It also removes the need to create new threads in the pool, which takes time. The rate at which new threads are added to the pool is typically one every two seconds, which is a loooooong time compared to switching between asynchronous threads.
Good Practice: Make your controller action methods asynchronous.
It is easy to make an existing action method asynchronous:
Index action method to be asynchronous, to return a task, and to await the calls to asynchronous methods to get the categories and products, as shown highlighted in the following code:
public async Task<IActionResult> Index()
{
HomeIndexViewModel model = new
(
VisitorCount = (new Random()).Next(1, 1001),
Categories = await db.Categories.ToListAsync(),
Products = await db.Products.ToListAsync()
);
return View(model); // pass model to view
}
ProductDetail action method in a similar way, as shown highlighted in the following code:
public async Task<IActionResult> ProductDetail(int? id)
{
if (!id.HasValue)
{
return BadRequest("You must pass a product ID in the route, for example,
/Home/ProductDetail/21");
}
Product? model = await db.Products
.SingleOrDefaultAsync(p => p.ProductId == id);
if (model == null)
{
return NotFound($"ProductId {id} not found.");
}
return View(model); // pass model to view and then return result
}
Test your knowledge and understanding by answering some questions, get some hands-on practice, and explore this chapter's topics with deeper research.
Answer the following questions:
_ViewStart and _ViewImports do when created in the Views folder?_Layout.cshtml, how do you output the content of the current view?_Layout.cshtml, how do you output a section that the current view can supply content for, and how does the view supply the contents for that section?View method inside a controller's action method, what paths are searched for the view by convention?The Northwind.Mvc project has a home page that shows categories, but when the View button is clicked, the website returns a 404 Not Found error, for example, for the following URL:
https://localhost:5001/category/1
Extend the Northwind.Mvc project by adding the ability to show a detail page for a category.
A few years ago, Stephen Cleary wrote an excellent article for MSDN Magazine explaining the scalability benefits of implementing async action methods for ASP.NET. The same principles apply to ASP.NET Core, but even more so, because unlike the old ASP.NET as described in the article, ASP.NET Core supports asynchronous filters and other components.
Read the article at the following link:
Controllers are where the business logic of your website runs, so it is important to test the correctness of that logic using unit tests, as you learned in Chapter 4, Writing, Debugging, and Testing Functions.
Write some unit tests for HomeController.
Good Practice: You can read more about how to unit test controllers at the following link: https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing
Use the links on the following page to learn more about the topics covered in this chapter:
In this chapter, you learned how to build large, complex websites in a way that is easy to unit test by registering and injecting dependency services like database contexts and loggers and is easier to manage with teams of programmers using ASP.NET Core MVC. You learned about configuration, authentication, routes, models, views, and controllers.
In the next chapter, you will learn how to build and consume services that use HTTP as the communication layer, aka web services.