Currently, when we run the application, all of the vehicle records are displayed on one page. At the moment we only have 12 records. But if we had hundreds, or thousands, or even millions of vehicles in the database a user would most likely balk at having to scroll and hunt for one she is interested in on a single page.
In this module we are going to implement a paging feature so that the user can search through pages of vehicles in a more digestible manor.
- Add a pageNumber parameter to the Home Controller
- Unit Test the Pagination feature
- Create the View Model
- Refactor the Index method results
- Create the Paging Tag Helper
- Register the custom Tag Helper
- Add a PageLinkTagHelper element to the View
- Style the Page Link buttons
- Add a paging route
- Fix the HomeController Tests
- Unit Test the PageLinkTagHelper component
- What's Next
Add a pageNumber parameter to the Home Controller
Modify the home controller with the following code.
FredsCars\Controllers\HomeController.cs
// using FredsCars.Data;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FredsCars.Controllers
{
public class HomeController : Controller
{
private IVehicleRepository _repo;
public int PageSize = 5;
public HomeController(IVehicleRepository repo)
{
_repo = repo;
}
public async Task<ViewResult> Index(int pageNumber = 1)
{
return View(await _repo.Vehicles
.Skip((pageNumber - 1) * PageSize)
.Take(PageSize)
.Include(v => v.VehicleType)
.ToListAsync());
}
}
}
Restart the application and the results should look similar to the following. Only the first five vehicles from the database are rendered. This is the first page.

To get to the second page navigate to the following URL:http://localhost:40080/?pageNumber=2
The question mark (?
) in the URL signifies the start of a querystring. A QueryString is a part of a URL that contains key-value pairs used to pass information to the web application. It typically appears after a question mark (?
) in the URL, with multiple parameters separated by ampersands (&
). We will see examples of the ampersand separator when we add more QueryString parameters for sorting.
In the above URL, there is one key/value pair; pageNumber=2. So the value of 2 will be passed as an argument to the matching named parameter in the Home Controller Index method.
The next five vehicles are rendered for page 2.

To get to the last two records on the third page navigate to the following URL:http://localhost:40080/?pageNumber=
3

In the code above we modified the Home Controller first by declaring a public property of type int named PageSize and initialized it to the value 5.
public int PageSize = 5;
This public property is used to control how many vehicle records we take for each page. And, since it is a public property we can easily set it to any value we like from the calling code. This will make testing easier and also make the code more flexible. For instance, we may want to create a feature allowing the user to select how many vehicles show per page.
The final two changes are in the Index method. First we added an optional parameter of type int named pageNumber to the signature of the Index method.
public async Task<ViewResult> Index(int pageNumber = 1)
The value of pageNumber in the URL QueryStrings above will be passed to the pageNumber parameter in the Index method specifying which page number should be selected from the database. Matching URL QueryString values to action method parameters is done by the MVC model binding system. If there is no pageNumber value in the URL’s QueryString the pageNumber parameter will have a default value of one as declared in the Index method’s signature. This is what makes it an optional parameter.
The ASP.Net Core model binding system can also match route segment values to parameters in methods. For instance, the default route in Program.cs is:{controller = home}/{action = index}/{id?}
.
So, for a URL like vehicles/edit/1
, the request would go to the Vehicles controller (which we have not created yet), then to the edit action method, and pass the optional id parameter value of one to an id parameter in the edit method.
The final change we made is in the body of the Index method.
return View(await _repo.Vehicles
.Skip((pageNumber - 1) * PageSize)
.Take(PageSize)
.Include(v => v.VehicleType)
.ToListAsync());
Here we used the LINQ methods Skip
and Take
against the Vehicles IQueryable to go to the page number specified, subtract one, and multiply the result times the page size.
So, if the page number is 2 and we subtract one, the result is one. If you multiply 1 times 5, you are skipping the first five records to start at the specified page number of 2. From there we take five records. Simple! Right? Don’t worry if paging logic seems a little difficult at first. Just follow the pattern and eventually it will start to make sense.
Unit Test the Pagination feature
At this point it would be a good idea to test the home controller again. First of all we want to test the new feature; pagination. Secondly we want to make sure our existing tests for the home controller are still working.
NOTE: Unit Tests are typically automated and run nightly in a team development environment. But, for our purposes we will keep an eye on the status of our tests manually throughout this book.
Add the following test called Can_Page_Results
to the HomeControllerTests
class by modifying the HomeControllerTests.cs
file with the code below.
FredsCars.Tests\Controllers\HomeControllerTests.cs
... existing code ...
public class HomeControllerTests
{
... existing code ...
[Fact]
public async Task Can_Page_Results()
{
// Arrange
// 1 - create a List<T> with test items
var vehicles = new List<Vehicle>
{
new Vehicle
{
Id = 1,
Make = "Make1",
Model = "Model1",
VehicleType = new VehicleType
{
Id = 1,
Name = "Car"
}
},
new Vehicle
{
Id = 2,
Make = "Make2",
Model = "Model2",
VehicleType = new VehicleType
{
Id = 2,
Name = "Car"
}
},
new Vehicle
{
Id = 3,
Make = "Make3",
Model = "Model3",
VehicleType = new VehicleType
{
Id = 3,
Name = "Truck"
}
},
new Vehicle
{
Id = 4,
Make = "Make4",
Model = "Model4",
VehicleType = new VehicleType
{
Id = 4,
Name = "Jeep"
}
},
new Vehicle
{
Id = 5,
Make = "Make5",
Model = "Model5",
VehicleType = new VehicleType
{
Id = 5,
Name = "Jeep"
}
}
};
// 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension
var mockVehiclesIQueryable = vehicles.BuildMock();
// 3 - build mock IVehicleRepository
Mock<IVehicleRepository> mockVehicleRepo =
new Mock<IVehicleRepository>();
mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);
HomeController controller =
new HomeController(mockVehicleRepo.Object);
controller.PageSize = 3;
// Act
IEnumerable<Vehicle>? result =
(await controller.Index(2)).ViewData.Model
as IEnumerable<Vehicle>;
// Assert
Vehicle[] vehicleArray = result?.ToArray()
?? Array.Empty<Vehicle>();
Assert.True(vehicleArray.Length == 2);
Assert.Equal("Make4", vehicleArray[0].Make);
Assert.Equal("Make5", vehicleArray[1].Make);
}
}
... existing code ...
In the unit test code above we are testing that the Home Controller’s Index method can now page through data utilizing the changes we made earlier in adding a PageSize
property to the controller and utilizing the Skip
and Take
LINQ methods on the IQueryable
.
This code is very similar to the first unit test we wrote for the home controller’s Index method. First, in the arrange section, we create a List<Vehicle>
with test data, use the MockQueryable
Moq extension to convert the vehicles list into an IQueryable
that the asynchronous code in the Index method can deal with, and create a mock of the IVehicleRepository
interface specifying its Vehicles
property returns the test data from the mock Vehicles IQueryable
. Finally we create an instance of the home controller and inject the mock Vehicle Repo object into its constructor. We also set the controller’s PageSize
property to three for the test.
In the act section we asyncronously call the contoller’s Index method passing the value of 2 as the argument to the pageNumber parameter. From there we get the Model from the ViewData
property of the returned ViewResult object, and convert the model to an IEnumerable<Vehicle>
. Finally, we assign the IEnumerable<Vehicle> result to a variable called result.
Lastly, in the assert section, we convert the result to an array to make it easier to work with and assign it to a variable named vehicleArray of type Vehicle[]
(or Vehicle array).
We first assert that the array’s length is 2 which it should be since the test data is comprised of five Vehicle objects in the List, the PageSize is three, and we called the second page.
We then assert that the Make property of the fist and second records in the result (the 4th and 5th records of the test data) are equal to “Make4” and “Make5”.
If you run the test in the Test Explorer, you should see a green circle with a checkmark indicating the test succeeded and is in a passing state.

And if you run all of the tests in the Test project all of the previous tests should pass.

The original test for Can_Access_VehicleList_FromVehicleRepo
still passes even though we do not pass an argument to the controller’s Index method for the pageNumber.
IEnumerable<Vehicle>? result =
(await controller.Index(/* no parameter passed here for pageNumber */)).ViewData.Model
as IEnumerable<Vehicle>;
This is because we defined the pageNumber parameter as an optional parameter by giving it a default value of 1.
public async Task<ViewResult> Index(int pageNumber = 1)
Create the View Model
Now that we have paging working in the Home Controller’s Index method, we need a way to get all of the paging information down to the view so that it can actually implement the feature for the user.
This is where View Models come in.
In ASP.NET Core, a ViewModel is a class that serves as a data container we can use to pass information from a controller to a view.
Instead of just passing the Vehicles IQueryable
property of the repository with the results down to the View as the model, we are going to embed this information into a ViewModel along with the paging information. That way we can pass all of the information we need down to the View in one fell swoop in one big strongly typed object.
Add a folder named ViewModels to the Models folder. In the new ViewModels folder create a class called PagingInfo and modify it with the code below.
FredsCars\Models\ViewModels\PagingInfo.cs
namespace FredsCars.Views.ViewModels
{
public class PagingInfo
{
// Total number of items in the database
public int TotalItemCount { get; set; }
public int PageSize { get; set; }
public int PageIndex { get; set; }
public int TotalPageCount =>
(int)Math.Ceiling((decimal)TotalItemCount / PageSize);
}
}
The PagingInfo
class shown above is not the actual ViewModel itself, but a container class for all of the paging info we need to pass to the view. We are going to embed this class and the Vehicle results into one View Model and pass that to the view.
The PagingInfo
class will contain all of the information the View needs to implement paging:
- TotalItemCount: the total number of records in the repository.
- PageSize: number of items per page.
- PageIndex: the current page.
- TotalPageCount: a calculated property. We get the total number of pages by dividing the total item count by the page size. This may end up being a floating point number with a decimal like 2.3. So we use the C#
Math.Ceiling
method to round up to the next whole integer for instance, 3.
For the second part of our View Model, create a class named VehiclesListViewModel in the ViewModels folder and modify it with the code shown below.
FredsCars\Models\ViewModels\VehiclesListViewModel.cs
namespace FredsCars.Views.ViewModels
{
public class VehiclesListViewModel
{
public List<Vehicle> Vehicles { get; set; } = new();
public PagingInfo PagingInfo { get; set; } = new();
}
}
In the View Model code above in the new class named VehiclesListViewModel
, we have embedded a PagingInfo
class property as well as a second property named Vehicles of type List<Vehicle>
(List of Vehicle). We have initialized both properties to their respective types using the new method. The new method is syntactical sugar so we don’t have to spell out each property’s type when it can be inferred by C#. The following is how the longer form would look.
public List<Vehicle> Vehicles { get; set; } = new List<Vehicle>();
public PagingInfo PagingInfo { get; set; } = new PagingInfo();
Refactor the Index method results
Now that we have a ViewModel structure ready to go it is time to refactor the Index method to include the paging information in the result the view will need as well as the Vehicle IQueryable containing one page of Vehicles.
Modify the Home Controller with the following code.
FredsCars\Controllers\HomeController.cs
// using FredsCars.Data;
using FredsCars.Models.Repositories;
using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FredsCars.Controllers
{
public class HomeController : Controller
{
private IVehicleRepository _repo;
public int PageSize = 5;
public HomeController(IVehicleRepository repo)
{
_repo = repo;
}
public async Task<ViewResult> Index(int pageNumber = 1)
=> View(new VehiclesListViewModel
{
Vehicles = await _repo.Vehicles
.Skip((pageNumber - 1) * PageSize)
.Take(PageSize)
.Include(v => v.VehicleType)
.ToListAsync(),
PagingInfo = new PagingInfo
{
PageIndex = pageNumber,
PageSize = this.PageSize,
TotalItemCount = _repo.Vehicles.Count()
}
});
}
}
In the code above we have incorporated the paging information into the model we pass to the view.
First, we have the replaced the return word syntax for the Index method:
public async Task<ViewResult> Index(int pageNumber = 1)
{
return View(/* model results */);
}
with a method bodied expression using the Lambda syntax and Lambda operator (=>):
public async Task<ViewResult> Index(int pageNumber = 1)
=> View(new VehiclesListViewModel
{
Vehicles = ...,
PagingInfo = ...
}
});
Expression-bodied members were introduced to C# in version 6.0 (and have been expanded upon since) and provide a concise way to define methods, properties, and other members using lambda-like syntax.
We didn’t change anything about the way we retrieve a page set of Vehicle results. But, now instead of passing the results directly to the view we assign it to the Vehicles property of the new VehiclesListViewModel class.
public async Task<ViewResult> Index(int pageNumber = 1)
=> View(new VehiclesListViewModel
{
Vehicles = await _repo.Vehicles
.Skip((pageNumber - 1) * PageSize)
.Take(PageSize)
.Include(v => v.VehicleType)
.ToListAsync(),
PagingInfo = new PagingInfo
{
PageIndex = pageNumber,
PageSize = this.PageSize,
TotalItemCount = _repo.Vehicles.Count()
}
});
At the same time we set all of the paging info in a new PagingInfo
class and assign that to the PagingInfo property of the VehiclesListViewModel
class. We set the PageIndex property to the incoming pageNumber parameter, the PageSize property to the similarly named class PageSize
property using the this
keyword to denote the class PageSize
property rather than the PageInfo
class PageSize property; Although Visual Studio dims the this
keyword telling us that it is not needed and Visual Studio could infer our meaning without it. But it is still good to be explicit if you prefer.
Create the Paging Tag Helper
Now that we have all of the supporting structure in place we can create a custom tag helper to implement paging for the user to page through results.
Create a folder on the root of the FredsCars project called TagHelpers. In the TagHelpers folder create a new class called PageLinkTagHelper and modify its contents with the code below.
FredsCars\TagHelpers\PageLinkTagHelper.cs
using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace FredsCars.TagHelpers
{
[HtmlTargetElement("div", Attributes = "page-model")]
public class PageLinkTagHelper : TagHelper
{
private IUrlHelperFactory _urlHelperFactory;
public PageLinkTagHelper(IUrlHelperFactory urlHelperFactory)
{
_urlHelperFactory = urlHelperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext? ViewContext { get; set; }
public PagingInfo? PageModel { get; set; }
public string? PageAction { get; set; }
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
if (ViewContext != null && PageModel != null)
{
IUrlHelper urlHelper
= _urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder divTag = new TagBuilder("div");
for (int i = 1; i < PageModel.TotalPageCount; i++)
{
TagBuilder aTag = new TagBuilder("a");
aTag.Attributes["href"] = urlHelper.Action(PageAction,
new { pageNumber = i });
aTag.InnerHtml.Append(i.ToString());
divTag.InnerHtml.AppendHtml(aTag);
}
output.Content.AppendHtml(divTag.InnerHtml);
}
}
}
}
In the code above we created a Tag Helper class called PageLinkTagHelper by inheriting from TagHelper.
public class PageLinkTagHelper : TagHelper
We also use the HtmlTargetElement
Attribute to tell the Tag Helper to become active for any div element that has an html attribute named page-model.
[HtmlTargetElement("div", Attributes = "page-model")]
public class PageLinkTagHelper : TagHelper
{
...
}
We then use dependency injection to set a private variable called _urlHelperFactory
to an IURLHelperFactory
instance. We will use the IUrlHelperFactory
instance to create an IURLHelper
later in the code which will help us create the page links dynamically.
Next we lay out three class properties:
The first is named ViewContext of type ViewContext:
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext? ViewContext { get; set; }
A ViewContext is used during View execution to help us grab the HTTP context from within a view. Here we have used the HtmlAttributeNotBound
attribute to tell ASP.Net Core that this property is not bound to an html attribute in the targeted div tag. In addition we mark the property with the ViewContext attribute to specify that it should be set with the current ViewContext when created.
The second property is called PageModel of type PagingInfo
(our paging container class embedded in our ViewModel earlier). This property will be bound to an html attribute in the targeted div element named page-model.
public PagingInfo? PageModel { get; set; }
The third property is called PageAction of type string. This property will be bound to an html attribute in the targeted div element named page-action.
public string? PageAction { get; set; }
Next we override the Process method of the base TagHelper class which executes the tag helper.
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
...
}
The Process method takes in a TagHelperContext
which contains information associated with execution of the current TagHelper, and a TagHelperOutput
instance which is used to represent the output of a TagHelper.
Within the Process method we capture an instance of an IUrlHelper
factory and assign it to a variable of type IUrlHelper
named urlHelper.
IUrlHelper urlHelper
= _urlHelperFactory.GetUrlHelper(ViewContext);
Here we use the IUrlHelperFactory's GetUrlHelper
method and pass to it the ViewContext instance (our first class property laid out above).
Next we create a div tag by using a new TagBuilder
class instance and pass to it the string “div”. This tag will serve as the div element in the final output html that acts as an html container for the page links.
TagBuilder divTag = new TagBuilder("div");
Finally, we use a for-loop to lay out the page links from Page One to the Total Page Count; Page n.
for (int i = 1; i < PageModel.TotalPageCount; i++)
{
TagBuilder aTag = new TagBuilder("a");
aTag.Attributes["href"] = urlHelper.Action(PageAction,
new { pageNumber = i });
aTag.InnerHtml.Append(i.ToString());
divTag.InnerHtml.AppendHtml(aTag);
}
Within the body of the for-loop, we create an anchor tag for each page link needed again using an instance of the TagBuilder
class.
TagBuilder aTag = new TagBuilder("a");
We then use our IUrlHelper
factory instance’s Action method to create a URL for each page link needed. We pass to the urlHelper.Action
method our PageAction property which will specify the action method for the current controller to use (bound to the targeted div element’s page-action attribute) and an object that contains route values; In this case, a single route value name being pageNumber and its value being the current integer value of i, the current iteration number.
aTag.Attributes["href"] = urlHelper.Action(PageAction,
new { pageNumber = i });
Next, we append the integer i of the current iteration to the anchor tag’s inner html and append the anchor tag itself to the output results represented by the variable divTag we created earlier as our Tag Helper’s html div container.
aTag.InnerHtml.Append(i.ToString());
divTag.InnerHtml.AppendHtml(aTag);
Once we exit the for-loop, we append the divTag container html to the output content.
output.Content.AppendHtml(divTag.InnerHtml);
Register the custom Tag Helper
Earlier in this chapter we registered the built in Microsoft Tag Helpers in the ViewImports
file. We can use the same file to register our custom Tag Helpers like the PageLink Tag Helper.
Modify the ViewImports
file in the Views folder with the contents shown below in bold blue text.
FredsCars\Views\ViewImports.cshtml
@using FredsCars.Models
@using FredsCars.Models.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, FredsCars
In the code above we registered all custom Tag Helpers that can be found in the FredsCars namespace including its subfolder namespaces like FredsCars/TagHelpers
.
We also added a new using statement for FredsCars.Models.ViewModels
so we won’t have to include a using statement for ViewModel classes in every view.
Add a PageLinkTagHelper element to the View
In this section we can finally plug the custom PageLink Tag Helper into the View. Modify the Home Controller’s Index method with the code shown below in bold blue font.
FredsCars\Views\Home\Index.cshtml
@model VehiclesListViewModel
@{
ViewData["Title"] = "Welcome";
}
<div class="container-fluid my-4 text-center">
<h1>Welcome to Fred's Cars!</h1>
Where you'll always find the right car, truck, or jeep.<br />
Thank you for visiting our site!
<div class="container-fluid mx-0 row"
style="margin-top: 20px;">
<!-- Categories -->
<div class="col-4 col-md-3 col-lg-2"
style="border-right: 2px solid black">
<div class="d-grid gap-2 button-grid">
<a asp-controller="Vehicles"
asp-action="Index"
class="btn btn-primary button">
<b>ALL</b></a>
<a asp-controller="Vehicles"
asp-action="Index"
class="btn btn-outline-primary button">
<b>CARS</b></a>
<a asp-controller="Vehicles"
asp-action="Index"
class="btn btn-outline-primary button">
<b>TRUCKS</b></a>
<a asp-controller="Vehicles"
asp-action="Index"
class="btn btn-outline-primary button">
<b>JEEPS</b>
</a>
</div>
</div>
<!-- Results -->
<div class="col">
<h3 class="bg-dark text-success">ALL Results</h3>
<table class="results table table-striped">
<thead>
<tr>
<th>
</th>
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].Status)
</th>
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].Year)
</th>
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].Make)
</th>
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].Model)
</th>
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].Color)
</th>
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].VehicleType)
</th>
</tr>
</thead>
<tbody>
@foreach (var v in Model.Vehicles)
{
<tr>
<td>
<img src="@v.ImagePath"
class="result-image" />
</td>
<td>
@Html.DisplayFor(modelItem => v.Status)
</td>
<td>
@Html.DisplayFor(modelItem => v.Year)
</td>
<td>
@Html.DisplayFor(modelItem => v.Make)
</td>
<td>
@Html.DisplayFor(modelItem => v.Model)
</td>
<td>
@Html.DisplayFor(modelItem => v.Color)
</td>
<td>
@Html.DisplayFor(modelItem => v.Price)
</td>
<td>
@Html.DisplayFor(modelItem => v.VehicleType.Name)
</td>
</tr>
}
</tbody>
</table>
<div id="page-links" page-model="@Model.PagingInfo"
page-action="Index">
</div>
</div>
</div>
</div>
In the code above for the Index View the first change we made was at the top of the file to change the Model type from just an IEnumerable of Vehicles to the new VehiclesList View Model.
@model VehiclesListViewModel
NOTE: In the VehiclesListViewModel class we have changed the Vehicles property from an IEnumerable<Vehicle> to a List<Vehicle> to make it easier to work with when laying out the table header column names based on the name of each property in a Vehicle class. More on this coming up.
The next change is where we lay out the table header column names. Now that the model is a VehiclesListViewModel
class rather then an IEnumerable<Vehicle>
we have to access the Vehicles property of the model to get each Vehicle property name for each header column name. We do this by accessing each property by index. For instance, the first index (0) returns the Status property of a Vehicle and as a parameter to the HTML helper’s DisplayNameFor
method lays out the name of the property. Since LINQ expressions like the Lambda expressionmodel => model.Vehicles[0].Status
are deferred, accessing a Vehicle by index in this way will not error out if the Vehicles property is null or empty.
<th>
@Html.DisplayNameFor(model => model.Vehicles[0].Status)
</th>
The third small change comes where we lay out the results in the foreach loop. I just simply changed the variable name in the for loop from ‘item’ to ‘v’. So, we need to change the variable in all the Lambda expressions from item to v.
@foreach (var v in Model.Vehicles)
{
...
<td>
@Html.DisplayFor(modelItem => v.Status)
</td>
...
}
And finally, the change we have all been waiting for and what the build up in this module has been all about, we insert a div element with the page-model (and page-action) html attributes so that our custom tag helper will recognize it and lay out the page links via the C# code and logic in the tag helper.
<div id="page-links" page-model="@Model.PagingInfo"
page-action="Index">
</div>
The page-model html attribute will be set to the value of the PagingInfo class coming down from the controller’s Index method and fed back up to the PageLink tag helper’s PageModel property. The page-action html attribute is set to the literal string value “Index” and will be fed to the tag helper’s PageAction property.
If you restart the application and navigate to http://localhost:40080 you should get results similar to the following with the page links at the bottom.

If you hover over a link, you can see that the corresponding page number is appended to the URL in a querystring in the format
?pageNumber = N
.

Let’s inspect the tag helper’s html in the web development tools. Right click one of the page links and select inspect in the browser. You’ll be taken to the Elements Tab in the web dev tools window and the page link will be highlighted. From there we can see the entire page links div html.

This shows us the crux of what a tag helper can do for us. A tag helper lays out and renders html based on the C# code and logic of the developer who wrote the tag helper.
Now click on one of the links in the browser.

In the screen shot above I have clicked on the third page link. It shows two results because there are twelve records with a page size of 5. So the third and last page show two results.
Style the Page Link buttons
Well we have everything working with the new page links but they do not look very nice at the moment. Let’s fix that now with some bootstrap styling.
Modify PageLinkTagHelper.cs with the code shown below.
FredsCars\TagHelpers\PageLinkTagHelper.cs
using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace FredsCars.TagHelpers
{
[HtmlTargetElement("div", Attributes = "page-model")]
public class PageLinkTagHelper : TagHelper
{
private IUrlHelperFactory _urlHelperFactory;
public PageLinkTagHelper(IUrlHelperFactory urlHelperFactory)
{
_urlHelperFactory = urlHelperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext? ViewContext { get; set; }
public PagingInfo? PageModel { get; set; }
public string? PageAction { get; set; }
public bool PagingClassesEnabled { get; set; }
public string? PagingBtnClass { get; set; }
public string? PagingBtnClassNormal { get; set; }
public string? PagingBtnClassSelected { get; set; }
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
if (ViewContext != null && PageModel != null)
{
IUrlHelper urlHelper
= _urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder divTag = new TagBuilder("div");
for (int i = 1; i <= PageModel.TotalPageCount; i++)
{
TagBuilder aTag = new TagBuilder("a");
aTag.Attributes["href"] = urlHelper.Action(PageAction,
new { pageNumber = i });
if (PagingClassesEnabled)
{
aTag.AddCssClass(PagingBtnClass ?? "");
aTag.AddCssClass(i == PageModel.PageIndex
? PagingBtnClassSelected ?? "" : PagingBtnClassNormal ?? "");
}
aTag.InnerHtml.Append(i.ToString());
divTag.InnerHtml.AppendHtml(aTag);
}
output.Content.AppendHtml(divTag.InnerHtml);
}
}
}
}
In the code above we only made some very small changes in two sections of the PageLinkTagHelper class but it will make the code much more flexible and reusable as far as styling goes.
In the first section we added four public class properties to dynamically set the styling and also decide whether or not to use dynamic styling at all from html attributes on the targeted div element.
public bool PagingClassesEnabled { get; set; }
public string PagingBtnClass { get; set; }
public string PagingBtnClassNormal { get; set; }
public string PagingBtnClassSelected { get; set; }
PagingClassEnabled
will map to the targeted div element’s paging-class-enabled html attribute, PagingBtnClass
will map to paging-btn-class, and so on.
- PagingClassesEnabled: property specifies whether or not to use dynamic css styling.
- PagingBtnClass: property specifies the CSS class to use for a paging button.
- PagingBtnClassNormal: property specifies the normal CSS class to use for all unselected buttons.
- PagingBtnClassSelected: property specifies the CSS class to use for the selected button.
In the second section we use an if block to dynamically apply the styles from the targeted div element’s html attributes that are mapped to the custom tag helper’s public button styling properties we have declared above.
for (int i = 1; i <= PageModel.TotalPageCount; i++)
{
TagBuilder aTag = new TagBuilder("a");
aTag.Attributes["href"] = urlHelper.Action(PageAction,
new { pageNumber = i });
if (PagingClassesEnabled)
{
aTag.AddCssClass(PagingBtnClass ?? "");
aTag.AddCssClass(i == PageModel.PageIndex
? PagingBtnClassSelected ?? "" : PagingBtnClassNormal ?? "");
}
aTag.InnerHtml.Append(i.ToString());
divTag.InnerHtml.AppendHtml(aTag);
}
This should be pretty self explanatory. But, one thing to note is the ternary operator used to dynamically apply normal or selected styling to a button.
aTag.AddCssClass(i == PageModel.PageIndex
? PagingBtnClassSelected ?? "" : PagingBtnClassNormal ?? "");
The ternary operator can be used like an if-then statement in a more concise manor.(condition) ? (expression) else (expression)
This can be read as:
(if condition is true)
then (evaluate this expression)
else (evaluate this expression)
Here, the code can be read as:If (i == PageModel.PageIndex) then
PagingBtnClassSelected
else
PagingBtnClassNormal
In other words if i equals the current PageIndex then apply the PagingBtnClassSelected
(btn-primary) CSS class to the button’s CSS otherwise apply PagingBtnClassNormal
(btn-outline-dark).
Another thing to note is that since the PagingBtnClass
styles are nullable strings meaning they may not be set, we also used the null coalescing operator twice int the if-then clause:
// if PagingingBtnClassSelected is null then use the value ""
// or empty string
PagingBtnClassSelected ?? ""
// if PagingBtnClassNormal is null then use the value ""
// or empty string
PagingBtnClassNormal ?? ""
Next, modify the paging div element in the Home Controller’s Index method.
FredsCars\Views\Home\Index.cshtml
... existing code ...
<div id="page-links"
page-model="@Model.PagingInfo"
page-action="Index"
paging-classes-enabled="true"
paging-btn-class="btn"
paging-btn-class-normal="btn-outline-dark"
paging-btn-class-selected="btn-primary"
class="btn-group mt-3">
</div>
... existing code ...
Again, the code tweak above to the targeted div element should be self explanatory. The new html attributes map to the custom tag helper’s public class button properties we just set up. And, now we can reuse this custom tag helper in other parts of the application or even other applications and apply any unique styling to fit any layout.
Add a paging route
Right now the paging works just as it should. The button links in the button group point to URLs which include a querystring to specify the page number to go to.
Take the second button link for example. It’s URL looks like this:http://localhost:40080/?pageNumber=2
Again, this does the job. But a more attractive URL might look like this:http://localhost:40080/Page2
ASP.Net Core makes it easy to accomplish this through it’s routing system features. Let’s add a route in Program.cs to make this happen. Modify Program.cs with the following code.
FredsCars\FredsCars\Program.cs
... existing code ...
app.UseStaticFiles();
// Add custom routes
app.MapControllerRoute("paging",
"Vehicles/Page{pageNumber}",
new { Controller = "Home", action = "Index" });
/*** Add endpoints for contoller actions and
the default route ***/
app.MapDefaultControllerRoute();
... existing code ...
In the code above we have added a new route to the application using the MapControllerRoute
method of the WebApplication
object (app). MapControllerRoute
takes three parameters.
For the first parameter we named the route “paging”.
In the second parameter we defined the pattern an incoming URL should match to be directed to this route.
Vehicles/Page{pageNumber}
We used the third parameter to create a new object that contains default route values for route parameters.new { Controller = "Home", action = "Index" }
In the URL the tag helper lays out for each button link, no controller was specified so the link goes back to the current controller, which in our case is Home. Then if you recall we used an IURLHelper’s Action method to set the action to Index and a route value named pageNumber to be tacked on to the URL as a querystring.
TagBuilder aTag = new TagBuilder("a");
aTag.Attributes["href"] = urlHelper.Action(PageAction,
new { pageNumber = i });
Before adding the new route, this resulted in the original format of the URLs:http://localhost:40080/?pageNumber=2
One of the features of the ASP.Net Core routing system is that not only are incoming requests directed to the appropriate route, but outgoing routes are also generated using route patterns in our custom defined routes.
Since our new route has default route values where controller = home and action = index, and the URLs we generate for the button links in the tag helper via the urlHelper.Action
method match those values, and the custom route expects an incoming variable named pageNumber, and our generated button link URLs provide that variable with the same name in a querystring key/value pair, the ASP.Net Core routing system generates the button link URLs in the new custom route’s pattern:http://localhost:40080/Vehicles/Page2
Doesn’t this look better?
Some people would call this a composite URL because it is more human readable.
Restart the application, navigate to http://localhost:40080 in the browser, and hover over a button link in the paging button group at the bottom of the screen.

In the above screenshot you can see that the button link URLs were generated in the pattern of the new route.
Click on one of the paging button links and you will be directed to that page number.

Fix the HomeController Tests
At this point I want to test our new component, the PageLinkTagHelper
. But, before we do that let’s make sure all of our current tests are still working. If we perform a test run in Test Explorer on all of our tests, we see that both of our HomeController tests fail.

This is typical of unit testing. During development requirements change and we refactor our code. As the code changes, units of code and their corresponding unit tests we thought we had all squared away and tested sometimes fail. This is especially true in agile environments where the pace of development is rapid and fluid.
Let’s see if we can dig in to see what’s happening here.
The problem for the test Can_Access_VehicleList_FromVehicleRepo
is that the Home controller’s Index method no longer returns an IEnumerable<Vehicle>
. It now returns a VehiclesListViewModel
.
So, we need to change the Act section’s statement from:
// Act
IEnumerable<Vehicle>? result =
(await controller.Index()).ViewData.Model
as IEnumerable<Vehicle>;
to:
// Act
VehiclesListViewModel? result =
(await controller.Index()).ViewData.Model
as VehiclesListViewModel;
We also need to change the first statement of the Assert section from:
// Assert
Vehicle[] vehicleArray = result?.ToArray()
?? Array.Empty<Vehicle>();
to:
Vehicle[] vehicleArray = result?.Vehicles.ToArray()
?? Array.Empty<Vehicle>();
At this point the Can_Access_VehicleList_FromVehicleRepo
test should pass.

Now, let’s turn our attention to the Can_Page_Results
test.
The fix for this test is the same as for the Can_Access_VehicleList_FromVehicleRepo
test. But, remember to include the pageNumber parameter with a value of two in the Act section statement.
// Act
VehiclesListViewModel? result =
(await controller.Index(2)).ViewData.Model
as VehiclesListViewModel;
Here is the entire HomeControllerTests.cs
file with the changes included.
FredsCars\FredsCars.Tests\Controllers\HomeControllerTests.cs
using FredsCars.Controllers;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using FredsCars.Models.ViewModels;
using MockQueryable;
using Moq;
namespace FredsCars.Tests.Controllers
{
public class HomeControllerTests
{
[Fact]
public async Task Can_Access_VehicleList_FromVehicleRepo()
{
// Arrange
// 1 - create a List<T> with test items
var vehicles = new List<Vehicle>
{
new Vehicle
{
Id = 1,
Make = "Make1",
Model = "Model1",
VehicleType = new VehicleType
{
Id = 1,
Name = "Car"
}
},
new Vehicle
{
Id = 2,
Make = "Make2",
Model = "Model2",
VehicleType = new VehicleType
{
Id = 1,
Name = "Car"
}
},
new Vehicle
{
Id = 3,
Make = "Make3",
Model = "Model3",
VehicleType = new VehicleType
{
Id = 2,
Name = "Truck"
}
},
new Vehicle
{
Id = 4,
Make = "Make4",
Model = "Model4",
VehicleType = new VehicleType
{
Id = 3,
Name = "Jeep"
}
}
};
// 2 - build mock using MockQueryable.Moq extension
var mockVehiclesIQueryable = vehicles.BuildMock();
// 3 - build mock IVehicleRepository
Mock<IVehicleRepository> mockVehicleRepo =
new Mock<IVehicleRepository>();
mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);
HomeController controller = new HomeController(mockVehicleRepo.Object);
// Act
VehiclesListViewModel? result =
(await controller.Index()).ViewData.Model
as VehiclesListViewModel;
// Assert
Vehicle[] vehicleArray = result?.Vehicles.ToArray()
?? Array.Empty<Vehicle>();
Assert.True(vehicleArray.Length == 4);
int carCount = vehicleArray.Where(v => v.VehicleType.Name == "Car").Count();
Assert.Equal(2, carCount);
Assert.True(vehicleArray[2].Make == "Make3" &&
vehicleArray[2].Model == "Model3");
Assert.True(vehicleArray[3].Make == "Make4" &&
vehicleArray[3].Model == "Model4");
}
[Fact]
public async Task Can_Page_Results()
{
// Arrange
// 1 - create a List<T> with test items
var vehicles = new List<Vehicle>
{
new Vehicle
{
Id = 1,
Make = "Make1",
Model = "Model1",
VehicleType = new VehicleType
{
Id = 1,
Name = "Car"
}
},
new Vehicle
{
Id = 2,
Make = "Make2",
Model = "Model2",
VehicleType = new VehicleType
{
Id = 2,
Name = "Car"
}
},
new Vehicle
{
Id = 3,
Make = "Make3",
Model = "Model3",
VehicleType = new VehicleType
{
Id = 3,
Name = "Truck"
}
},
new Vehicle
{
Id = 4,
Make = "Make4",
Model = "Model4",
VehicleType = new VehicleType
{
Id = 4,
Name = "Jeep"
}
},
new Vehicle
{
Id = 5,
Make = "Make5",
Model = "Model5",
VehicleType = new VehicleType
{
Id = 5,
Name = "Jeep"
}
}
};
// 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension
var mockVehiclesIQueryable = vehicles.BuildMock();
// 3 - build mock IVehicleRepository
Mock<IVehicleRepository> mockVehicleRepo =
new Mock<IVehicleRepository>();
mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);
HomeController controller =
new HomeController(mockVehicleRepo.Object);
controller.PageSize = 3;
// Act
VehiclesListViewModel? result =
(await controller.Index(2)).ViewData.Model
as VehiclesListViewModel;
// Assert
Vehicle[] vehicleArray = result?.Vehicles.ToArray()
?? Array.Empty<Vehicle>();
Assert.True(vehicleArray.Length == 2);
Assert.Equal("Make4", vehicleArray[0].Make);
Assert.Equal("Make5", vehicleArray[1].Make);
}
}
}
And now all of the HomeController Test should pass.

The Can_Page_Results
test proves we can receive the right set of Vehicles corresponding to a page number. But we also need to prove the controller can send paging info correctly. Let’s add one more test called Can_Return_PagingInfo
before we move on to testing the PageLinkTagHelper component.
Add the following test to the HomeControllerTests.cs file.
FredsCars\FredsCars.Tests\Controllers\HomeControllerTests.cs
... existing code ...
[Fact]
public async Task Can_Return_PagingInfo()
{
// Arrange
// 1 - create a List<T> with test items
var vehicles = new List<Vehicle>
{
new Vehicle
{
Id = 1,
Make = "Make1",
Model = "Model1",
VehicleType = new VehicleType
{
Id = 1,
Name = "Car"
}
},
new Vehicle
{
Id = 2,
Make = "Make2",
Model = "Model2",
VehicleType = new VehicleType
{
Id = 2,
Name = "Car"
}
},
new Vehicle
{
Id = 3,
Make = "Make3",
Model = "Model3",
VehicleType = new VehicleType
{
Id = 3,
Name = "Truck"
}
},
new Vehicle
{
Id = 4,
Make = "Make4",
Model = "Model4",
VehicleType = new VehicleType
{
Id = 4,
Name = "Jeep"
}
},
new Vehicle
{
Id = 5,
Make = "Make5",
Model = "Model5",
VehicleType = new VehicleType
{
Id = 5,
Name = "Jeep"
}
}
};
// 2 - build mock IQueryable<Vehicle> using MockQueryable.Moq extension
var mockVehiclesIQueryable = vehicles.BuildMock();
// 3 - build mock IVehicleRepository
Mock<IVehicleRepository> mockVehicleRepo =
new Mock<IVehicleRepository>();
mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);
HomeController controller =
new HomeController(mockVehicleRepo.Object);
controller.PageSize = 3;
// Act
VehiclesListViewModel? result =
(await controller.Index(2)).ViewData.Model
as VehiclesListViewModel;
// Assert
PagingInfo PagingInfo = result?.PagingInfo!;
Assert.Equal(2, PagingInfo.PageIndex);
Assert.Equal(3, PagingInfo.PageSize);
Assert.Equal(5, PagingInfo.TotalItemCount);
Assert.Equal(2, PagingInfo.TotalPageCount);
}
}
}
The buildup in the arrange and act sections are the same in the Can_Return_PagingInfo
test as they are in Can_Page_Results
. But the assert section is different because now we are testing that the properties of the PagingInfo class of the returned VehiclesListViewModel class are what we would expect from that build up. All of the tests should be in a passing state at this point.
Unit Test the PageLinkTagHelper component
One of the nice things about a custom tag link helper like the PageLinks tag helper is that we don’t have all of our C# paging logic embedded in the controller and the view. So the code is much cleaner and readable. The other benefit is that as a component the PageLinkTagHelper class is testable.
Add a new folder to the FredsCars.Tests project named TagHelpers. In the new TagHelpers folder add a class named PageLinkTagHelperTests.
FredsCars\FredsCars.Tests\TagHelpers\PageLinkTagHelperTests.cs
using FredsCars.Models.ViewModels;
using FredsCars.TagHelpers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Moq;
namespace FredsCars.Tests.TagHelpers
{
public class PageLinkTagHelperTests
{
[Fact]
public void Can_Generate_PageLinks()
{
// Arrange
var urlHelper = new Mock<IUrlHelper>();
urlHelper.SetupSequence(x =>
x.Action(It.IsAny<UrlActionContext>()))
.Returns("Test/Page1")
.Returns("Test/Page2")
.Returns("Test/Page3");
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory.Setup(f =>
f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
var viewContext = new Mock<ViewContext>();
PageLinkTagHelper pageLinkTagHelper =
new PageLinkTagHelper(urlHelperFactory.Object)
{
ViewContext = viewContext.Object,
PageModel = new PagingInfo
{
PageIndex = 2,
TotalItemCount = 28,
PageSize = 10
},
};
TagHelperContext tagHelperCtx = new TagHelperContext(
new TagHelperAttributeList(),
new Dictionary<object, object>(), "");
var content = new Mock<TagHelperContent>();
TagHelperOutput tagHelperoutput = new TagHelperOutput("div",
new TagHelperAttributeList(),
(cache, encoder) => Task.FromResult(content.Object));
// Act
pageLinkTagHelper.Process(tagHelperCtx, tagHelperoutput);
// Assert
Assert.Equal(@"<a href=""Test/Page1"">1</a>"
+ @"<a href=""Test/Page2"">2</a>"
+ @"<a href=""Test/Page3"">3</a>",
tagHelperoutput.Content.GetContent());
}
}
}
In the unit test code above, we test that the PageLink tag helper can generate page links. In the arrange section we setup all of the mocks and behaviors we will need to call the tag helper’s Process
method.
First, we set up a mock for an IUrlHelper
and use the Mock.SetupSequence
method to define a sequence of URLs the IUrlHelper
returns via its Action
method.
// Arrange
var urlHelper = new Mock<IUrlHelper>();
urlHelper.SetupSequence(x =>
x.Action(It.IsAny<UrlActionContext>()))
.Returns("Test/Page1")
.Returns("Test/Page2")
.Returns("Test/Page3");
This is the first time we are seeing the SetupSequence
method to define behavior of a member rather then SetupGet
. Each time Action is called the next URL in the sequence is returned as we will see below.
We also specify that the Action member can take in any parameter value that is a UrlActionContext
using the It.IsAny<T>
method.
Next, we mock up an IUrlHelperFactory
and define the behavior of its GetUrlHelper
method again using It.IsAny
to specify it can take in any ActionContext
value as a parameter and it returns the mocked IUrlHelper’s object.
var urlHelperFactory = new Mock<IUrlHelperFactory>();
urlHelperFactory.Setup(f =>
f.GetUrlHelper(It.IsAny<ActionContext>()))
.Returns(urlHelper.Object);
After setting up IURlHelperFactory
and IUrlHelper
we create a mocked ViewContext
method and instantiate an instance of the PageLinkTagHelper
class setting its ViewContext
property to the mocked ViewContext
and its PageModel
property to an instance of the PagingInfo
class with test values for it’s properties; PageIndex
, TotalItemCount
, and PageSize
.
var viewContext = new Mock<ViewContext>();
PageLinkTagHelper pageLinkTagHelper =
new PageLinkTagHelper(urlHelperFactory.Object)
{
ViewContext = viewContext.Object,
PageModel = new PagingInfo
{
PageIndex = 2,
TotalItemCount = 28,
PageSize = 10
},
};
Finally, we need to create instances of TagHelperContext
and TagHelperOutput
to use as arguments when we call the Process
method of the tag helper. We also have to mock a TagHelperContent
object to use as an argument to the TagHelperOutput
instance.
TagHelperContext tagHelperCtx = new TagHelperContext(
new TagHelperAttributeList(),
new Dictionary<object, object>(), "");
var content = new Mock<TagHelperContent>();
TagHelperOutput tagHelperoutput = new TagHelperOutput("div",
new TagHelperAttributeList(),
(cache, encoder) => Task.FromResult(content.Object));
In the act section we call the tag helper’s Process method feeding as arguments to its parameters the TagHelperContext
and TagHelperOutput
we created in the arrange section.
// Act
pageLinkTagHelper.Process(tagHelperCtx, tagHelperoutput);
And finally in the assert section, we test that the PageLink tag helper can indeed return the html for a series of of URLs as anchor links.
// Assert
Assert.Equal(@"<a href=""Test/Page1"">1</a>"
+ @"<a href=""Test/Page2"">2</a>"
+ @"<a href=""Test/Page3"">3</a>",
tagHelperoutput.Content.GetContent());
What’s Next
In this module we created a custom tag helper to lay out the button links for paging. Along the way we used a View Model to pass information from the controller to the view that the view needs to render the PageLinks tag helper. We then learned how to register the tag helper and applied dynamic styling to it. We also learned how to create a custom route in ASP.Net Core.
In the next module we are going to add sorting by column header.