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:
- In
HomeController.cs
, add a new action method, as shown in the following code:
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);
}
- In the
Views/Home
folder, add a new file namedProductsThatCostMoreThan.cshtml
. - Modify the contents, as shown in the following code:
@using Northwind.EntityModels
@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>
}
- In the
Views/Home
folder, openIndex.cshtml
. - Add the following form element below the visitor count and above the Products heading and its listing of products. This will provide a form for the user to enter a price. The user can then click Submit to call the action method that shows only products that cost more than the entered price:
<h3>Query products by price</h3>
<form asp-action="ProductsThatCostMoreThan" method="GET">
<input name="price" placeholder="Enter a product price" />
<input type="submit" />
</form>
- Start the
Northwind.Mvc
website project using thehttps
launch profile. - On the home page, enter a price in the form, for example,
50
, and then click Submit. - Note the table of the products that cost more than the price that you entered, as shown in Figure 14.9:
Figure 14.9: A filtered list of products that cost more than £50
- Close Chrome and shut down the web server.
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 for 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.
In an earlier task, you imported the Microsoft.EntityFrameworkCore`` namespace so that you could use the
Include` extension method. You are about to use another extension method that requires that namespace to be imported.
It is easy to make an existing action method asynchronous:
- At the top of
HomeController.cs
, make sure the namespace for adding EF Core extension methods is imported, as shown in the following code:
// To use the Include and ToListAsync extension methods.
using Microsoft.EntityFrameworkCore;
- In
HomeController.cs
, modify theIndex
action method to be asynchronous and await the calls to asynchronous methods to get the categories and products, as shown in the following code:
[ResponseCache(Duration = 10 /* seconds */,
Location = ResponseCacheLocation.Any)]
public async Task<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: Random.Shared.Next(1, 1001),
Categories: await _db.Categories.ToListAsync(),
Products: await _db.Products.ToListAsync()
);
return View(model); // Pass the model to the view.
}
- Modify the
ProductDetail
action method in a similar way, as shown highlighted in the following code:
public async Task<IActionResult> ProductDetail(int? id,
- In the
ProductDetail
action method, await the calls to asynchronous methods to get the product, as shown highlighted in the following code:
Product? model = await _db.Products.Include(p => p.Category)
.SingleOrDefaultAsync(p => p.ProductId == id);
- Start the
Northwind.Mvc
website project using thehttps
launch profile. - Note that the functionality of the website is the same, but trust that it will now scale better.
- Close Chrome and shut down the web server.