Jonas Fagerberg - ASP - Net Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient - How To Build A Video Course Website (2019)
Jonas Fagerberg - ASP - Net Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient - How To Build A Video Course Website (2019)
2
MVC, Razor Pages, API,
JSON Web Tokens &
HttpClient
How to Build a Video Course Website
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Overview.............................................................................................................................. 1
Setup................................................................................................................................ 2
Book Version.................................................................................................................... 2
Source Code..................................................................................................................... 2
Other Books by the Author.............................................................................................. 3
Online Courses and Free Content by the Author ............................................................ 3
Disclaimer – Who Is This Book for? ................................................................................. 4
Rights ............................................................................................................................... 4
About the Author ............................................................................................................ 4
Part 1: MVC How to Build a Video Course Website ........................................................... 7
1. The Use Case.................................................................................................................... 9
Introduction ..................................................................................................................... 9
The Use Case ................................................................................................................... 9
The User Interface (MVC) .......................................................................................... 10
Login and Register User ............................................................................................. 10
The Administrator Interface (Razor Pages) ............................................................... 10
Conclusion ..................................................................................................................... 11
Login and Register ..................................................................................................... 11
The User Dashboard View ......................................................................................... 11
The Course View ........................................................................................................ 12
The Video View .......................................................................................................... 15
The Administrator Dashboard Razor Page ................................................................ 16
A Typical Administrator Index Razor Page................................................................. 16
A Typical Administrator Create Razor Page ............................................................... 17
A Typical Administrator Edit Razor Page ................................................................... 18
A Typical Administrator Delete Razor Page ............................................................... 19
Index
Overview
I want to welcome you to ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens &
HttpClient. This book will guide you through creating a video course membership site
secured with JSON Web Tokens.
This book’s target audience is developers who want to learn how to build ASP.NET Core
2.2 MVC, Razor Page, and API applications. The API has JSON Web Token (JWT) auth-
entication and authorization, and the Razor Pages calls the API with HttpClient. You should
be an intermediate level C# developer with some experience in MVC, Entity Framework,
HTML5, and CSS3. The book presupposes that you have a solid C# foundation with good
knowledge in OOP, Linq/Lambda, generics, and asynchronous calls; this is not a book
about the C# language.
You will learn ASP.NET Core 2.2 by building three applications in five projects. The first
application is a UI for users registered with the membership site built with the MVC tem-
plate; the second is an administration UI built with Razor Pages; the third is an API secured
with JSON Web Token authentication and authorization that the administration applica-
tion calls with HttpClient. All three applications use several services with differing purpos-
es. Apart from the three application projects, a project for shared resources is created as
well as a database project with the sole purpose of handling the shared Entity Framework
Core 2.2 database. When finished, you will have created a fully functioning video course
website, where users can register to get access to video content, and administrators can
add and modify course content and users.
You should already be familiar with MVC 5 or ASP.NET Core to get the most from this book;
it delivers the content in a fast, no-fluff way. The book is practical and tactical, where you
will learn as you progress through the modules and build real web applications in the
process. To spare you countless pages of fluff (filler material), only valuable information,
pertinent to the task at hand, is discussed. The benefit is a shorter and more condensed
book, which will save you time and give you a more enjoyable experience.
The goal is to learn ASP.NET Core 2.0 by building web projects: an experience that you can
put in your CV when applying for a job or a consultant position, or when negotiating a
higher salary.
By the end of this book, you will be able to create ASP.NET Core 2.2 applications on your
own, which can create, edit, delete, and view data in a database.
1
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Setup
In this book, you will be using C#, HTML, and Razor with Visual Studio 2019 version 16.0.0
or later. You can even use Visual Studio Community 2019, which you can download for
free from www.visualstudio.com/downloads.
You can develop ASP.NET Core 2.2 applications on Mac OS X and Linux, but then you are
restricted to the ASP.NET Core libraries that don’t depend on .NET Framework, which
requires Windows.
The applications in this book will be built using ASP.NET 2.2 without .NET Framework.
You will install additional libraries using NuGet packages when necessary, throughout the
book.
The complete code for all applications is available on GitHub with a commit for each task.
https://github.com/csharpschool/VideoOnDemand22
Book Version
The current version of this book: 1.0
Errata: https://github.com/csharpschool/VideoOnDemand22/issues
Contact: [email protected]
Source Code
The source code accompanying this book is shared under the MIT License and can be
downloaded on GitHub, with a commit for each task.
https://github.com/csharpschool/VideoOnDemand22
2
Overview
Below is a list of the most recent books by the author. The books are available on Amazon.
3
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The examples in this book are presented using the free Visual Studio 2019 (version 16.0.0)
Community version and ASP.NET Core 2.2. You can download Visual Studio 2019 (version
16.0.0) here: www.visualstudio.com/downloads
Rights
All rights reserved. The content is presented as is and the publisher and author assume no
responsibility for errors or omissions. Nor is any liability assumed for damages resulting
from the use of the information in the book or the accompanying source code.
It is strictly prohibited to reproduce or transmit the whole book, or any part of the book,
in any form or by any means without the prior written permission of the author.
4
Overview
In the year 2000, after working as a Microsoft Office developer consultant for a couple of
years, he wrote his second book about Visual Basic 6.0.
From 2000 to 2004, he worked as a Microsoft instructor with two of the largest
educational companies in Sweden teaching Visual Basic 6.0. When Visual Basic.NET and
C# were released, he started teaching those languages, as well as the .NET Framework. He
was also involved in teaching classes at all levels, from beginner to advanced developers.
In 2005, Jonas shifted his career toward consulting once again, working hands-on with the
languages and framework he taught.
Jonas wrote his third book, C# Programming, aimed at beginner to intermediate develop-
ers in 2013, and in 2015 his fourth book, C# for Beginners – The Tactical Guide, was
published. Shortly after that his fifth book, ASP.NET MVC 5 – Building a Website: The
Tactical Guidebook, was published. In 2017 he wrote three more books: ASP.NET Core 1.1
Web Applications, ASP.NET Core 1.1 Web API, and ASP.NET Core 2.0 Web Applications. In
2019 he wrote the book ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens &
HttpClient.
Jonas specifically writes all books and video courses with the student in mind.
5
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
6
Part 1:
MVC
How to Build a Video Course Website
1. The Use Case
As a first step, they would like a demo version using dummy data to get a feel for the
application. The dummy data can be seeded into the tables when creating the SQL Server
database.
YouTube should be used to store the videos, to keep costs down. No API or functionality
for uploading videos is necessary for the final application. It is enough for the administra-
tor to be able to paste in a link to a video stored in a YouTube account when adding a new
video with the admin user interface.
The finished solution should contain five projects: The first is called VOD.Common, and
will contains all entity classes, DTOs, and a couple of services as well as other classes
shared between the projects. The second is the VOD.Database project that contains
services for data manipulation and CRUD operations as well as the database context and
the database migrations. The third is a user interface for regular users called VOD.UI. The
fourth application is named VOD.Admin and is used by administrators to perform CRUD
operations on the tables in the database. The fifth is named VOD.API and is used to
perform CRUD operations in the database as well as create JSON Web Tokens.
9
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The first view after login should be a dashboard, displaying the courses available to the
user. When clicking on a course, display the course curriculum in a list below a marquee
and some information about the course. Each course can have multiple modules, which
can have multiple videos and downloadable content. Downloadable content should open
in a separate browser tab.
When the user clicks on a video listing, open a new view, where the video player contains
video, but the automatic play is disabled. Display information about the course and a
description of the video below the video player. To the right of the video player, a
thumbnail image for the next video in the module should be displayed, as well as buttons
to the previous and next video. Disable the buttons if no video is available.
The menu should have a logo on the far left and a settings menu to the far right.
Don’t use the database entity classes as view models; instead, each view should use a view
model, which contains the necessary Data Transfer Objects (DTOs) and other properties.
Convert entities to DTO objects with AutoMapper and send them to the views.
When these pages are displayed, a menu with a Home link should be available (takes the
visitor to the login page).
10
1. The Use Case
If the logged in user is an administrator, a drop-down menu should appear to the right of
the logo, containing links to views for CRUD operations in the database connected to the
site. There should also be a dashboard on the main Index page where the admin can click
to open the Index pages associated with the different tables in the database and perform
CRUD operations (the same links as in the menu).
Conclusion
After careful consideration, these are the views and controls necessary for the application
to work properly.
Below is a mock-up image of the Login and Create views. Note the icons in the text boxes;
icons from Google’s Material Icons will represent them.
The application will collect the user’s email and password when registering with the site,
and that information will be requested of the visitor when logging in. There will also be a
checkbox asking if the user wants to remain logged in when visiting the site, the next time.
11
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
three to a row, to make them large enough, which means that the view model must
contain a collection of collections, defined by a course DTO.
Each course DTO should contain properties for the course id, course title, description, a
course image, and a marquee image. Display each course as a card with the course image,
title, description, and a button leading to the course view.
The marquee image, the course image (as a thumbnail in the marquee), the title, and de-
scription should be in the top card.
Below the top card to the left, the course modules and their content should be listed. Note
that there are two possible types of content in a module: videos and downloads. Each
video should display a thumbnail, title, description, and the length of the video (duration).
List the downloads as links with a descriptive title.
12
1. The Use Case
To the right of the list of modules is the instructor bio, which contains a thumbnail, name,
and description of the instructor.
To pull this off, the course view model needs to have a Course DTO, an Instructor DTO,
and a list of Module DTOs. Each Instructor DTO should contain the avatar, name, and de-
scription of the instructor teaching a course. The Module DTO should contain the module
id, title, and lists of Video DTOs and Download DTOs.
A Video DTO should contain the video id, title, description, duration, a thumbnail, and the
URL to the video. Clicking a video will load the video into the Video view. Autoplay should
be disabled.
A Download DTO should contain a title and the URL to the content. When clicking the link,
the content should open in a new browser tab.
13
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
14
1. The Use Case
To pull this off, the video view model must contain a VideoDTO, an InstructorDTO, a
CourseDTO, and a LessonInfoDTO. The LessonInfoDTO contains properties for lesson
number, number of lessons, video id, title, and thumbnail properties for the previous and
next videos in the module.
15
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
16
1. The Use Case
The Razor Page should have a Create button that posts the data to the server, a Back to
List button that takes the user back to the Index page, and a Dashboard button that takes
the user back to the main Index page.
17
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The Razor Page also has a Save button that posts the data to the server, a Back to List
button that takes the user back to the Index page, and a Dashboard button that takes the
user back to the main Index page.
18
1. The Use Case
19
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
20
2. Setting Up the Solution
Overview
The customer wants you to build the solution with Visual Studio 2019, ASP.NET Core 2.2,
and the ASP.NET Core Web Application template. The first step will be to create the
solution and install all the necessary NuGet packages not installed with the project tem-
plate. The template will install the basic MVC plumbing and a Home controller with Index
and Policy action methods, and their corresponding views.
1. Open Visual Studio 2019 and click the Create a new project button in the wizard.
2. Select ASP.NET Core Web Application in the template list and click the Next
button (see image below).
3. Name the project VOD.UI in the Name field.
21
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
4. Name the solution VideoOnDemand in the Solution name field. It should not end
with .UI.
5. Click the OK button.
6. Select .NET Core and ASP.NET Core 2.2 in the drop-downs.
7. Select Web Application (Model-View-Controller) in the template list.
8. Click the Change Authentication button and select Individual User Accounts in
the pop-up dialog; this will make it possible for visitors to register and log in with
your site using an email and a password (see image below).
a. Select the Individual User Accounts radio button.
b. Select Store user account in-app in the drop-down.
c. Click the OK button in the pop-up dialog.
9. Click the Create button in the wizard dialog.
10. Open appsettings.json and add the following connection string. It’s important to
add the connection string as a single line of code.
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=VOD;Trusted_Connection=True;
MultipleActiveResultSets=true"
}
22
2. Setting Up the Solution
23
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
24
2. Setting Up the Solution
25
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
It is no longer possible to manage NuGet packages with a project.json file. That is handled
by the.csproj file, which can be edited directly from the IDE.
Installing AutoMapper
AutoMapper will be used to map an entity (database table) object to a Data Transfer
Object (DTO), which is used to transport data to the views. You can either add the
following row to the <ItemGroup> node in the .csproj file manually and save the file or use
the NuGet manager to add AutoMapper.
<PackageReference Include=
"AutoMapper.Extensions.Microsoft.DependencyInjection" Version="6.1.0" />
The following listing shows you how to use the NuGet manager to install packages.
1. Right click on the Dependencies node in the Solution Explorer and select
Manage NuGet Packages in the context menu.
26
2. Setting Up the Solution
5. Select the package in the list; it will probably be the first package in the list.
6. Make sure that you use the latest stable version (6.1.0).
7. Click the Install button.
To verify the installed package, you can open the .csproj file by right-clicking on the project
node and selecting Edit VOD.UI.csproj, or you can expand the Dependencies -NuGet folder
in the Solution Explorer.
To create the database, you must create an initial migration to tell Entity Framework how
the database should be set up. You do this by executing the add-migration command in
the Package Manager Console.
After a successfully created migration, you execute the update-database command in the
same console to create the database. After creating the database, you can view it in the
SQL Server Object Explorer, available in the View menu.
Add a separate project called VOD.Common for the entity and context database classes
shared by all projects. Reference this project from the other projects.
27
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Right click on the VideoOnDemand solution in the Solution Explorer and select
Add-New Project in the menu.
2. Select Class Library (.NET Core) in the template list and click the Next button.
3. Name the project VOD.Common and click the Create button.
4. Remove the file Class1.cs.
5. Repeat step 1 through 4 and name the project VOD.Database.
6. Add a reference to the Common project in the Database project.
a. Right click on the Dependencies node in the Database project and select
Add Reference in the context menu.
b. Check the checkbox for the VOD.Common project and click the OK
button.
28
2. Setting Up the Solution
29
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
30
2. Setting Up the Solution
[NotMapped]
public IList<Claim> Claims { get; set; } = new List<Claim>();
}
31
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
32
2. Setting Up the Solution
Admin to its Name column, and ADMIN to its NormalizedName column. Right
click on the table node and select View Data to open the table.
It may take a second or two for the SQL Server node to populate in the SQL Server Object
Explorer. When it has, expand the server named MSSQLLocalDB and then the VOD
database, where you find the new tables. The tables prefixed with AspNet stores user
account information, and EF uses them when a user registers and logs in. In this course,
you will use the AspNetUsers, AspNetRoles, AspNetUserClaims, and AspNetUserRoles
tables when implementing registration and login for your users, and to determine if a user
is an administrator.
33
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Summary
In this chapter, you created the three initial projects that will be used throughout the
remainder of this book to create a user interface (UI), share resources, and communicate
with the database. You also installed the AutoMapper NuGet package, which later will be
used to map database entity objects to Data Transfer Objects (DTOs), which provide the
views with data.
Next, you will redirect the Home/Index action to display the Account/Login page; this will
display the login form when the application starts. Then you will style the login form, mak-
ing it look more professional.
34
3. Login
3. Login
Introduction
In this chapter, you will make the login page available as soon as a visitor navigates to the
web application. To achieve this, you will redirect from the Home/Index action to the
Account/Login action.
Because the login page only should be displayed to visitors who haven’t already logged in,
you will use Dependency Injection to make the SignInManager available from the con-
troller, making it possible to check if the user is logged in.
Not only can built-in framework services be injected, but you can configure your classes
and interfaces for DI in the Startup class.
Now, you will use DI to pass in the SignInManager to a constructor in the HomeController
class and store it in a private variable. The SignInManager and its User type need two
using statements: Microsoft.AspNetCore.Identity and VOD.Common.Entities.
35
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
return View();
}
5. Run the application by pressing F5 or Ctrl+F5 (without debugging) on the
keyboard.
6. The login page should be displayed. If you look at the URL, it should point to
/Identity/Account/login on the localhost (your local IIS server) because of the
RedirectToPage method call.
36
3. Login
37
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
38
3. Login
1. Right click on the Areas folder and select Add-New Scaffolded Item in the menu.
2. Select the Identity option in the dialog’s left menu.
3. Select the Identity template in the template list.
4. Click the Add button.
39
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Open the _Layout view and add a link to Google’s Material Icons library to both
<environment> elements inside the <head> element. You can read more about it
here: https://google.github.io/material-design-icons/
<environment include="Development">
...
<link href="https://fonts.googleapis.com/icon?
family=Material+Icons" rel="stylesheet">
</environment>
2. Open the Login page in the Areas/Identity/Pages/Account folder.
3. Remove the <h1> title element.
4. Add the class login-register to the <div> with the row class. Later, you will add
the CSS selector to the login-register.css file.
5. Add the class offset-md-4 to the <div> with the col-md-4.
6. Replace the Email <label> element with an <i> element for the alternate_email
icon from the Material icons library.
Replace: <label asp-for="Input.Email"></label>
With: <i class="material-icons">alternate_email</i>
7. Repeat step 5 for the Password <label> but change the icon from
alternate_email to lock inside the <i> element.
8. Add the placeholder attribute with the text Email to the Email <input> element.
The placeholder is instructional text displayed inside the textbox that is removed
when the user types in the control.
<input asp-for="Input.Email" class="form-control"
placeholder="Email" />
9. Remove the validation <span> elements below the two <input> elements.
10. Add the placeholder attribute with the text Password to the Password <input>
element.
11. Remove the form-group containing the Register a new user and Forgot your
Password? <p> elements and all its content.
12. Remove the <div> with the col-md-6 and offset-md-2 classes and all its content.
40
3. Login
The form should look like this after the layout change.
@{
ViewData["Title"] = "Log in";
}
41
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<div class="checkbox">
<label asp-for="Input.RememberMe">
<input asp-for="Input.RememberMe" />
@Html.DisplayNameFor(m =>
m.Input.RememberMe)
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
Log in</button>
</div>
</form>
</section>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Note that you might have to clear the browser history for the changes to be applied.
Add a 40px top margin to the row, to push it down a little from the navigation bar.
.login-register {
margin-top: 40px;
}
Next, add 35px left padding to all the form controls, and remove the border radius to
give the textboxes sharp corners.
.login-register .form-control {
Padding-left: 35px;
border-radius: 0;
}
42
3. Login
Next, position the icons absolute and give them 8px padding to align them nicely with
the textboxes.
.login-register .material-icons {
position: absolute;
padding: 8px;
}
Next, change the color of the icon and the placeholder text to a light blue when hovering
over the textboxes. Some browsers dim the color on hover; use opacity to show full
color.
.login-register .form-group:hover .material-icons,
.login-register .form-group:hover ::placeholder {
color: #2580db;
/* Some browsers dim the color on hover,
use opacity to show full color */
opacity: 1;
}
The last thing to style is the submit button. Make the button take up the entire width of
its container and remove the border-radius to give it sharp corners.
.login-register button {
width: 100%;
border-radius: 0;
}
43
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Summary
In this chapter, you learned how to scaffold Login and Register Razor Pages and how to
redirect to a Razor Page from an MVC controller action.
Then, you changed the layout of the Login page, and applied CSS and Bootstrap classes to
its elements, to make it look nicer to the user.
Next, you will change the layout of the Account/Register page and apply CSS and Boot-
strap classes to its elements.
44
4. Register User
4. Register User
Introduction
In this chapter, you will alter the layout of the Account/Register page and style it with CSS
and Bootstrap. You can reach the page from a link in the navigation bar.
Overview
The task appointed to you by the company is to make sure that visitors have a nice user
experience when registering with the site, using the Account/Register page. The finished
Register page should look like the image below.
45
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
46
4. Register User
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
47
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
6. Try to log in to the site with the email you just registered. The application should
redirect you to the Home/Index view.
7. Close the application from Visual Studio.
8. Open the SQL Server Object Explorer from the View menu.
9. Expand the MSSQLLocalDB node, and then your database.
10. Expand the Tables node and right click on the AspNetUsers table. See image
below.
11. Right click on the table and select View Data to open the table in Visual Studio.
12. The table should contain the user you added. See image below.
48
4. Register User
Summary
In this chapter, you changed the layout of the Register page and applied CSS and Bootstrap
classes to spruce up its elements. You also registered a user and logged in using the
account.
Next, you will change the layout of the navigation bar, and style it with CSS and Bootstrap
classes. You will also create a drop-down menu for logout and settings options and remove
their links from the navigation bar.
49
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
50
5. Modifying the Navigation Bar
Overview
Your task is to change the appearance of the navigation bar. It should be white with a logo
to the far left. The Home and Privacy links should be visible to the left in the navigation
bar and the drop-down menu to the far right, which should be visible when the user is
logged in.
To control when the links are displayed, you need to inject the SignInManager and User-
Manager to the _LoginPartial view.
@inject SignInManager<VODUser> SignInManager
@inject UserManager<VODUser> UserManager
To achieve this, you will have to alter the _Layout and _LoginPartial views.
51
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Right click on the wwwroot/css folder and select Add-New Item in the context
menu.
2. Select the Style Sheet template and name the file navigation.css.
52
5. Modifying the Navigation Bar
b. Add the avatar image to the right of the username inside the <a> tag and
decorate the <img> element with the classes img-circle and avatar. Set
the image height to 40.
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#"
id="user-drop-down" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
@User.Identity.Name <img src="~/images/avatar.png"
class="img-circle avatar" height="40" />
</a>
</li>
8. Add a <div> element decorated with the dropdown-menu class below the <a>
element you just added. This will be the container for the menu options.
9. Move the <a> element below the drop-down menu into the <div> decorated
with the dropdown-menu class. Add the dropdown-item class to the <a>
53
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
element to transform it into a menu item. Remove the nav-link and text-dark
classes. Change the text to Manage.
<a class="dropdown-item" asp-area="Identity"
asp-page="/Account/Manage/Index" title="Manage">Manage</a>
10. Move the <form> element below the drop-down menu into the <div> decorated
with the dropdown-menu class. Add the dropdown-item class to the <form>
element to transform it into a menu item. Add the id logout-menu-button to the
<button> element inside the <form> element; you will use it to target the button
with CSS.
<form class="dropdown-item form-inline" asp-area="Identity" asp-
page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index",
"Home", new { area = "" })">
<button id="logout-menu-button" type="submit" class="nav-link
btn btn-link text-dark">Logout</button>
</form>
11. Remove the <li> elements that contained the <a> and <from> elements that you
moved.
12. Add a CSS selector for the logout-menu-button id in the navigation.css file.
Remove all padding from the button to align it with the other menu item.
#logout-menu-button {
padding: 0;
}
<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="user-drop-down"
role="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
@User.Identity.Name <img src="~/images/avatar.png"
54
5. Modifying the Navigation Bar
Summary
In this chapter, you modified the navigation bar and added a drop-down menu, all to make
it look more professional and appealing to the user.
Next, you will figure out what Data Transfer Objects are needed to display the data in the
Membership views.
55
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
56
6. Data Transfer Objects
In some solutions, the DTOs are the same as the entities used to create the database. In
this solution, you will create DTOs for data transfer only, and entities for database CRUD
(Create, Read, Update, Delete) operations. The objects are then transformed from one to
the other using AutoMapper that you installed earlier.
Overview
Your task is to figure out what DTOs are needed to display the necessary data in the three
Membership views: Dashboard, Course, and Video.
The DTOs
The best way to figure out what data is needed is to go back and review the use case and
look at the mock-view images. Here they are again for easy reference.
57
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Dashboard view
58
6. Data Transfer Objects
Course View
59
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Video View
By studying the Dashboard view image, you can surmise the data needed for a single
course card: course image, title, description, and a button leading to the course view
(course id). But if you examine the Course view image, you can see that the course also
has a marquee image.
How do you translate this into a class? Let’s do it together, property by property.
Looking at the Course and Video view images, you can see that they are more complex
than the Dashboard view. They both have three distinct areas. The Course view has a
description area with a marquee image, a list of modules, and an instructor bio. Each
module also has lists of videos and downloads. The Video view has a video area with a
description and video information, an area for navigating to previous and next videos, and
an instructor bio.
60
6. Data Transfer Objects
The first class will be called CourseDTO, and have the following properties:
61
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The sixth class is LessonInfoDTO, which you will use in the Coming Up section of the Video
view.
62
6. Data Transfer Objects
9. Open the _ViewImports.cshtml file in the UI project and add a using statement
to the DTOModels and the DTOModels-UI folders to make the files in those
namespaces available from the views.
@using VOD.Common.DTOModels
@using VOD.Common.DTOModels.UI
63
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
There will be three view models, although you could argue that the first one isn’t strictly
necessary, because it contains only one property. I beg to differ, however, because it will
be easier to update the view with more data if the need should arise.
The first view model is DashboardViewModel, which has only one property. The property
data type is somewhat complex; it is a list containing a list. The reason for using a list in a
list is that you want to display three course cards on each row. An easy way to make sure
that is possible is to add lists containing a maximum of three CourseDTOs, one list for each
row, to the outer list.
64
6. Data Transfer Objects
The third view model is the VideoViewModel, which contains a VideoDTO, an Instructor-
DTO, a CourseDTO, and a LessonInfoDTO.
65
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Summary
In this chapter, you figured out the Data Transfer Objects (DTOs) needed to display the
data in the views. You also figured out how to transport multiple DTOs to the view with
one model, a view model.
Next, you will learn how the data will be stored in a data source using entity classes.
66
7. Entity Classes
7. Entity Classes
Introduction
In this chapter, you will add the entity classes needed to store data in the database. In the
next chapter, you will create the tables corresponding to the entity classes you add in this
chapter.
Now that you have defined the DTOs, you can figure out what the data objects, the enti-
ties, should contain. There will not always be a 1-1 match between a DTO and an entity;
that’s where an object mapper comes into the picture. In a later chapter, you will use
AutoMapper to convert an entity to a DTO.
Overview
Your task is to use your knowledge about the DTOs to create a set of entity classes that
will make up the data sources. Remember that an entity doesn’t have to contain all
properties of a DTO and that sometimes it will contain more properties.
The Entities
Let’s go back and review the DTOs one at a time and decide which of their properties
belong in the entities. Some of the entity properties need restrictions, like maximum
length, required, and if it’s a primary key in the table.
The Video entity needs the same properties that the DTO has, but it could use a few more.
The Video entity needs a ModuleId navigation property, as well as a property for the
actual module to know to what module it belongs. You will also add navigation properties
for the courseId and the course. Navigation properties can be used to avoid complex LINQ
joins when loaded with data.
67
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
A video can only belong to one module in this scenario. If you want a video available in
multiple modules, you need to implement a many-to-many relationship entity between
the Video and Module entities. In this application, it is enough that a video only can belong
to one module and that a module can have multiple videos associated with it.
You could also add a CourseId navigation property, to avoid lengthy joins.
Note that the property names don’t have to be the same in the DTO and the entity; there
can even be different properties altogether. AutoMapper can be configured to map
between properties with different names and types. By default, it uses auto-mapping
between properties with identical names.
68
7. Entity Classes
Apart from the name, description, and avatar properties, the Instructor entity needs a
unique id property.
Apart from the DTO properties, the Course entity needs a unique id, an instructor id and
a single Instructor entity, and a list of Module entities.
The single Instructor property is the 1 in the 1-many relationship between the Course and
Instructor entities.
69
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The list of Module entities is the many in a 1-many relationship between the Course entity
and the Module entities. A course can have many modules, but a module can only belong
to one course.
You could change this behavior by implementing another entity that connects the Course
and the Module entities, creating a many-many relationship. Here you’ll implement the 1-
many relationship.
Apart from the DTO properties, the Module entity needs a unique id and a navigation
property to its Course entity.
The single Course entity is the 1 in a 1-many relationship between the Course entity and
the Module entity. A module can only belong to one course, but a course can have many
modules.
The lists of Video and Download entities are the many part of the 1-many relationships
between them and a Module entity; in other words, a collection property in an entity class
signifies that many records of that type can be associated with the entity. For instance, an
order has a 1-many relationship with its order rows, where one order can have many order
rows. A module can have many videos and downloads, and a download and a video can
only belong to one module.
70
7. Entity Classes
In earlier versions of Entity Framework, a composite primary key – a primary key made up
of more than one property – could be defined using attributes in the entity class. In Entity
Framework Core, you define them in the DbContext class, which you will do in a later
chapter.
Depending on the order you implement the entity classes, you might end up with proper-
ties that reference entity classes that you must add. For instance, the Courses entity has a
property called Instructor, which is dependent on the Instructor class.
71
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
72
7. Entity Classes
73
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
[MaxLength(80), Required]
public string Name { get; set; }
[MaxLength(1024)]
public string Description { get; set; }
[MaxLength(1024)]
public string Thumbnail { get; set; }
}
74
7. Entity Classes
Summary
In this chapter, you discovered and implemented the entity classes, and their properties
and restrictions.
Next, you will create the database tables from the entity classes you just added to the
Common project.
75
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
76
8. Creating the Database Tables
You will also seed the new tables in the database with initial data, which makes it easier
for you to follow along as you create the various views because then they will have familiar
data to display.
When the tables have been created and seeded, you will create a data repository class
called UIReadService in the next chapter, using an interface named IUIReadService. When
implemented, you will register it as a service in the ConfigureServices method in the
Startup class, which will prepare the application to use data from the database.
Overview
Your first objective is to create the tables for storing video-related data in the database
and seed them with data. The second objective is to create a data repository that can
communicate with the database tables. After implementing these steps, the application
can work with data from the database.
You can then inject the VODContext class into the constructor of the UIReadService class
to perform CRUD (Create, Read, Update, Delete) operations on the tables.
77
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
78
8. Creating the Database Tables
// Composite key
builder.Entity<UserCourse>().HasKey(uc =>
new { uc.UserId, uc.CourseId });
1. Open the Package Manager Console and select VOD.Database in the right drop-
down.
2. Execute the following command to create the migration data.
add-migration CreateEntityTables
3. Execute the following command to make the migration changes in the database.
update-database
4. Open the SQL Server Object Explorer and make sure that the tables are in the
database.
79
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The seed data is added using a static method called Initialize, which you will need to add
to the class.
If you want to recreate the database every time migrations are applied, you add the
following two code lines at the beginning of the Initialize method; this could be useful in
certain test scenarios where you need a clean database. You will not add them in this
exercise because you want to keep the data you add between migrations.
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
To add data to a table, you create a list of the entity type and add instances to it. Then you
add that list to the entity collection (the DbSet for that entity), in the VODContext class,
using the context object passed into the Initialize method.
Note that the order in which you add the seed data is important because some tables may
be related to other tables and need the primary keys from those tables.
1. Add a public class called DbInitializer to the Migrations folder in the Database
project.
2. Add a public static method called RecreateDatabase to the class. It should take
the VODContext as a parameter. Call this method if you need to recreate the
database; this deletes all data in the entire database.
public static void RecreateDatabase(VODContext context)
{
}
80
8. Creating the Database Tables
5. To avoid repeating dummy data, you will reuse text from a variable with some
Lorem Ipsum text throughout the seeding process. You can generate Lorem
Ipsum text at the following URL: http://loripsum.net/.
var description = "Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.";
6. Add three variables for email, admin role id, and user id used throughout the
Initialize method. The email address should be in the AspNetUsers table; if not,
then register a user with that email address or change the variable value to an
email address in the table. The user should be an administrator; if not, open the
AspNetUserRoles table and add a record using the user id and 1 (or the id you
gave the Admin role in the AspNetRoles table) in the RoleId column.
var email = "[email protected]";
var adminRoleId = string.Empty;
var userId = string.Empty;
7. Try to fetch the user id from the AspNetUsers table using the Users entity.
if (context.Users.Any(r => r.Email.Equals(email)))
userId = context.Users.First(r => r.Email.Equals(email)).Id;
8. Add an if-block that checks if the user id was successfully fetched. All the
remaining code should be placed inside this if-block.
if (!userId.Equals(string.Empty))
{
}
9. Use the Instructors entity to add instructor data to the Instructors table in the
database if no data has been added.
if (!context.Instructors.Any())
{
var instructors = new List<Instructor>
{
new Instructor {
Name = "John Doe",
Description = description.Substring(20, 50),
Thumbnail = "/images/Ice-Age-Scrat-icon.png"
},
81
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
new Instructor {
Name = "Jane Doe",
Description = description.Substring(30, 40),
Thumbnail = "/images/Ice-Age-Scrat-icon.png"
}
};
context.Instructors.AddRange(instructors);
context.SaveChanges();
}
10. Use the Courses entity to add course data to the Courses table in the database if
no data has been added.
if (!context.Courses.Any())
{
var instructorId1 = context.Instructors.First().Id;
var instructorId2 = int.MinValue;
var instructor = context.Instructors.Skip(1).FirstOrDefault();
if (instructor != null) instructorId2 = instructor.Id;
else instructorId2 = instructorId1;
82
8. Creating the Database Tables
context.Courses.AddRange(courses);
context.SaveChanges();
}
11. Try to fetch the course ids from the newly added courses. These ids will be used
in other tables when referencing courses.
var courseId1 = int.MinValue;
var courseId2 = int.MinValue;
var courseId3 = int.MinValue;
if (context.Courses.Any())
{
courseId1 = context.Courses.First().Id;
course = context.Courses.Skip(2).FirstOrDefault();
if (course != null) courseId3 = course.Id;
}
12. Use the UserCourses entity to connect users and courses.
if (!context.UserCourses.Any())
{
if (!courseId1.Equals(int.MinValue))
context.UserCourses.Add(new UserCourse
{ UserId = userId, CourseId = courseId1 });
if (!courseId2.Equals(int.MinValue))
context.UserCourses.Add(new UserCourse
{ UserId = userId, CourseId = courseId2 });
if (!courseId3.Equals(int.MinValue))
context.UserCourses.Add(new UserCourse
{ UserId = userId, CourseId = courseId3 });
context.SaveChanges();
}
13. Use the Modules entity to add module data to the Modules table in the
database if no data has been added.
if (!context.Modules.Any())
{
var modules = new List<Module>
{
83
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
module = context.Modules.Skip(2).FirstOrDefault();
if (module != null) moduleId3 = module.Id;
else moduleId3 = moduleId1;
}
15. Use the Videos entity to add video data to the Videos table in the database if no
data has been added.
if (!context.Videos.Any())
{
var videos = new List<Video>
{
new Video { ModuleId = moduleId1, CourseId = courseId1,
Title = "Video 1 Title",
Description = description.Substring(1, 35),
Duration = 50, Thumbnail = "/images/video1.jpg",
Url = "https://www.youtube.com/watch?v=BJFyzpBcaCY "
},
new Video { ModuleId = moduleId1, CourseId = courseId1,
Title = "Video 2 Title",
Description = description.Substring(5, 35),
84
8. Creating the Database Tables
context.Downloads.AddRange(downloads);
context.SaveChanges();
}
17. Inject the VODContext class into the Configure method in the Startup class.
85
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
19. Call the DbInitializer.Initialize method with the db object, above the
app.UseAuthentication method call, when the application starts to add the seed
data.
DbInitializer.Initialize(db);
20. To fill the tables with the seed data, you must start the application (Ctrl+F5).
21. Stop the application and remove or comment out the call to the
DbInitializer.Initialize method. Call this method only when you seed the
database.
22. Open the SQL Server Object Explorer.
23. Right click on the entity tables and select View Data to verify that they contain
seed data.
24. If for some reason the data hasn’t been added, then recreate the database by
calling the RecreateDatabase method you added earlier once from the
Configure method in the Startup class. Remember to add a new user with the
[email protected] email address and assign it the Admin role that you must add to the
database as you did in an earlier chapter.
The complete code for the Configure method in the Startup class:
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
VODContext db)
{
if (env.IsDevelopment())
86
8. Creating the Database Tables
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Summary
In this chapter, you created the application-related tables in the database and seeded
them with data.
Next, you will create a data repository service that communicates with the database tables
and gives the application access to data from the database.
87
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
88
9. The Database Read Service
The service is injected into a second service in the UI project called UIReadService, using
an interface named IUIReadService. You will register the IUIReadService service in the
ConfigureServices method in the Startup class, which will make the application use the
data from the database.
The Admin project doesn’t have a special read service and will, therefore, use the DbRead-
Service service directly.
Overview
Your first objective is to create a reusable service that communicates with the database
tables from other services that need to read from the database. Your second objective is
to create a service that the UI application can use to fetch data from the database using
the first service through dependency injection.
Implement the methods as generic asynchronous methods that can handle any entity and
therefore fetch data from any table in the database.
89
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
90
9. The Database Read Service
Since the method will return all records in the table, no parameters are necessary.
91
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Because the method will return a subset of the records in the table, a Lambda expression
must be passed-in to the method. The lambda expression is defined by an Expression<
Func<TEntity, bool>> expression, which means that the expression must return either
true or false.
92
9. The Database Read Service
Because the method will return one record from the table, a Lambda expression will be
passed-in to the method. The lambda expression is defined by an Expression<Func<
TEntity, bool>> expression, which means that the expression must return either true or
false.
The Lambda expression limits the result, which is returned asynchronously by calling the
SingleOrDefaultAsync Linq method.
93
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
define the method, which would generate an exception. Return the result as a
Task because the method is asynchronous.
Task<TEntity> SingleAsync<TEntity>(Expression<Func<TEntity, bool>>
expression) where TEntity : class;
3. Add the SingleAsync method to the DbReadService class, either manually or by
using the Quick Actions light bulb button. If you auto-generate the method with
Quick Actions, you must remove the throw statement. Don’t forget to add the
async keyword to the method to enable asynchronous calls within the method.
4. You can call the SingleOrDefaultAsync Linq method with the expression to
return the data asynchronously.
return await _db.Set<TEntity>().Where(expression)
.SingleOrDefaultAsync();
5. Save all files.
94
9. The Database Read Service
A Lambda expression that returns true if the record is in the table will be passed-in to the
method. The Lambda is defined by an Expression<Func<TEntity, bool>> expression, which
means that it must return either true or false.
The Lambda expression determines the result that is returned asynchronously by calling
the AnyAsync Linq method on the entity.
95
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
To find the navigation properties, you will use the Model object on the VODContext
context (_db). The Model object has a method called FindEntityType that returns the type
of the generic TEntity data type. On that result, you then can call the GetNavigations
method to fetch the navigation properties. You can then call the Select method to fetch
the names of the returned navigation properties.
When you have the names, you can iterate over them and call the Include Linq method on
the result from the _db.Set<TEntity> method and pass in each property name.
The second Include method you create will take two generics (TEntity1, TEntity2) and call
the first Include method for each of the two generics; the second method isn’t strictly
necessary since you can call the first Include method twice.
3. Add the Include method to the DbReadService class, either manually or by using
the Quick Actions light bulb button. If you auto-generate the method with Quick
96
9. The Database Read Service
Actions, you must remove the throw statement. Don’t forget to add the async
keyword to the method to enable asynchronous calls within the method.
4. Fetch the names of the navigation properties inside the TEntity class by calling
the FindEntityType, GetNavigations and Select methods.
var propertyNames =
_db.Model.FindEntityType(typeof(TEntity)).GetNavigations().Select(
e => e.Name);
5. Iterate over the property names you fetched and call the Include Linq method on
the _db.Set<TEntity> method to add data to the navigation properties.
foreach (var name in propertyNames)
_db.Set<TEntity>().Include(name).Load();
6. Open the IDbReadService interface and add the definition for the second
Include method that will take two generic types. You define the generic types as
a comma-separated list inside the brackets.
void Include<TEntity1, TEntity2>() where TEntity1 : class where
TEntity2 : class;
7. Add the Include method to the DbReadService class.
8. Call the first Include method with the two generic types.
Include<TEntity1>();
Include<TEntity2>();
9. Save all files.
97
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The ToSelectList method has two string parameters and an extension parameter prefixed
with the this keyword. The valueField parameter holds the name of the value property
(usually an id) that is stored in the background. The textField property holds the name of
the description property; its value is displayed in the drop-down. Each selectable item in
the drop-down uses both properties.
This generic method can convert a list of any entity. You choose the table to read from by
defining the desired entity for the list when calling the method.
The extension parameter defined with the this keyword, named items, is passed into an
instance of the SelectList class, which the method returns.
return new SelectList(items, valueField, textField);
98
9. The Database Read Service
99
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
10. To test the GetAsync methods you created, call one without parameter and one
with a Lambda expression on the _db service. Store the result in variables named
result2 and result3.
var result2 = await _db.GetAsync<Download>(); // Fetch all
100
9. The Database Read Service
Summary
In this chapter, you created a service for reading data from the database. This service will
be used from the Admin and UI projects to fetch data through other services.
Next, you will add a repository service designed for calls from the UI project to the
database.
101
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
102
10. The UI Data Service
Overview
Now, you will implement an Interface named IUIReadService in a class called UIRead-
Service to create a service that can communicate with the database tables through the
IDbReadService that you created in the previous chapter. To make sure that service’s
methods return correct data, you will call them from a controller.
1. Right click on the Dependencies node in the VOD.UI project in the Solution
Explorer and select Add-Add Reference and make sure that the VOD.Database
and VOD.Common projects are selected and click the OK button.
2. Open the VOD.Database project.
3. Right click on the Services folder and select Add-New Item.
4. Select the Interface template.
5. Name it IUIReadService and click the Add button. Add the public access modifier
to the interface, to make it accessible from all projects that reference the
VOD.Database project.
6. Add an asynchronous method description for the GetCourses method. It should
return an IEnumerable of Course entities as a task. Resolve any missing using
statements.
103
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
104
10. The UI Data Service
#region Constructor
public UIReadService(IDbReadService db)
{
_db = db;
}
#endregion
#region Methods
Public async Task<IEnumerable<Course>> GetCourses(string userId)
{
throw new NotImplementedException();
}
105
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
106
10. The UI Data Service
3. Use the _db service variable to fetch all courses for a specific user.
a. Call the GetAsync method on the _db service for the UserCourses entity
to fetch all the course id and user id combinations from the database for
the logged-in user.
var userCourses = await _db.GetAsync<UserCourse>(uc =>
uc.UserId.Equals(userId));
4. Call the GetCourses method on the _db object above the if-statement in the
Index action in the HomeController to fetch all courses related to a specific user.
Copy the user id from the user you used to seed the database and pass it into
the method. Store the result in a variable named courses.
var courses = (await _db.GetCourses("88f17367-d202-4383-a54e-
ec15e86e532e")).ToList();
5. Place a breakpoint on the if-statement in the Index action.
6. Run the application and verify that the courses variable contains the courses.
7. Stop the application.
107
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Remove the throw statement from the GetCourse method in the UIReadService
class.
2. Because the Course object has navigation properties to Instructor and Module
entities, you want to load them as well. You can achieve this by calling the
Include method on the _db service.
_db.Include<Course, Module>();
3. Try to fetch data for the UserCourse entity matching the userId and courseId
parameters in the GetCourse method. Call the SingleAsync method on the _db
service with the UserCourse entity and the ids and store the result in a variable
called userCourse. If the result is null, then the user doesn’t have access to the
desired course.
var userCourse = await _db.SingleAsync<UserCourse>(c =>
c.UserId.Equals(userId) && c.CourseId.Equals(courseId));
4. Return the default value (null) for the Course entity if the user doesn’t have
access to the course.
if (userCourse == null) return default;
8. Call the GetCourse method on the _db object in the Index action in the
HomeController to fetch the desired course for a specific user. Copy the user id
from the user you used to seed the database and pass it into the method along
with the course id for the desired course. Store the result in a variable named
course.
var course = db.GetCourse("88f17367-d202-4383-a54e-ec15e86e532e",
1);
6. Run the application and verify that the course and its navigation properties are
available in the course variable.
7. Stop the application.
108
10. The UI Data Service
1. Remove the throw statement from the GetVideo method in the UIReadService
class.
2. The view model that will use the data from this method needs associated data as
well. You, therefore, should include the Course entity, which in turn will make
sure that the Module and Instructor entities are loaded.
_db.Include<Course>();
3. Fetch the video matching the video id in the videoId parameter passed into the
GetVideo method by calling the SingleAsync method on the _db service variable.
var video = await _db.SingleAsync<Video>(v =>
v.Id.Equals(videoId));
4. Check that a video is returned and return the default value (null) for the Video
entity.
if (video == null) return default;
5. Check that the user can view the video belonging to the course specified by the
CourseId property of the video object. Return the default value for the Video
entity if the user doesn’t have access.
var userCourse = await _db.SingleAsync<UserCourse>(c =>
c.UserId.Equals(userId) &&
c.CourseId.Equals(video.CourseId));
if (userCourse == null) return default;
6. Return the video in the video variable.
return video;
109
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
7. Call the GetVideo method on the _db object from the Index action in the
HomeController to fetch the desired video for a specific user. Copy the user id
from the user you used to seed the database and pass it into the method along
with the video id for the desired video. Store the result in a variable named
video.
var video = await _db.GetVideo("88f17367-d202-4383-a54e-
ec15e86e532e", 1);
8. Run the application and verify that the video is displayed in the video variable.
9. Stop the application.
return video;
}
1. Remove the throw statement from the GetVideos method in the UIReadService
class.
2. Include the video objects when fetching the data for the Module entity.
_db.Include<Video>();
3. Fetch the module matching the module id in the moduleId parameter passed
into the GetVideos method by calling the SingleAsync method for the Module
entity on the _db service variable.
var module = await _db.SingleAsync<Module>(m =>
m.Id.Equals(moduleId));
110
10. The UI Data Service
4. Check that a module was returned and return the default value for a List<Video>
if it is null.
if (module == null) return default(List<Video>);
5. Check that the user can view the video belonging to the course specified by the
CourseId property in the module object. Return the default value for a list of
Video entities if the user doesn’t have access.
var userCourse = await _db.SingleAsync<UserCourse>(uc =>
uc.UserId.Equals(userId) &&
uc.CourseId.Equals(module.CourseId));
7. Call the GetVideos method on the db object to fetch the desired videos related
to a specific user and module. Copy the user id from the user you used to seed
the database and pass it into the method along with the module id for the. Store
the result in a variable named videos.
var videos = (await _db.GetVideos("88f17367-d202-4383-a54e-
ec15e86e532e", 1)).ToList();
8. Run the application and verify that the videos are displayed in the video variable.
9. Stop the application.
return module.Videos;
111
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
You will inject the IUIReadService into another controller that you will add in an
upcoming chapter.
Summary
In this chapter, you created a service that communicates with the database tables through
the IDbReadService service in the Database project to use live data from the database in
the application.
112
11. The Membership Controller and AutoMapper
Using AutoMapper removes tedious and boring work, code that you otherwise would have
to implement manually to convert one object to another, with the risk of writing
erroneous conversion code.
Overview
You will begin by adding the Membership controller and its action methods. Then you will
use dependency injection to inject the previously mentioned objects into the controller’s
constructor and save them in private class-level variables.
Then you will set up AutoMapper’s configuration in the Startup.cs file. With that setup
complete, you can proceed with the actual mappings in the action methods.
113
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Three action methods are needed to serve up the views. The first is the Dashboard action,
which displays the user’s courses. From each course card in the Dashboard view, the user
can click a button to open the course, using the second action method called Course. The
Course view lists the content for that course. Open in the Video view when a user clicks a
video item; the Video action method generates the view.
114
11. The Membership Controller and AutoMapper
public MembershipController() { }
9. Inject IHttpContextAccessor into the constructor and save the user from it to a
variable called user. Resolve any missing using statements.
public MembershipController(IHttpContextAccessor
httpContextAccessor) {
var user = httpContextAccessor.HttpContext.User;
}
10. Inject the UserManager into the constructor and call its GetUserId method. Save
the user id in a private class-level variable called _userId. Resolve any missing
using statements.
private readonly string _userId;
public MembershipController(IHttpContextAccessor
httpContextAccessor, UserManager<VODUser> userManager)
{
var user = httpContextAccessor.HttpContext.User;
_userId = userManager.GetUserId(user);
}
11. Inject IMapper into the constructor to get access to AutoMapper in the
controller. Save the instance to a private, read-only, class-level variable called
_mapper.
12. Inject the IUIReadService interface into the constructor and save the instance to
a private class-level variable called _db.
public MembershipController(
IHttpContextAccessor httpContextAccessor,
UserManager<VODUser> userManager, IMapper mapper, IUIReadService db)
{
var user = httpContextAccessor.HttpContext.User;
_userId = userManager.GetUserId(user);
_mapper = mapper;
_db = db;
}
115
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
[HttpGet]
public IActionResult Dashboard()
{
return View();
}
[HttpGet]
public IActionResult Course(int id)
{
return View();
}
[HttpGet]
public IActionResult Video(int id)
{
return View();
}
}
Configuring AutoMapper
For AutoMapper to work properly, you must add configuration to the ConfigureServices
method in the Startup.cs file. The configuration tells AutoMapper how to map between
objects, in this case between entities and DTOs. Default mapping can be achieved by speci-
fying the class names of the objects to be mapped, without naming specific properties.
With default matching, only properties with the same name in both classes will be match-
ed.
A more granular mapping is possible by specifying exactly which properties that match. In
this scenario, the property names can be different in the classes.
116
11. The Membership Controller and AutoMapper
3. Add a mapping for the Video entity and VideoDTO classes inside the config
block. Since the properties of interest are named the same in both classes, no
specific configuration is necessary. Resolve any missing namespaces.
cfg.CreateMap<Video, VideoDTO>();
4. Add a mapping for the Download entity and the DownloadDTO classes inside
the cfg block. Here specific configuration is necessary because the properties are
named differently in the two classes.
cfg.CreateMap<Download, DownloadDTO>()
.ForMember(dest => dest.DownloadUrl,
src => src.MapFrom(s => s.Url))
.ForMember(dest => dest.DownloadTitle,
src => src.MapFrom(s => s.Title));
5. Now do the same for the Instructor, Course, and Module entities and their
DTOs. Note that there are no mappings for the LessonInfoDTO because we don’t
need any.
6. Create a variable called mapper below the ending curly brace for the configura-
tion block. Assign the result from a call to the CreateMapper method on the
previously created config object to the mapper variable.
var mapper = config.CreateMapper();
7. Add the mapper object as a singleton instance to the services collection, as you
did with the IUIReadService.
services.AddSingleton(mapper);
8. Open the Home controller and replace the call to the View method in the return
statement with a call to the RedirectToAction method and redirect to
Membership controller’s Dashboard action when the user is logged in.
return RedirectToAction("Dashboard", "Membership");
117
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
cfg.CreateMap<Instructor, InstructorDTO>()
.ForMember(dest => dest.InstructorName,
src => src.MapFrom(s => s.Name))
.ForMember(dest => dest.InstructorDescription,
src => src.MapFrom(s => s.Description))
.ForMember(dest => dest.InstructorAvatar,
src => src.MapFrom(s => s.Thumbnail));
cfg.CreateMap<Download, DownloadDTO>()
.ForMember(dest => dest.DownloadUrl,
src => src.MapFrom(s => s.Url))
.ForMember(dest => dest.DownloadTitle,
src => src.MapFrom(s => s.Title));
cfg.CreateMap<Course, CourseDTO>()
.ForMember(dest => dest.CourseId, src =>
src.MapFrom(s => s.Id))
.ForMember(dest => dest.CourseTitle,
src => src.MapFrom(s => s.Title))
.ForMember(dest => dest.CourseDescription,
src => src.MapFrom(s => s.Description))
.ForMember(dest => dest.MarqueeImageUrl,
src => src.MapFrom(s => s.MarqueeImageUrl))
.ForMember(dest => dest.CourseImageUrl,
src => src.MapFrom(s => s.ImageUrl));
cfg.CreateMap<Module, ModuleDTO>()
.ForMember(dest => dest.ModuleTitle,
src => src.MapFrom(s => s.Title));
});
118
11. The Membership Controller and AutoMapper
The purpose of the Dashboard action method is to fill the DashboardViewModel with the
appropriate data, using the _db database service that you added earlier. The UIRead-
Service object was injected into the Membership constructor through the IUIReadService
parameter, using dependency injection that you configured in the ConfigureServices
method in the Startup class.
Your next task will be to fill the view model using AutoMapper, mapping data from the
_db database entities to DTO objects used in views that you will add in coming chapters.
The view will be able to display as many courses as the user has access to, but only three
to a row, which means that you will have to divide the list of courses into a list of lists, with
three CourseDTO objects each; this will make it easy to loop out the cards in the view.
To refresh your memory, this is the view that this action method will be serving up.
119
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Open the MembershipController class and locate the Dashboard action method.
2. Replace the IActionResult return type to async Task<IActionResult> for all three
actions to enable asynchronous calls.
3. Call the Map method on the _mapper variable in the Dashboard action method
to convert the result from a call to the GetCourses method on the _db variable;
don’t forget to pass in the logged in user’s id, not a hardcoded value. Calling the
method fetches all the courses for the user and converts them into CourseDTO
objects. Store the result in a variable named courseDtoObjects.
var courseDtoObjects = _mapper.Map<List<CourseDTO>>(
await _db.GetCourses(_userId));
4. If you haven’t already, place a breakpoint on the return statement at the end of
the Dashboard action method.
5. Run the application with debugging (F5).
6. The execution should halt in the Dashboard action method. If not, you can
navigate to http://localhost:xxxxx/Membership/Dashboard to hit the breakpoint.
7. Inspect the courseDtoObjects variable to verify that it contains CourseDTO
objects with data. If the object is empty, then log in as the user that was used to
seed the database; the user information is in the AspNetUsers table.
120
11. The Membership Controller and AutoMapper
121
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
14. Inspect the dashboardModel variable and verify that its Courses property
contains at least one list of CourseDTO objects.
15. Stop the application in Visual Studio and remove the breakpoint.
return View(dashboardModel);
}
The purpose of the Course action method is to fill an instance of the CourseViewModel
with the appropriate data using the _db database service that you added earlier. The
122
11. The Membership Controller and AutoMapper
UIReadService was injected into the Membership constructor through the IUIReadService
parameter using dependency injection, which you configured in the ConfigureServices
method in the Startup class.
Your next task will be to fill the view model using AutoMapper, to map data from the _db
entities to DTO objects used in the Course view that you will add in an upcoming chapter.
The view will display the selected course and its associated modules. Each module will list
the videos and downloadable content associated with it. The instructor bio will also be
displayed beside the module list.
To refresh your memory, this is the view that this action method will be serving up.
123
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
124
11. The Membership Controller and AutoMapper
1. Open the MembershipController class and locate the Course action method.
2. Fetch the course matching the id passed into the Course action and the logged in
user’s user id, by calling the GetCourse method on the _db variable. Store the
result in a variable called course.
var course = await _db.GetCourse(_userId, id);
3. Call the Map method on the _mapper variable to convert the course you just
fetched into a CourseDTO object. Store the result in a variable named
mappedCourseDTOs.
var mappedCourseDTO = _mapper.Map<CourseDTO>(course);
4. Call the Map method on the _mapper variable to convert the Instructor object
in the course object into an InstructorDTO object. Store the result in a variable
named mappedInstructorDTO.
var mappedInstructorDTO =
_mapper.Map<InstructorDTO>(course.Instructor);
5. Call the Map method on the _mapper variable to convert the Modules collection
in the course object into a List<ModuleDTO>. Store the result in a variable
named mappedModuleDTOs.
var mappedModuleDTOs =
_mapper.Map<List<ModuleDTO>>(course.Modules);
125
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
12. Ignore the error message displayed in the browser; it is displayed because there
is no Dashboard view. Replace Dashboard with Course/1 in the URI and navigate
to http://localhost:xxxxx/Membership/Course/1 to hit the breakpoint.
13. Inspect the courseModel variable to verify that it contains a course, an
instructor, and modules with videos and downloads.
14. Stop the application in Visual Studio and remove the breakpoint.
return View(courseModel);
}
126
11. The Membership Controller and AutoMapper
The model will be filled with appropriate data, using the _db database service that you
added earlier. The UIReadService was injected into the Membership controller’s con-
structor through the IUIReadService parameter, using dependency injection. You
configured the DI in the ConfigureServices method in the Startup class.
Your next task is to fill the view model using AutoMapper; mapping data from the _db
database entities to DTO objects used in the Video view.
The Video view will display the selected video, information about the video, buttons to
select the next and previous videos, and an instructor bio.
To refresh your memory, this is the view the Video action will display.
127
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Open the MembershipController class and locate the Video action method.
2. Call the _db.GetVideo method to fetch the video matching the id passed into the
Video action, and the logged in user’s id. Store the result in a variable called
video.
var video = await _db.GetVideo(_userId, id);
3. Call the _db.GetCourse method to fetch the course matching the course id from
the video object, and the logged in user’s id. Store the result in a variable called
course.
var course = await _db.GetCourse(_userId, video.CourseId);
4. Call the _mapper.Map method to convert the Video object into a VideoDTO
object. Store the result in a variable named videoDTO.
var videoDTO = _mapper.Map<VideoDTO>(video);
128
11. The Membership Controller and AutoMapper
5. Call the _mapper.Map method to convert the course object into a CourseDTO
object. Store the result in a variable named courseDTO.
var courseDTO = _mapper.Map<CourseDTO>(course);
6. Call the _mapper.Map method to convert the Instructor object in the course
object into an InstructorDTO object. Store the result in a variable named
instructorDTO.
var instructorDTO = _mapper.Map<InstructorDTO>(
course.Instructor);
7. Call the _db.GetVideos method to fetch all the videos matching the current
module id. You need this data to get the number of videos in the module, and to
get the index of the current video. Store the videos in a variable called videos.
var videos = (await _db.GetVideos(_userId, video.ModuleId))
.OrderBy(o => o.Id).ToList();
10. Fetch the id for the previous video in the module by calling the
ElementAtOrDefault method on the videos collection. Store its id in a variable
called previousId.
var previous = videos.ElementAtOrDefault(index - 1);
var previousId = previous == null ? 0 : previous.Id;
11. Fetch the id, title, and thumbnail for the next video in the module by calling the
ElementAtOrDefault method on the videos collection. Store the values in
variables called nextId, nextTitle, and nextThumb.
var next = videos.ElementAtOrDefault(index + 1);
var nextId = next == null ? 0 : next.Id;
var nextTitle = next == null ? string.Empty : next.Title;
var nextThumb = next == null ? string.Empty : next.Thumbnail;
12. Create an instance of the VideoViewModel class named videoModel.
var videoModel = new VideoViewModel
{
129
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
};
13. Assign the three mapped collections: mappedCourseDTO,
mappedInstructorDTO, and mappedVideoDTOs to the videoModel object’s
Course, Instructor, and Video properties. Create an instance of the
LessonInfoDTO for the LessonInfo property in the videoModel object and assign
the variable values to its properties. The LessonInfoDTO will be used with the
previous and next buttons, and to display the index of the current video.
var videoModel = new VideoViewModel
{
Video = videoDTO,
Instructor = instructorDTO,
Course = courseDTO,
LessonInfo = new LessonInfoDTO
{
LessonNumber = index + 1,
NumberOfLessons = count,
NextVideoId = nextId,
PreviousVideoId = previousId,
NextVideoTitle = nextTitle,
NextVideoThumbnail = nextThumb
}
};
14. Return the videoModel object with the View method.
return View(videoModel);
15. Place a breakpoint on the return statement at the end of the Video action.
16. Run the application with debugging (F5).
17. Navigate to http://localhost:xxxxx/Membership/Video/1 to hit the breakpoint.
18. Inspect the videoModel object to verify that it contains a video, a course, an
instructor, and a lesson info object.
19. Stop the application in Visual Studio and remove the breakpoint.
130
11. The Membership Controller and AutoMapper
return View(videoModel);
}
131
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Summary
In this chapter, you added configuration for entity and DTO classes to AutoMapper in the
Startup class. You also implemented the Membership controller and injected the neces-
sary services into its constructor. Then you implemented the three actions (Dashboard,
Course, and Video) that render their corresponding views in coming chapters.
Next, you will implement the Dashboard view, and render it from the Dashboard action.
132
12. The Dashboard View
The courses are displayed three to a row, to make them the optimal size.
Overview
Your task is to use the view model in the Dashboard action to render a view that displays
the user’s courses in a list. Display each course as a card with the course image, title,
description, and a button that opens the Course view for that course.
133
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
134
12. The Dashboard View
6. Add a class selector named no-border-radius that sets the radius to 0. Use this
selector on elements that should have a square look (no rounded corners).
.no-border-radius {
border-radius:0;
}
7. Add a class selector named no-left-padding that sets the left padding to 0. This
selector will be used to remove any undesired left padding on elements.
.no-left-padding {
padding-left: 0;
}
8. Add a class selector named no-right-padding that sets the right padding to 0.
This selector will be used to remove any undesired right padding on elements.
.no-right-padding {
padding-right: 0;
}
9. Add a class selector named no-top-padding that sets the top padding to 0. This
selector will be used to remove any undesired top padding on elements.
.no-top-padding {
padding-top: 0;
}
10. Add a class selector named no-bottom-padding that sets the bottom padding to
0. This selector will be used to remove any undesired bottom padding on
elements.
.no-bottom-padding {
padding-bottom: 0;
}
11. Add a class selector named vertical-align that sets the vertical align to middle to
center the content vertically.
.vertical-align {
vertical-align: middle;
}
12. Add a class selector named small-bottom-margin that adds a 10px bottom
margin to the element.
.small-bottom-margin{
margin-bottom:10px;
}
135
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
13. Add a class selector named small-top-margin that adds a 10px top margin to the
element.
.small-top-margin{
margin-top:10px;
}
14. Add a class selector named small-left-margin that adds a 10px left margin to the
element.
.small-left-margin{
margin-left:10px;
}
15. Add a class selector named no-margin that removes all margins from the
element.
.no-margin{
margin:0;
}
16. Add a class selector named text-small that scales down the text.
.text-small {
font-size:small;
}
17. Add a class selector named text-large that scales up the text.
.text-large {
font-size:larger;
}
18. Add a class selector named no-text-decoration that removes any decoration
from the element, such as underlining on anchor tags.
.no-text-decoration {
text-decoration:none;
}
19. Add a class selector named overflow-hidden that forces the content to fit into its
container.
.overflow-hidden{
overflow:hidden;
}
20. Save the files.
136
12. The Dashboard View
4. Open the Views folder and verify that the Membership folder and Dashboard
view exists.
5. Visual Studio can get confused when a view is scaffolded, and display errors that
aren’t real. Close the view and open it again to get rid of those errors.
6. Open the _ViewImports view and add a using statement for the
VOD.UI.Models.MembershipViewModels namespace, to get access to the
DashboardViewModel class.
137
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
@using VOD.UI.Models.MembershipViewModels
7. Add an @model directive for the DashboardViewModel class at the beginning of
the view.
@model DashboardViewModel
8. Open the HomeController class and locate the Index action.
9. Since we won’t be using cookies, you can comment out the UseCookiePolicy
method in the Configure method in the Startup class; it activates the cookie
middleware in the HTTP request pipeline.
//app.UseCookiePolicy();
10. Also comment out the cookie configuration in the ConfigureServices method in
the Startup class that displays the cookie message in the browser.
//services.Configure<CookiePolicyOptions>(options =>
//{
// options.CheckConsentNeeded = context => true;
// options.MinimumSameSitePolicy = SameSiteMode.None;
//});
11. Start the application without debugging (Ctrl+F5) and log in if necessary. The text
Dashboard should be displayed in the browser if the Dashboard view was
rendered correctly.
@{
ViewData["Title"] = "Dashboard";
}
<h1>Dashboard</h1>
For now, the view will only display a view title and the course titles; later the courses will
be displayed as cards.
138
12. The Dashboard View
@{
139
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
ViewData["Title"] = "Dashboard";
}
<h1>Dashboard</h1>
<hr>
@foreach (var dashboardRow in Model.Courses)
{
<div class="row">
@foreach (var course in dashboardRow)
{
<h4>@course.CourseTitle</h4>
}
</div>
}
Creating the _CourseCardPartial Partial View
Instead of cluttering the Dashboard view with the course card markup, you will create and
render a partial view called _CourseCardPartial for each course. A Bootstrap card will be
used to display the course information.
140
12. The Dashboard View
2. Name the view _CourseCardPartial and check the Create as a partial view
checkbox before clicking the Add button.
141
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
8. Add an <img> element to the previous <div> and decorate it with a Bootstrap
class called card-img-top and the no-border-radius CSS class to give it square
corners. The class will be used when styling the image. Add the CourseImageUrl
property in the view model as the image source.
<img class="card-img-top no-border-radius"
src="@Model.CourseImageUrl">
9. Add a <div> element below the image and decorate it with the card-body
Bootstrap class. This is the area where the video information is displayed.
<div class="card-body">
</div>
10. Add an <h3> element in the previous <div> and decorate it with a Bootstrap class
named card-title. Add the CourseTitle property in the view model to it.
<h3 class="card-title">@Model.CourseTitle</h3>
11. Add a <p> element for the CourseDescription view model property below the
<h3> element. Add the Bootstrap class card-text to the element.
<p class="card-text">@Model.CourseDescription</p>
12. Add an <a> element below the description and style it as a blue button with the
btn btn-primary Bootstrap classes. Also, add the no-border-radius CSS class to
give it square corners. Use the CourseId view model property in the href URL to
determine which course will be fetched by the Course action and displayed by
the Course view. Add the text View Course to the button.
<a class="btn btn-primary no-border-radius"
href="~/Membership/Course/@Model.CourseId">View Course</a>
13. Open the Dashboard view.
14. Replace the <h4> element with the <partial> Tag Helper, or with a call to the
older PartialAsync method, that will render the _CourseCardPartial partial view
for each course.
@foreach (var course in dashboardRow)
{
<partial name="_CourseCardPartial" model="@course" />
@*@await Html.PartialAsync("_CourseCardPartial", course)*@
}
142
12. The Dashboard View
15. Save all files and refresh the Dashboard view in the browser.
16. Stop the application in Visual Studio.
Summary
In this chapter, you added the Dashboard view and the _CourseCardPartial partial view
and styled them with Bootstrap and your own CSS .
Next, you will add the Course view and the _ModuleVideosPartial, _ModuleDownloads-
Partial, and _InstructorBioPartial partial views that are part of the Course view. Then you
will style them with CSS and Bootstrap.
143
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
144
13. The Course View
Overview
Your task is to use the view model in the Course action and render a view that displays a
marquee, course image, title, and description as a separate row below the Back to Dash-
board button at the top of the view. Below that row, a second row divided into two
columns should be displayed. Add rows in the left column for each module in the course
and list the videos for each module. Display the instructor’s bio in the right column.
145
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
146
13. The Course View
Then, you will add a button that navigates to the Dashboard view, a marquee image,
course information, an instructor bio, and modules with videos and downloads. You will
create three partial views, one called _InstructorBioPartial for the instructor bio, one
called _ModuleVideosPartial for the videos, and one called _ModuleDownloadsPartial
for downloads. Style the three areas with Bootstrap and CSS.
5. Close the Course view and open it again to get rid of any errors.
6. Add an @model directive for the CourseViewModel class at the beginning of the
view.
@model CourseViewModel
147
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
@{
ViewData["Title"] = "Course";
}
<h1>Course</h1>
Place the button on a separate row that takes up the entire page width. Add the row and
col-sm-12 Bootstrap classes to two nested <div> elements to add the row and the column.
148
13. The Course View
6. Add a <i> element inside the <a> element and decorate it with the material-
icons class and the icon name keyboard_arrow_left inside the tag to add a caret
(<) icon. Also, add the vertical-align and no-left-margin classes to center the
chevron vertically and remove the left margin.
<i class="material-icons vertical-align no-left-margin">
keyboard_arrow_left</i>
7. Add a <span> with the text Back to Dashboard after the <i> tag in the <a>
element. If you place the <i> and <span> side-by-side on the same row without a
space, the button text will get closer to the icon. Add the vertical-align class to
center the chevron vertically.
<span class="vertical-align">Back to Dashboard</span>
8. Save the view and refresh it in the browser. A blue button with the text < Back to
Dashboard should be visible at the top of the view.
9. Click the button to navigate to the Dashboard view.
10. Click the button in one of the cards in the Dashboard view to get back to the
Course view.
@{
ViewData["Title"] = "Course";
}
149
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Place the card on a separate row that takes up the entire page width. Add the Bootstrap
row and col-sm-12 classes to two nested <div> elements; this will create a row and a
column. Use the card and card-body Bootstrap classes to style the card <div> elements.
Use a <div> to display the marquee image as a background image inside the card.
Add the course title as an <h1> element and the course description as an <h4> element
inside the card-body <div>.
150
13. The Course View
The markup for the course information row in the Course view:
<div class="row">
<div class="col-sm-12">
<div class="card no-border-radius">
<div class="marquee" style="background-image:url(
'@Model.Course.MarqueeImageUrl');">
<img src="@Model.Course.CourseImageUrl">
</div>
<div class="card-body">
<h1 class="card-title">@Model.Course.CourseTitle</h1>
<h4 class="card-text">
@Model.Course.CourseDescription
</h4>
</div>
</div>
</div>
151
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
</div>
Open the membership.css file and style the cover image by making its width automatic
and the height 140px. Use absolute positioning to place the image at the bottom of the
marquee. Add a 30px margin to move the image away from the marquee borders. Add a
4px solid white border around the image and give it a subtle border radius of 2px.
.card .marquee img {
width: auto;
height: 140px;
position: absolute;
bottom: 0;
margin: 30px;
border: 4px solid #FFF;
border-radius: 2px;
}
Now, style the marquee. Make it cover the entire width of its container, give it a height of
400px, and hide any overflow. The marquee position must be relative for the course image
to be positioned correctly. Make the background image cover the entire available space.
.card .marquee {
width: 100%;
height: 400px;
overflow: hidden;
background-size: cover;
/* Relative positioning of the marquee is needed for the cover
image’s absolute position */
position: relative;
}
152
13. The Course View
1. Open the Course view and add a <div> element decorated with the row
Bootstrap class below the previous closing row </div> containing the marquee.
<div class="row"></div>
2. Add two <div> elements inside the row <div>. Decorate the first <div> with the
col-sm-9 Bootstrap class, and the second with the col-sm-3 class. This will make
the first column take up ¾ of the row width and the second column ¼ of the row
width.
<div class="col-md-9">
@*Add modules here*@
</div>
<div class="col-md-3">
@*Add instructor bio here*@
</div>
The markup for the new row and columns in the Course view:
<div class="row">
<div class="col-sm-9">@*Add modules here*@</div>
<div class="col-sm-3">@*Add instructor bio here*@</div>
</div>
153
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
154
13. The Course View
</div>
4. Add a <div> inside the card <div> and decorate it with the card-body Bootstrap
class. Add an <h5> element containing the ModuleTitle property.
<div class="card-body">
<h5>@module.ModuleTitle</h5>
</div>
5. Add a horizontal rule below the previous <div> and add the CSS class no-margin
to remove any margin that is applied to the <hr> element by default.
<hr class="no-margin">
6. Save the files and refresh the browser. The module titles for the course you
selected should be listed below the marquee.
155
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Use the Bootstrap media classes to display the video information in a uniform way.
156
13. The Course View
157
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
12. Add the video thumbnail to the previous <div> and use the image URL in the
current video’s Thumbnail property for the <img> element’s src property. Set
the image width to 100.
<img src="@video.Thumbnail" width="100">
13. Add a <div> decorated with the media-body Bootstrap class below the media-
left <div>. This will be the (right) video information area.
<div class="media-body">
</div>
14. Add an <h5> element for the video title inside the media-body <div>. Add the
view model’s Title property to the element. Add the no-margin CSS class to
remove any margins from the heading.
<h5 class="no-margin">@video.Title</h5>
15. Add a <p> element decorated with the no-margin and text-muted below the
title. The latter class will display the video’s duration and icon with a muted font.
Add an <i> element with an alarm icon; use the material-icons class and add the
text alarm inside the <i> element. Add the duration from the current video’s
Duration property followed by the text minutes inside a <span> element after
the <i> element. Add the vertical-align and text-small CSS classes to both the <i>
and <span> elements.
<p class="text-muted no-margin">
<i class="material-icons vertical-align text-small">alarm</i>
<span class="vertical-align text-small">
@video.Duration minutes</span>
</p>
16. Add the video description in a <p> element below the duration; use the current
video’s Description property. Add the no-margin CSS class to the element.
<p class="no-margin">@video.Description</p>
17. When you refresh the Course view in the browser, you’ll see that the video items
need some additional styling, which you will do next.
158
13. The Course View
Change the background color to blue when hovering over a video item.
.module-video:hover {
background-color: #5ab3ff;
}
Remove any text decoration from the link when hovering over a video item.
.module-video a:hover {
text-decoration: none;
}
159
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
.module-video .media-body {
margin-left: 5px;
}
Push the title up 5px to align it with the top of the thumbnail image.
.module-video h5 {
margin-top: -5px;
}
Use the Bootstrap card classes to display the download information uniformly.
160
13. The Course View
3. Add an if-block, checking that the current module’s Downloads collection isn’t
null or empty, below the videos if-block.
@if (module.Downloads != null && module.Downloads.Count > 0)
{
}
4. Add a horizontal line inside the if-block for the Downloads collection and
decorate it with a CSS class called no-margin that will be used later to remove
the element’s margin.
<hr class="no-margin">
5. Add a <div> decorated with the card-body Bootstrap class below the <hr>
element inside the if-block. Add the no-bottom-padding CSS class to it to
remove any padding at the bottom of the element.
<div class="card-body no-bottom-padding"></div>
6. Add an <h5> element with the text Downloads inside the previous <div>.
<h5>Downloads</h5>
7. Add a <div> decorated with the card-body Bootstrap class below the previous
<div> element inside the if-block. Add the no-top-padding CSS class to it to
remove any top padding to the element.
<div class="card-body no-top-padding"></div>
8. Render the partial view inside the previous <div> element. Pass in the
Downloads collection from the current module to the <partial> Tag Helper,
which renders the _ModuleDownloadsPartial and displays the download links.
<partial name="_ModuleDownloadsPartial" model="@module.Downloads" />
161
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
browser tab. The <a> element should display a download icon and the text from
the current download’s DownloadTitle property.
<li>
<a href="@download.DownloadUrl" target="_blank">
<i class="material-icons">save_alt</i>
<span> @download.DownloadTitle</span>
</a>
</li>
15. Save the files and refresh the Course view in the browser. A section with
download links should be displayed in the module lists, where downloadable
content is available.
<ul>
@foreach (var download in Model)
{
<li>
<a href="@download.DownloadUrl" target="_blank">
<i class="material-icons">save_alt</i>
<span> @download.DownloadTitle</span>
</a>
</li>
}
</ul>
The markup for rendering the _ModuleDownloadsPartial view in the Course view:
@if (module.Downloads != null && module.Downloads.Count > 0)
{
<hr class="no-margin">
<div class="card-body no-bottom-padding">
<h5>Downloads</h5>
</div>
162
13. The Course View
The complete code for the modules, videos, and downloads in the Course view:
<div class="col-md-9">
@*Add modules here*@
@foreach (var module in Model.Modules)
{
<div class="card small-top-margin no-border-radius">
<div class="card-body">
<h5>@module.ModuleTitle</h5>
</div>
<hr class="no-margin">
@if (module.Videos != null && module.Videos.Count > 0)
{
<partial name="_ModuleVideosPartial"
model="@module.Videos" />
}
163
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
164
13. The Course View
7. Add a <div> decorated with the card-body Bootstrap class inside the card <div>.
<div class="card-body">
</div>
8. Add an <img> element inside the card-body <div> for the InstructorThumbnail
property in the view model. Decorate the <div> with the rounded-circle
Bootstrap class and a CSS class called avatar, which will be used when styling the
instructor’s thumbnail.
<img src="@Model.InstructorAvatar" class="avatar rounded-circle">
9. Add an <h4> element for the InstructorName property in the view model inside
the card-body <div>.
<h4>@Model.InstructorName</h4>
10. Add an <h5> element with the text Instructor inside the card-body <div>.
Decorate it with the text-primary Bootstrap class to make the text blue.
<h5 class="text-primary">Instructor</h5>
11. Add a <p> element for the view model’s InstructorDescription property inside
the card-body <div>.
<p>@Model.InstructorDescription</p>
12. Save the files and refresh the browser to save the changes.
Center the text in the instructor-bio container and add a 10px top margin.
165
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
.instructor-bio {
text-align: center;
margin-top: 10px;
}
Style the avatar to have a blue circle with 8px padding around it and make the image
diameter 120px. Create the circle with the rounded-circle Bootstrap class, which styles
the border of an element.
.instructor-bio .rounded-circle {
border: 2px solid #2d91fb;
padding: 8px;
height: 120px;
width: 120px;
}
Summary
In this chapter, you created the Course view and its three partial views: _ModuleVideos-
Partial, _ModuleDownloadsPartial, and _InstructorBioPartial. You also used Bootstrap to
create rows and columns in a responsive design and styled the views with Bootstrap and
CSS.
Next, you will create the Video view that displays the video.
166
14. The Video View
Overview
Your task is to use the view model in the Video action and render a view that displays a
course-image, video duration, title, and description as a separate column, on a new row,
below the Back to Course button at the top of the view. Add a second column to the right
of the video player column. The upper part should contain the _VideoComingUpPartial
partial view, and the lower part the _InstructorBioPartial partial view.
Here, the <video> element shows the video, but you can use any HTML5 video player you
like that can play YouTube videos.
167
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
168
14. The Video View
Then, you will add a button that navigates to the Course view. Place the video player below
the button, along with information about the video. To the right of the video player, in a
separate column, the Coming Up section, with the Previous and Next buttons, will be
displayed. Display the instructor’s bio below that section. You will create two partial views
for the video player and the Coming Up section called _VideoPlayerPartial and _Video-
ComingUpPartial. Reuse the _InstructorBioPartial partial view to display the instructor’s
bio. Style the three areas with Bootstrap and CSS.
169
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
5. Close the Video view and open it again to get rid of any errors.
6. Add an @model directive for the VideoViewModel class at the beginning of the
view.
@model VideoViewModel
7. Save all the files.
8. Start the application without debugging (Ctrl+F5). Click on one of the courses in
the Dashboard view and then on one of the video links in the Course view.
Display the text Video in the browser if the Video view renders correctly.
@{
ViewData["Title"] = "Video";
}
<h1>Video</h1>
Adding the Back to Course Button
Now, add a button that takes the user back to the video’s Course view.
170
14. The Video View
1. Open the Course view and copy the first <div>, which contains the Back to
Dashboard button. Switch back to the Video view and replace the <h1> heading
with it.
<div class="row small-bottom-margin">
<div class="col-sm-12">
<a class="btn btn-primary no-border-radius no-left-padding"
asp-action="Dashboard">
<i class="material-icons vertical-align no-left-margin">
keyboard_arrow_left</i>
<span class="vertical-align">Back to Dashboard</span>
</a>
</div>
</div>
2. Change the name of the action to Course in the asp-action attribute; this will
make the <a> element call the Course action in the Membership controller.
3. To send the video’s course id to the action method, you need to add an asp-
route-id parameter and assign it the course id from the model.
<a class="btn btn-primary no-border-radius no-left-padding"
asp-action="Course" asp-route-id="@Model.Course.CourseId">
4. Replace the text Dashboard in the <span> with the course title from the model’s
Course.CourseTitle property.
<span class="vertical-align">
Back to @Model.Course.CourseTitle</span>
5. Start the application without debugging (Ctrl+F5). Click on a course button in the
Dashboard view, and then on a video link in the Course view.
6. The browser should display a blue button with the text Back to xxx at the top of
the page. Click the button to get back to the Course View.
7. Click on a video link to get back to the Video view.
@{
ViewData["Title"] = "Video";
}
171
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<div class="col-sm-3">
@*Place the Coming Up and Instructor Bio sections here*@
</div>
</div>
3. Save the file.
172
14. The Video View
173
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Open the Video view and render a partial view named _VideoPlayerPartial
inside the <div> decorated with the col-sm-9 Bootstrap class. Only render the
partial view if the Model, Video, LessonInfo, and Course objects contain data.
@if (Model != null && Model.Video != null &&
Model.LessonInfo != null && Model.Course != null)
{
<partial name="_VideoPlayerPartial" model="@Model" />
}
2. Add a partial view called _VideoPlayerPartial to the Views/membership folder.
3. Delete all code in the view and save it.
4. Close and open the view to get rid of any errors.
5. Add an @model directive to the VideoViewModel class. The view needs this
view model to display all the information; the data is stored in several objects in
the model.
@model VideoViewModel
6. Add a <div> decorated with the card Bootstrap class below the @model
directive. Add the no-border-radius class to give it square corners.
<div class="card no-border-radius">
174
14. The Video View
</div>
7. Add an if-block inside the card <div> that checks that the Video.Url property in
the view model isn’t null.
@if (Model.Video.Url != null)
{
}
8. Add a <div> element decorated with a class named video-player inside the if-
block. This element will act as a container for the <iframe> so that the video can
be viewed in 16:9 in full width.
<div class="video-player"></div>
9. Add an <iframe> inside the previous <div> and assign the model’s video URL to
its src attribute. You can copy an embed link from YouTube as a starting point
and replace the current Url for the videos in the database in case they don’t
work. Make sure that you remove the settings for width and height. You will
style the <iframe> and its container in the membership.css file later.
<iframe src="@Model.Video.Url" frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope;
picture-in-picture" allowfullscreen></iframe>
10. Add a <div> decorated with the card-body Bootstrap class below the if-block.
<div class="card-body">
</div>
11. Add an <h2> element for the Video.Title property from the view model inside
the previous <div>.
<h2>@Model.Video.Title</h2>
12. Add a <p> element for the lesson information; decorate it with the text-muted
CSS class to make the text a muted light gray. Add a video icon, display the
video’s position and the number of videos in the module, a watch icon, and the
video length followed by the text minutes. Use the LessonInfo.LessonNumber
and LessonInfo.NumberOfLessons properties to display the video’s position and
the number of videos. Use the Video.Duration property to display how long the
video is. Add the previously defined vertical-align and text-small classes to align
and change size of the icons.
<p class="text-muted">
<i class="material-icons vertical-align text-small">movie</i>
175
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Lesson @Model.LessonInfo.LessonNumber/
@Model.LessonInfo.NumberOfLessons
<i class="material-icons vertical-align text-small">alarm</i>
@Model.Video.Duration minutes
</p>
13. Add a <div> decorated with a class named video-course below the <p> element;
this class will be used as a selector to style its content. This is the container for
the video thumbnail and the video title.
<div class="video-course">
</div>
14. Add an <img> element inside the previous <div>. Assign the value form the
Course.CourseImageUrl property to the src attribute.
<img src="@Model.Course.CourseImageUrl">
15. Add a <span> element decorated with the vertical-align and text-large CSS
classes below the <img> element. Add the Course.CourseTitle property from the
view model to the <span>.
<span class="vertical-align text-large">
@Model.Course.CourseTitle</span>
16. Add a horizontal line below the card-body <div> and remove its margins.
<hr class="no-margin">
17. Add a <div> decorated with the card-body Bootstrap class below the <hr>
element. Add the Video.Description property from the view model to it.
<div class="card-body">
@Model.Video.Description
</div>
18. Save all the files and navigate to a video in the browser. You should see the video
information and the video loaded in the player.
176
14. The Video View
<div class="video-player">
<iframe src="@Model.Video.Url" frameborder="0"
allow="accelerometer; autoplay; encrypted-media;
gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
}
<div class="card-body">
<h2>@Model.Video.Title</h2>
<p class="text-muted">
<i class="material-icons vertical-align text-small">movie</i>
Lesson @Model.LessonInfo.LessonNumber/
@Model.LessonInfo.NumberOfLessons
<i class="material-icons vertical-align text-small">alarm</i>
@Model.Video.Duration minutes
</p>
<div class="video-course">
<img src="@Model.Course.CourseImageUrl">
<span class="vertical-align text-large">
@Model.Course.CourseTitle</span>
</div>
</div>
<hr class="no-margin">
<div class="card-body">
@Model.Video.Description
</div>
</div>
Let’s start by styling the video player and its container. Add a selector named video-
player to style the container; give it relative positioning and 56.25% top padding. These
settings are essential to scale the <iframe> to 16:9 full width; the settings need to be set
with the !important keyword to override the default settings for the video player.
.video-player {
position: relative !important;
padding-top: 56.25% !important;
}
Next, let’s style the size and position of the <iframe> itself. It must be positioned
absolutely to place it at the top left corner of its container. Assign 100% width and height
to make the player cover the entire width and height of its container.
177
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
.video-player iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
5. The first module should have at least three videos so that you can use the
Previous and Next buttons properly when you test the Coming Up section of the
Video view.
178
14. The Video View
179
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
180
14. The Video View
</div>
7. Display a thumbnail for the current video, in the card, if the NextVideoId
property is 0. Otherwise, display the thumbnail for the next video. Use the
CurrentVideoThumbnail and NextVideoThumbnail properties from the view
model to display the correct image.
@if (Model.NextVideoId == 0)
{
<img src="@Model.CurrentVideoThumbnail"
class="img-responsive">
}
else
{
<img src="@Model.NextVideoThumbnail" class="img-responsive">
}
8. Add a <div> decorated with the card-body Bootstrap class below the previous
if/else-blocks. This is the container for the Coming Up information.
<div class="card-body">
</div>
9. Add a <p> element with the text COURSE COMPLETED and an <h5> element for
the CurrentVideoTitle property from the view model in the card-body <div> if
the NextVideoId property is 0. Otherwise, add a <p> element with the text
COMING UP and an <h5> element for the NextVideoTitle property.
@if (Model.NextVideoId == 0)
{
<p>COURSE COMPLETED</p>
<h5>@Model.CurrentVideoTitle</h5>
}
else
{
<p>COMING UP</p>
<h5>@Model.NextVideoTitle</h5>
}
10. Add a <div> element for the Previous and Next buttons below the previous
if/else-block inside the card-body <div>. Decorate it with the btn-group
Bootstrap class and add the role attribute set to group.
<div class="btn-group" role="group">
</div>
181
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
11. Add an if-block checking if the PreviousVideoId property in the view model is 0
inside the <div> element decorated with the btn-group Bootstrap class; if it is,
then disable the Previous button. Use the PreviousVideoId in the <a> element’s
href attribute to target the correct video.
@if (Model.PreviousVideoId == 0)
{
<a class="btn" disabled>Previous</a>
}
else
{
<a class="btn btn-default"
href="~/Membership/Video/@Model.PreviousVideoId">
Previous
</a>
}
12. Add an if-block checking if the NextVideoId property in the view model is 0
below the previous if/else-block inside the btn-group <div>; if it is, then disable
the Next button. Use the NextVideoId in the <a> element’s href attribute to
target the correct video.
@if (Model.NextVideoId == 0)
{
<a class="btn" disabled>Next</a>
}
else
{
<a class="btn btn-default"
href="~/Membership/Video/@Model.NextVideoId">Next</a>
}
13. Open the Video view.
14. Render the _VideoComingUpPartial partial view inside the <div> decorated with
the col-sm-3 Bootstrap class. Pass in the LessonInfo object from the view model
to the partial view. Surround the <partial> Tag Helper if-block that checks that
the view model and the LessonInfo object are not null.
<div class="col-sm-3">
@if (Model != null && Model.LessonInfo != null)
{
<partial name="_VideoComingUpPartial"
model="@Model.LessonInfo" />
}
182
14. The Video View
</div>
15. Save all the files and navigate to a video in the browser. You should see the
Coming Up section beside the video.
<div class="card-body">
@if (Model.NextVideoId == 0)
{
<p>COURSE COMPLETED</p>
<h5>@Model.CurrentVideoTitle</h5>
}
else
{
<p>COMING UP</p>
<h5>@Model.NextVideoTitle</h5>
}
183
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
@if (Model.NextVideoId == 0)
{
<a class="btn" disabled>Next</a>
}
else
{
<a class="btn btn-primary" href="~/Membership/Video/
@Model.NextVideoId">Next</a>
}
</div>
</div>
</div>
}
Make each of the buttons take up 50% of the button group’s width and remove their
rounded corners.
.coming-up .btn-group .btn {
width: 50%;
border-radius:0;
}
Change the mouse pointer to show that the button is disabled when the disabled
attribute is applied, and the mouse pointer is hovering over the button.
.coming-up .btn-group [disabled]:hover {
cursor: not-allowed;
}
184
14. The Video View
Scale the thumbnail for the next video to fit the container.
.coming-up img {
width: 100%;
height: auto;
}
185
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<partial name="_InstructorBioPartial"
model="@Model.Instructor" />
}
3. Save the file and refresh the Video view in the browser. The
_InstructorBioPartial partial view should be displayed below the
_VideoComingUpPartial partial view.
@{
ViewData["Title"] = "Video";
}
<div class="row">
<div class="col-sm-9">
@*Place the video player here*@
@if (Model != null && Model.Video != null &&
Model.LessonInfo != null && Model.Course != null)
{
<partial name="_VideoPlayerPartial" model="@Model" />
}
</div>
<div class="col-sm-3">
@*Place the Coming Up and Instructor Bio sections here*@
@if (Model != null && Model.LessonInfo != null)
{
<partial name="_VideoComingUpPartial"
model="@Model.LessonInfo" />
186
14. The Video View
Summary
In this chapter, you added the Video view and its partial views.
In the next part of the book, you will begin building the administrator UI with Razor Pages.
187
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
188
Part 2:
Razor Pages
How to Build the Administrator Website
15. Adding the Admin Project
You will be using the Web Application project template when creating the Admin project;
this is a new template that wasn’t available in ASP.NET Core 1.1. It makes it possible to
create a lightweight application using Razor Pages instead of creating a full-fledged MVC
application with models, views, and controllers.
A good candidate for this type of application is a small company web page that contains a
few pages of data with a navigation menu and maybe a few forms that the visitor can fill
out.
Even though you are working with Razor Pages (and not views), they are still part of the
same MVC framework, which means that you don’t need to learn a whole new framework
to create Razor Pages if you already know MVC.
Although it’s possible to contain all code, C#, Razor syntax, and HTML in the same file, this
is not the recommended practice. Instead, you create two files, one .cshtml.cs code-
behind C# file and one .cshtml file for HTML and Razor syntax. The Solution Explorer
displays the two files as one node inside the Pages folder.
The Razor Page looks and behaves much like a regular view; the difference is that it has a
code-behind file that sort of acts as the page’s model and controller in one.
One easy way to determine if a .cshtml file is a page (and not a view) is to look for the
@page directive, which should be present in all Razor Pages.
Just like views, the Razor Pages have a _Layout view for shared HTML and imported
JavaScripts and CSS style sheets. They also have a _ViewImports view with using-
statements, namespaces, and Tag Helpers that are available in all pages.
191
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The template will install the basic Razor Pages plumbing and an Account folder for the
account Razor Pages that handles logging in and out from the Admin application. In prev-
ious versions of ASP.NET Core, MVC controllers implemented the account functionality;
from ASP.NET Core 2.2 both MVC and Razor Page applications use the account Razor
Pages; the project template hides the pages by default, but they can be scaffolded if
needed.
Because the project template adds a lot of files that the Admin application doesn’t use,
you will delete them before beginning the implementation. Most of the files handle
database migrations, which you already have added to the VOD.Common project.
192
15. Adding the Admin Project
12. Add a reference to the Common and Database projects by right clicking on the
Dependencies node and selecting Add-Reference.
13. Delete the Data folder and all its content. This folder contains database-related
files that already exist in the Common project.
14. Open the _Layout view and add a link to the Material Icons library; you can find
the icons here: https://material.io/tools/icons/?style=baseline.
<link rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons">
15. Open the _ViewImports view and replace the VOD.Admin.Data using statement
with VOD.Common.Entities.
16. Build the project and fix the errors. Remove any faulty using statements and
replace ApplicationDbContext with VODContext in the ConfigureServices
method in the Startup class.
17. In the _Layout view, comment out or delete the <partial> element that displays
the cookie consent partial view.
@*<partial name="_CookieConsentPartial" />*@
18. In the Startup class, replace the IdentityUser with VODUser and add a call to the
AddRoles service after the call to the AddDefaultIdentity method in the
ConfigureServices method to enable role-based security.
services.AddDefaultIdentity<VODUser>()
.AddRoles<IdentityRole>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<VODContext>();
19. Expand the Areas folder and right click on the Identity folder, then Select Add-
New Scaffolded Item.
193
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
20. Select Identity in the dialog’s left menu and select the Identity template in the
middle of the dialog, then click the Add button.
21. Check the Account\Login and Account\Register checkboxes, and VODContext in
the Data context class drop-down.
22. Click the Add button; this should create a new folder called Account in the
Areas-Identity-Pages folder. The folder should contain the Login and Register
Razor Pages. If an error occurs saying that no object reference is available, then
close Visual Studio, open the solution, and try again.
23. Open the Startup class and locate the ConfigureServices method.
24. Change the default IdentityUser class defined for the AddDefaultIdentity service
method to the VODUser class in the Common project. You need to resolve the
VOD.Common.Entities namespace.
25. Open the _LoginPartial view and replace all occurrences of the IdentityUser
class with the VODUser class from the Common project.
26. Comment out the code for cookie policy options in the ConfigureServices
method.
//services.Configure<CookiePolicyOptions>(options =>
//{
// options.CheckConsentNeeded = context => true;
// options.MinimumSameSitePolicy = SameSiteMode.None;
//});
27. Comment out the call to the UseCookiePolicy method in the Configure method.
//app.UseCookiePolicy();
28. Save all files. Right click on the Admin project in the Solution Explorer and select
Set as StartUp Project and then press F5 on the keyboard to run the application.
Log in as one of the users you have previously added, and then log out.
194
15. Adding the Admin Project
195
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
196
15. Adding the Admin Project
// options.MinimumSameSitePolicy = SameSiteMode.None;
//});
services.AddDbContext<VODContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<VODUser>()
.AddRoles<IdentityRole>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<VODContext>();
services.AddMvc().SetCompatibilityVersion(
CompatibilityVersion.Version_2_2);
}
app.UseHttpsRedirection();
app.UseStaticFiles();
//app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc();
}
Summary
In this chapter, you created the VOD.Admin project that will enable administrators to
create, update, delete, add, and view data that will be available to regular users visiting
the VOD.UI website.
197
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Next, you will start building a user interface for administrators by adding a dashboard and
an Admin menu that the visitor uses when navigating to the Razor Pages that will manipu-
late the data in the database.
198
16. The Administrator Dashboard
You will create the dashboard using two partial views, one for the dashboard called
_DashboardPartial and one for its items (cards) called _CardPartial. Render the dash-
board’s partial view in the main Index page located in the Pages folder and restrict it to
logged in users. Render the partial views with the <partial> Tag Helper.
You will also remove the Policy link and change the text to Dashboard for the Home link.
Then you will move the menu into a partial view named _MenuPartial and render it from
the _Layout view and restrict it to logged in users that belong to the Admin role.
199
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Then you will create a partial view named _MenuPartial to which you will move the re-
maining menu and then reference it from the _Layout partial view with the <partial> Tag
Helper.
1. Open the _Layout partial view in the Admin project’s Pages-Shared folder.
2. Delete the <li> elements that contain the Privacy <a> element.
3. Change the text to Dashboard for the Home <li> element.
4. Right click on the Pages-Shared folder and select Add-New Item.
5. Add a Razor View named _MenuPartial and remove all code in it. You use this
template because a C# code-behind file is unnecessary. Delete all code in the
new Razor view.
6. Open the _Layout partial view and cut out the <ul> element containing the
Dashboard <li> element and paste it into the _MenuPartial partial view.
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area=""
asp-page="/Index">Dashboard</a>
</li>
</ul>
7. Open the _Layout view and use the <Partial> element to render the
_MenuPartial view below <Partial> element that renders the _LoginPartial view.
<partial name="_MenuPartial" />
8. Run the application. Log out if you are logged in. The navigation menu should
now only have the Dashboard, Register, and Login links.
9. Now, you will modify the menu to be displayed only when logged in.
10. You need to inject the SignInManager for the current user at the top of the
_MenuPartial view to be able to check if the user is logged in.
@inject SignInManager<VODUser> SignInManager
11. Surround the <ul> element with an if-block that uses the SignInManager’s
IsSignedIn method to check that the user is logged in. Also, call the IsInRole
method on the User entity to check that the user belongs to the Admin role.
@if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin")) { }
200
16. The Administrator Dashboard
The first step is to add a method to the DbReadService in the Database project that
returns the necessary statistics for the dashboard from the database.
The second step is to create a partial view called _CardPartial that will be used to render
the dashboard items (cards).
201
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The third step is to modify the Index view and its code-behind file to receive data from the
database through the DbReadService.
Use the Count method on the entities in the method to return their number of records.
202
16. The Administrator Dashboard
Task<List<TEntity>> GetAsync<TEntity>(Expression<Func<TEntity,
bool>> expression) where TEntity : class;
Task<TEntity> SingleAsync<TEntity>(Expression<Func<TEntity, bool>>
expression) where TEntity : class;
Task<bool> AnyAsync<TEntity>(Expression<Func<TEntity, bool>>
expression) where TEntity : class;
Implement the view model with a class called CardViewModel in a folder called Models.
203
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The partial view will use the CardViewModel as a model to render the data from the
database.
204
16. The Administrator Dashboard
9. Add a <p> element for the model’s Description property below the <h3>
element.
10. Add a <span> element below the innermost <div> element for the icon; use the
model’s Icon property to define which icon to display.
<span class="material-icons card-icon">@Model.Icon.ToLower()
</span>
1. Open the C# file for the Index Razor Page in the Pages folder.
2. Add a tuple variable called Cards to the class. It should contain parameters of the
CardViewModel class for each value returned from the Count method in the
DbReadService class.
public (CardViewModel Instructors, CardViewModel Users,
CardViewModel Courses, CardViewModel Modules,
CardViewModel Videos, CardViewModel Downloads) Cards;
3. Use DI to inject the IDbReadService interface from the Database project into the
IndexModel constructor and name it db. Store the service instance in a private
205
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
variable named _db on class-level; this will give access to the database
throughout the file.
private readonly IDbReadService _db;
206
16. The Administrator Dashboard
Cards = (
Instructors: new CardViewModel
{
BackgroundColor = "#9c27b0",
Count = instructors,
Description = "Instructors",
Icon = "person",
Url = "./Instructors/Index"
},
Users: new CardViewModel
{
BackgroundColor = "#414141",
Count = users,
Description = "Users",
Icon = "people",
Url = "./Users/Index"
},
Courses: new CardViewModel
{
BackgroundColor = "#009688",
Count = courses,
Description = "Courses",
Icon = "subscriptions",
Url = "./Courses/Index"
},
Modules: new CardViewModel
{
BackgroundColor = "#f44336",
Count = modules,
Description = "Modules",
Icon = "list",
Url = "./Modules/Index"
},
Videos: new CardViewModel
{
BackgroundColor = "#3f51b5",
Count = videos,
Description = "Videos",
Icon = "theaters",
Url = "./Videos/Index"
},
Downloads: new CardViewModel
{
207
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
BackgroundColor = "#ffcc00",
Count = downloads,
Description = "Downloads",
Icon = "import_contacts",
Url = "./Downloads/Index"
}
);
}
}
208
16. The Administrator Dashboard
I suggest that you refresh the browser after each change you make to the dashboard.css
file.
Add a selector for the card class. Add a margin of 3.3rem; rem is relative to the font size
of the root element. Float the card to the left and set its width to 24% of its container
and its minimal width to 200px. Make the text color white and remove any text
decoration from the links (the underlining). Remove the border-radius to give the cards
square corners.
a.card {
margin: .33rem;
float: left;
width: 24%;
min-width: 200px;
color: #fff;
text-decoration: none;
border-radius: 0px;
}
209
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Next, add a hover effect to the card by adding the :hover selector to a new card selector.
Dial down the opacity to 80%; this makes it look like the color changes when the mouse
pointer is hovering over the card.
a.card:hover {
opacity: .8;
}
Add 15px padding between the card’s border and its content.
a .card-content {
padding: 15px;
}
Remove the top and bottom margin for the <h3> element and the bottom margin for the
<p> element.
a .card-data h3 {
margin-top: 0;
margin-bottom: 0;
}
a .card-data p {
margin-bottom: 0;
}
Style the icon with absolute positioning inside its container. Place the icon 25px from the
right side and 30% from the top. Change the font size to 35px and its opacity to 0.2.
a .card-icon {
position: absolute;
right: 25px;
top: 22%;
font-size: 50px;
opacity: .2;
}
210
16. The Administrator Dashboard
You will also restrict access to the dashboard to logged in users belonging to the Admin
role.
1. Add a <br /> element above the <partial> element in the Index page.
2. Add a <div> element below the <br /> element and decorate it with the row
Bootstrap class to create a new row.
<div class="row"></div>
3. Add another <div> inside the previous <div> and decorate it with column and
offset Bootstrap classes for small and medium device sizes.
<div class="offset-sm-2 col-sm-8 offset-md-4 col-md-6"></div>
4. Move the <partial> element inside the column <div> and copy it.
5. Paste in the copied code and change the model from Instructors to Users.
<partial name="_CardPartial" for="Cards.Instructors" />
<partial name="_CardPartial" for="Cards.Users" />
6. Run the application. Two cards should be displayed, one for instructors and one
for users. Clicking on them will display an error page because you must add the
necessary Index Razor Pages first.
7. Copy the <div> element decorated with the row class and all its content. Paste it
in two more times so that you end up with three rows below one another.
8. Change the model parameter for the cards to reflect the remaining pages:
Courses, Modules, Videos, and Downloads.
9. Run the application. All six entity cards should be displayed.
10. Use the @inject directive in the Index Razor Page to Inject the SignInManager
for the User entity below the using statements; this will give access to the
IsSignedIn method in the SignInManager that checks if a user is logged in.
@inject SignInManager<VODUser> SignInManager
11. Add an if-block around the HTML below the <br /> element. Call the IsSignedIn
method and pass in the User entity to it and check that the user belongs to the
Admin role by calling the IsInRole method on the User entity.
@if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin"))
211
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
}
12. Run the application and log out; the dashboard shouldn’t be visible. Log in as a
regular user; the dashboard shouldn’t be visible.
13. Log in as an Admin user for the dashboard to be visible.
<br />
<div class="row">
<div class="offset-sm-2 col-sm-8 offset-md-4 col-md-6">
<partial name="_CardPartial" for="Cards.Courses" />
<partial name="_CardPartial" for="Cards.Modules" />
</div>
</div>
<div class="row">
<div class="offset-sm-2 col-sm-8 offset-md-4 col-md-6">
<partial name="_CardPartial" for="Cards.Videos" />
<partial name="_CardPartial" for="Cards.Downloads" />
</div>
</div>
}
212
16. The Administrator Dashboard
Summary
In this chapter, you added a dashboard for administrators. The cards (items) displayed in
the dashboard were added as links to enable navigation to the other Index Razor Pages
you will add in upcoming chapters.
In the next chapter, you will add a menu with the same links as the dashboard cards.
213
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
214
17. The Admin Menu
You will create the menu in a partial view called _AdminMenuPartial that is rendered in
the _Layout view, using the <partial> Tag Helper.
Overview
Your task is to create a menu for all the Index Razor Pages, in a partial view named
_AdminMenuPartial, and then render it from the _Layout view.
215
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
3. Use the @inject directive to inject the SignInManager for the VODUser entity.
This will give access to the IsSignedIn method in the SignInManager that checks if
a user is logged in.
@inject SignInManager<VODUser> SignInManager
4. Add an if-block that checks if the user is signed in and belongs to the Admin role.
@if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin")) { }
216
17. The Admin Menu
5. You can find the markup for the menu on Bootstrap’s website (version 4.3). Add
an <li> element inside the if-block and decorate it with the dropdown and nav-
item Bootstrap class. This will be the container for the button that opens the
menu and the menu itself.
<li class="nav-item dropdown">
</li>
6. Add an <a> element that will act as the link in the navigation bar.
<a class="nav-link dropdown-toggle" data-toggle="dropdown"
href="#" role="button" aria-haspopup="true" aria-
expanded="false">Admin</a>
7. Create the drop-down menu section of the menu by adding a <ul> element
decorated with the drop-down-menu Bootstrap class, below the <a> element.
<ul class="dropdown-menu">
</ul>
8. Add an <a> element for each of the Index Razor Pages that you create. You can
figure out all the folder names by looking at the entity class names; a folder
should have the same name as the entity property in the VODContext class in
the Database project. The URL path in the href attribute on the <a> element
should contain the page folder followed by /Index. Also, add a suitable
description in the <a> element.
<a class="dropdown-item" href="/Instructors/Index">Instructor</a>
9. Open the _ManuPartial view and use the <partial> element to render the
_AdminMenuPartial partial view. Place the <partial> element below the <li>
element for the Dashboard link.
<partial name="_AdminMenuPartial" />
10. Save all the files and run the application (F5) and make sure that you are logged
in as an administrator. Click the Admin menu to open it. Clicking any of the menu
items will display an empty page because you haven’t added the necessary Index
Razor Pages yet.
217
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Summary
In this chapter, you added the Admin menu and targeted the Index Razor Pages that you
will add throughout this part of the book.
In the next chapter, you will create a custom Tag Helper that will render the buttons you
will add to the Razor Pages.
218
18. Custom Button Tag Helper
A C# class builds the Tag Helper’s HTML element with C# code. Then you insert the Tag
Helper into the views and pages as an HTML markup-like element.
The class must inherit from the TagHelper class, or another class that inherits that class,
and implement a method called Process, which creates or modifies the HTML element.
Tag Helper attributes can be added as properties in the class, or dynamically to a
collection, by adding them to the HTML Tag Helper element.
Overview
Your task is to create a custom Tag Helper called btn. You’ll inherit the AnchorTagHelper
class to reuse the anchor tag’s functionality, such as attributes. You will add a new string
property called Icon that can be used to specify an icon to be displayed left of the button’s
description. Here, we use Google’s Material Icons library, but you could use any icon
library of your choosing.
219
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
You should be able to configure the following with the Tag Helper: anything that you can
configure for an anchor tag plus an icon to be displayed. Apply the default styles (btn-sm
and btn-default); you can override them by adding other Bootstrap btn classes.
You can use the HtmlTargetElement attribute to limit the scope of the Tag Helper to a
specific HTML element type.
[HtmlTargetElement("my-tag-helper")]
public class MyTagHelperTagHelper : TagHelper
The Tag Helper will produce an <a> element styled as a Bootstrap button.
To make the Tag Helper available in Razor Pages, you need to add an @addTagHelper
directive that includes all Tag Helpers in the project assembly, to the _ViewImports view.
@addTagHelper *, VOD.Admin
When you add the Tag Helper to the view, it’s very important that you use a closing tag;
otherwise, the Tag Helper won’t work.
<btn></btn>
220
18. Custom Button Tag Helper
4. Inherit from the AnchorTagHelper class instead of the default TagHelper class to
inherit the anchor tag’s functionality; this saves you the trouble of implementing
it in your class.
[HtmlTargetElement("tag-name")]
public class PageButtonTagHelper : AnchorTagHelper
{
public override void Process(TagHelperContext context,
TagHelperOutput output) { }
}
5. Add a constructor that gets the IHtmlGenerator service injected and add it to
the base class; this is a must because the AnchorTagHelper class demands such a
constructor.
public BtnTagHelper(IHtmlGenerator generator) : base(generator)
{
}
6. Change the HtmlTargetElement attribute to btn; this will be the Tag Helper’s
“element” name; you use <btn></btn> to add the Tag Helper to the Razor page,
and when rendered, it will turn into an anchor tag. It’s not a real HTML element,
but it looks like one, to blend in with the HTML markup. It will, however,
generate a real HTML anchor tag element when rendered.
[HtmlTargetElement("btn")]
7. Because the AnchorTagHelper class doesn’t have an Icon property to specify an
icon, you need to add it to the class.
a. Icon (string): The name of the icon from Google’s Material Icons to
display on the button. Don’t add this attribute to the HTML element if
you want a button without an icon. It should have an empty string as a
default value. You can find icon names here:
https://material.io/tools/icons/?style=baseline.
public string Icon { get; set; } = string.Empty;
8. Add constants for the different button styles that should be available and the
icon provider.
const string btnPrimary = "btn-primary";
const string btnDanger = "btn-danger";
const string btnDefault = "btn-default";
const string btnInfo = "btn-info";
221
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
11. Call the Process method in the base class that builds the output anchor element
at the end of the method.
base.Process(context, output);
12. Add a <btn> Tag Helper to the Index page on the Pages folder. Remember that
from ASP.NET Core 2.2 the asp-page attribute will result in an empty href output
element if the referenced page doesn’t exist. To test the Tag Helper, you can
reference the Error page in the Pages folder to get a non-empty href value.
<btn asp-page="Error">Error</btn>
13. Start the application (Ctrl+F5) and hover over the link to ensure that the href
contains the specified page. Open the developer tools window (F12) to see that
the <btn> Tag Helper rendered as an anchor tag.
<a href="/Error">Error</a>
222
18. Custom Button Tag Helper
#region Constants
const string btnPrimary = "btn-primary";
const string btnDanger = "btn-danger";
const string btnDefault = "btn-default";
const string btnInfo = "btn-info";
const string btnSucess = "btn-success";
const string btnWarning = "btn-warning";
// Google's Material Icons provider name
const string iconProvider = "material-icons";
#endregion
base.Process(context, output);
}
}
1. Begin by fetching the asp-page attribute if it exists. You will use its value to
determine which Bootstrap button type should be applied using the previously
added constants.
223
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
2. Now, fetch the class attribute if it exists. You will append the Bootstrap classes
to its current value.
var classAttribute = context.AllAttributes.SingleOrDefault(p =>
p.Name.ToLower().Equals("class"));
3. Add a string variable called buttonStyle and add the value from the btnDefault
constant to it. This variable will be assigned a value based on the value of the
asp-page value.
var buttonStyle = btnDefault;
4. Add an if-block that checks if the asp-page attribute exists.
if (aspPageAttribute != null) {}
5. Fetch the value of the asp-page attribute inside the if-block.
var pageValue = aspPageAttribute.Value.ToString().ToLower();
6. Assign the Bootstrap button type to the buttonType variable you added earlier
based on the page value inside the if-block. The error value is only added to test
the button and can be removed when the <btn> Tag Helper is complete.
buttonStyle =
pageValue.Equals("create") ? btnPrimary :
pageValue.Equals("delete") ? btnDanger :
pageValue.Equals("edit") ? btnSucess :
pageValue.Equals("index") ? btnPrimary :
pageValue.Equals("details") ? btnInfo :
pageValue.Equals("/index") ? btnWarning :
pageValue.Equals("error") ? btnDanger :
btnDefault;
7. Below the if-block, use the buttonStyle variable when adding Bootstrap classes
to a variable called bootstrapClasses.
var bootstrapClasses = $"btn-sm {buttonStyle}";
8. Add an if-else-block that checks if the class attribute exists.
if (classAttribute != null) {}
else {}
9. Inside the if-block, fetch the value from the class attribute and store it in a
variable called css.
var css = classAttribute.Value.ToString();
224
18. Custom Button Tag Helper
10. Below the css variable, add another if-block that checks that the class attribute
doesn’t already contain Bootstrap button style classes; if it does, then skip the
code in the if-block.
if (!css.ToLower().Contains("btn-")) {}
11. Inside the previous if-block, remove the class attribute from the element and
rebuild the class attribute and its content with the added Bootstrap classes.
output.Attributes.Remove(classAttribute);
classAttribute = new TagHelperAttribute(
"class", $"{css} {bootstrapClasses}");
output.Attributes.Add(classAttribute);
12. Inside the else-block, you add the class attribute to the element directly because
it doesn’t have that attribute.
output.Attributes.Add("class", bootstrapClasses);
13. Run the application (F5) and make sure that the element is rendered as a red
button with the text Error.
14. Stop the application.
if (aspPageAttribute != null)
{
225
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (classAttribute != null)
{
var css = classAttribute.Value.ToString();
if (!css.ToLower().Contains("btn-"))
{
output.Attributes.Remove(classAttribute);
classAttribute = new TagHelperAttribute("class",
$"{css} {bootstrapClasses}");
output.Attributes.Add(classAttribute);
}
}
else
{
output.Attributes.Add("class", bootstrapClasses);
}
#endregion
base.Process(context, output);
}
226
18. Custom Button Tag Helper
1. Above the base.Process method call, add an if-block that checks that the Icon
property has a value; an icon can only be added if the property has a valid icon
name.
if (!Icon.Equals(string.Empty)) {}
2. Inside the if-block, fetch the already existing content between the start and end
tags, if any, and store it in a variable called content. If there is content, then add
a space ( ) before the content.
var childContext = output.GetChildContentAsync().Result;
var content = childContext.GetContent().Trim();
if (content.Length > 0) content = $" {content}";
3. Add an <i> element in the if-block with the icon information from the
iconProvider constant and the Icon property before the content stored in the
content variable displayed in a <span> element to make it easier to style.
output.Content.SetHtmlContent($"<i class='{iconProvider}'
style='display: inline-flex; vertical-align: top; line-height:
inherit;font-size: medium;'>{Icon}</i> <span style='font-size:
medium;'>{content}</span>");
5. Run the application and make sure that the icon is displayed in front of the text
inside the button.
6. Stop the application.
7. To style the button, you need to fetch the style attribute from the Tag Helper
below the if-block and assign values to it or add it if it’s missing. Store the style in
a variable named style and the existing value in a variable named styleValue.
var style = context.AllAttributes.SingleOrDefault(s =>
s.Name.ToLower().Equals("style"));
var styleValue = style == null ? "" : style.Value;
8. Create a TagHelperAttribute instance and add the style attribute and its values
to it. Display the anchor tag as part of the flex content on the row where it is
displayed, remove the border radius to give it square corners, and remove the
text decoration (underlining).
var newStyle = new TagHelperAttribute("style",
227
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
$"{styleValue} display:inline-flex;border-radius:0px;
text-decoration: none;");
9. Append the new styling if the style attribute exists, otherwise add it.
if (style != null) output.Attributes.Remove(style);
output.Attributes.Add(newStyle);
10. Add another <btn> Tag Helper without any text below the previous one.
<btn asp-page="Error" icon="create"></btn>
11. Run the application. One of the buttons should display an icon and text, the
other one only an icon. Also, make sure that the buttons have square corners
and that no underlining is visible when hovering over the button. Remove the
two <btn> Tag Helpers from the Index view when you stop the application.
output.Content.SetHtmlContent($"<i class='{iconProvider}'
style='display: inline-flex; vertical-align: top;
line-height: inherit;font-size: medium;'>{Icon}</i>
<span style='font-size: medium;'>{content}</span>");
}
228
18. Custom Button Tag Helper
#region Constants
const string btnPrimary = "btn-primary";
const string btnDanger = "btn-danger";
const string btnDefault = "btn-default";
const string btnInfo = "btn-info";
const string btnSucess = "btn-success";
const string btnWarning = "btn-warning";
// Google's Material Icons provider name
const string iconProvider = "material-icons";
#endregion
229
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (classAttribute != null)
{
var css = classAttribute.Value.ToString();
if (!css.ToLower().Contains("btn-"))
{
output.Attributes.Remove(classAttribute);
classAttribute = new TagHelperAttribute(
"class", $"{css} {bootstrapClasses}");
output.Attributes.Add(classAttribute);
}
}
else
{
output.Attributes.Add("class", bootstrapClasses);
}
#endregion
#region Icon
if (!Icon.Equals(string.Empty))
{
var childContext = output.GetChildContentAsync().Result;
var content = childContext.GetContent().Trim();
if (content.Length > 0) content = $" {content}";
output.Content.SetHtmlContent($"<i class='{iconProvider}'
style='display: inline-flex; vertical-align: top;
line-height: inherit;font-size: medium;'>{Icon}</i>
230
18. Custom Button Tag Helper
#endregion
base.Process(context, output);
}
}
Summary
In this chapter, you implemented a custom button Tag Helper and tested it in a Razor Page.
You learned a dynamic way to find out what attributes have been added and read their
values.
The purpose of the Tag Helper you created is to replace links with Bootstrap-styled
buttons. You can, however, use Tag Helpers for so much more.
In the upcoming chapters, you will use the button Tag Helper to add buttons to the various
Razor Pages you will create for the Admin UI.
In the next chapter, you will add a new service for writing data to the database.
231
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
232
19. The Database Write Service
The Admin project doesn’t have an existing service for writing data and will, therefore,
use the DbWriteService service you will create in the Database project directly.
Overview
Your objective is to create a data service that adds, updates, and deletes data in the data-
base tables.
Implement the methods as generic methods that can handle any entity and therefore add
or modify data in any table in the database.
233
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
#region Constructor
public DbWriteService(VODContext db)
{
_db = db;
}
#endregion
}
234
19. The Database Write Service
memory; this is the only asynchronous method in the interface and class since you want
to be able to make many changes and persist them together; therefore, asynchronous calls
are unnecessary because no long-running tasks are involved.
The method should return a Boolean value specifying if the database call was successful.
235
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
return false;
}
}
Because the method adds a new generic entity, it must be passed in as a parameter with
the same type as the Add method’s defining entity.
Since the underlying data in the database hasn’t changed, there’s no point in returning a
value from the method; you only re-throw any exception that occurs.
Avoid calling the AddAsync method on the context, unless it’s a special value generator
(see a comment from Microsoft’s website below). Instead, call the Add method on the
context, which makes Entity Framework begin tracking the entity in-memory.
// This method is async only to allow special value generators
The Add context method will incur overhead since persisting the entity to the database
requires calling the SaveChangesAsync method; that call should be asynchronous since it
is a long-running task that could benefit from using the thread pool, freeing up resources.
public void Add<TEntity>(TEntity item) where TEntity : class
{
}
236
19. The Database Write Service
3. Add the Add method to the DbWriteService class, either manually or by using
the Quick Actions light bulb button. If you auto-generate the method with Quick
Actions, you must remove the throw statement.
4. Add a try/catch-block where the catch-block throws the exception to the calling
method.
5. Call the Add method on the _db context inside the try-block and pass in the item
to add from the method’s item parameter; you don’t have to specify the generic
TEntity type to access the table associated with the defining entity; the item
infers it.
_db.Add(item);
6. Save all files.
Since the method removes a generic entity, it must be passed in as a parameter with the
same type as the Delete method’s defining entity.
237
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Because there are no changes to the underlying database, there’s no point in returning a
value from the method; you only re-throw any exception that occurs.
The Remove context method will incur overhead because persisting the changes to the
database requires calling the SaveChangesAsync method; that call should be asyn-
chronous since it is a long-running task that could benefit from using the thread pool,
freeing up resources.
public void Delete<TEntity>(TEntity item) where TEntity : class { }
238
19. The Database Write Service
try
{
_db.Set<TEntity>().Remove(item);
}
catch
{
throw;
}
}
The Update method will update an entity that Entity Framework tracks in-memory. The
entity will not update in the database before calling the SaveChangesAsync method.
Since the method updates a generic entity, it must be passed in as a parameter with the
same type as the Update method’s defining entity.
Because the underlying data in the database is unmodified, there’s no point in returning a
value from the method; you only re-throw any exception that occurs.
The Update context method will incur overhead since the underlying data in the database
is unmodified until calling the SaveChangesAsync method; that call should be asyn-
chronous since it is a long-running task that could benefit from using the thread pool,
freeing up resources.
public void Update<TEntity>(TEntity item) where TEntity : class { }
239
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
6. Above the Update method, check if the entity is being tracked by Entity
Framework.
var entity = _db.Find<TEntity>(item.GetType().GetProperty("Id")
.GetValue(item));
7. Detach the entity if it is being tracked above the Update method; you do this to
be able to update the entity in the item parameter.
if (entity != null) _db.Entry(entity).State =
Microsoft.EntityFrameworkCore.EntityState.Detached;
8. Save all files.
_db.Set<TEntity>().Update(item);
}
catch
{
throw;
}
}
240
19. The Database Write Service
Summary
In this chapter, you created a service for writing data to the database. This service will be
used from the Admin project to manipulate data.
Next, you will add a user service that will be used from the Admin project to manage
users in the AspNetUsers table and their roles in the AspNetUserRoles table.
241
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
242
20. The User Service
Overview
Your objective is to create a data service that adds, updates, and deletes data in the
AspNetUsers and AspNetUserRoles database tables.
The methods will not be implemented as generic methods since they only will be used
with the AspNetUsers and AspNetUserRoles database tables.
243
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Add a new folder named Admin to the DTOModels folder in the Common
project.
2. Add a public class named ButtonDTO to the DTOModels-Admin folder in the
Common project; this will be the model class for the next partial view.
3. Add three public int properties named Id, CourseId, and ModuleId, and a string
property named UserId. They will hold the ids for the buttons in the partial view.
4. To make it easier to handle the string UserId and the int Id properties that will
be added to the same attribute when used (never at the same time), add
another string property named ItemId that returns either the value from the Id
property converted to a string or the UserId.
public string ItemId { get { return Id > 0 ? Id.ToString() :
UserId; } }
5. Add four constructors; the first with three int parameters for the course id,
module id, and id; the second with two int parameters for the course id and id;
the third with one int parameter for the id; the fourth with one string parameter
for the user id.
6. Save the class.
#region Constructors
public ButtonDTO(int courseId, int moduleId, int id)
{
244
20. The User Service
CourseId = courseId;
ModuleId = moduleId;
Id = id;
}
245
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
246
20. The User Service
_db = db;
_userManager = userManager;
}
7. Open the Startup class and add the UserService service to the ConfigureServices
method.
services.AddScoped<IUserService, UserService>();
8. Save the files.
247
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
248
20. The User Service
}
).ToListAsync();
}
249
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Task<IEnumerable<UserDTO>> GetUsersAsync();
Task<UserDTO> GetUserAsync(string userId);
}
250
20. The User Service
Decorate the Email and Password properties with the [Required] attribute.
251
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Also, format the text displayed for the Password property as a password with dots and
give it a maximum length of 100 characters.
Format the displayed text for the value in the ConfirmPassword property as a password
with dots and compare it with the value in the Password property with the [Compare]
attribute.
[Required]
252
20. The User Service
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage =
"The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
253
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
254
20. The User Service
The first thing the method should do is to fetch the user from the AspNetUsers table
matching the value of the Id property in the passed-in object. Store the user in a variable
called dbUser.
Then, the dbUser needs to be checked to make sure that it isn’t null and that the email in
the passed-in object isn’t an empty string. You could add more checks to see that the email
is a valid email address, but I leave that as an extra exercise for you to solve on your own.
Then you assign the email address from the passed-in user to the dbUser fetched from the
database to update it.
Next, you need to find out if the user in the database – matching the passed-in user id in
the user object – is an administrator. You do that by calling the IsInRoleAsync method on
the UserManager instance; save the result in a variable named isAdmin. To avoid
misspelling the name of the Admin role, you should add it to a variable named admin
above the IsInRoleAsync call and use the variable in the call.
If the value in the isAdmin variable is true and the value in the IsAdmin property in the
user object is false, then the admin role checkbox is unchecked in the UI; remove the role
from the AspNetUserRoles table by calling the RemoveFromRoleAsync method on the
UserManager instance.
If the value in the IsAdmin property in the user parameter is true and the value in the
isAdmin variable is false, then the admin role checkbox is checked in the UI; add the role
to the AspNetUserRoles table by awaiting a call to the AddToRoleAsync method on the
UserManager instance.
255
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Then await the result from the SaveChangesAsync method and return true if the data was
persisted to the database, otherwise return false.
256
20. The User Service
dbUser.Email = user.Email;
#region Admin Role
var admin = "Admin";
var isAdmin = await _userManager.IsInRoleAsync(dbUser, admin);
257
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
instance. The method should take a string parameter named userId representing the user
to remove.
Task<bool> DeleteUserAsync(string userId);
The first thing the method should do is to fetch the user from the AspNetUsers table
matching the value of the userId parameter. Store the user in a variable called dbUser.
Then check that the dbUser isn’t null to make sure that the user exists and return false if
it doesn’t exist.
Next, you remove the roles associated with the user id in the AspNetUserRoles table.
Fetch the roles by calling the GetRolesAsync method and remove them from the user by
calling the RemoveFromRolesAsync method on the _userManager instance.
Then, you remove the user from the AspNetUsers table by calling the DeleteAsync
method on the _userManager instance.
Return the result from the DeleteAsync method to signal if the user was removed or not.
258
20. The User Service
userRoles);
8. Remove the user from the AspNetUsers table.
var deleted = await _userManager.DeleteAsync(dbUser);
9. Return the result from the DeleteAsync method to signal if the user was
removed or not.
return deleted.Succeeded;
10. Save all files.
259
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
5. Return the string as is if it is shorter or of equal length to the value in the length
parameter.
if (value.Length <= length) return value;
6. Truncate the string and add three ellipses at the end to indicate that it is part of
a longer text.
return $"{value.Substring(0, length)} ...";
Let’s add the IsNullOrEmptyOrWhiteSpace method that checks if a string is empty, null,
or contains only whitespace.
260
20. The User Service
Let’s add the IsNullOrEmptyOrWhiteSpace method that checks if a string array contains
an empty, null, or whitespace value.
return false;
return false;
}
261
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
262
20. The User Service
8. Use the Password property if it contains a value, otherwise use the value in the
passwordHash property. Return null if either password isn’t equal to the
password hash stored in the AspNetUser table for the user.
if (loginUser.Password.Length > 0)
{
var password = _userManager.PasswordHasher
.VerifyHashedPassword(user, user.PasswordHash,
loginUser.Password);
if (password == PasswordVerificationResult.Failed)
return null;
}
else
{
if (!user.PasswordHash.Equals(loginUser.PasswordHash))
return null;
}
9. Add the claims to the VODUser’s Claims collection if the includeClaims
parameter is true.
if (includeClaims) user.Claims =
await _userManager.GetClaimsAsync(user);
10. Return the user.
return user;
11. Save all files.
263
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (loginUser.Password.Length > 0)
{
var password = _userManager.PasswordHasher
.VerifyHashedPassword(user, user.PasswordHash,
loginUser.Password);
if (password == PasswordVerificationResult.Failed)
return null;
}
else
{
if (!user.PasswordHash.Equals(loginUser.PasswordHash))
return null;
}
if (includeClaims) user.Claims =
await _userManager.GetClaimsAsync(user);
return user;
}
catch
{
throw;
}
}
Summary
In this chapter, you created a service for handling users and their roles in the AspNetUsers
and AspNetUserRoles database tables. This service will be used from the Admin project
to handle user data and assign administrator privileges to users.
264
20. The User Service
Next, you will begin adding the Razor Pages that make up the administrator UI.
265
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
266
21. The User Razor Pages
You will use the ViewData container to send data to the pages that display it in drop-
downs; this prevents the item indices to display as numbers in text boxes.
Overview
In this chapter, you will create the User Razor Pages; this enables the administrator to
display, add, update, and delete data in the AspNetUsers and the AspNetUserRoles tables.
In this scenario where the entity classes don’t link the two database tables, you use a view
model class called UserDTO to pass the data from the code-behind to the page with either
a property of type UserDTO or IEnumerable<UserDTO> declared directly in the code-
behind. Remember that the code-behind doubles as a model and controller.
All the PageModel classes in the Razor Page code-behinds need access to the IUserService
service in the Admin project to fetch and modify data in the AspNetUsers and
AspNetUserRoles tables in the database. The easiest way to achieve this is to add a
constructor to the class and use DI to inject the service into the class.
The code-behind file belonging to a Razor Page can be accessed by expanding the Razor
Page node and opening the nested .cshtml.cs file.
Each Razor Page comes with a predefined @page directive signifying that it is a Razor Page
and not an MVC view. It also has an @model directive defined that is linked directly to the
code-behind class; through this model, you can access the public properties that you add
to the class from the HTML in the page using Razor syntax.
267
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The code-behind class comes with an empty OnGet method that can be used to fetch data
for the Razor Page, much like an HttpGet action method in a controller.
You can also add an asynchronous OnPostAsync method that can be used to handle posts
from the client to the server; for instance, a form that is submitted. This method is like the
HttpPost action method in a controller.
As you can see, the code-behind class works kind of like a combined controller and model.
You can use controllers, if needed, to handle requests that don’t require a Razor Page,
such as logging in and out a user.
The [TempData] attribute can be shared between Razor Pages because it works on top of
session state. You will take advantage of this when sending a message from one Razor
Page to another, and display it using the <alert> Tag Helper that you will implement in the
next chapter.
You will prepare for the Tag Helper by adding a [TempData] property called Alert (string)
to the code-behind class for the Razor Pages you create. This property will hold the
message assigned in one of the Create, Edit, or Delete Razor Pages and display it in the
Tag Helper that you will add to the Index Razor Page. By adding the property to several
code-behind classes, the message can be changed as needed because the session state
only creates one backing property.
There will be five Razor Pages in the Users folder when you have added them all: Index,
Create, Edit, Details, and Delete.
268
21. The User Razor Pages
Inject the IUserService service into the IndexModel class’s constructor in the Razor Page
and stored it in a private field called _userService to be able to add, read, and modify user
data.
Then you need to inject the IUserService into the constructor that you will add to the class.
Store the service instance in a private class-level variable called _userService.
Then fetch all users with the GetUsersAsync method on the _userService object in the
OnGet method and store them in an IEnumerable<UserDTO> collection called Users; this
collection will be part of the model passed to the HTML Razor Page.
Add a string property called Alert and decorate it with the [TempData] attribute. This
property will get its value from the other Razor Pages when a successful result has been
achieved, such as adding a new user.
1. Open the _Layout view and change the container <div> to be fluid; making the
content resize when the browser size changes.
<div class="container-fluid">
269
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
9. Add the async and Task keyword to the OnGet action method to enable
asynchronous calls within. Rename the method OnGetAsync. Call the
asynchronous GetUsersAsync method on the _userServices service inside the
OnGetAsync method and store the result in the Users property you just added.
public async Task OnGetAsync()
{
Users = await _userService.GetUsersAsync();
}
10. Add a public string property named Alert to the class and decorate it with the
[TempData] attribute. This property will get its value from the other Razor Pages
when a successful result has been achieved, such as adding a new user.
[TempData]
public string Alert { get; set; }
11. Save all files.
270
21. The User Razor Pages
#region Constructor
public IndexModel(IUserService userService)
{
_userService = userService;
}
#endregion
271
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
dashboard. Assign /Index to the asp-page attribute to target the main Index
Razor Page; add dashboard to the icon attribute, text-light to the class attribute
to change the default dark gray text color to white, and Dashboard between the
start and end tags.
<btn class="text-light" asp-page="/Index" icon="dashboard">
Dashboard</btn>
272
21. The User Razor Pages
Use the ViewData object to add a Title property with the text Users to it. This value will
show on the browser tab.
Add an if-block that checks that the user is signed in and belongs to the Admin role. All
remaining code should be placed inside the if-block so that only administrators can view
it.
@if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin")) { }
Add a <div> decorated with the Bootstrap row class to create a new row of data on the
page. Then add a <div> decorated with the Bootstrap col-md-8 and offset-md-2 classes to
create a column that has been offset by two columns (pushed in from the left) inside the
row.
Add a page title displaying the text Users using an <h1> element inside the column <div>.
Use the <partial> Tag Helper to add the _PageButtonsPartial Razor View to add the Create
New and Dashboard buttons below the <h1> heading.
<partial name="_PageButtonsPartial" />
Add a table with four columns where the first three have the following headings: Email,
Admin, and Id. The fourth heading should be empty. Decorate the <table> element with
the Bootstrap table class. Also, add a table body to the table.
Iterate over the users in the Model.Users property – the Users property you added to the
code-behind file – and display the data in the Email, IsAdmin, and Id properties in the first
three columns.
Use the <partial> Tag Helper to add the _TableRowButtonsPartial Razor View to add the
Edit and Delete buttons to a new <td> column inside the table. Don’t forget to add the
model attribute containing an instance of the ButtonDTO class; the instance’s ItemId
273
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
property is needed to fetch the correct user for the Razor Page. Also, add a separate <btn>
Tag Helper for the Details Razor Page below the previous <partial> Tag Helper; don’t forget
to pass in the user id through the asp-route-id attribute.
<td style="min-width:150px;">
<partial name="_TableRowButtonsPartial" model="@user.ButtonDTO" />
<btn class="float-right" style="margin-right:5px;"
asp-page="Details" icon="edit" asp-route-id="@user.Id"></btn>
</td>
Add an empty <div> decorated with the col-md-2 Bootstrap class below the previous
column <div> to fill the entire row with columns. A Bootstrap row should have 12 columns.
Bootstrap is very forgiving if you forget to add up the columns on a row.
274
21. The User Razor Pages
</div>
</div>
7. Move the <h1> element inside the first column <div> and display the value from
the Title attribute inside it.
<h1>@ViewData["Title"]</h1>
8. Use the <partial> Tag Helper to add the _PageButtonsPartial Razor View to add
the Create New and Dashboard buttons below the <h1> heading.
<partial name="_PageButtonsPartial" />
9. Add a table with four columns where the first three have the following headings:
Email, Admin, and Id. The fourth heading should be empty. Decorate the <table>
element with the Bootstrap table class and add a 20px top margin. Also, add a
table body to the table.
<table style="margin-top:20px;" class="table">
<thead>
<tr>
<th>Email</th>
<th>Admin</th>
<th>Id</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
10. Iterate over the users in the Model.Users property in the <tbody> element and
display the data in the Email, IsAdmin, and Id properties in the first three
columns. The DisplayFor method will add an appropriate HTML element for the
property’s data type and display the property value in it.
<tbody>
@foreach (var user in Model.Users)
{
<tr>
<td>@Html.DisplayFor(modelItem => user.Email)</td>
<td>@Html.DisplayFor(modelItem => user.IsAdmin)</td>
<td>@Html.DisplayFor(modelItem => user.Id)</td>
</tr>
}
</tbody>
275
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
11. Use the <partial> Tag Helper to add the _TableRowButtonsPartial Razor View to
add the Edit and Delete buttons to a new <td> column inside the table. Don’t
forget to add the asp-model attribute with an instance of the ButtonDTO for the
current user in the iteration; the id is needed to fetch the correct user for the
Razor Page that is opened. Add a separate <btn> Tag Helper for the Details Razor
Page below the previous <partial> Tag Helper; don’t forget to pass in the user id
though the asp-route-id attribute.
<td style="min-width:150px;">
<partial name="_TableRowButtonsPartial"
model="@user.ButtonDTO" />
276
21. The User Razor Pages
<th></th>
</tr>
</thead>
<tbody>
@foreach (var user in Model.Users)
{
<tr>
<td>@Html.DisplayFor(modelItem =>
user.Email)</td>
<td>@Html.DisplayFor(modelItem =>
user.IsAdmin)</td>
<td>@Html.DisplayFor(modelItem =>
user.Id)</td>
<td style="min-width:150px;">
<partial name="_TableRowButtonsPartial"
model="@user.ButtonDTO" />
<btn class="float-right"
style="margin-right:5px;"
asp-page="Details" icon="edit"
asp-route-id="@user.Id">
</btn>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-md-2">
</div>
</div>
}
The IUserService service must be injected into the CreateModel class’s constructor in the
Razor Page code-behind and stored in a private field called _userService to add a new
user.
277
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Then you need to inject the UserService into the constructor that you will add to the class.
Store the service instance in a private class-level variable called _userService to add the
user to the database.
No data is needed to display the Create Razor Page, but it needs a RegisterUserDTO
variable called Input that can pass the form data from the UI to the OnPostAsync code-
behind method; call the AddUserAsync method on the _userService object to create the
user in the database.
Add a string property called Alert and decorate it with the [TempData] attribute. This
property will be assigned a message to be displayed in the Index Razor Page after the form
278
21. The User Razor Pages
data has been processed successfully in the OnPostAsync method and the action redirects
to the Index Razor Page.
In the OnPostAsync method, you need to check that the model state is valid before
performing any other action.
If the asynchronous AddUserAsync method returns true in the Succeeded property of the
method’s result, then assign a message to the Alert property that is used in the Index
Razor Page and redirect to that page by returning a call to the RedirectToPage method.
Iterate over the errors in the result.Errors collection and add them to the ModelState
object with the AddModelError method so that the errors can be used in the UI validation
when redisplaying the form to the user.
1. Add a Razor Page to the Pages-Users folder, using the template with the same
name, and name it Create.
2. Expand the Create node in the Solution Explorer and open the Create.cshtml.cs
file.
3. Add the [Authorize] attribute to the class and specify that the Admin role is
needed to access the page from the browser.
[Authorize(Roles = "Admin")]
public class CreateModel : PageModel
4. Inject IUserService into a constructor and save the injected object in a class-level
variable called _userService. The variable will give you access to the service from
any method in the class.
private readonly IUserService _userService;
public CreateModel(IUserService userService)
{
_userService = userService;
}
5. Add a public class-level RegisterUserDTO variable decorated with the
[BindProperty] attribute called Input. This property will be bound to the form
controls in the HTML Razor Page.
[BindProperty] public RegisterUserDTO Input { get; set; } =
new RegisterUserDTO();
6. Add a public string property called Alert and decorate it with the [TempData]
attribute. This property will get its value from the other Razor Pages when a
successful result has been achieved, such as adding a new user.
279
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
280
21. The User Razor Pages
#region Constructor
public CreateModel(IUserService userService)
{
_userService = userService;
}
#endregion
#region Actions
public async Task OnGetAsync()
{
}
if (result.Succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Created a new account for {Input.Email}.";
return RedirectToPage("Index");
}
281
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Use the ViewData object to add a Title property with the text Create a new account to it.
This value shows on the browser tab.
Add an if-block that checks that the user is signed in and belongs to the Admin role. All
remaining code should be placed inside the if-block so that only administrators can view
it.
@if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin")) { }
Add a <div> decorated with the Bootstrap row class to create a new row of data on the
page. Then add a <div> decorated with the Bootstrap col-md-8 and offset-md-2 classes to
create a column that has been offset by four columns (pushed in from the left) inside the
row.
Add a page title displaying the text in the ViewData object’s Title property using an <h1>
element inside the column <div>.
Use the <partial> Tag Helper to add the _BackToIndexButtonsPartial partial Razor View
that contain the Back to List and Dashboard buttons. Add the <partial> Tag Helper below
the <h1> heading.
<partial name="_BackToIndexButtonsPartial" />
Add an empty <p></p> element to create some distance between the buttons and the
form.
Add a form that validates on all errors using the <form> element and a <div> element with
the asp-validation-summary Tag Helper.
282
21. The User Razor Pages
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
</form>
Add a <div> decorated with the form-group class below the previous <div> inside the
<form> element. Add a <label> element with its asp-for attribute assigned a value from
the Input.Email model property, inside the previous <div> element.
<label asp-for="Input.Email"></label>
Add an <input> element with its asp-for attribute assigned a value from the Input.Email
model property below the <label> element. Decorate the <input> element with the form-
control class to denote that the element belongs to a form and gives it nice styling.
<input asp-for="Input.Email" class="form-control" />
Add a <span> element with its asp-validation-for attribute assigned a value from the
Input.Email model property, below the <input> element. Decorate the <span> element
with the text-danger class to make the text red.
<span asp-validation-for="Input.Email" class="text-danger"></span>
Copy the form-group decorated <div> you just finished and paste it in twice. Modify the
pasted in code to target the Password and ConfirmPassword model properties.
Add a submit button above the closing </form> element and decorate it with the btn and
btn-success Bootstrap classes to make it a styled green button.
Load the _ValidationScriptsPartial partial view inside a @section block named Scripts
below the if-block to load the necessary UI validation scripts.
283
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
5. Display the text in the Title property inside the <h1> element.
<h1>@ViewData["Title"]</h1>
6. Add an if-block that checks that the user is signed in and belongs to the Admin
role below the <h1> element.
@if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin")) { }
7. Add a <div> decorated with the Bootstrap row class to create a new row of data
on the page. Then add a <div> decorated with the Bootstrap col-md-8 and offset-
md-2 classes to create a column that has been offset by two columns inside the
row.
<div class="row">
<div class="col-md-8 offset-md-2">
</div>
</div>
8. Move the <h1> heading inside the column <div>.
9. Use the <partial> Tag Helper to add the _BackToIndexButtonsPartial partial Razor
View that contains the Back to List and Dashboard buttons. Add the <partial> Tag
Helper below the <h1> heading.
<partial name="_BackToIndexButtonsPartial" />
10. Add an empty <p></p> element to create some distance between the buttons
and the form.
<p></p>
11. Add a form that validates on all errors using the <form> element and a <div>
element with the asp-validation-summary Tag Helper. Decorate the <div>
element with the text-danger Bootstrap class to give the text a red color.
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
</form>
12. Add a <div> decorated with the form-group class below the previous <div> inside
the <form> element.
a. Add a <label> element with its asp-for attribute assigned a value from
the Input.Email model property, inside the previous <div> element.
b. Add an <input> element with its asp-for attribute assigned a value from
the Input.Email model property below the <label> element.
c. Add a <span> element with its asp-validation-for attribute assigned a
value from the Input.Email model property below the <input> element.
284
21. The User Razor Pages
Decorate the <span> element with the text-danger class to give the text
red color.
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email"
class="text-danger"></span>
</div>
13. Copy the form-group decorated <div> you just finished and paste it in twice.
Modify the pasted in code to target the Password and ConfirmPassword model
properties.
14. Add a submit button above the closing </form> element and decorate it with the
btn and btn-success Bootstrap classes to make it a styled green button.
<button type="submit" class="btn btn-success">Create</button>
15. Load the _ValidationScriptsPartial partial view inside a @section block named
Scripts below the if-block, to load the necessary UI validation scripts.
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
16. Run the application (Ctrl+F5) and click the Users card on the main dashboard or
select Users in the Admin menu. Make sure the Razor Page is displaying the
users in a table and that the buttons are present. Click the Create New button to
open the Create Razor Page. Try to add a new user and check that it is in the
AspNetUsers table in the database. Also, try the Back to List and Dashboard
buttons to navigate to the Users/Index and Pages/Index Razor Pages
respectively.
@model CreateModel
@{
ViewData["Title"] = "Create a new account";
}
285
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password"
class="form-control" />
<span asp-validation-for="Input.Password"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.ConfirmPassword"></label>
<input asp-for="Input.ConfirmPassword"
class="form-control" />
<span asp-validation-for="Input.ConfirmPassword"
class="text-danger"></span>
</div>
<button type="submit" class="btn btn-success">Create
</button>
</form>
</div>
</div>
}
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial") }
286
21. The User Razor Pages
287
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
7. Call the GetUserAsync method in the _userService service instance and assign
the fetched user to the Input property. The Input property is part of the model
sent to the Edit page and is bound to the form controls.
public async void OnGet(string id)
{
Alert = string.Empty;
Input = await _userService.GetUserAsync(id);
}
8. Replace the call to the AddUserAsync method with a call to the
UpdateUserAsync method in the OnPostAsync method.
var result = await _userService.UpdateUserAsync(Input);
9. Because the result from the UpdateUserAsync is a Boolean value, you must
remove the IsSucceeded property that doesn’t exist.
10. Change the text assigned to the Alert to:
if (result)
{
Alert = $"User {Input.Email} was updated.";
return RedirectToPage("Index");
}
11. Remove the foreach loop and its contents.
[BindProperty]
public UserDTO Input { get; set; } = new UserDTO();
[TempData]
public string Alert { get; set; }
288
21. The User Razor Pages
{
Alert = string.Empty;
Input = _userService.GetUserAsync(id);
}
if (result)
{
Alert = $"User {Input.Email} was updated.";
return RedirectToPage("Index");
}
}
return Page();
}
}
289
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.IsAdmin"></label>
<input asp-for="Input.IsAdmin"
class="form-control" />
<span asp-validation-for="Input.IsAdmin"
class="text-danger"></span>
</div>
<button type="submit" class="btn btn-success">
Save</button>
</form>
</div>
</div>
}
290
21. The User Razor Pages
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
291
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
[BindProperty]
public UserDTO Input { get; set; } = new UserDTO();
[TempData]
public string Alert { get; set; }
#endregion
#region Constructor
public DeleteModel(IUserService userService)
{
_userService = userService;
}
#endregion
#region Actions
Public async Task OnGetAsync(string userId)
{
Alert = string.Empty;
Input = await _userService.GetUserAsync(id);
}
if (result)
292
21. The User Razor Pages
{
Alert = $"User {Input.Email} was deleted.";
return RedirectToPage("Index");
}
}
return Page();
}
#endregion
}
6. Remove all the form-group decorated <div> elements and their contents. The
controls are no longer needed, since no data is altered with the form.
7. Add hidden <input> elements for the Input.Email and Input.IsAdmin properties
below the existing hidden <input> element.
<input type="hidden" asp-for="Input.Id" />
<input type="hidden" asp-for="Input.Email" />
<input type="hidden" asp-for="Input.IsAdmin" />
293
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
8. Change the text to Delete and the Bootstrap button style to btn-danger on the
submit button.
<button type="submit" class="btn btn-danger">Delete</button>
<dl class="dl-horizontal">
<dt>@Html.DisplayNameFor(model => model.Input.Id)</dt>
<dd>@Html.DisplayFor(model => model.Input.Id)</dd>
<dt>@Html.DisplayNameFor(model => model.Input.Email)</dt>
<dd>@Html.DisplayFor(model => model.Input.Email)</dd>
<dt>@Html.DisplayNameFor(model => model.Input.IsAdmin)</dt>
<dd>@Html.DisplayFor(model => model.Input.IsAdmin)</dd>
</dl>
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
294
21. The User Razor Pages
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
To enroll a user in a course, you select a course in the drop-down and click the Add button,
and to revoke access to a course, you click the Remove button for the course. Inject the
IDbReadService and IDbWriteService services into the DetailsModel class’s constructor in
the Details Razor Page code-behind to handle these two scenarios.
295
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
3. Enter the name Details into the Razor Page name field and click the Add button.
Then you need to inject the IDbReadService and IDbWriteService services into the
constructor that you will add to the class. Store the service instances in private class-level
variables called _dbRead and _dbWrite.
Add a public SelectList property named Courses; this collection will hold the courses
available to the user. It needs to be a SelectList because the drop-down needs the data in
that format.
Add a public int property named CourseId and decorate it with the BindPropertry
attribute so that it can be bound to the value selected in the drop-down. Also, decorate it
with the Display attribute and assign the text Available Courses to its Name property. The
Display attribute will change the text in the label describing the drop-down.
296
21. The User Razor Pages
Add a public UserDTO property named Customer that will hold the necessary user data to
display on the page.
Add a try/catch block to the OnPostAsync method that you will add. In the try-block, save
the user id and course id combination by creating an instance of the UserCourse entity.
Below the catch-block, reload the collections before rendering the page.
1. Expand the Details node in the Solution Explorer and open the Details.cshtml.cs
file.
2. Add the [Authorize] attribute to the class and specify that the Admin role is
needed to access the page from the browser.
[Authorize(Roles = "Admin")]
public class DetailsModel : PageModel
3. Inject IDbReadService and IDbWriteService services into a constructor and save
the injected objects in class-level variables called _dbRead and _dbWrite. The
variables will give you access to the services from any method in the class.
private readonly IDbReadService _dbRead;
private readonly IDbWriteService _dbWrite;
public DetailsModel(IDbReadService dbReadService, IDbWriteService
dbWriteService)
{
_dbRead = dbReadService;
_dbWrite = dbWriteService;
}
4. Add an IEnumerable<Course> collection named Courses as a public property
and instantiate it to an empty list to avoid null reference errors; this collection
will hold the courses that the user/customer has access to.
public IEnumerable<Course> Courses { get; set; } = new
List<Course>();
5. Add a public SelectList property named Courses; this collection will hold the
courses that the user/customer can enroll in. It needs to be a SelectList because
the drop-down needs the data in that format.
public SelectList AvailableCourses { get; set; }
6. Add a public int property named CourseId and decorate it with the
BindPropertry attribute so that it can be bound to the value selected in the
drop-down. Also, decorate it with the Display attribute and assign the text
Available Courses to its Name property. The Display attribute will change the
text in the label describing the drop-down.
297
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
12. Use the course ids stored in the usedCourseIds variable to exclude courses that
the user already is enrolled in when creating the SelectList of available courses.
Store the available courses in the AvailableCourses property you added earlier.
You need to call the ToSelectList extension method you created earlier to
convert the course collection into a SelectList.
var availableCourses = await _dbRead.GetAsync<Course>(uc =>
!usersCourseIds.Contains(uc.Id));
AvailableCourses = availableCourses.ToSelectList("Id", "Title");
298
21. The User Razor Pages
13. Add a string parameter named id to the OnGetAsync method and call the
FillViewData method with the id.
public async Task OnGetAsync(string id)
{
await FillViewData(id);
}
14. Add a new asynchronous method called OnPostAddAsync and call it when the
Add button is clicked to enroll a customer in a course. The method should have a
string parameter named userId and return a Task<IActionResult>, which
essentially makes it the same as an HttpPost MVC action method.
public async Task<IActionResult> OnPostAddAsync(string userId) { }
15. Add a try/catch-block and add a new instance of the UserCourse class and assign
its values from the userId parameter and the CourseId property you added
earlier that gets its value from the drop-down selection. Don’t forget to call the
SaveChangesAsync method to persist the changes to the database.
try
{
_dbWrite.Add(new UserCourse { CourseId = CourseId,
UserId = userId });
var succeeded = await _dbWrite.SaveChangesAsync();
}
catch
{
}
16. Call the FillViewData method with the value from the userId parameter below
the catch-block to load the course and user data. Then render the page by calling
the Page method.
await FillViewData(userId);
return Page();
17. Add a new asynchronous method called OnPostRemoveAsync and call it when
the Remove button for a course is clicked to revoke access to the course. The
method should have a string parameter named userId and an int parameter
named courseId, and return a Task<IActionResult>, which essentially makes it
the same as an HttpPost MVC action method.
public async Task<IActionResult> OnPostRemoveAsync(int courseId,
string userId) { }
299
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
18. Add a try/catch-block and make sure that the user-course combination exists by
fetching the record from the database before removing it; you can check for a
null value to determine if the record exists. Call the Delete method on the
_dbWrite service with the UserCourse instance you fetched. Don’t forget to call
the SaveChangesAsync method to persist the changes in the database.
try
{
var userCourse = await _dbRead.SingleAsync<UserCourse>(uc =>
uc.UserId.Equals(userId) &&
uc .CourseId.Equals(courseId));
if (userCourse != null)
{
_dbWrite.Delete(userCourse);
await _dbWrite.SaveChangesAsync();
}
}
catch
{
}
19. Call the FillViewData method with the value from the userId parameter below
the catch-block to load the course and user data. Then render the page by calling
the Page method.
await FillViewData(userId);
return Page();
The complete code in the Details code-behind file:
public class DetailsModel : PageModel
{
#region Properties and Variables
private readonly IDbReadService _dbRead;
private readonly IDbWriteService _dbWrite;
public IEnumerable<Course> Courses { get; set; } =
new List<Course>();
public SelectList AvailableCourses { get; set; }
300
21. The User Razor Pages
#region Constructor
public DetailsModel(IDbReadService dbReadService,
IDbWriteService dbWriteService)
{
_dbRead = dbReadService;
_dbWrite = dbWriteService;
}
#endregion
301
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
}
await FillViewData(userId);
return Page();
}
if (userCourse != null)
{
_dbWrite.Delete(userCourse);
await _dbWrite.SaveChangesAsync();
}
}
catch
{
}
await FillViewData(userId);
return Page();
}
Use the ViewData object to add a Title property with the text Details to it. This value will
be displayed on the browser tab and in the page header.
Add an if-block that checks that the user is signed in and belongs to the Admin role. All
remaining code should be placed inside the if-block so that only administrators can view
it.
302
21. The User Razor Pages
Add a <div> decorated with the Bootstrap row class to create a new row of data on the
page. Then add a <div> decorated with the Bootstrap col-md-8 and offset-md-2 classes to
create a column that has been offset by two columns (pushed in from the left) inside the
row.
Add a page title displaying the text in the ViewData object’s Title property and the user’s
email using an <h1> element inside the column <div>.
Use the <partial> Tag Helper to add the _BackToIndexButtonsPartial partial Razor View
that contains the Back to List and Dashboard buttons. Add the <partial> Tag Helper below
the <h1> heading.
<partial name="_BackToIndexButtonsPartial" />
Add an if-block that checks if there are any courses that the user can enroll in; if there
aren’t, then the drop-down and Add button shouldn’t be rendered.
Add a form inside the if-block that posts to the OnPostAddAsync method when clicking
the Add button. The asp-page-handler attribute on the submit button determines which
method to call when more than one post method is available. The value should not contain
OnPost or Async, only the unique part of the name; in this case Add.
<form method="post" style="margin:20px 0px;">
...
<button type="submit" asp-page-handler="Add" class="btn
btn-success">Add</button>
...
</form>
The form should contain a <select> list (drop-down) with the available courses from the
AvailableCourses SelectList, and a submit button with the text Add. You also need to add
a hidden field for the user id so that it gets posted back to the server. You can place a <div>
decorated with the input-group Bootstrap class around the drop-down and the button,
and a <div> decorated with the input-group-append Bootstrap class around the button to
make them appear as one control.
303
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Add a new row <div> with a column <div> decorated with the col-md-8 offset-md-2
Bootstrap classes. Add a table with two columns where the first displays the title Courses.
Display all the courses that the customer is enrolled in with a Remove button for each
inside the <tbody>. The asp-page-handler attribute on the Remove submit button should
contain the value Remove.
8. Use the <partial> Tag Helper to add the _BackToIndexButtonsPartial partial Razor
View that contains the Back to List and Dashboard buttons. Add the <partial> Tag
Helper below the <h2> heading.
<partial name="_BackToIndexButtonsPartial" />
9. Add a form decorated with the method attribute set to post inside an if-block that
checks that there are courses available that the customer can enroll in. The
finished form will post to the OnPostAddAsync method when the Add submit
button is clicked.
@if (Model.AvailableCourses.Count() > 0)
304
21. The User Razor Pages
{
<form method="post" style="margin:20px 0px;">
</form>
}
10. Add a hidden field for the user id inside the form.
<input type="hidden" asp-for="Customer.Id" name="userId" />
11. Add a <div> decorated with the form-group class below the <input> holding the
hidden user id.
a. Add a <label> element with its asp-for attribute, assigned a value from
the CourseId model property, inside the form-group <div> element.
b. Add a <div> element decorated with the input-group Bootstrap class
below the label.
c. Add a <select> element below the label with its asp-for attribute
assigned a value from the CourseId model property and its asp-items
assigned values from the AvailableCourses model property.
<select asp-for="CourseId" class="form-control" asp-
items="@Model.AvailableCourses"></select>
305
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
@{
ViewData["Title"] = "Details";
}
<div class="form-group">
<label asp-for="CourseId"
class="control-label"></label>
306
21. The User Razor Pages
<div class="row">
<div class="col-md-8 offset-md-2">
<table style="margin-top:20px;" class="table">
<thead>
<tr>
<th>Course</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var course in Model.Courses)
{
<tr>
<td style="vertical-align:middle">
@Html.DisplayFor(modelItem =>
course.Title)</td>
<td style="width:110px;">
<form method="post">
<input type="hidden"
asp-for="Customer.Id"
name="userId" />
<input type="hidden"
asp-for="@course.Id"
name="courseId" />
307
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<button type="submit"
asp-page-handler="Remove"
class="btn btn-danger
float-right">Remove
</button>
</form>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-md-2">
</div>
</div>
}
Summary
In this chapter, you used the UserService service for handling users and user roles in the
AspNetUsers and AspNetUserRoles database tables from the Razor Pages you added to
the Users folder. The pages you added perform CRUD operations on the previously men-
tioned tables.
You also added a Details Razor Page that enables users to enroll and leave courses. The
page uses the DbReadService and DbWriteService services to persist the data to the
database.
Next, you will create a new Tag Helper that displays the text from the Alert property you
added to the code-behind of the Razor Pages.
308
22. The Alert Tag Helper
The Alert property you added earlier to the Razor Pages will be used to store the message
that is assigned in the OnPostAsync method when the data has been modified. You can
see an example of the alert message under the heading in the image below. The message
element is a <div> decorated with the Bootstrap alert classes.
The [TempData] attribute is relatively new to ASP.NET Core and can be used with
properties in controllers and Razor Pages to store read-once data. It is particularly useful
for redirection when the data is stored by one request and read by a subsequent request.
Keep and Peek methods can be used to examine the data without deletion.
Because the [TempData] attribute builds on top of session state, different Razor Pages can
share it. You will use this to send a message from one Razor Page to another and display
it using the <alert> Tag Helper that you will implement in this chapter.
You can add the <alert> Tag Helper with or without the alert-type attribute; the alert-type
attribute uses the default success alert type if left out. You can assign primary, secondary,
success, danger, warning, info, light, or dark to the alert-type attribute.
<alert alert-type="success">@Model.Alert</alert>
<alert>@Model.Alert</alert>
309
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
310
22. The Alert Tag Helper
311
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
13. Add an <alert> element inside row and column <div> elements above the already
existing <div> decorated with the row class. Add the @Model.Alert property
between the start and end tag and assign one of the following values to the
alert-type attribute: primary, secondary, success, danger, warning, info, light, or
dark (you can skip this attribute if you want the default success type). You can
use HTML markup inside the start and end tag.
<div class="row">
<div class="col-md-8 offset-md-2" style="padding-left:0;
padding-right:0;">
<alert alert-type="success">@Model.Alert</alert>
</div>
</div>
14. Open the Index Razor Page in the Pages folder.
15. Add an <alert> element inside row and column <div> elements above the already
existing <div> decorated with the row class. Assign danger to the alert-type
attribute to give it a red background.
<div class="row">
<div class="col-md-8 offset-md-2" style="padding-left:0;
padding-right:0;">
<alert alert-type="danger">@Model.Alert</alert>
</div>
</div>
16. Save all files and start the application.
17. Open the Users/Index page and add a new user. A success message should be
displayed with the Tag Helper when redirecting to the Index page.
18. Edit the user you just added. A success message should be displayed with the
Tag Helper when redirecting to the Index page.
19. Delete the user you added. A success message should be displayed with the Tag
Helper when removing the user and redirecting to the Index page.
312
22. The Alert Tag Helper
#endregion
The complete markup to add the Tag Helper to the Index Razor Page:
<div class="row">
<div class="col-md-10 offset-md-1"
style="padding-left:0;padding-right:0;">
313
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<alert alert-type="success">@Model.Alert</alert>
</div>
<div class="col-md-1">
</div>
</div>
Summary
In this chapter, you created an Alert Tag Helper that displays the text from content
between its start and end tags. You also used the Alert property you added to the code-
behind of the Razor Pages to style the alert background.
Next, you will create a new service that communicates with the DbReadService and
DbWriteService services to read and modify data in the database. By doing this, you only
must inject one service into the constructors of the Razor Pages.
314
23. The AdminEFService
The IAdminSerivce interface will declare asynchronous generic CRUD methods named
GetAsync, SingleAsync, AnyAsync, CreateAsync, UpdateAsync, and DeleteAsync. The
methods will take one or two generic types named TSource and TDestination that
determines which entity and DTO are affected by the call. The TSource type defies the
entity in the database, and TDestination is the DTO type to return from the method.
The AdminEFService class implements the IAdminSerivce interface, and its methods call
methods in the DbReadService and DbWriteService services to perform CRUD operations
in the database, and AutoMapper is used to convert the entity data into a DTO and vice
versa.
DTOs will transfer data between the services and the Admin UI. You will create one DTO
class for each entity; larger systems that need more granularity or better data optimization
use several DTOs for each entity to transfer different data based on the scenario.
315
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
generic types named TSource and TDestination and have a Boolean parameter
named include that determines if data for the entity’s navigation properties
should be loaded. The generic types must be restricted to classes because entity
classes must be reference types.
Task<List<TDestination>> GetAsync<TSource, TDestination>(bool
include = false) where TSource : class where TDestination : class;
4. Copy the previous method definition and add a function expression that uses the
TSource type and returns a bool. By adding an expression, you can use Lambda
expressions to build a predicate that can filter the result on property values.
Task<List<TDestination>> GetAsync<TSource, TDestination>
(Expression<Func<TSource, bool>> expression, bool include = false)
where TSource : class where TDestination : class;
5. Copy the previous method definition and change the return type to a single
TDestination object and rename the method SingleAsync. This method will
return a single object or null based on the predicate.
Task<TDestination> SingleAsync<TSource, TDestination>(
Expression<Func<TSource, bool>> expression, bool include = false)
where TSource : class where TDestination : class;
6. Copy the previous method definition and rename it CreateAsync, then change
the return type to a single int value that will contain the id of the created entity.
Also, replace the two parameters with a TSource parameter named item, which
is the entity to add.
Task<int> CreateAsync<TSource, TDestination>(TSource item) where
TSource : class where TDestination : class;
7. Copy the previous method definition and rename it UpdateAsync, then change
the return type to a single bool value that specifies if the CRUD operation
succeeded.
Task<bool> UpdateAsync<TSource, TDestination>(TSource item) where
TSource : class where TDestination : class;
8. Copy the previous method definition and rename it DeleteAsync. Also, remove
the TDestination generic type and replace the item parameter with the same
expression you used earlier. The TDestination type is unnecessary since no data
transformation will occur.
Task<bool> DeleteAsync<TSource>(Expression<Func<TSource, bool>>
expression) where TSource: class;
316
23. The AdminEFService
9. Copy the previous method definition and rename it AnyAsync. This method will
return true if EF can find a matching record in the table represented by the
TEntity generic type.
Task<bool> AnyAsync<TEntity>(Expression<Func<TEntity, bool>>
expression) where TEntity : class;
1. Add a public class named AdminEFService to the Services folder in the Database
project and implement the IAdminService interface.
2. Add the async keyword to all the methods to enable asynchronous calls in them.
317
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
3. Install the AutoMapper NuGet package to the VOD.Database project like you did
in the VOD.UI project.
4. Add a constructor and inject the DbReadService, DbWriteService, and IMapper
services and store their instances in read-only class-level variables named
_dbRead, _dbWrite, and _mapper.
private readonly IDbReadService _dbRead;
private readonly IDbWriteService _dbWrite;
private readonly IMapper _mapper;
9. Copy the code from the previous GetAsync method and rename it SingleAsync.
Replace the throw statement with the copied code and the name of the
GetAsync method to SingleAsync.
318
23. The AdminEFService
15. Wrap the code inside the DeleteAsync method in a try-block and let the catch-
block return false to denote that an error occurred.
16. Remove the throw statement from the CreateAsync method and a try/catch.
17. Convert the passed-in TSource DTO into a TDestination entity with AutoMapper
inside the try-block.
var entity = _mapper.Map<TDestination>(item);
18. Call the _dbWrite.Add method to add the entity to Entity Framework’s in-
memory entity tracking; this does not persist it to the database.
_dbWrite.Add(entity);
19. Call the asynchronous _dbWrite.SaveChanges method to persist the entity in the
database. Store the result in a variable named succeeded.
var succeeded = await _dbWrite.SaveChangesAsync();
20. Return the id from the persisted entity if the succeeded variable is true,
otherwise return -1 to denote that the EF couldn’t persist the entity.
if (succeeded) return (int)entity.GetType().GetProperty("Id")
.GetValue(entity);
}
Catch { }
return -1;
319
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
21. Copy the code inside the CreateAsync method and replace the throw statement
in the UpdateAsync method with it. Replace the Add method call with a call to
the Update method.
_dbWrite.Update(entity);
22. Remove the if-statement and return the result from the SaveChangesAsync
method. Return false from the catch-block.
return await _dbWrite.SaveChangesAsync();
}
Catch { }
return false;
23. Call the AnyAsync method on the DbReadService service instance inside the
AnyAsync method and return the result.
return await _dbRead.AnyAsync(expression);
#region Properties
private readonly IDbReadService _dbRead;
private readonly IDbWriteService _dbWrite;
private readonly IMapper _mapper;
#endregion
#region Constructor
public AdminEFService(IDbReadService dbReadService,
IDbWriteService dbWrite, IMapper mapper)
{
_dbRead = dbReadService;
_dbWrite = dbWrite;
_mapper = mapper;
}
#endregion
320
23. The AdminEFService
if (include) _dbRead.Include<TSource>();
var entities = await _dbRead.GetAsync<TSource>();
return _mapper.Map<List<TDestination>>(entities);
}
321
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
_dbWrite.Add(entity);
var succeeded = await _dbWrite.SaveChangesAsync();
if (succeeded) return (int)entity.GetType()
.GetProperty("Id").GetValue(entity);
}
catch { }
return -1;
}
public async Task<bool> UpdateAsync<TSource, TDestination>(
TSource item) where TSource : class where TDestination : class
{
try
{
var entity = _mapper.Map<TDestination>(item);
_dbWrite.Update(entity);
return await _dbWrite.SaveChangesAsync();
}
catch { }
return false;
}
In this application, you will create one DTO per entity; in larger applications, it is not
uncommon to be more granular and create at least one per HTTP verb or type of action.
322
23. The AdminEFService
1. Add the following DTO classes inside the DTOModels-Admin folder in the
Common project.
323
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
formatted string; use this property in drop-downs to display values from more than one
property. You will add the VideoDTO and DownloadDTO DTOs momentarily.
public class ModuleDTO
{
public int Id { get; set; }
[MaxLength(80), Required]
public string Title { get; set; }
324
23. The AdminEFService
325
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
AutoMapper Mappings
Adding the AutoMapper service changed with the release version 6.1.0 of the AutoMap-
per.Extensions.Microsoft.DependencyInjection NuGet package. In earlier versions, you
called the AddAutoMapper method on the service object in the ConfigureServices
method in the Startup class; in the 6.1.0 version, you pass in the mapped types to the
AddAutoMapper method.
Example (6.0.0):
services.AddAutoMapper();
Example (6.1.0):
services.AddAutoMapper(typeof(Startup), typeof(Instructor),
typeof(Course), typeof(Module), typeof(Video), typeof(Download));
326
23. The AdminEFService
11. Copy the Video map and paste it in. Change the entity to Download and the DTO
to DownloadDTO.
12. Copy the Video map and paste it in. Change the entity to Module and the DTO to
ModuleDTO.
13. Remove the mapping from Module to Module.Title.
14. Change the ignore statement from Module to Videos.
327
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
15. Copy the previous ignore statement and change from Videos to Downloads.
16. Copy the Module map and paste it in. Change the entity to Course and the DTO
to CourseDTO.
17. Remove the ignore statement for Videos.
18. Change Title to Name.
.ForMember(d => d.Instructor, a => a.MapFrom(c =>
c.Instructor.Name))
19. Create a mapping from Instructor to InstructorDTO and reverse it.
CreateMap<Instructor, InstructorDTO>().ReverseMap();
The complete code in the MapProfile class:
public class MapProfile : Profile
{
public MapProfile()
{
CreateMap<Instructor, InstructorDTO>().ReverseMap();
CreateMap<Course, CourseDTO>()
.ForMember(d => d.Instructor, a => a.MapFrom(c =>
c.Instructor.Name))
.ReverseMap()
.ForMember(d => d.Instructor, a => a.Ignore());
CreateMap<Module, ModuleDTO>()
.ForMember(d => d.Course, a => a.MapFrom(c =>
c.Course.Title))
.ReverseMap()
.ForMember(d => d.Course, a => a.Ignore())
.ForMember(d => d.Downloads, a => a.Ignore())
.ForMember(d => d.Videos, a => a.Ignore());
CreateMap<Video, VideoDTO>()
.ForMember(d => d.Module, a => a.MapFrom(c =>
c.Module.Title))
.ForMember(d => d.Course, a => a.MapFrom(c =>
c.Course.Title))
.ReverseMap()
.ForMember(d => d.Module, a => a.Ignore())
.ForMember(d => d.Course, a => a.Ignore());
CreateMap<Download, DownloadDTO>()
.ForMember(d => d.Module, a => a.MapFrom(c =>
328
23. The AdminEFService
c.Module.Title))
.ForMember(d => d.Course, a => a.MapFrom(c =>
c.Course.Title))
.ReverseMap()
.ForMember(d => d.Module, a => a.Ignore())
.ForMember(d => d.Course, a => a.Ignore());
}
}
Summary
In this chapter, you created DTOs and added AutoMapper configuration mappings
between DTOs and entities.
329
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
330
24. The Remaining Razor Pages
Some of the pages have drop-down elements that you must add because the User pages
don’t have any. You add a drop-down to the form by adding a <select> element and assign
a collection of SelectList items to it; you can create such a list by calling the ToSelectList
extension method on a List<TEntity> collection; the method resides in the ListExtensions
class you added earlier to the Common project. You can send the collection to the page
with the ViewData object and use the ViewBag object to assign it to the <select> element.
public void OnGet()
{
ViewData["Modules"] = (await _dbRead.Get<Module>()).ToSelectList(
"Id", "Title");
}
<div class="form-group">
<label asp-for="Input.ModuleId" class="control-label"></label>
<select asp-for="Input.ModuleId" class="form-control"
asp-items="ViewBag.Modules"></select>
</div>
331
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
332
24. The Remaining Razor Pages
333
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
334
24. The Remaining Razor Pages
335
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<div class="row">
<div class="col-md-10 offset-md-1"
style="padding-left:0;padding-right:0;">
<alert alert-type="danger">@Model.Alert</alert>
</div>
<div class="col-md-1">
</div>
</div>
5. Open the Index Razor Page in the Pages folder and paste in the HTML you copied
above the first row inside the if-block.
6. Open the Index.cshtml.cs file in the Instructors folder and copy the Alert property.
[TempData] public string Alert { get; set; }
7. Open the Index.cshtml.cs file in the Pages folder and paste in the Alert property
above the constructor.
336
24. The Remaining Razor Pages
6. Add a try/catch-block and move the already existing code inside the try-block
and call the Page method at the end of the try-block to display the Razor Page;
this is necessary when using Task<IActionResult> as a return type.
7. Inside the catch-block, replace the throw-statement with code that assigns an
error message to the Alert property and redirects to the main Index page in the
Pages folder.
Alert = "You do not have access to this page.";
return RedirectToPage("/Index");
8. Replace the call to the GetUserAsync with a call to the GetAsync method in the
IAdminService and specify the Instructor entity as the TSource and the
InstructorDTO as the TDestination type. Pass in true to the method to load the
navigation properties for the Instructor entity.
Items = await _db.GetAsync<Instructor, InstructorDTO>(true);
9. Save all files.
#region Constructor
public IndexModel(IAdminService db)
{
_db = db;
}
#endregion
337
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
Alert = "You do not have access to this page.";
return RedirectToPage("/Index");
}
}
}
@{
ViewData["Title"] = "Instructors";
}
338
24. The Remaining Razor Pages
<div class="row">
<div class="col-md-10 offset-md-1">
<h1>@ViewData["Title"]</h1>
339
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
10. Replace the IUserService injection to IAdminService and name the backing
variable _db.
private readonly IAdminService _db;
public CreateModel(IAdminService db)
{
_db = db;
}
11. Replace the call to the AddUserAsync with a call to the CreateAsync method in
the IAdminService and specify InstructorDTO as the TSource and the Instructor
as the TDestination type. Check if the creation was successful and save the result
in a variable called succeeded. If the method returns a value greater than 0, then
the creation was successful since the returned value is the id of the created
resource.
var succeeded = (await _db.CreateAsync<InstructorDTO,
Instructor>(Input)) > 0;
4. Replace the result.Succeeded property with the succeeded variable.
5. Change the text in the Alert property to Created a new Instructor: followed by
the name of the instructor. Change the Input.Title property to Input.Name.
Alert = $"Created a new Instructor: {Input.Name}.";
6. Remove the foreach loop.
7. Save all files.
#region Constructor
public CreateModel(IAdminService db)
{
_db = db;
}
340
24. The Remaining Razor Pages
#endregion
#region Actions
public async Task OnGetAsync()
{
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Created a new Instructor: {Input.Name}.";
return RedirectToPage("Index");
}
}
341
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
@{
ViewData["Title"] = "Add Instructor";
}
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Description"></label>
<input asp-for="Input.Description"
class="form-control" />
<span asp-validation-for="Input.Description"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Thumbnail"></label>
<input asp-for="Input.Thumbnail"
class="form-control" />
<span asp-validation-for="Input.Thumbnail"
class="text-danger"></span>
</div>
342
24. The Remaining Razor Pages
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
343
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
#region Constructor
public EditModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id)
{
try
{
Input = await _db.SingleAsync<Instructor, InstructorDTO>(
s => s.Id.Equals(id));
return Page();
}
344
24. The Remaining Razor Pages
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Updated Instructor: {Input.Name}.";
return RedirectToPage("Index");
}
}
345
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Description"></label>
<input asp-for="Input.Description"
class="form-control" />
<span asp-validation-for="Input.Description"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Thumbnail"></label>
<input asp-for="Input.Thumbnail"
class="form-control" />
<span asp-validation-for="Input.Thumbnail"
class="text-danger"></span>
</div>
346
24. The Remaining Razor Pages
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
347
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
the method call and paste it in above the return statement in the OnPostAsync
method.
Input = await _db.SingleAsync<Instructor, InstructorDTO>(s =>
s.Id.Equals(id));
9. Add a variable named id above the ModelState.IsValid if-block in the
OnPostAsync action and assign the Id from the Input object.
var id = Input.Id;
10. Rename the result variable succeeded.
11. Replace the DeleteUserAsync method with the DeleteAsync method in the
IAdminService and specify Instructor as the TSource. This method doesn’t have
a TDestination since it will return a Boolean value specifying if EF could delete
the entity.
var succeeded = await _db.DeleteAsync<Instructor>(d =>
d.Id.Equals(id));
12. Replace the text in the Alert property with Deleted Instructor: followed by the
name of the instructor. Change the Input.Title property to Input.Name.
Alert = $"Deleted Instructor: {Input.Name}.";
13. Save all files.
#region Constructor
public DeleteModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
348
24. The Remaining Razor Pages
if (ModelState.IsValid)
{
var succeeded = await _db.DeleteAsync<Instructor>(d =>
d.Id.Equals(id));
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Deleted Instructor: {Input.Name}.";
return RedirectToPage("Index");
}
}
return Page();
}
#endregion
}
349
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<partial name="_DeletePageButtons"
model="@Model.Input.ButtonDTO" />
<p></p>
<dl class="dl-horizontal">
<dt>@Html.DisplayNameFor(model => model.Input.Name)</dt>
<dd>@Html.DisplayFor(model => model.Input.Name)</dd>
350
24. The Remaining Razor Pages
<dt>@Html.DisplayNameFor(model =>
model.Input.Description)</dt>
<dd>@Html.DisplayFor(model =>
model.Input.Description)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Thumbnail)</dt>
<dd>@Html.DisplayFor(model =>
model.Input.Thumbnail)</dd>
</dl>
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
3. Replace all occurrences of the Instructor entity and the InstructorDTO with
Course and CourseDTO.
4. Save all files.
351
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
#region Constructor
public IndexModel(IAdminService db)
{
_db = db;
}
#endregion
352
24. The Remaining Razor Pages
5. Add a variable named description for the truncated description in the foreach
loop and assign the truncated string from the item.Description property.
var description = item.Description.Truncate(100);
6. Change the <td> elements to display the values from the properties in the item
loop variable. Add and remove <td> elements as needed.
7. Use the description variable instead of the item.Description property.
<td>@Html.DisplayFor(modelItem => description)</td>
8. Change the width of the button column to 100px; there are only two buttons for
each row in this table.
9. Save all files.
@{
ViewData["Title"] = "Courses";
}
<div class="row">
<div class="col-md-10 offset-md-1">
<h1>@ViewData["Title"]</h1>
353
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<th>Title</th>
<th>Instructor</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
var description =
item.Description.Truncate(100);
<tr>
<td>@Html.DisplayFor(modelItem =>
item.Title)</td>
<td>@Html.DisplayFor(modelItem =>
item.Instructor)</td>
<td>@Html.DisplayFor(modelItem =>
description)</td>
<td style="min-width:100px;">
<partial name="_TableRowButtonsPartial"
model="@item.ButtonDTO" /></td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-md-1">
</div>
</div>
}
354
24. The Remaining Razor Pages
try
{
return Page();
}
catch
{
return RedirectToPage("/Index", new { alert =
"You do not have access to this page." });
}
6. Add a dynamic Modules property named Instructors and fetch all instructors
and convert them into a SelectList instance above the return statement in the
OnGetAsync action; add the same code above the return statement inside the
OnPostAsync method.
ViewData["Instructors"] = (await _db.GetAsync<Instructor,
InstructorDTO>()).ToSelectList("Id", "Name");
7. Change the text in the Alert property to Created a new Course: followed by the
course title.
Alert = $"Created a new Course: {Input.Title}.";
8. Save all files.
#region Constructor
public CreateModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync()
355
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
try
{
ViewData["Instructors"] = (await _db.GetAsync<Instructor,
InstructorDTO>()).ToSelectList("Id", "Name");
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Created a new Course: {Input.Title}.";
return RedirectToPage("Index");
}
}
return Page();
}
#endregion
}
356
24. The Remaining Razor Pages
3. Change the content in the form-group <div> elements to display the values from
the properties in the Input variable. Add and remove form-group <div> elements
as needed.
4. Use the data in the ViewData object to create a drop-down for all instructors.
<div class="form-group">
<label asp-for="Input.Instructor" class="control-label">
</label>
<select asp-for="Input.InstructorId" class="form-control"
asp-items="ViewBag.Instructors"></select>
</div>
5. Save all files.
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Description"></label>
357
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<input asp-for="Input.Description"
class="form-control" />
<span asp-validation-for="Input.Description"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.ImageUrl"></label>
<input asp-for="Input.ImageUrl"
class="form-control" />
<span asp-validation-for="Input.ImageUrl"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.MarqueeImageUrl"></label>
<input asp-for="Input.MarqueeImageUrl"
class="form-control" />
<span asp-validation-for="Input.MarqueeImageUrl"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.InstructorId"
class="control-label"></label>
<select asp-for="Input.InstructorId"
class="form-control"
asp-items="ViewBag.Instructors"></select>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
358
24. The Remaining Razor Pages
3. Replace all occurrences of the Instructor entity and the InstructorDTO with
Course and CourseDTO.
4. Add a dynamic Modules property named Instructors and fetch all instructors
and convert them into a SelectList instance below the Alert property assignment
in the OnGetAsync action and above the return statement inside the
OnPostAsync method.
ViewData["Instructors"] = (await _db.GetAsync<Instructor,
InstructorDTO>()).ToSelectList("Id", "Name");
5. Replace the text in the Alert property to Updated Course: followed by the course
title. Replace the Name property with the Title property.
Alert = $"Updated Course: {Input.Title}.";
6. Save all files.
#region Constructor
public EditModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id)
{
try
{
Alert = string.Empty;
ViewData["Instructors"] = (await _db.GetAsync<Instructor,
InstructorDTO>()).ToSelectList("Id", "Name");
Input = await _db.SingleAsync<Course, CourseDTO>(s =>
359
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
s.Id.Equals(id), true);
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Updated Course: {Input.Title}.";
return RedirectToPage("Index");
}
}
360
24. The Remaining Razor Pages
<div class="form-group">
<label asp-for="Input.Instructor" class="control-label">
</label>
<select asp-for="Input.InstructorId" class="form-control"
asp-items="ViewBag.Instructors"></select>
</div>
5. Save all files.
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Description"></label>
<input asp-for="Input.Description"
class="form-control" />
<span asp-validation-for="Input.Description"
class="text-danger"></span>
361
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
</div>
<div class="form-group">
<label asp-for="Input.ImageUrl"></label>
<input asp-for="Input.ImageUrl"
class="form-control" />
<span asp-validation-for="Input.ImageUrl"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.MarqueeImageUrl"></label>
<input asp-for="Input.MarqueeImageUrl"
class="form-control" />
<span asp-validation-for="Input.MarqueeImageUrl"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.InstructorId"
class="control-label"></label>
<select asp-for="Input.InstructorId"
class="form-control"
asp-items="ViewBag.Instructors"></select>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
362
24. The Remaining Razor Pages
4. Pass in true as a second parameter to the SingleAsync method inside both the
OnGetAsync and the OnPostAsync methods.
Input = await _db.SingleAsync<Course, CourseDTO>(s =>
s.Id.Equals(id), true);
5. Change the text in the Alert property to Deleted Course: followed by the course
title.
Alert = $"Deleted Course: {Input.Title}.";
6. Save all files.
#region Constructor
public DeleteModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id)
{
try
{
Alert = string.Empty;
Input = await _db.SingleAsync<Course, CourseDTO>(s =>
s.Id.Equals(id), true);
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
363
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
}
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Deleted Course: {Input.Title}.";
return RedirectToPage("Index");
}
}
364
24. The Remaining Razor Pages
<partial name="_DeletePageButtons"
model="@Model.Input.ButtonDTO" />
<p></p>
<dl class="dl-horizontal">
<dt>@Html.DisplayNameFor(model =>
model.Input.Title)</dt>
<dd>@Html.DisplayFor(model => model.Input.Title)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Description)</dt>
<dd>@Html.DisplayFor(model => description)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.ImageUrl)</dt>
<dd>@Html.DisplayFor(model => model.Input.ImageUrl)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.MarqueeImageUrl)</dt>
<dd>@Html.DisplayFor(model =>
365
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
model.Input.MarqueeImageUrl)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Instructor)</dt>
<dd>@Html.DisplayFor(model =>
model.Input.Instructor)</dd>
</dl>
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial") }
366
24. The Remaining Razor Pages
#region Constructor
public IndexModel(IAdminService db)
{
_db = db;
}
#endregion
367
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
@{
ViewData["Title"] = "Modules";
}
<div class="row">
<div class="col-md-10 offset-md-1">
<h1>@ViewData["Title"]</h1>
368
24. The Remaining Razor Pages
item.Course)</td>
<td style="min-width:100px;">
<partial name="_TableRowButtonsPartial"
model="@item.ButtonDTO" /></td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-md-1">
</div>
</div>
}
369
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
#region Constructor
public CreateModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync()
{
try
{
ViewData["Courses"] = (await _db.GetAsync<Course,
CourseDTO>()).ToSelectList("Id", "Title");
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Created a new Module: {Input.Title}.";
return RedirectToPage("Index");
}
}
370
24. The Remaining Razor Pages
return Page();
}
#endregion
}
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
371
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Course"
class="control-label"></label>
<select asp-for="Input.CourseId"
class="form-control"
asp-items="ViewBag.Courses"></select>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
372
24. The Remaining Razor Pages
5. Replace the text in the Alert property to Updated Module: followed by the
course title.
Alert = $"Updated Module: {Input.Title}.";
6. Save all files.
#region Constructor
public EditModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id, int courseId)
{
try
{
ViewData["Courses"] = (await _db.GetAsync<Course,
CourseDTO>()).ToSelectList("Id", "Title");
Input = await _db.SingleAsync<Module, ModuleDTO>(s =>
s.Id.Equals(id) && s.CourseId.Equals(courseId));
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
373
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Updated Module: {Input.Title}.";
return RedirectToPage("Index");
}
}
return Page();
}
#endregion
}
374
24. The Remaining Razor Pages
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Course"
class="control-label"></label>
<select asp-for="Input.CourseId"
class="form-control"
asp-items="ViewBag.Courses"></select>
</div>
375
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
#region Constructor
public DeleteModel(IAdminService db)
{
_db = db;
376
24. The Remaining Razor Pages
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id, int courseId)
{
try
{
Input = await _db.SingleAsync<Module, ModuleDTO>(s =>
s.Id.Equals(id) && s.CourseId.Equals(courseId), true);
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Deleted Module: {Input.Title}.";
return RedirectToPage("Index");
}
}
377
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<partial name="_DeletePageButtons"
model="@Model.Input.ButtonDTO" />
<p></p>
<dl class="dl-horizontal">
<dt>@Html.DisplayNameFor(model =>
model.Input.Title)</dt>
<dd>@Html.DisplayFor(model => model.Input.Title)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Course)</dt>
<dd>@Html.DisplayFor(model => model.Input.Course)</dd>
</dl>
378
24. The Remaining Razor Pages
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
379
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
#region Constructor
public IndexModel(IAdminService db)
{
_db = db;
}
#endregion
}
}
@{
ViewData["Title"] = "Videos";
}
380
24. The Remaining Razor Pages
<div class="row">
<div class="col-md-10 offset-md-1">
<h1>@ViewData["Title"]</h1>
381
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
</tr>
}
</tbody>
</table>
</div>
<div class="col-md-1">
</div>
</div>
}
382
24. The Remaining Razor Pages
#region Constructor
public CreateModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync()
{
try
{
ViewData["Modules"] = (await _db.GetAsync<Module,
ModuleDTO>(true)).ToSelectList("Id", "CourseAndModule");
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (ModelState.IsValid)
{
var id = Input.ModuleId;
Input.CourseId = (await _db.SingleAsync<Module, ModuleDTO>(
s => s.Id.Equals(id))).CourseId;
var succeeded = (await _db.CreateAsync<VideoDTO,
Video>(Input)) > 0;
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Created a new Video: {Input.Title}.";
return RedirectToPage("Index");
383
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
}
}
384
24. The Remaining Razor Pages
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Description"></label>
<input asp-for="Input.Description"
class="form-control" />
<span asp-validation-for="Input.Description"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Duration"></label>
<input asp-for="Input.Duration"
class="form-control" />
<span asp-validation-for="Input.Duration"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Thumbnail"></label>
<input asp-for="Input.Thumbnail"
class="form-control" />
<span asp-validation-for="Input.Thumbnail"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Url"></label>
<input asp-for="Input.Url" class="form-control" />
<span asp-validation-for="Input.Url"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Module"
class="control-label"></label>
<select asp-for="Input.ModuleId"
class="form-control"
asp-items="ViewBag.Modules"></select>
385
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
386
24. The Remaining Razor Pages
8. Assign the course id from the fetched module to the Input.CourseId property
above the UpdateAsync call in the OnPostAsync. This is necessary in case a
different module has been selected in the Modules drop-down.
Input.CourseId = (await _db.SingleAsync<Module, ModuleDTO>(s =>
s.Id.Equals(id))).CourseId;
1. Replace the text in the Alert property with Updated Video: followed by the title
of the video.
Alert = $"Updated Video: {Input.Title}.";
2. Save all files.
#region Constructor
public EditModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id, int courseId,
int moduleId)
{
try
{
ViewData["Modules"] = (await _db.GetAsync<Module,
ModuleDTO>(true)).ToSelectList("Id", "CourseAndModule");
Input = await _db.SingleAsync<Video, VideoDTO>(s =>
s.Id.Equals(id) && s.ModuleId.Equals(moduleId) &&
s.CourseId.Equals(courseId), true);
return Page();
}
catch
387
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (ModelState.IsValid)
{
var id = Input.ModuleId;
Input.CourseId = (await _db.SingleAsync<Module, ModuleDTO>(
s => s.Id.Equals(id))).CourseId;
var succeeded = await _db.UpdateAsync<VideoDTO,
Video>(Input);
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Updated Video: {Input.Title}.";
return RedirectToPage("Index");
}
}
388
24. The Remaining Razor Pages
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Description"></label>
<input asp-for="Input.Description"
class="form-control" />
389
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<span asp-validation-for="Input.Description"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Duration"></label>
<input asp-for="Input.Duration"
class="form-control" />
<span asp-validation-for="Input.Duration"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Thumbnail"></label>
<input asp-for="Input.Thumbnail"
class="form-control" />
<span asp-validation-for="Input.Thumbnail"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Url"></label>
<input asp-for="Input.Url" class="form-control" />
<span asp-validation-for="Input.Url"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Course"
class="control-label"></label>
<input asp-for="Input.Course" readonly
class="form-control" />
</div>
<div class="form-group">
<label asp-for="Input.Module"
class="control-label"></label>
<select asp-for="Input.ModuleId"
class="form-control"
asp-items="ViewBag.Modules"></select>
</div>
390
24. The Remaining Razor Pages
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
#region Constructor
public DeleteModel(IAdminService db)
{
_db = db;
}
#endregion
391
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
#region Actions
public async Task<IActionResult> OnGet(int id, int courseId,
int moduleId)
{
try
{
Input = await _db.SingleAsync<Video, VideoDTO>(s =>
s.Id.Equals(id) && s.ModuleId.Equals(moduleId) &&
s.CourseId.Equals(courseId), true);
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (ModelState.IsValid)
{
var succeeded = await _db.DeleteAsync<Video>(s =>
s.Id.Equals(id) && s.ModuleId.Equals(moduleId) &&
s.CourseId.Equals(courseId));
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Deleted Video: {Input.Title}.";
return RedirectToPage("Index");
}
}
return Page();
392
24. The Remaining Razor Pages
}
#endregion
}
<partial name="_DeletePageButtons"
model="@Model.Input.ButtonDTO" />
<p></p>
<dl class="dl-horizontal">
393
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<dt>@Html.DisplayNameFor(model =>
model.Input.Title)</dt>
<dd>@Html.DisplayFor(model => model.Input.Title)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Description)</dt>
<dd>@Html.DisplayFor(model =>
model.Input.Description)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Duration)</dt>
<dd>@Html.DisplayFor(model => model.Input.Duration)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Thumbnail)</dt>
<dd>@Html.DisplayFor(model =>
model.Input.Thumbnail)</dd>
<dt>@Html.DisplayNameFor(model => model.Input.Url)</dt>
<dd>@Html.DisplayFor(model => model.Input.Url)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Course)</dt>
<dd>@Html.DisplayFor(model => model.Input.Course)</dd>
<dt>@Html.DisplayNameFor(model =>
model.Input.Module)</dt>
<dd>@Html.DisplayFor(model => model.Input.Module)</dd>
</dl>
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
394
24. The Remaining Razor Pages
#region Constructor
public IndexModel(IAdminService db)
{
_db = db;
}
#endregion
395
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
return RedirectToPage("/Index");
}
}
}
@{
ViewData["Title"] = "Downloads";
}
396
24. The Remaining Razor Pages
<div class="row">
<div class="col-md-10 offset-md-1">
<h1>@ViewData["Title"]</h1>
397
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
3. Replace all occurrences of the Video entity and the VideoDTO with Download
and DownloadDTO.
4. Change the text in the Alert property to Created a new Download: followed by
the title of the download.
Alert = $"Created a new Download: {Input.Title}.";
5. Save all files.
#region Constructor
public CreateModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync()
{
try
{
ViewData["Modules"] = (await _db.GetAsync<Module,
ModuleDTO>(true)).ToSelectList("Id", "CourseAndModule");
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
398
24. The Remaining Razor Pages
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Created a new Download: {Input.Title}.";
return RedirectToPage("Index");
}
}
399
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Url"></label>
<input asp-for="Input.Url" class="form-control" />
<span asp-validation-for="Input.Url"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Module"
class="control-label"></label>
<select asp-for="Input.ModuleId"
class="form-control"
asp-items="ViewBag.Modules"></select>
</div>
400
24. The Remaining Razor Pages
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
#region Constructor
public EditModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id, int courseId,
int moduleId)
{
try
{
ViewData["Modules"] = (await _db.GetAsync<Module,
401
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
ModuleDTO>(true)).ToSelectList("Id", "CourseAndModule");
Input = await _db.SingleAsync<Download, DownloadDTO>(s =>
s.Id.Equals(id) && s.ModuleId.Equals(moduleId) &&
s.CourseId.Equals(courseId), true);
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Updated Download: {Input.Title}.";
return RedirectToPage("Index");
}
}
return Page();
}
#endregion
}
402
24. The Remaining Razor Pages
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Title"></label>
<input asp-for="Input.Title" class="form-control" />
<span asp-validation-for="Input.Title"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Url"></label>
<input asp-for="Input.Url" class="form-control" />
<span asp-validation-for="Input.Url"
class="text-danger"></span>
403
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
</div>
<div class="form-group">
<label asp-for="Input.Course"
class="control-label"></label>
<input asp-for="Input.Course" readonly
class="form-control" />
</div>
<div class="form-group">
<label asp-for="Input.Module"
class="control-label"></label>
<select asp-for="Input.ModuleId"
class="form-control"
asp-items="ViewBag.Modules"></select>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
404
24. The Remaining Razor Pages
#region Constructor
public DeleteModel(IAdminService db)
{
_db = db;
}
#endregion
#region Actions
public async Task<IActionResult> OnGetAsync(int id, int courseId,
int moduleId)
{
try
{
Input = await _db.SingleAsync<Download, DownloadDTO>(s =>
s.Id.Equals(id) && s.ModuleId.Equals(moduleId) &&
s.CourseId.Equals(courseId), true);
return Page();
}
catch
{
return RedirectToPage("/Index", new {
alert = "You do not have access to this page." });
}
}
if (ModelState.IsValid)
{
405
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (succeeded)
{
// Message sent back to the Index Razor Page.
Alert = $"Deleted Download: {Input.Title}.";
return RedirectToPage("Index");
}
}
return Page();
}
#endregion
}
406
24. The Remaining Razor Pages
<partial name="_DeletePageButtons"
model="@Model.Input.ButtonDTO" />
<p></p>
<dl class="dl-horizontal">
<dt>@Html.DisplayNameFor(model => model.Input.Title)</dt>
<dd>@Html.DisplayFor(model => model.Input.Title)</dd>
<dt>@Html.DisplayNameFor(model => model.Input.Url)</dt>
<dd>@Html.DisplayFor(model => model.Input.Url)</dd>
<dt>@Html.DisplayNameFor(model => model.Input.Course)</dt>
<dd>@Html.DisplayFor(model => model.Input.Course)</dd>
<dt>@Html.DisplayNameFor(model => model.Input.Module)</dt>
<dd>@Html.DisplayFor(model => model.Input.Module)</dd>
</dl>
<form method="post">
<div asp-validation-summary="All"
class="text-danger"></div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}
407
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Summary
In this chapter, you implemented the rest of the Razor Pages needed in the administration
application by reusing already created Razor Pages.
In the next section of the book, you will implement an API that you will call from the
Admin project with HttpClient from the Razor Pages. Then you will secure the API with
JSON Web Tokens (JWT).
408
Part 3:
API, HttpClient & JWT
How to Build and Secure an API
25. The API Project
Because the controllers all implement the same methods for different entities and they
call the same generic methods, it will be very easy to copy the first controller as a template
for the others and replace the entity and DTO classes. The exception is the User entity that
uses the UserService service to perform its CRUD operations.
DTOs will transfer data between the API back-end and Admin front-end, and AutoMapper
is used to transform the data from DTO to Entity and vice versa. Each entity has one DTO.
A Request sent over HTTP(S) contains three parts: a verb (what you want to do), headers
(additional information), and content if any. The verb corresponds with the type of action
411
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
you want to take; for instance, Get, Put, Post, and Delete. The headers can contain the
length of the content and other information. Whether content is present or not is deter-
mined by the type of action you take; deleting an item does not require any information
to be sent with the request since all information is available in the URL.
When the server completes its operation, a response is sent back, with a similar structure
to the request; it has a status code that signals if the operation was successful or not,
headers and content that may include information. In the image below the response
returns with a status code of 201, which means that the API could create the item success-
fully. The content type Text specifies that text is being sent back with the response, and
the content (body) of the message contains the text that was created and returned.
The API builds on top of this, so it is important to understand it. The main idea is that the
server is stateless, which means that it doesn’t maintain a constant connection and every
request is a single roundtrip call. You will have to send all the information needed to
complete a request within the header and content with every API call.
412
25. The API Project
When you make an API call, you need to provide a verb that specifies your intentions, what
you want the server to do. The most common are: Get, which retrieves a resource, which
can be a JSON file, an Entity Framework entity, or some other data; Post, which creates a
new resource on the server; Put, which updates an entire existing resource that has been
fetched earlier and now contains changes; Patch, which updates parts of an existing
resource through a JSON Patch Document, sending only the changed data in the request;
and Delete, which removes an existing resource. There are many more verbs that won’t
be covered in this chapter because they don’t have any function in the API you are
building.
We have talked about resources, but what are they? A resource can be a person, invoices,
payments, products, and other things that can be represented by objects in the system,
like entities and DTOs, that can be fetched, added, updated, and deleted.
Often, a resource is part of a context. That resource could be a single entity, like a person,
but it could also be an invoice with related invoice items (an invoice without items doesn’t
make sense). So, keep in mind that a resource is not always a single entity. If you want to
dig into this in more detail, you can look at Domain Driven Design (DDD).
The URIs are paths to the resources in your system, e.g., api.yourserver.com/courses. A
query string transport a non-data element, such as a flag that specifies how many objects
to return in a pagination scenario, or It could be a search string.
In this chapter, you will expand on what you have built in previous chapters and add an
API that performs CRUD operations on the entities in the database. To achieve this, you
will use services that you already have created and the Postman tool. In an upcoming
chapter, you will use HttpClient to call the API from the Razor Pages in the Admin project.
Status Codes
To distinguish between successful and unsuccessful requests, the response that is sent
back from the server as a result of your call will contain a status code. Status codes in the
200 range signals that the request was successful. Status codes in the 400 range signals
that you made an error when making the request. Status codes in the 500 range signals
that the server failed to fulfill your request. The table below shows the most common
status codes. The ones that you really should pay attention to are 200 OK (the request was
successful); 201 Created (the API created the resource); 400 Bad Request (something was
wrong with the request; a bad URI, for instance); 401 Unauthorized (you are not logged
in); 403 Forbidden (you are logged in, but are not permitted to access the resource); 404
413
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Not Found (the resource isn’t available); and 500 Internal Error (The server could not fulfill
the request, perhaps due to database failure).
Traditionally you returned these status codes from your API by wrapping the response in
one of the built-in methods; this is still possible, but as you will see shortly, this is not
necessary.
By adding the [ApiController] attribute to the controller class, you specify that this is an
API controller that won’t return HTML views or pages. One benefit of using this attribute
is that it automatically performs data binding from the data in the body of the request to
the receiving action’s parameter data type, which means that you don’t have to add the
[FromBody] attribute to the parameters in the actions. If the body contains data that
doesn’t correspond to a property in the receiving type, that data will be ignored and lost.
When creating the action methods that receive the request and generate the response,
you should decorate them with the appropriate verb attribute (even though it isn’t strictly
necessary in all scenarios, it is good practice). The most common attributes are: HttpGet,
HttpPost, HttpPut, HttpPatch, and HttpDelete. Action methods can be named whatever
you like if you apply the correct attributes to them. By studying the code below, you can
glean that it is the method, not the class, that is the endpoint where the request ends up;
a URI could be http://localhost:6600/api/courses; note that you don’t add the name of the
action method because the associated verb implies it.
The Route attribute can contain the name of the controller, or you can use square brackets
to infer the controller’s name from the class name.
414
25. The API Project
[Route("api/[controller]")].
[Route("api/videos")]
[ApiController]
public class VideosController : ControllerBase
{
[HttpPost]
public async Task<ActionResult<VideoDTO>> Post(VideoDTO model)
{
try
{
if (model == null)
return BadRequest("No entity provided");
415
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
What Is REST?
REST stands for Representation State Transfer, which means that the client data and server
data should be separate from one another. It also means that the requests to the server
are stateless and close the connection with every call. Cache requests for better perfor-
mance, and use URIs to reach the server API. (You can read more about REST in Roy
Fielding’s doctoral dissertation that introduced the concept.)
There are possible problems with REST. One is that it is too difficult to adhere to REST
completely; there is a dogma around it where some developers condemn APIs that are not
100% RESTful. Most developers adhere to the more pragmatic thought around REST – that
it should be implemented to suit the goals of the project. It would be very cumbersome
and time-consuming to build a completely RESTful API for every small application you
create, where only a subset of REST is necessary. Most projects require you to be
productive and move the project forward, not to be rigid and implement REST completely
when it isn’t needed or warranted.
Building a completely RESTful system can make it more maintainable in the long run, but
the adherence to a principle versus the job you must do can make it backfire. To me, it’s
more important to use the necessary parts of REST such as statelessness, caching, and
other things we have talked about than to implement it fully. Not everyone agrees with
me on this.
Designing URIs
When you build the API, you should consider how to design the URIs. You can append a
query string with a question mark after the URI.
Examples:
http://localhost:6600/api/courses/1/modules/2/videos
http://localhost:6600/api/courses/1/modules/2/videos/3
http://localhost:6600/api/courses/1/modules/2/videos/3?include=true
There is a relationship between the resource and the verb you use. Let’s use the /Courses
endpoint as an example. When using the Get (read) verb with that endpoint, a list of all
courses is sent back in the response. Use Post (create) to create a new resource. Use Put
(update) to perform a batch update that updates several resources at the same time. Use
Delete to delete a resource; you shouldn’t be able to delete all resources with one request.
416
25. The API Project
Let’s see what happens when we target a single item endpoint like /Courses/123. Get
(read) fetches the resource identified by the id 123 and sends it back in the response; Post
(create) should return an error because it isn’t possible to create a new resource while
targeting an already existing resource; Put (update) updates the targeted resource; Delete
removes the targeted resource from the data source.
The following table shows what should happen when sending a request.
The following table shows what the response should contain (the returned data). Note
that when making an update to a single resource, you can either send the updated item
back with the response or only send back the status code; the reasoning is that the client
already has all the updated information because it came to the API endpoint from the
client.
417
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Postman
In this chapter, you will use a tool called Postman to send raw HTTP requests to the API;
in the next chapter, you will use HttpClient to call the API from the Admin UI.
There are two settings that you should temporarily shut off when testing this API. The first
is SSL certificate verification, which stops Postman from requesting an SSL certificate
when calling over HTTPS; the second is Automatically follow redirects, which stops the
API’s routing system from redirecting to and displaying an error or login page. You find the
settings under the File-Settings menu option.
To make it easier to call the API, you should assign a memorable port number for the API
project in its settings in Visual Studio; you will do this when you create the VOD.API
project.
418
25. The API Project
419
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Example (6.0.0):
services.AddAutoMapper();
Example (6.1.0):
services.AddAutoMapper(typeof(Startup), typeof(Instructor),
typeof(Course), typeof(Module), typeof(Video), typeof(Download));
1. Right click on the Solution node in the Solution Explorer and select Add-New
Project.
2. Select the ASP.NET Core Web Application template and click the Next button.
3. Name the project VOD.API and click the Create button.
4. Select .NET Core and ASP.NET Core 2.2 in the drop-downs.
5. Select the API project template and click the Create button.
6. Open the Project settings.
a. Right click on the project’s node in the Solution Explorer.
b. Select the Properties option in the context menu.
c. Click on the Debug tab to the left in the dialog.
d. Change the URL in the App URL field to a memorable value, for instance,
6600.
e. Uncheck the Enable SLL option (don’t forget to check it when you go to
production).
f. Above the URL settings, uncheck the Launch Browser option to keep
Visual Studio from opening the browser when the API project starts.
420
25. The API Project
421
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
15. Right click on the API project in the Solution Explorer and select Set as StartUp
Project.
16. Close and reopen the solution in Visual Studio.
#region Constructor
public InstructorsController(IAdminService db, LinkGenerator
linkGenerator)
{
_db = db;
_linkGenerator = linkGenerator;
}
#endregion
422
25. The API Project
The action methods should either return or receive objects of the InstructorDTO class
because we don’t want to use the entities directly.
1. Add a method named Get with a Boolean parameter named include with a
default value of false to the class. Because the method calls other asynchronous
methods, it should be decorated with the async keyword and return a Task. The
task should wrap an ActionResult<List<InstructorDTO>>.
[HttpGet()]
public async Task<ActionResult<List<InstructorDTO>>> Get(bool
include = false) { }
2. Add a try/catch-block to the method and return a 500 Internal Server Error status
code in the catch-block.
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
3. In the try-block, return the result from calling the GetAsync method in the
IAdminService instance with the include parameter’s value. Specify the
Instructor entity as the TSource type, and the InstructorDTO class as the
TDestination type; TDestination determines the method’s return type.
return await _db.GetAsync<Instructor, InstructorDTO>(include);
4. Start the API application.
5. Start Postman and open a new tab.
6. Select the Get action in the drop-down to the left of the URI field.
7. Enter the http://localhost:6600/api/instructors URI into the text field and click
the Send button.
423
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
424
25. The API Project
"Database Failure");
}
}
1. Copy the previous Get action method and paste in the copied code.
2. Change the return data type to Task<ActionResult<InstructorDTO>>.
3. Specify that the parameter is named id and is of type int in the HttpGet
attribute.
[HttpGet("{id:int}")]
4. Add an int parameter named id.
5. Delete the code in the try-block.
6. Call the SingleAsync method in the IAdminService and use a Lambda expression
to pass in the id. Also, pass in the include parameter. Store the result in a
variable named dto.
var dto = await _db.SingleAsync<Instructor, InstructorDTO>(s =>
s.Id.Equals(id), include);
7. Return 404 Not Found if EF can’t find the instructor in the database.
if (dto == null) return NotFound();
8. Return the instructor below the if-statement.
9. Start the API application.
10. Open a new tab in Postman.
11. Select the Get action in the drop-down to the left of the URI field.
12. Enter the http://localhost:6600/api/instructors/1 URI, using one of the ids you
got in the previous get, into the text field and click the Send button.
425
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
return dto;
}
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
}
}
1. Copy the previous Get action method and paste in the copied code.
426
25. The API Project
7. Use the returned id to fetch the newly created instructor by calling the
SingleAsync method and store it in a variable named dto.
var dto = await _db.SingleAsync<Instructor, InstructorDTO>(s =>
s.Id.Equals(id));
8. Return 400 Bad Request if the API can’t find the entity in the database.
if (dto == null) return BadRequest("Unable to add the entity");
9. Use the LinkGenerator object to create a URI for the new instructor. Pass in the
name of the Get action that returns a single instructor, the name of the
controller, and the id of the created instructor. Store the URI in a variable named
uri.
var uri = _linkGenerator.GetPathByAction("Get", "Instructors", new
{ id });
10. Return the instructor and the URI with the Create status method; this will return
the status code 201 Created with the response data.
return Created(uri, dto);
13. Change the error message in the catch-block to Failed to add the entity.
14. Start the API application and open a new tab in Postman.
15. Select the Post action in the drop-down to the left of the URI field.
16. Enter the http://localhost:6600/api/instructors URI into the text field.
17. Select the Raw option and JSON (application/json) in the drop-down, and enter
JSON data for a new instructor in the Body tab.
{
427
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
428
25. The API Project
1. Copy the Post action method and paste in the copied code.
2. Change the HttpPost attribute to HttpPut and specify that it should take an id of
int type in the URI.
[HttpPut("{id:int}")]
3. Change the return type to Task<IActionResult> and add an int parameter named
id.
public async Task<IActionResult> Put(int id, InstructorDTO model)
4. Add an if-statement checking that the value of the id parameter and the Id
property in the model object are identical below the first if-statement; if not, the
status 400 BedRequest should be returned.
if (!id.Equals(model.Id)) return BadRequest("Differing ids");
5. Replace the rest of the code in the try-block with a call to the AnyAsync and
store the result in a variable named exists; the variable will be true if EF finds the
instructor.
var exists = await _db.AnyAsync<Instructor>(a => a.Id.Equals(id));
6. Change the 400 Bad Request status to 404 Not Found if the exists variable is
false.
if (!exists) return NotFound("Could not find entity");
429
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
7. Call the UpdateAsync method in the _db object and return the status 204 No
Content by calling the NoContent method if the call is successful.
if (await _db.UpdateAsync<InstructorDTO, Instructor>(model))
return NoContent();
8. Return 400 Bad Request below the catch-block.
return BadRequest("Unable to update the entity");
9. Change the error message in the catch-block to Failed to update the entity.
10. Start the API application and open a new tab in Postman.
11. Select the Put action in the drop-down to the left of the URI field.
12. Enter the http://localhost:6600/api/instructors/43 URI into the text field (use the
id from the instructor you added with the post earlier).
13. Enter new JSON data for the instructor to update in the Body tab. Select the Raw
option and JSON (application/json) in the drop-down.
{
"id": "43",
"name": "Updated Newt Newton",
"description": " Updated t amet, consectetur adipiscing elit",
"thumbnail": "/images/updatednewt.png"
}
14. Add the application/json content type to the Headers section (Key: Content-
Type and Value: application/json).
15. Click the Send button.
16. Note that the response only contains the 204 No Content status code.
17. Go back to the second Get tab and change the id in the URI to the id
corresponding to the instructor you updated. Make sure that the request
updated the instructor.
Example of returned data from the Get action after the Put:
{
"id": 43,
"name": "Updated Newt Newton",
"description": " Updated t amet, consectetur adipiscing elit",
"thumbnail": "/images/updatednewt.png",
"buttonDTO": {
"courseId": 0,
"moduleId": 0,
"id": 43,
430
25. The API Project
"userId": null,
"itemId": "43"
}
}
1. Copy the Get action method that returns a single instructor and paste in the
copied code.
2. Change the HttpPost attribute to HttpDelete.
[HttpDelete("{id:int}")]
431
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
4. Replace the SingleAsync method call and the if-statement with a call to the
AnyAsync method and an if-statement that returns 404 Not Found if the result is
false.
var exists = await _db.AnyAsync<Instructor>(a => a.Id.Equals(id));
if (!exists) return BadRequest("Could not find entity");
5. Replace the return statement with an if-statement that awaits the result from
calling the DeleteAsync method in the _db object and return 204 No Content by
calling the NoContent method if the call is successful.
if (await _db.DeleteAsync<Instructor>(d => d.Id.Equals(id)))
return NoContent();
6. Return 400 Bad Request below the catch-block.
return BadRequest("Failed to delete the entity");
7. Change the error message in the catch-block to Failed to delete the entity.
8. Start the API application and open a new tab in Postman.
9. Select the Delete action in the drop-down to the left of the URI field.
10. Enter the http://localhost:6600/api/instructors/43 URI into the text field (use the
id from the instructor you added with the post earlier).
11. Add the application/json content type to the Headers section (Key: Content-
Type and Value: application/json).
12. Click the Send button.
13. Note that the response only contains the 204 No Content status code.
14. Go back to the second Get tab and change the id in the URI to the id
corresponding to the instructor you deleted. Make sure that the request deleted
the instructor (Postman displays a 404 Not Found status code).
432
25. The API Project
To speed up the process of adding the Course controller, you will copy and modify the
Instructors controller and its action methods.
433
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
6. In the Post action directly below the CreateAsync method call, return 400 Bad
Request if the id is less than 1; this indicates that the API was unable to create
the entity.
if (id < 1) return BadRequest("Unable to add the entity");
7. Locate the Put action and add a second call to the AnyAsync method for the
Instructor entity above the previous AnyAsync method call.
var exists = await _db.AnyAsync<Instructor>(a =>
a.Id.Equals(model.InstructorId));
if (!exists) return NotFound("Could not find related entity");
8. Start the API application and call the action methods with Postman.
Example JSON response from a Get request without included data in Postman:
URI: http://localhost:6600/api/courses/2
{
"id": 2,
"imageUrl": "/images/course2.jpg",
"marqueeImageUrl": "/images/laptop.jpg",
"title": "Course 2",
"description": "Lorem ipsum dolor",
"instructorId": 2,
"instructor": null,
"modules": [],
"buttonDTO": {
"courseId": 0,
"moduleId": 0,
"id": 2,
"userId": null,
"itemId": "2"
}
}
Example JSON response from a Get request with included data in Postman:
URI: http://localhost:6600/api/courses/2?include=true
{
434
25. The API Project
"id": 2,
"imageUrl": "/images/course2.jpg",
"marqueeImageUrl": "/images/laptop.jpg",
"title": "Course 2",
"description": "Lorem ipsum dolor",
"instructorId": 2,
"instructor": "Jane Doe",
"modules": [
{
"id": 3,
"title": "Module 3",
"courseId": 2,
"course": "Course 2",
"videos": [],
"downloads": [],
"courseAndModule": "Module 3 (Course 2)",
"buttonDTO": {
"courseId": 2,
"moduleId": 0,
"id": 3,
"userId": null,
"itemId": "3"
}
}
],
"buttonDTO": {
"courseId": 0,
"moduleId": 0,
"id": 2,
"userId": null,
"itemId": "2"
}
}
URI: http://localhost:6600/api/courses
{
"imageUrl": "/images/course1.jpg",
"marqueeImageUrl": "/images/laptop.jpg",
"title": "New Course",
"description": "Lorem ipsum dolor",
435
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
"instructorId": 1
}
Note that no JSON object is returned with the response when making a Put request; only
the status code 204 No Content is returned. You can check for the updated entity by
making a Get request for it.
URI: http://localhost:6600/api/courses/51
{
"id": 51,
"imageUrl": "/images/course1.jpg",
"marqueeImageUrl": "/images/laptop.jpg",
"title": "Updated Course",
"description": "Updated description",
"instructorId": 1
}
436
25. The API Project
Note that no JSON object is posted in the body nor returned with the response when
making a Delete request; only the status code 204 No Content is returned. You can check
that the API removes the entity from the database by making a Get request for the deleted
entity; if the status 404 Not Found is returned, then the entity was successfully removed.
URI: http://localhost:6600/api/courses/51
#region Constructor
public CoursesController(IAdminService db,
LinkGenerator linkGenerator)
{
_db = db;
_linkGenerator = linkGenerator;
}
#endregion
#region Actions
[HttpGet()]
public async Task<ActionResult<List<CourseDTO>>> Get(
bool include = false)
{
try
{
return await _db.GetAsync<Course, CourseDTO>(include);
}
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
}
}
437
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
[HttpGet("{id:int}")]
public async Task<ActionResult<CourseDTO>> Get(int id,
bool include = false)
{
try
{
var dto = await _db.SingleAsync<Course, CourseDTO>(s =>
s.Id.Equals(id), include);
return dto;
}
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
}
}
[HttpPost]
public async Task<ActionResult<CourseDTO>> Post(CourseDTO model)
{
try
{
if (model == null) return BadRequest("No entity provided");
438
25. The API Project
new { id });
[HttpPut("{id:int}")]
public async Task<IActionResult> Put(int id, CourseDTO model)
{
try
{
if (model == null) return BadRequest("Missing entity");
if (!id.Equals(model.Id)) return BadRequest(
"Differing ids");
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
439
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
try
{
var exists = await _db.AnyAsync<Course>(a =>
a.Id.Equals(id));
To speed up the process of adding the Modules controller, you will copy and modify the
Courses controller and its action methods. The URI should expand the previous URI with
the course id and the modules route: api/courses/{courseId}/modules. The parameter
name courseId must have a corresponding int parameter with the same name in all the
action methods.
440
25. The API Project
8. Repeat the previous three bullets for the second Get action but use the
SingleAsync method instead. Also, check if the DTO is null above the already
existing if-statement and return BadRequest if it is.
var dto = courseId.Equals(0) ?
await _db.SingleAsync<Module, ModuleDTO>(s =>
s.Id.Equals(id), include) :
await _db.SingleAsync<Module, ModuleDTO>(s =>
s.Id.Equals(id) && s.CourseId.Equals(courseId), include);
441
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
corresponding id in the model object aren’t equal. If they differ, return 400 Bad
Request.
if (!model.CourseId.Equals(courseId))
return BadRequest("Differing ids");
11. In the Post action, change the parameter used in the Lambda expression for the
AnyAsync method call from model.InstructorId to the courseId parameter and
its type to Course.
12. In the Post action, add the courseId parameter to the Lambda expression for the
SingleAsync method call.
var dto = await _db.SingleAsync<Module, ModuleDTO>(s =>
s.CourseId.Equals(courseId) && s.Id.Equals(id));
13. In the Post action, replace the name of the Courses controller with Modules and
add the courseId parameter to the URI parameters in the GetPathByAction
method that generate the URI for the newly created entity.
var uri = _linkGenerator.GetPathByAction("Get", "Modules", new {
id, courseId });
14. In the Put action, change the parameter used in the Lambda expression from
model.InstructorId to the courseId parameter and the entity to Course for the
AnyAsync method call.
var exists = await _db.AnyAsync<Course>(a =>
a.Id.Equals(courseId));
if (!exists) return BadRequest("Could not find related entity");
15. In the Delete action, add the courseId parameter to the two Lambda
expressions.
var exists = await _db.AnyAsync<Module>(a => a.Id.Equals(id) &&
a.CourseId.Equals(courseId));
16. Copy the AnyAsync<Module> method call and its corresponding if-statement
from the Put action and paste it in below the AnyAsync call in the Delete action.
exists = await _db.AnyAsync<Module>(a => a.Id.Equals(id) &&
a.CourseId.Equals(courseId));
if (!exists) return BadRequest("Could not find entity");
17. Add the course id to the Lambda expression in the DeleteAsync method call.
if (await _db.DeleteAsync<Module>(d => d.Id.Equals(id) &&
d.CourseId.Equals(courseId))) return NoContent();
18. Start the API application and call the action methods with Postman.
442
25. The API Project
Example JSON response from a Get request without included data in Postman:
Note that the course, downloads, and videos properties are null.
URI: http://localhost:6600/api/courses/1/modules/1
{
"id": 1,
"title": "Module 1",
"courseId": 1,
"course": null,
"videos": null,
"downloads": null,
"courseAndModule": "Module 1 ()",
"buttonDTO": {
"courseId": 1,
"moduleId": 0,
"id": 1,
"userId": null,
"itemId": "1"
}
}
Example JSON response from a Get request with included data in Postman:
http://localhost:6600/api/courses/1/modules/1?include=true
{
"id": 1,
"title": "Module 1",
"courseId": 1,
"course": "Course 1",
"videos": [
{
"id": 1,
"title": "Video 1 Title",
"description": "orem ipsum dolor sit amet",
"duration": 50,
"thumbnail": "/images/video1.jpg",
"url": "https://www.youtube.com/embed/BJFyzpBcaCY",
"moduleId": 1,
"courseId": 1,
443
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
URI: http://localhost:6600/api/courses/1/modules
{
"title": "New Module",
444
25. The API Project
"courseId": 1
}
Note that no JSON object is returned with the response when making a Put request; only
the status code 204 No Content is returned. You can check that the entity was updated by
doing a Get request for the updated entity.
URI: http://localhost:6600/api/courses/1/modules/54
{
"id": 54,
"title": "Updated Module",
"courseId": 1
}
Note that no JSON object is posted in the body nor returned with the response when
making a Delete request; only the status code 204 No Content is returned. You can check
that the API removes the entity from the database by making a Get request for the deleted
entity; if the status 404 Not Found is returned, then the entity was successfully removed.
445
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
URI: http://localhost:6600/api/courses/1/modules/54
#region Constructor
public ModulesController(IAdminService db, LinkGenerator
linkGenerator)
{
_db = db;
_linkGenerator = linkGenerator;
}
#endregion
#region Actions
[HttpGet()]
public async Task<ActionResult<List<ModuleDTO>>> Get(int courseId,
bool include = false)
{
try
{
var dtos = courseId.Equals(0) ?
await _db.GetAsync<Module, ModuleDTO>(include) :
await _db.GetAsync<Module, ModuleDTO>(g =>
g.CourseId.Equals(courseId), include);
if (!include)
{
foreach (var dto in dtos)
{
dto.Downloads = null;
dto.Videos = null;
}
}
return dtos;
}
446
25. The API Project
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ModuleDTO>> Get(int id, int courseId,
bool include = false)
{
try
{
var dto = courseId.Equals(0) ?
await _db.SingleAsync<Module, ModuleDTO>(s =>
s.Id.Equals(id), include) :
await _db.SingleAsync<Module, ModuleDTO>(s =>
s.Id.Equals(id) && s.CourseId.Equals(courseId),
include);
return dto;
}
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
}
}
[HttpPost]
public async Task<ActionResult<ModuleDTO>> Post(int courseId,
ModuleDTO model)
{
try
{
if (courseId.Equals(0)) courseId = model.CourseId;
447
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (dto == null)
return BadRequest("Unable to add the entity");
[HttpPut("{id:int}")]
public async Task<ActionResult<ModuleDTO>> Put(int id, int courseId,
ModuleDTO model)
{
try
{
if (model == null) return BadRequest("No entity provided");
if (!id.Equals(model.Id))
return BadRequest("Differing ids");
448
25. The API Project
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id, int courseId)
{
try
{
var exists = await _db.AnyAsync<Module>(a =>
a.Id.Equals(id) && a.CourseId.Equals(courseId));
449
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
To speed up the process of adding the Videos controller, you will copy and modify the
Modules controller and its action methods. The URI should expand the previous URI with
the module id and the videos route: api/courses/{courseId}/modules/{moduleId}/videos.
The parameters named courseId and moduleId must have a corresponding int parameter
with the same names in all the action methods.
450
25. The API Project
12. In the Post action, replace the name of the Modules controller with Videos and
add the moduleId parameter to the URI’s parameters in the GetPathByAction
method that generate the URI for the newly created entity.
var uri = _linkGenerator.GetPathByAction("Get", "Videos", new {
id, courseId, moduleId });
13. In the Put action, add an if-statement below the other two if-statements at the
beginning of the try-block that checks if the Title property in the model object is
null or empty. You can call the IsNullOrEmptyOrWhiteSpace extension method
on the Title property to check the value. Return 400 Bad Request if it is null or
empty.
if (model.Title.IsNullOrEmptyOrWhiteSpace())
return BadRequest("Title is required");
14. In the Put action, copy the AnyAsync<Course> method call and its corresponding
if-statement and paste it in below the code you just copied. Replace the type
with Module and add a check for the moduleId parameter.
exists = await _db.AnyAsync<Module>(a => a.Id.Equals(moduleId) &&
a.CourseId.Equals(courseId));
if (!exists) return NotFound("Could not find related entity");
15. In the Put action, copy the previous AnyAsync<Module> method call and its
corresponding if-statement and paste it in below the code you copied. Replace
the courseId and moduleId parameters with their corresponding values in the
model object.
exists = await _db.AnyAsync<Module>(a =>
451
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
a.Id.Equals(model.ModuleId) &&
a.CourseId.Equals(model.CourseId));
if (!exists) return NotFound("Could not find related entity");
16. In the Delete action, add checks for the courseId and moduleId to the Lambda
expression for the AnyAsync<Video> and DeleteAsync<Video> method call.
var exists = await _db.AnyAsync<Video>(a => a.Id.Equals(id) &&
a.ModuleId.Equals(moduleId) && a.CourseId.Equals(courseId));
if (!exists) return BadRequest("Could not find entity");
if (await _db.DeleteAsync<Video>(d => d.Id.Equals(id) &&
d.ModuleId.Equals(moduleId) && d.CourseId.Equals(courseId)))
return NoContent();
17. Start the API application and call the action methods with Postman.
Example JSON response from a Get request without included data in Postman:
URI: http://localhost:6600/api/courses/1/modules/1/videos/1
{
"id": 1,
"title": "Video 1 Title",
"description": "orem ipsum dolor sit amet",
"duration": 50,
"thumbnail": "/images/video1.jpg",
"url": "https://www.youtube.com/embed/BJFyzpBcaCY",
"moduleId": 1,
"courseId": 1,
"course": null,
"module": null,
"buttonDTO": {
"courseId": 1,
"moduleId": 1,
"id": 1,
"userId": null,
"itemId": "1"
}
}
Example JSON response from a Get request with included data in Postman:
452
25. The API Project
URI: http://localhost:6600/api/courses/1/modules/1/videos/1?include=true
{
"id": 1,
"title": "Video 1 Title",
"description": "orem ipsum dolor sit amet",
"duration": 50,
"thumbnail": "/images/video1.jpg",
"url": "https://www.youtube.com/embed/BJFyzpBcaCY",
"moduleId": 1,
"courseId": 1,
"course": "Course 1",
"module": "Module 1",
"buttonDTO": {
"courseId": 1,
"moduleId": 1,
"id": 1,
"userId": null,
"itemId": "1"
}
}
Example JSON Post request object in Postman creating a video:
URI: http://localhost:6600/api/courses/1/modules/1/videos
{
"title": "New Video Title",
"description": "New Video Description",
"duration": 100,
"thumbnail": "/images/video1.jpg",
"url": "https://www.youtube.com/embed/BJFyzpBcaCY",
"moduleId": 1,
"courseId": 1
}
453
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
"duration": 100,
"thumbnail": "/images/video1.jpg",
"url": "https://www.youtube.com/embed/BJFyzpBcaCY",
"moduleId": 1,
"courseId": 1,
"course": "Course 1",
"module": "Module 1",
"buttonDTO": {
"courseId": 1,
"moduleId": 1,
"id": 53,
"userId": null,
"itemId": "53"
}
}
Note that no JSON object is returned with the response when making a Put request; only
the status code 204 No Content is returned. You can check for the updated entity by
making a Get request for it.
URI: http://localhost:6600/api/courses/1/modules/1/videos/53
{
"id": 53,
"title": "Updated Video Title",
"description": "Updated Video Description",
"duration": 100,
"thumbnail": "/images/video1.jpg",
"url": "https://www.youtube.com/embed/BJFyzpBcaCY",
"moduleId": 1,
"courseId": 1
}
Note that no JSON object is posted in the body nor returned with the response when
making a Delete request; only the status code 204 No Content is returned. You can check
that the API removes the entity from the database by making a Get request for the deleted
entity; if the status 404 Not Found is returned, then the entity was successfully removed.
454
25. The API Project
URI: http://localhost:6600/api/courses/1/modules/1/videos/53
#region Constructor
public VideosController(IAdminService db, LinkGenerator
linkGenerator)
{
_db = db;
_linkGenerator = linkGenerator;
}
#endregion
#region Actions
[HttpGet()]
public async Task<ActionResult<List<VideoDTO>>> Get(int courseId,
int moduleId, bool include = false)
{
try
{
var dtos = courseId.Equals(0) ?
await _db.GetAsync<Video, VideoDTO>(include) :
await _db.GetAsync<Video, VideoDTO>(g =>
g.CourseId.Equals(courseId) &&
g.ModuleId.Equals(moduleId), include);
return dtos;
}
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
}
}
455
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
[HttpGet("{id:int}")]
public async Task<ActionResult<VideoDTO>> Get(int id, int courseId,
int moduleId, bool include = false)
{
try
{
var dto = courseId.Equals(0) ?
await _db.SingleAsync<Video, VideoDTO>(s =>
s.Id.Equals(id), include) :
await _db.SingleAsync<Video, VideoDTO>(s =>
s.Id.Equals(id) && s.CourseId.Equals(courseId) &&
s.ModuleId.Equals(moduleId), include);
return dto;
}
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
}
}
[HttpPost]
public async Task<ActionResult<VideoDTO>> Post(int courseId,
int moduleId, VideoDTO model)
{
try
{
if (courseId.Equals(0)) courseId = model.CourseId;
if (moduleId.Equals(0)) moduleId = model.ModuleId;
if (model == null) return BadRequest("No entity provided");
if (!model.CourseId.Equals(courseId))
return BadRequest("Differing ids");
if (model.Title.IsNullOrEmptyOrWhiteSpace())
return BadRequest("Title is required");
456
25. The API Project
[HttpPut("{id:int}")]
public async Task<ActionResult<VideoDTO>> Put(int id, int courseId,
int moduleId, VideoDTO model)
{
try
{
if (model == null) return BadRequest("No entity provided");
if (!id.Equals(model.Id))
return BadRequest("Differing ids");
if (model.Title.IsNullOrEmptyOrWhiteSpace())
return BadRequest("Title is required");
457
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (!exists)
return NotFound("Could not find related entity");
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id, int courseId,
int moduleId)
{
try
{
var exists = await _db.AnyAsync<Video>(a => a.Id.Equals(id)
&& a.ModuleId.Equals(moduleId)
&& a.CourseId.Equals(courseId));
if (!exists) return BadRequest("Could not find entity");
458
25. The API Project
To speed up the process of adding the Downloads controller, you will copy and modify the
Videos controller and its action methods. Replace videos with downloads in the URI:
api/courses/{courseId}/modules/{moduleId}/downloads.
Example JSON response from a Get request without included data in Postman:
URI: http://localhost:6600/api/courses/1/modules/1/downloads/1
{
"id": 1,
"title": "ADO.NET 1 (PDF)",
"url": "https://some-url",
"moduleId": 1,
"courseId": 1,
459
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Example JSON response from a Get request with included data in Postman:
URI: http://localhost:6600/api/courses/1/modules/1/downloads/1?include=true
{
"id": 1,
"title": "ADO.NET 1 (PDF)",
"url": "https://some-url",
"moduleId": 1,
"courseId": 1,
"course": "Course 1",
"module": "Modeule 1",
"buttonDTO": {
"courseId": 1,
"moduleId": 1,
"id": 1,
"userId": null,
"itemId": "1"
}
}
URI: http://localhost:6600/api/courses/1/modules/1/downloads
{
"title": "New Download",
"url": "https://new-url",
"moduleId": 1,
"courseId": 1
460
25. The API Project
Note that no JSON object is returned with the response when making a Put request; only
the status code 204 No Content is returned. You can check the updated entity by making
a Get request for it.
URI: http://localhost:6600/api/courses/1/modules/1/downloads/25
{
"id": 25,
"title": "Updated Download",
"url": "https://updated-url",
"moduleId": 1,
"courseId": 1
}
461
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
that the API removes the entity from the database by making a Get request for the deleted
entity; if the status 404 Not Found is returned, then the entity was successfully removed.
URI: http://localhost:6600/api/courses/1/modules/1/downloads/25
#region Constructor
public DownloadsController(IAdminService db, IMapper mapper,
LinkGenerator linkGenerator)
{
_db = db;
_mapper = mapper;
_linkGenerator = linkGenerator;
}
#endregion
#region Actions
[HttpGet()]
public async Task<ActionResult<List<DownloadDTO>>> Get(int courseId,
int moduleId, bool include = false)
{
try
{
return courseId.Equals(0) || moduleId.Equals(0) ?
await _db.GetAsync<Download, DownloadDTO>(include) :
await _db.GetAsync<Download, DownloadDTO>(g =>
g.CourseId.Equals(courseId) &&
g.ModuleId.Equals(moduleId), include);
}
catch
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Database Failure");
462
25. The API Project
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<DownloadDTO>> Get(int id,
int courseId, int moduleId)
{
try
{
var entity = courseId.Equals(0) || moduleId.Equals(0) ?
await _db.SingleAsync<Download, DownloadDTO>(s =>
s.Id.Equals(id), true) :
await _db.SingleAsync<Download, DownloadDTO>(s =>
s.CourseId.Equals(courseId) &&
s.ModuleId.Equals(moduleId) && s.Id.Equals(id),
true);
[HttpPost]
public async Task<ActionResult<DownloadDTO>> Post(DownloadDTO model,
int courseId, int moduleId)
{
try
{
if (courseId.Equals(0)) courseId = model.CourseId;
if (moduleId.Equals(0)) moduleId = model.ModuleId;
if (model == null) return BadRequest("No entity provided");
if (!model.CourseId.Equals(courseId))
return BadRequest("Differing ids");
if (model.Title.IsNullOrEmptyOrWhiteSpace())
return BadRequest("Title is required");
463
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
[HttpPut("{id:int}")]
public async Task<IActionResult> Put(int courseId, int moduleId,
int id, DownloadDTO model)
{
try
{
if (model == null) return BadRequest("Missing entity");
if (!model.Id.Equals(id)) return BadRequest(
"Differing ids");
464
25. The API Project
if (model.Title.IsNullOrEmptyOrWhiteSpace())
return BadRequest("Title is required");
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id, int courseId,
int moduleId)
{
try
{
var exists = await _db.AnyAsync<Course>(g =>
g.Id.Equals(courseId));
if (!exists) return BadRequest(
465
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Summary
In this chapter, you built an API with multiple controllers that perform CRUD operations in
the database. You then tested the API using the Postman tool.
In the next chapter, you will implement an HttpClientFactory service that will handle the
HttpClient instances used when calling the API from the AdminAPIService service you will
implement using the IAdminService interface. By using that interface, you ensure that all
Razor Pages will continue to work without any refactoring.
You will use reflection to build the Uris needed to call the API from properties in generic
types, objects, and Lambda expressions.
466
25. The API Project
467
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
468
26. Calling The API With HttpClient
Reflection into generic types poses a challenge when the CRUD methods have Lambda
expressions; this means that you will have to tackle reflection for Lambda expressions,
which is more involved than reflection for regular types.
There are several ways HttpClient can integrate with an API. Here we will focus on one
that is a best practice by Microsoft, which uses an HTTP Client Factory released with
ASP.NET Core 2.1.
469
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
What is HttpClient?
HTTP is a request-response protocol between a client and a server, where the client
requests information from the server and the server sends a response back with a status
code and data. A web browser is such a client, but a more practical approach when
building an API is to use a tool, such as Postman as you did in the previous chapter. In this
chapter, you will use HttpClient to call the API from the Razor Pages in the Admin UI
project.
In the previous chapter, you learned that the client, which in that scenario was Postman,
can send HTTP Requests to the API on the server by specifying a verb, headers, and in some
cases content.
And that the server will send a response message back with a Status Code, Content
Headers, and in some cases data.
In this chapter you will use HttpClient, instead of Postman, to send requests and receive
responses from the API on the server. It does this by using one or more message handlers,
470
26. Calling The API With HttpClient
which are instances of the HttpMessageHandler class, that are executed by an instance of
the HttpClientHandler class. Each message handler can opt to pass it to the next handler
in the pipeline or send a response back; one such scenario is a caching handler that returns
cached data if available. This pattern is called the Delegating Handler Pattern.
An all too familiar way of using the HttpClient in examples on the Internet is to wrap the
creation of the instance in a using statement that will dispose of the object when finished;
Microsoft does not recommend this because instances of the HttpClient are meant to be
long-lived and reused. When disposing of an HttpClient instance, it disposes of the
underlying HttpClientHandler, and closes its connection; this can lead to performance
issues because reopening a connection takes time. In addition to that, you may run out of
sockets to create a new connection because of the time it takes to free up the disposed of
resources.
public class BadExample
{
private HttpClient _httpClient = new HttpClient();
public Example()
{
_httpClient.BaseAddress = new Uri("http://localhost:6600");
_httpClient.Timeout = new TimeSpan(0, 0, 30);
}
Another way to use HttpClient is to call one of its asynchronous methods, GetAsync in this
example, which returns a Task<HttpResponseMessage> with the status code and data
from the API. The method calls the EnsureSuccessStatusCode after the response has
returned to ensure that the request throws an exception if unsuccessful.
471
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The data returned in the response variable’s Content property isn’t readable text; to get
the data you must parse it into a string, byte array, or read it as a stream. You will use a
stream later in this chapter. The ReadStringAsync method will return the data as a JSON
object or array.
The JSON data deserializes into objects of the desired type with the JsonConvert.Deserial-
izeObject method, which is in the Newtonsoft.Json namespace that requires the Newton-
soft.Json NuGet package to be installed.
But why did the API return JSON? It did so because it is the default content type for the
API. It is not uncommon for an API to provide multiple content types such as JSON and
XML. You can change the formatter used by configuring it for the AddMvc method in the
ConfigureServices method in the Startup class, but you must remember to implement an
XML de-serializer and ask for that format in the request headers.
services.AddMvc(options =>
{
options.OutputFormatters.Insert(0, XmlSerializerOutputFormatter());
options.InputFormatters.Insert(0, XmlSerializerInputFormatter(
options));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
HTTP Headers
You can send additional information with a request or response by adding it to its
Headers section. A header value has a case-insensitive name followed by a colon and its
value. If the value consists of multiple value parts, then separate them with commas.
Name: value
Name: value1, value2
472
26. Calling The API With HttpClient
The client provides request headers that contain data about the resource or the client
itself. One example is the Accept header that tells the API which data type(s) the client
wants the response to return in.
Accept: application/json
Accept: application/json, text/html
The response headers can contain information generated by the API, such as the Content-
Type that tells the client how to deserialize the result.
Content-Type: application/json
It is best practice to provide the necessary headers because it improves reliability and is
mandatory in a RESTful API.
The content negotiation mechanism (that determines the format(s) the client expects and
the format(s) the server provides) depends on what Accept headers the request has and
the ContentType header of the response.
You can add Accept headers by calling the Add method on the DefaultRequestHeaders
collection of the _httpClient object in the constructor. It is best to clear the collection be-
fore adding your own headers.
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/xml"));
Using multiple media types requires you to implement de-serializers for all scenarios; in
the example above both JSON and XML are acceptable return types.
List<Course> courses;
if (response.Content.Headers.ContentType.MediaType ==
"application/json")
{
courses = JsonConvert.DeserializeObject<List<Course>>(content);
}
else if (response.Content.Headers.ContentType.MediaType ==
"application/xml")
{
var serializer = new XmlSerializer(typeof(List<Course>));
473
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
courses = (List<Course>)serializer.Deserialize(
new StringReader(content));
}
Note that the Accept headers will be the same for all requests when implementing the
headers directly on the HttpClient instance in the constructor. A better way to implement
the Accept headers and call the API is to use an instance of the HttpRequestMessage class.
public async Task Get()
{
var request = new HttpRequestMessage(HttpMethod.Get, "api/courses");
request.Headers.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
The preferred SendAsync method can handle any request as opposed to the GetAsync,
PostAsync, PutAsync, and DeleteAsync shortcut methods. Important to note is that the
shortcut methods don’t set or send Accept headers; you assign them on HttpClient level,
which might not be allowed or work in your solution; and assigning Accept headers for
each call is required in a RESTful system. As you have learned earlier, assigning the headers
on the HttpClient means that all requests will have the same headers, which means that
you would have to dispose of the current HttpClient and create a new instance to change
the Accept headers.
474
26. Calling The API With HttpClient
To use a stream, you create a HttpRequestMessage object, call the API with the Send-
Async method, and check for a successful response. But instead of calling the ReadAs-
StringAsync to read the data as a string, you call the RadAsStreamAsync method and use
a StreamReader object with JsonTextReader and JsonSerializer objects to read the data
as a stream, bypassing the in-memory storage that a string requires.
var content = await response.Content.ReadAsStreamAsync();
475
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
There is a huge drawback in the way the previous example with the stream treats data; all
the data must arrive before the stream can begin to deserialize it. To improve the
performance further, you can manipulate the HttpCompletionMode to begin streaming
as soon as the response headers arrive instead of waiting until all data has arrived.
Because we only wait for the headers to arrive, the data can begin streaming as soon as it
arrives. The only thing you must do for this to work is to pass in HttpCompletionMode.
ResponseHeadersRead as a second parameter to the SendAsync method.
var response = await _httpClient.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead);
You can also use a stream when posting data to the API. To achieve this, you use a
MemoryStream object to hold the serialized object that will be sent to the API and use a
StreamWriter object to write the data into the MemoryStream. Remember to keep the
stream open after the data has been written to the MemoryStream because it will be used
later when posting the data to the API. Then use a JsonTextWriter with the StreamWriter
to serialize the data into JSON with a JsonSerializer object. Don’t forget to flush the text
writer to ensure that the writer serializes all bytes.
476
26. Calling The API With HttpClient
When the data is in the MemoryStream, you must reset the stream to its start position to
be able to read it when posting the data. Then you use an HttpRequestMessage object
with its HttpMethod set to Post to call the API. As before, you must set the accept headers
to application/json on the request object to tell the API to serialize the data to JSON. Then,
create a StreamContent object that will be used to stream the Course object from the
MemoryStream to the API. Use the Content property of the request object to assign the
streamed content to its body and specify the ContentType as application/json to signal to
the server that the data is in JSON format.
Call the SendAsync method on the HttpClient object and pass in the request object to the
method. After ensuring a successful post, you can read the created object from the res-
ponse with the ReadAsStreamAsync method, much like you have done before.
contentStream.Seek(0, SeekOrigin.Begin);
// Create a request object that will send the streamed data to the API.
using (var request = new HttpRequestMessage(HttpMethod.Post,
"api/courses"))
{
477
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(
"application/json"));
You might encounter slower performance with streams in certain scenarios, but it is im-
portant to look at the big picture here; speed isn’t everything. Memory usage is also
important when creating temporary strings that requires the Garbage Collection to work
harder. When the Garbage Collection is used excessively the memory can start to fill up.
You should always strive to create as few objects as possible and release them to Garbage
Collection as soon as possible.
478
26. Calling The API With HttpClient
There is one more thing we can do to improve the performance, and that is to use
compression when sending data. To add compression, you add an AcceptEncoding
property to the headers and specify the type of compression you want to use; gzip is
available out-of-the-box.
request.Headers.AcceptEncoding.Add(
new StringWithQualityHeaderValue("gzip"));
You also need to enable gzip compression when the HttpClient object is instantiated.
private HttpClient _httpClient = new HttpClient(new HttpClientHandler()
{ AutomaticDecompression = DecompressionMethods.GZip });
Supporting Cancellation
Canceling a request can be beneficial to performance in that long-running tasks can be
stopped to free up resources that slow down or freeze the browser.
Because HttpClient is asynchronous and works with Tasks, canceling long-running re-
quests is possible if, for instance, the visitor to the site navigates away from the page
because it takes too long to load the information. Fetching a large resource can be
canceled instead of it continuing in the background. Here, you will learn how to prepare
the application for cancellation.
The application must handle two scenarios: the first is when you cancel the Task – when
the user decides to leave the page or with some other means cancels the loading of a
resource; the other handles timeouts gracefully.
The HttpClient listens for cancellation requests and stops the running Task(s) if
cancellation is triggered.
The CancellationTokenSource has two methods that can be used to cancel a request: the
first is Cancel that, let’s say, a button calls; the other is CancelAfter that cancels the
request after a set amount of time.
var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(3000);
479
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
After the time has elapsed, the method cancels the request and throws a TaskCancelled-
Exception, which a try/catch-block handles with cleanup code in its catch-block.
public CancellationExample()
{
_httpClient.BaseAddress = new Uri("http://localhost:6600");
_httpClient.Timeout = new TimeSpan(0, 0, 30);
_httpClient.DefaultRequestHeaders.Clear();
}
try
{
using (var response = await _httpClient.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken))
480
26. Calling The API With HttpClient
{
response.EnsureSuccessStatusCode();
var content = await response.Content
.ReadAsStreamAsync();
Using a HttpClientFactory
You have already learned that you shouldn’t use a using-block with the HttpClient since it
closes the underlying HttpMessageHandler and its connection, which can lead to
performance issues and socket exhaustion. What you want to do is to keep the HttpClient
and its connection open for multiple calls; the seemingly obvious solution would be to
create a static instance of the HttpClient, but that is not advisable because it will retain its
settings when you want them to change; for instance, when changing environment from
development to production. It can also cause problems with the Azure Platform as a
Service (PaaS) cloud services.
using (var httpClient = new HttpClient())
{
}
481
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Apart from solving socket exhaustion and DNS problems when switching environment, it
also gives you a central location for naming and configuring HttpClients; for instance,
when communicating with more than one API or microservice.
You can configure the HttpClient(s) that you want to use as named clients in the Config-
ureServices method in the Startup class; here you can specify a base address, timeout,
clear the request headers, and specify a decompression method. You can also create typed
HttpClients, but that is beyond the scope of this book.
services.AddHttpClient("AdminClient", client =>
{
client.BaseAddress = new Uri("http://localhost:6600");
client.Timeout = new TimeSpan(0, 0, 30);
client.DefaultRequestHeaders.Clear();
}).ConfigurePrimaryHttpMessageHandler(handler => new HttpClientHandler()
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip
});
Then you use dependency injection to receive an instance through the IHttpClientFactory
interface that creates the HttpClient(s) and handles the pool of handlers.
public class HttpClientFactoryExample
{
private readonly IHttpClientFactory _factory;
private CancellationTokenSource cancellationTokenSource =
new CancellationTokenSource();
482
26. Calling The API With HttpClient
Handling Errors
It’s important to handle errors gracefully to keep the application from crashing. You can
inspect the response’s IsSuccessStatusCode property to see if an error has occurred.
Inspect the response’s StatusCode property to find out more about the error. You let the
EnsureSuccessStatusCode method handle all other errors.
483
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
response.EnsureSuccessStatusCode();
}
}
Sometimes inspecting the status codes aren’t enough; in those cases, you might have to
read more information from the response body. One such scenario would be handling
validation errors that can contain more information than only a status code.
else if (response.StatusCode.Equals(HttpStatusCode.UnprocessableEntity))
{
// Status Code: 422. Read response body.
var errorStream = await response.Content.ReadAsStreamAsync();
484
26. Calling The API With HttpClient
1. Open the Startup class in the Admin project and locate the ConfigureServices
and configure the AdminClient HttpClient service described earlier.
services.AddHttpClient("AdminClient", client =>
{
client.BaseAddress = new Uri("http://localhost:6600");
client.Timeout = new TimeSpan(0, 0, 30);
client.DefaultRequestHeaders.Clear();
}).ConfigurePrimaryHttpMessageHandler(handler =>
new HttpClientHandler () {
AutomaticDecompression = System.Net.DecompressionMethods.GZip
});
2. Add a public class called AdminAPIService to the Services folder in the Common
project. You add it to the Common project because it won’t use the database
directly.
3. Implement the IAdminService interface in the class; this should add its methods
implemented with NotImplementedException exception to the class.
4. Add a new public empty interface named IHttpClientFactoryService to the
Services folder in the Common project; you will add methods to the interface
later.
public interface IHttpClientFactoryService
{
}
485
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
public HttpClientFactoryService(IHttpClientFactory
httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
8. Add a CancellationTokenSource variable to the HttpClientFactoryService class
and instantiate it in the constructor.
private CancellationTokenSource _cancellationTokenSource;
9. Add a CancellationToken variable to the class and assign the Token property
from the _cancellationTokenSource object in the constructor. By adding the
cancellation token to the class instead of inside the method, you can reuse it and
can cancel many requests at the same time.
private readonly CancellationToken _cancellationToken;
10. Open the Startup class in the Admin project and replace the AdminEFService
class with AdminAPIService class in the IAdminService service declaration.
services.AddScoped<IAdminService, AdminAPIService>();
11. In the Startup class, add a service declaration for the IHttpClientFactoryService
interface and HttpClientFactoryService class.
486
26. Calling The API With HttpClient
services.AddScoped<IHttpClientFactoryService,
HttpClientFactoryService>();
12. Save all files.
#region Constructor
public HttpClientFactoryService(IHttpClientFactory
httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
}
#endregion
}
#region Constructor
public AdminAPIService(IHttpClientFactoryService http)
{
_http = http;
}
#endregion
#region Methods
public Task<bool> AnyAsync<TEntity>(Expression<Func<TEntity, bool>>
expression) where TEntity : class
{
487
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
488
26. Calling The API With HttpClient
489
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Add a public static class named StreamExtensions to the Extensions folder in the
Common project.
2. Add an asynchronous method named SerializeToJsonAndWriteAsync that
returns a Task and has six parameters.
a. stream of type Stream; is the stream that the method works on.
b. objectToWrite of type T; is the data to serialize.
c. encoding of type Encoding; is the data encoding to use for the data.
d. bufferSize of type int; determines how many bytes of the stream that
will be processed at a time.
e. leaveOpen of type bool; keeps the stream open after the initial
StreamWriter disposes of it true.
f. resetStream of type bool; restes the stream to position 0 after writing it
if true.
public static async Task SerializeToJsonAndWriteAsync<T>(
this Stream stream, T objectToWrite, Encoding encoding,
int bufferSize, bool leaveOpen, bool resetStream = false) { }
3. Return exceptions if the stream or encoding parameters are null, or if it isn’t
possible to write to the stream.
490
26. Calling The API With HttpClient
491
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
492
26. Calling The API With HttpClient
2. Return exceptions if the stream is null, or if it isn’t possible to read from the
stream.
3. Add a using-block for the StreamReader that that will deserialize the JSON data
into an instance of the object type.
493
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
4. Add a using-block for the JsonTextreader that uses the stream inside the
previous using-block to read JSON through the stream object.
5. Inside the second using-block, create an instance of the JsonSerializer and store
it in a variable named jsonSerializer.
6. Call the Deserialize method on the jsonSerializer object and pass in the
jsonTextReader instance to convert the JSON data into an instance of the object
type.
7. Save the file.
You will add methods to this class as you add functionality to the HttpClientFactory class.
494
26. Calling The API With HttpClient
495
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (!token.IsNullOrEmptyOrWhiteSpace())
requestHeader.Headers.Authorization =
new AuthenticationHeaderValue("bearer", token);
if (httpMethod.Equals(HttpMethod.Get))
requestHeader.Headers.AcceptEncoding.Add(
new StringWithQualityHeaderValue("gzip"));
return requestHeader;
}
496
26. Calling The API With HttpClient
{
var stream = new MemoryStream();
await stream.SerializeToJsonAndWriteAsync(content,
new UTF8Encoding(), 1024, true);
stream.Seek(0, SeekOrigin.Begin);
requestMessage.Content.Headers.ContentType =
new MediaTypeHeaderValue("application/json");
497
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
return requestMessage;
}
498
26. Calling The API With HttpClient
2. Check if the response has an error with its IsSuccessStatusCode property and, if
successful, calls the EnsureSuccessStatusCode method on the response object to
be certain that no other errors have occurred.
if (!response.IsSuccessStatusCode) { }
else response.EnsureSuccessStatusCode();
3. Inside the previous if-block, add an object variable named validationErrors and
assign null to it. Add a string variable named message and assign it an empty
string.
4. Inside the previous if-block, add an if-block that checks for the 422
Unprocessable Entity status code. If returning that status code, deserializing the
response content for validation error messages is possible.
if (response.StatusCode == HttpStatusCode.UnprocessableEntity)
{
var errorStream = await response.Content.ReadAsStreamAsync();
validationErrors = errorStream.ReadAndDeserializeFromJson();
message = "Could not process the entity.";
}
5. Add else if-blocks for the most frequently returned errors (400 Bad Request, 401
Unauthorized, 403 Forbidden, 404 Not Found) and assign an appropriate
message to the message variable.
6. Throw an HttpResponseException exception at the end of the if-block and pass
in the validationErrors and message variables.
throw new HttpResponseException(response.StatusCode, message,
validationErrors);
499
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (response.StatusCode == HttpStatusCode.UnprocessableEntity)
{
var errorStream =
await response.Content.ReadAsStreamAsync();
validationErrors = errorStream.ReadAndDeserializeFromJson();
message = "Could not process the entity.";
}
else if (response.StatusCode == HttpStatusCode.BadRequest)
message = "Bad request.";
else if (response.StatusCode == HttpStatusCode.Forbidden)
message = "Access denied.";
else if (response.StatusCode == HttpStatusCode.NotFound)
message = "Could not find the entity.";
else if (response.StatusCode == HttpStatusCode.Unauthorized)
message = "Not Logged In.";
500
26. Calling The API With HttpClient
used in the API because all records will be returned from the database; they are only
needed to create a valid URI.
Dictionary<string, object> _properties = new Dictionary<string,
object>();
4. Get the generic TSource type and store it in a variable named type.
var type = typeof(TSource);
5. Call the GetProperty reflection method on the type variable to try to fetch the Id,
CourseId, and ModuleId properties if they exist and store them in variables with
the same names; if a property doesn’t exist, its variable will contain null.
var id = type.GetProperty("Id");
var moduleId = type.GetProperty("ModuleId");
var courseId = type.GetProperty("CourseId");
6. Check each property variable and add its name and the value 0 to the _properties
collection if it exists. Note that the key name is in camel-case because the route
ids for the controllers are in camel-case.
if (id != null) _properties.Add("id", 0);
if (moduleId != null) _properties.Add("moduleId", 0);
if (courseId != null) _properties.Add("courseId", 0);
7. Add a try-block that surrounds the NotImplementedException in the GetAsync
method that only has a bool parameter. Add a catch-block that throws the
exception up the call chain.
8. Call the GetProperties method above the throw-statement in the try-block.
GetProperties<TSource>();
9. Place a breakpoint on the row with the NotImplementedException.
10. Start the Admin application with debugging and click the Instructor card on the
main page.
501
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
11. When the execution halts, inspect the _properties collection and add 0 to a key
named for the Id property in the Instructor entity.
12. Stop the application in Visual Studio.
Create a URI with the Generic Type and the Ids in the _properties Collection
To create a URI, you need to use the id names and values in the _properties collection and
the name of the generic TSource type that defines the methods. Because the URI has the
entity names and their ids, you need to send the values from the properties in the
collection even if they are 0. The code you add to the Get actions later ignores 0 values.
502
26. Calling The API With HttpClient
api/instructors
api/courses
api/courses/{courseId}
api/courses/{courseId}/modules
api/courses/{courseId}/modules/{moduleId}
api/courses/{courseId}/modules/{moduleId}/videos
The FormatUriWithoutIds<TSource>() method should try to fetch the value for the
courseId and moduleId keys in the collection and add their path to the URI if they exist
and return the finished URI.
object courseId;
bool succeeded = _properties.TryGetValue("courseId", out courseId);
if (succeeded) uri = $"{uri}/courses/0";
503
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
9. Start the Admin application with debugging and click the Instructor card on the
main page.
10. When the execution halts at the breakpoint, inspect the uri variable in the
GetAsync method and make sure that it contains a valid URI.
11. Stop the application in Visual Studio.
uri = $"{uri}/{typeof(TSource).Name}s";
return uri;
}
504
26. Calling The API With HttpClient
The GetListAsync method should return a list of TResponse objects wrapped in a Task
because the method is asynchronous and calls other asynchronous methods; it should also
have tree string parameters named uri, serviceName, and token. The uri parameter will
contain the URI from the FormatUriWithoutIds method call; the serviceName parameter
will contain the name of the HttpClient that you have configured in the Startup class; the
token parameter will be used in the next chapter when you implement authorization with
JSON Web Tokens.
Because you might want to inspect the HTTP status code and validation errors when an
exception has occurred, you will throw an HttpResponseException exception.
505
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The code for the GetListAsync extension method in the HttpClientExtensions class:
public static async Task<List<TResponse>> GetListAsync<TResponse>(
this HttpClient client, string uri, CancellationToken cancellationToken,
string token = "")
{
try
{
var requestMessage = uri.CreateRequestHeaders(HttpMethod.Get,
token);
506
26. Calling The API With HttpClient
The complete code for the GetListAsync method in the HttpClientFactoryService class:
public async Task<List<TResponse>> GetListAsync<TResponse>(string uri,
string serviceName, string token = "") where TResponse : class
{
try
{
if (new string[] { uri, serviceName }
.IsNullOrEmptyOrWhiteSpace())
throw new HttpResponseException(HttpStatusCode.NotFound,
"Could not find the resource");
507
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The complete code for the GetAsync method in the AdminAPIService class:
public async Task<List<TDestination>> GetAsync<TSource,
TDestination>(bool include = false) where TSource : class where
TDestination : class
{
try
{
GetProperties<TSource>();
string uri = FormatUriWithoutIds<TSource>();
return await _http.GetListAsync<TDestination>($"{uri}?include=
{include.ToString()}", "AdminClient");
}
catch
{
throw;
}
508
26. Calling The API With HttpClient
To find out the ids to use in the URI when calling the API, you must use reflection to extract
them from the Lambda expression passed in to the GetProperties method; this is trickier
than finding ids in a generic type because recursion is involved when several ids are part
of the Lambda.
The GetProperties method calls a method named ResolveExpression that picks apart the
Lambda expression and uses recursion to fetch the left and right parts of each binary
expression that contains an and (&&) operator. For each left and right expression, the
GetExpressionProperties method is called to extract the property and value from the
expression.
If you want to deepen your knowledge about reflection and Lambda expressions, then the
LiteDB NoSQL Document Store is an excellent example you can browse on GitHub. You can
find the code here: https://github.com/mbdavid/LiteDB.
Each part of the Lambda has a left and a right part, where the left is a MemberExpression
that contains the name of the property in the Lambda comparison and a ConstantExpres-
sion that holds the comparison value.
Consider the following Lambda expression; its left part is the FirstName property, and its
right part is the name Luke.
p => p.FirstName.Equals("Luke")
LambdaExpression
{
Body = BinaryExpression
{
Left = BinaryExpression
{
NodeType = Equal,
Left = MemberExpression
{
509
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
Member.Name = "FirstName"
},
Right = ConstantExpression
{
Value = "Luke"
}
}
}
}
Consider the following Lambda expression that has two parts; its left part is the Binary-
Expression for the FirstName property and its right part is another BinaryExpression for
the LastName property.
p => p.FirstName.Equals("Luke") && p.LastName.Equals("Skywalker")
LambdaExpression
{
Body = BinaryExpression
{
Left = BinaryExpression
{
NodeType = Equal,
Left = MemberExpression
{
Member.Name = "FirstName"
},
Right = ConstantExpression
{
Value = "Luke"
}
},
Right = BinaryExpression
{
NodeType = Equal,
Left = MemberExpression
{
Member.Name = "LastName"
},
Right = ConstantExpression
{
Value = "Skywalker"
510
26. Calling The API With HttpClient
}
}
}
}
Consider the following Lambda expression that has three parts; its left part is a Binary-
Expression with a left BinaryExpression part for the FirstName property and a right
BinaryExpression part for the LastName property; the third expression is the right Binary-
Expression part of the first BinaryExpression for the Age property.
p => p.FirstName.Equals("Luke") && p.LastName.Equals("Skywalker") &&
p.Age < 50
An AndAlso node type combines the two BinaryExpressions. The outer left BinaryExpress-
ion contains two other BinaryExpressions for the FirstName and LastName properties,
and the outer right BinaryExpression contains the Age property.
LambdaExpression
{
Body = BinaryExpression
{
NodeType = AndAlso,
Left = BinaryExpression
{
NodeType = AndAlso,
Left = BinaryExpression
{
NodeType = Equal,
Left = MemberExpression
{
Member.Name = "FirstName"
},
Right = ConstantExpression
{
Value = "Luke"
}
},
Right = BinaryExpression
{
NodeType = Equal,
Left = MemberExpression
{
Member.Name = "LastName"
511
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
},
Right = ConstantExpression
{
Value = "Skywalker"
}
}
},
Right = BinaryExpression
{
NodeType = LessThan,
Left = MemberExpression
{
Member.Name = "Age"
},
Right = ConstantExpression
{
Value = 50
}
}
}
}
As you can see from the examples, recursion will be necessary to find all the properties
and values in the Lambda expression. Let’s make it a little easier by restricting the Lambda
expression to use and (&&) to combine the individual property expressions and the Equals
method to compare the values of the property and the comparison value. Note that ==
and Equals are evaluated differently by the logic, so to be consistent, you will only imple-
ment the code for the Equals method.
512
26. Calling The API With HttpClient
This void method should have an Expression parameter that will contain a single express-
ion from the Lambda expression; an expression for one property comparison, for example:
p.FirstName.Equals("Luke")
3. Fetch the first argument from the body object’s Arguments collection.
var argument = body.Arguments[0];
4. Add an if-block that checks that the argument is a MemberExpression.
if (argument is MemberExpression) { }
5. Inside the if-block, cast the argument to MemberExpression and store it in a
variable named memberExpression.
var memberExpression = (MemberExpression)argument;
513
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (argument is MemberExpression)
{
var memberExpression = (MemberExpression)argument;
var value = ((FieldInfo)memberExpression.Member).GetValue(
((ConstantExpression)memberExpression.Expression).Value);
_properties.Add(memberExpression.Member.Name, value);
}
}
Inspect the node type for each expression to find out the type of expression; if it contains
an and (&&) operator for instance, and if it calls the ResolveExpression method for each
expression, left and right. You can, of course, check for other expression types such as or
(||) if you like, but it’s not necessary for this solution.
514
26. Calling The API With HttpClient
7. Fetch the properties of the expression inside the else if-block by calling the Get-
ExpressionProperties method and pass in the expression variable.
GetExpressionProperties(expression);
515
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
7. Use the value in the typeName variable to check that the name isn’t Instructor or
Course, which corresponds to one of the two top-level entities Instructor and
Course. Also, check that the courseId property isn’t already in the _properties
dictionary collection. If the expression is true, then add the courseId property
with the value 0; this is necessary because there must always be a course id
available in the collection to create a correct URI if the URI isn’t for a top-level
route.
if (!typeName.Equals("Instructor") && !typeName.Equals("Course")
&& !_properties.ContainsKey("courseId"))
_properties.Add("courseId", 0);
8. Save all files.
_properties.Clear();
ResolveExpression(lambda.Body);
516
26. Calling The API With HttpClient
if (!typeName.Equals("Instructor") &&
!typeName.Equals("Course") &&
!_properties.ContainsKey("courseId"))
_properties.Add("courseId", 0);
}
catch { throw; }
}
517
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
throw;
}
518
26. Calling The API With HttpClient
return uri;
}
519
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
1. Open the HttpClientExtensions class and copy the GetListAsync method and
rename the copy GetAsync.
2. Add another generic type named TRequest to the method definition.
3. Add a TRequest parameter named content to the left of the token parameter.
4. Change the return type of the GetAsync method to a single instance.
public static async Task<TResponse> GetAsync<TResponse, TRequest>(
this HttpClient client, string uri, CancellationToken
cancellationToken, TRequest content, string token = "")
5. Below the requestMessage variable, call the CreateRequestContent method on
the requestMessage variable and pass in the content parameter to it if the
content parameter has a value.
if (content != null) await requestMessage.CreateRequestContent(
content);
if (content != null)
await requestMessage.CreateRequestContent(content);
520
26. Calling The API With HttpClient
await response.CheckStatusCodes();
return stream.ReadAndDeserializeFromJson<TResponse>();
}
}
catch
{
throw;
}
}
The complete code for the GetAsync method in the HttpClientFactoryService class:
public async Task<TResponse> GetAsync<TResponse>(string uri, string
serviceName, string token = "")
{
try
{
if (new []{ uri, serviceName }.IsNullOrEmptyOrWhiteSpace())
throw new HttpResponseException(HttpStatusCode.NotFound,
"Could not find the resource");
521
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
catch
{
throw;
}
}
The complete code for the SingleAsync method in the AdminAPIService class:
public async Task<TDestination> SingleAsync<TSource, TDestination>(
Expression<Func<TSource, bool>> expression, bool include = false)
where TSource : class where TDestination : class
{
try
{
GetProperties(expression);
string uri = FormatUriWithIds<TSource>();
return await _http.GetAsync<TDestination>($"{uri}?include=
{include.ToString()}", "AdminClient");
}
522
26. Calling The API With HttpClient
catch
{
throw;
}
}
1. Open the HttpClientExtensions class and add a new async static method named
PostAsync defined by the two generic TResponse and TRequest types that
return a DTO of the TResponse type. The API creates an object of type TRequest.
The method should work on the HttpClient service you registered in the Startup
class and have a string parameter named uri, a TRequest parameter named
content, which is the content used to create the record in the database, a
CancellationToken parameter named cancellationToken, and a string parameter
named token for the JWT token you will add in the next chapter.
public static async Task<TResponse> PostAsync<TRequest,
TResponse>(this HttpClient client, string uri, TRequest content,
CancellationToken cancellationToken, string token = "") { }
2. Add a try/catch-block in the method where the catch-block throws any
exceptions up the call chain.
3. In the try-block, add a using-block that calls the CreateRequestHeaders
extension method that you created earlier to add header information to the
HttpRequestMessage that calls the Post action in the API.
using (var requestMessage = uri.CreateRequestHeaders(
HttpMethod.Post, token)) { }
4. Inside the previous using-block, add another using-block that calls the
CreateRequestContent extension method you created earlier on the
requestMessage variable to add the object from the TRequest parameter.
using ((await requestMessage.CreateRequestContent(content))
.Content) { }
523
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
5. Inside the previous using-block, add another using-block that calls the
SendAsync method in the HttpClient service you configured in the Startup class
and saves the returned HttpResponseMessage object in a variable named
responseMessage. Pass in the requestMessage object, the completion option
set to ResponseHeadersRead, and the cancellation token.
using (var responseMessage = await client.SendAsync(
requestMessage, HttpCompletionOption.ResponseHeadersRead,
cancellationToken)) { }
6. Inside the previous using-block, call the CheckStatusCode extension method you
created earlier to check if there are any response errors.
await responseMessage.CheckStatusCodes();
7. Call the DeserializeResponse extension method you created earlier to convert
the JSON response object into a TResponse object below the CheckStatusCode
method call.
return await responseMessage.DeserializeResponse<TResponse>();
8. Save all files.
The complete code for the PostAsync<TResponse, TRequest> extension method in the
HttpClientExtensions Class:
public static async Task<TResponse> PostAsync<TRequest, TResponse>(
this HttpClient client, string uri, TRequest content, CancellationToken
cancellationToken, string token = "")
{
try
{
using (var requestMessage = uri.CreateRequestHeaders(
HttpMethod.Post, token))
{
using ((await requestMessage.CreateRequestContent(
content)).Content)
{
using (var responseMessage = await client.SendAsync(
requestMessage, HttpCompletionOption.ResponseHeadersRead,
cancellationToken))
{
await responseMessage.CheckStatusCodes();
return await responseMessage.DeserializeResponse
<TResponse>();
}
524
26. Calling The API With HttpClient
}
}
}
catch
{
throw;
}
}
525
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
{
try
{
if (new string[] { uri, serviceName }
.IsNullOrEmptyOrWhiteSpace())
throw new HttpResponseException(HttpStatusCode.NotFound,
"Could not find the resource");
526
26. Calling The API With HttpClient
if (idProperty != null)
{
var id = idProperty.GetValue(source);
if (id != null && (int)id > 0) _properties.Add("id", id);
}
if (moduleProperty != null)
{
var moduleId = moduleProperty.GetValue(source);
if (moduleId != null && (int)moduleId > 0)
_properties.Add("moduleId", moduleId);
}
if (courseProperty != null)
{
var courseId = courseProperty.GetValue(source);
if (courseId != null && (int)courseId > 0)
_properties.Add("courseId", courseId);
}
}
catch
{
_properties.Clear();
throw;
}
}
527
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The complete code for the CreateAsync method in the AdminAPIService class:
public async Task<int> CreateAsync<TSource, TDestination>(TSource item)
where TSource : class where TDestination : class
{
try
{
GetProperties(item);
528
26. Calling The API With HttpClient
1. Open the HttpClientExtensions class and copy the PostAsync method and
rename the copy PutAsync.
2. Change HttpMethod.Post to HttpMethod.Put for the CreateRequestHeaders
method call.
The complete code for the PutAsync<TRequest , TResponse> extension method in the
HttpClientExtensions class:
public static async Task<TResponse> PutAsync<TRequest, TResponse>(this
HttpClient client, string uri, TRequest content, CancellationToken
cancellationToken, string token = "")
{
try
{
using (var requestMessage =
uri.CreateRequestHeaders(HttpMethod.Put, token))
{
using ((await requestMessage.CreateRequestContent(content))
.Content)
{
using (var responseMessage = await client.SendAsync(
requestMessage, HttpCompletionOption.ResponseHeadersRead,
529
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
cancellationToken))
{
await responseMessage.CheckStatusCodes();
return await responseMessage
.DeserializeResponse<TResponse>();
}
}
}
}
catch
{
throw;
}
}
530
26. Calling The API With HttpClient
.IsNullOrEmptyOrWhiteSpace())
throw new HttpResponseException(HttpStatusCode.NotFound,
"Could not find the resource");
531
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
GetProperties(item);
string uri = FormatUriWithIds<TDestination>();
var response = await _http.PutAsync<TSource, TSource>(item, uri,
"AdminClient");
return true;
}
catch {
return false;
}
}
1. Open the HttpClientExtensions class and copy the PutAsync method and
rename the copy DeleteAsync.
2. Remove the TRequest and TResponse generic types from the method.
3. Remove the TRequest content parameter.
4. Replace the TResponse return type with the string type.
5. Remove the using-block for the CreateRequestContent method. A delete
shouldn’t have any data in its body; an id should be enough, which the URI
provides.
6. Replace the HttpMethod.Put with HttpMethod.Delete.
7. Replace the DeserializeResponse method call with a call to the ReadStringAsync
method on the response’s Content property.
return await responseMessage.Content.ReadAsStringAsync();
8. Save all files.
The complete code for the DeleteAsync extension method in the HttpClientExtensions
class:
public static async Task<string> DeleteAsync(this HttpClient client,
string uri, CancellationToken cancellationToken, string token = "")
{
try
{
532
26. Calling The API With HttpClient
533
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The complete code for the DeleteAsync method in the HttpClientFactoryService class:
public async Task<string> DeleteAsync(string uri, string serviceName,
string token = "")
{
try
{
if (new string[] { uri, serviceName }
.IsNullOrEmptyOrWhiteSpace())
throw new HttpResponseException(HttpStatusCode.NotFound,
"Could not find the resource");
1. Copy the code in the UpdateAsync method in the AdminAPIService class and
replace the NotImplementedException code it in the DeleteAsync method with
the copied code.
2. Add the async keyword to the method if you haven’t already.
3. Replace the item parameter in the GetProperties method call with the
expression parameter.
4. Replace the generic TDestination type with the generic TSource type for the
FormatUriWithIds method call.
5. Replace the _http.PutAsync method call with a call to the _http.DeleteAsync
method and remove the TSource generic type and the item parameter.
var response = await _http.DeleteAsync(uri, "AdminClient");
6. Save all files.
7. Start the API and Admin projects and click on the Instructor card.
534
26. Calling The API With HttpClient
8. Click the Delete button and change some values in the form, then click the
Delete button.
9. The Index Razor Page no longer displays the instructor.
10. Now, test the CRUD operations for courses, modules, videos, and downloads.
11. Close the application in Visual Studio.
The complete code for the DeleteAsync method in the AdminAPIService class:
public async Task<bool> DeleteAsync<TSource>(Expression<Func<TSource,
bool>> expression) where TSource : class
{
try
{
GetProperties(expression);
string uri = FormatUriWithIds<TSource>();
var response = await _http.DeleteAsync(uri, "AdminClient");
return true;
}
catch
{
return false;
}
}
Summary
In this chapter, you have learned how to use reflection to find properties and their
values from generic types, objects, and Lambda expressions.
You have also learned how to build two services that use extension methods to call the
API to perform CRUD operations.
In the next chapter, you will learn how to create JSON Web Tokens (JWT) and use them
to secure the API with authorization and authentication. You will use the JWTs in
subsequent calls to the API.
535
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
536
27. JSON Web Tokens (JWT)
A JWT is a small self-contained token (a JSON string) that contains credentials, claims, and
other information as needed to authenticate the token and authorize access to resources
based on the claims. The goal of a JWT is to pass information from a client to the server
(the API) and use that information without needing to look up information in a data store.
The third part is an encrypted signature that ensures that the token is valid.
537
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The token is generated by the server for each user and then sent to the server from the
client with every request to authorize access to resources based on the claims in the token;
the claims are part of the encrypted token’s payload.
The three parts of the token are encoded with Base64Encode and separated by periods
(.). The signature is a hash of the header, payload, and a secret stored on the server; if the
signature is invalid, the token as a whole is invalid.
Encrypted signature:
HMACSH256(
Base64UrlEncode(header) + "." + Base64UrlEncode(payload),
SECRET
)
Complete token:
Base64Encode(header) + "." +
Base64Encode(payload) + "." +
Base64Encode(SECRET)
Base64Encode(header).Base64Encode(payload).Base64Encode(signature)
Example Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG
4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQss
w5c
538
27. JSON Web Tokens (JWT)
1. The client sends user credentials (username and password) to the server to
generate a JWT token, which will be used to authorize the user on subsequent
calls.
2. The server generates a token based on the verified user’s roles and/or claims,
and other information, and sends the token to the client.
3. The client then sends that token as a bearer token in all request headers when
calling the API. The API controlers or actions should have authorization
implemented.
4. The server validates the token, and if valid, grants access based on the claims
listed in the token. The most common reason for an invalid token is that it has
expired and needs to be regenerated.
To create a separation of concerns and to make the code more readable, you will create a
service named TokenService in the API project that will contain all the code needed to
create and verify a token. Inject the service into the controller of the TokenController class
that can be called to generate and fetch tokens.
1. Open the Common project and add a public class named TokenDTO to the
DTOModels folder.
2. Add a public string property named Token with an empty string as its default
value.
3. Add a public DateTime property named TokenExpires and assign the value
default to it.
539
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
4. Add a public read-only bool property named TokenHasExpired that returns true
if the token has its default value and false if the token’s expiration date and time
are in the future.
public bool TokenHasExpired { get { return TokenExpires == default
? true : !(TokenExpires.Subtract(DateTime.UtcNow).Minutes > 0); }}
5. Add one empty constructor (that uses the default values) and one that has a
string parameter named token and a DateTime parameter named expires and
assign them to their corresponding properties inside the constructor.
public TokenDTO()
{
}
}
540
27. JSON Web Tokens (JWT)
541
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
#region Constructors
public TokenService(IConfiguration configuration,
IUserService userService)
{
_configuration = configuration;
_users = userService;
}
#endregion
1. Add a private method named GetClaims to the TokenService class that has a
VODUser parameter named user and a bool parameter named includeUser-
Claims. The latter parameter determines if the user’s claims should include the
token with the default claims. The method should return a list of Claim objects.
private List<Claim> GetClaims(VODUser user,
bool includeUserClaims) { }
542
27. JSON Web Tokens (JWT)
2. Create a collection of Claim objects with the most common claims: user name,
email, and a unique id.
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
3. Add the user’s claims to the previous collection if the includeUserClaims
parameter is true. Don’t add the Token and TokenExpires claims because then
the old claim and expiration date will be added to the new token, making it
bloated with old information.
if (includeUserClaims)
foreach (var claim in user.Claims)
if (!claim.Type.Equals("Token") &&
!claim.Type.Equals("TokenExpires")) claims.Add(claim);
4. Return the claims collection from the method.
5. Save all files.
if (includeUserClaims)
foreach (var claim in user.Claims)
if (!claim.Type.Equals("Token") &&
!claim.Type.Equals("TokenExpires")) claims.Add(claim);
return claims;
}
543
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
An instance of the JwtSecurityToken class is used to create a token object containing the
necessary information to generate the finished token.
var jwtToken = new JwtSecurityToken (
issuer: "http://csharpschool.com",
audience: "http://csharpschool.com",
notBefore: now,
expires: now.AddDays(duration),
claims: claims,
signingCredentials: credentials
);
• Issuer: this property specifies the domain that the token originated from. This
value can be set to null if the token is used within the same domain.
• audience: this property specifies the domain of the intended audience.
• notBefore: this property specifies the earliest date and time you can use the
token.
• expires: this property specifies when the token expires.
• claims: this property holds a list of claims associated with the user.
• signingCredential: this property holds the encrypted secret key.
1. Open the appsettings.json file in the API project and add a long string to a
property named SigningSecret containing the secret that will be used to verify
that the token is authentic and originated from the application’s server. Also,
add a property named Duration that determines how long a new token will be
valid.
"Jwt": {
"SigningSecret": "A-VeRy-LonG-AnD-seCUre-StrInG",
"Duration": 10 // minutes, days, or ...
}
544
27. JSON Web Tokens (JWT)
2. Add a private method named CreateToken to the TokenService class that takes
a list of claims and returns an instance of the TokenDTO class.
private TokenDTO CreateToken(IList<Claim> claims) { }
3. Add a try/catch-block where the catch throws any exception up the call chain.
4. In the try-block, use the configuration service injected through the constructor
to fetch the secret key and then convert it to an 8-bit unsigned integer array.
var signingKey = Convert.FromBase64String(
_configuration["Jwt:SigningSecret"]);
5. Use symmetric encryption to encrypt the secret key (the same secret and
encryption will be used later when authenticating the token). Here, we use
HMACSHA256 that computes a Hash-based Message Authentication Code
(HMAC) by using the SHA256 hash function. This hash will then be compared to
the hashed key from the server when a user tries to access the API with the
token.
var credentials = new SigningCredentials(
new SymmetricSecurityKey(signingKey),
SecurityAlgorithms.HmacSha256Signature);
6. Fetch the token duration from the appsettings.json file and the current date and
time.
var duration = int.Parse(_configuration["Jwt:Duration"]);
var now = DateTime.UtcNow;
7. Use the gathered information to create a token object.
var jwtToken = new JwtSecurityToken
(
issuer: "http://your-domain.com",
audience: "http://audience-domain.com",
notBefore: now,
expires: now.AddDays(duration),
claims: claims,
signingCredentials: credentials
);
8. Create an instance of the JwtSecurityTokenHandler class and use it to create the
token. Then return the token as a TokenDTO object.
var jwtTokenHandler = new JwtSecurityTokenHandler();
var token = jwtTokenHandler.WriteToken(jwtToken);
return new TokenDTO(token, jwtToken.ValidTo);
545
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
546
27. JSON Web Tokens (JWT)
1. Open the UserDTO class in the Common project and add a TokenDTO property
named Token.
public TokenDTO Token { get; set; }
547
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if (currentTokenClaimExpires == null)
await _userManager.AddClaimAsync(dbUser, newTokenExpires);
else
await _userManager.ReplaceClaimAsync(dbUser,
currentTokenClaimExpires, newTokenExpires);
9. Below the isAdmin variable, add a Claim variable named adminClaim with the
key from the admin variable and the value true.
var admin = "Admin";
var isAdmin = await _userManager.IsInRoleAsync(dbUser, admin);
var adminClaim = new Claim(admin, "true");
10. Remove the Admin role and claim if the user is an admin in the database, but the
UserDTO object that will update the database says that the user no longer is an
admin.
if (isAdmin && !user.IsAdmin)
{
// Remove Admin Role
await _userManager.RemoveFromRoleAsync(dbUser, admin);
548
27. JSON Web Tokens (JWT)
if (includeClaims) user.Claims =
await _userManager.GetClaimsAsync(user);
dbUser.Email = user.Email;
549
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
// Add or replace the claims for the token and expiration date
var userClaims = await _userManager.GetClaimsAsync(dbUser);
var currentTokenClaim = userClaims.SingleOrDefault(c =>
c.Type.Equals("Token"));
var currentTokenClaimExpires = userClaims.SingleOrDefault(c =>
c.Type.Equals("TokenExpires"));
if (currentTokenClaim == null)
await _userManager.AddClaimAsync(dbUser, newTokenClaim);
else
await _userManager.ReplaceClaimAsync(dbUser,
currentTokenClaim, newTokenClaim);
if (currentTokenClaimExpires == null)
await _userManager.AddClaimAsync(dbUser, newTokenExpires);
else
await _userManager.ReplaceClaimAsync(dbUser,
currentTokenClaimExpires, newTokenExpires);
}
#endregion
550
27. JSON Web Tokens (JWT)
551
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
if(loginUser.Password.IsNullOrEmptyOrWhiteSpace() &&
loginUser.PasswordHash.IsNullOrEmptyOrWhiteSpace())
return null;
if (loginUser.Password.Length > 0)
{
var password =
_userManager.PasswordHasher.VerifyHashedPassword(user,
user.PasswordHash, loginUser.Password);
if (password == PasswordVerificationResult.Failed)
return null;
}
else
{
if (!user.PasswordHash.Equals(loginUser.PasswordHash))
return null;
}
return user;
}
catch
{
throw;
}
}
552
27. JSON Web Tokens (JWT)
4. Return the result from a call to the UpdateUserAsync method on the _user
service object and pass it the userDTO object as data.
5. Save all files.
553
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
return token;
}
catch
{
throw;
}
}
554
27. JSON Web Tokens (JWT)
if (!userId.Equals(user.Id))
throw new UnauthorizedAccessException();
6. Return a new TokenDTO object with the data from the fetched user.
return new TokenDTO(user.Token, user.TokenExpires);
7. Save all files.
if (!userId.Equals(user.Id))
throw new UnauthorizedAccessException();
It will have an asynchronous Post action named GenerateTokenAsync that has a Login-
UserDTO parameter and returns a TokenDTO wrapped in an ActionResult, which enables
it to return status codes with the data or as errors if something goes wrong.
It should also have an asynchronous Get action named GetTokenAsync that has a Login-
UserDTO parameter and a string parameter named userId; like the GenerateTokenAsync
method, this method should return a TokenDTO wrapped in an ActionResult.
555
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
556
27. JSON Web Tokens (JWT)
[HttpPost]
public async Task<ActionResult<TokenDTO>> GenerateTokenAsync(
LoginUserDTO loginUserDto)
{
try
{
var jwt = await _tokenService.GenerateTokenAsync(
loginUserDto);
return jwt;
}
catch
{
return Unauthorized();
557
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
}
}
[HttpGet("{userId}")]
public async Task<ActionResult<TokenDTO>> GetTokenAsync(
string userId, LoginUserDTO loginUserDto)
{
try
{
var jwt = await _tokenService.GetTokenAsync(loginUserDto,
userId);
return jwt;
}
catch
{
return Unauthorized();
}
}
}
558
27. JSON Web Tokens (JWT)
{
"token": "eyJhbGciOiJodHRwOi8vd3d3LncLCJqdGkiOiJhYzhkMWQ3MC
.eyJzdWIiOiJhQGIuYyIsImVtYWlsIjoiYUBiLmMiLCJqdGkiO
.ZSFIe9cR9p9I6AglEwdaONaZ10MMderB0MLZSFIe9cR9p9I6A",
"tokenExpires": "2019-05-05T11:23:16Z",
"tokenHasExpired": false
}
7. You can check that the token and expiration claims were successfully added to
the user by examining the Token and TokenExpires fields for the user in the
AspNetUsers table and the user’s claims in the AspNetUserClaims table.
8. Open a new tab in Postman and select GET in the drop-down to the left of the
URI field.
9. Enter the URI with the user id in the text field http://localhost:6600/api/token/
88f17367-d303-4383-a54e-ec15e86e532e. You can get the user id, email, and
password hash from the AspNetUsers table
10. Click the Headers link and add a Content-Type header for application/json.
11. Copy the body from the Post tab and switch to the Get tab. Paste in the copied
user object in the Body.
12. Click the Send button. Postman receives the created token.
13. Stop the application in Visual Studio.
559
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
"true"));
});
The code below shows how to add authorization to a controller or action with the Admin
policy.
[Authorize(Policy = "Admin")]
Add the VODUser claim when registering a new user with one of the Admin or UI sites.
Add the Admin claim when registering a new user with the Admin site or added or
removed based on the Is Admin checkbox on the Edit Razor Page; you have already added
the code for the Is Admin checkbox in the UpdateUserAsync method in the UserService
class. The Admin role should also be added to new users when registered with the Admin
site; this role gives the user access to the Admin UI and has nothing to do with authoriza-
tion in the API.
Now, you will add the claims and role in the Register Razor Pages for the Admin and UI
sites. You should immediately see that the role has been added to the new user because
the cards on the Index Razor Page in the Admin UI will only be visible if the user has the
Admin role. You can check that the claims have been added by looking in the AspNetUsers
and AspNetUserClaims tables.
var identityResult = await _userManager.AddClaimAsync(user,
new Claim("VODUser", "true"));
560
27. JSON Web Tokens (JWT)
if (identityResult.Succeeded) _logger.LogInformation(
"Added the Admin Role to the User.");
4. Open the Register.cshtml.cs file in the UI project.
5. Locate the if-block that checks the result.Succeeded property and add the
VODUser claim to the user at the beginning of the if-block; users registered with
the UI project should not have the Admin claim. You could log a message for a
successfully added claim.
var identityResult = await _userManager.AddClaimAsync(user,
new Claim("VODUser", "true"));
if (identityResult.Succeeded) _logger.LogInformation(
"Added the VODUser Claim to the User.");
6. Start the API and Admin projects and click the Instructors card; the Index Razor
Page should list all instructors.
7. Stop the application in Visual Studio.
8. Add the Authorize attribute with a policy named Admin to the Instructors-
Controller class.
[Authorize(Policy = "Admin")]
9. Start the API and Admin projects and click the Instructors card; the API throws
the following exception: System.InvalidOperationException: 'The AuthorizationPolicy
named: 'Admin' was not found.'. The reason for this exception is that you haven’t
configured the policy in the Startup class yet.
10. Stop the application in Visual Studio.
11. Open the Startup class in the API project and add the policies to the Configure-
Services method.
services.AddAuthorization(options =>
{
options.AddPolicy("VODUser", policy =>
policy.RequireClaim("VODUser", "true"));
options.AddPolicy("Admin", policy =>
policy.RequireClaim("Admin", "true"));
});
12. Start the API and Admin projects and click the Instructors card; The Index Razor
Page displays the following error message above the cards. The reason for this
error is that you have added authorization to the InstructorsController but the
user JWT token is unverified, and therefore the user hasn’t been authenticated
and can’t be authorized based on the claims in the token.
561
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
13. Click the Courses card; the Index Razor Page should display the courses.
14. Stop the application in Visual Studio.
15. Add the same Authorize attribute you used with the InstructorsController to all
other controllers in the API project except the TokenController that should be
available to anonymous users.
Replace the default Identity-based authentication with the JWT Bearer authentication
scheme to add JWT token-based authentication for the user’s token; you do this by adding
new default authentication options. The DefaultAuthenticateScheme option activates
JWT token authentication, and the DefaultChallengeScheme option prompts the user to
sign in.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
})
When the default authentication scheme is defined, you must configure the JWT Bearer
options. You do this by chaining on the AddJwtBearer method; this will make the authenti-
cation look for a JWT token in the headers of the API call.
The same secret key that was used to create the token must be hashed with the same
algorithm and added to the configuration to ensure that the token is valid.
There’s no set way to configure the bearer token authentication; you must determine the
correct validation options based on your API. The settings below have loose security
settings where neither the issuer nor the audience is validated. The ValidateLifetime prop-
erty specifies whether the expiration should be validated; the ValidateIssuerSigningKey
562
27. JSON Web Tokens (JWT)
determines whether to evaluate the signing key in the IssuerSigningKey property; the
ClockSkew property specifies the value for the earliest correct date and time; the Require-
HttpsMetadata determines whether HTTPS is required to call the API.
.AddJwtBearer(options =>
{
var signingKey = new SymmetricSecurityKey(Convert.FromBase64String(
Configuration["Jwt:SigningSecret"]));
You also need to add the authentication middleware above the AddMvc middleware in
the Configure method in the Startup class.
app.UseAuthentication();
1. Open the Startup class in the API project and add the previously described code
to the ConfigureServices method above the Authorization configuration you
added earlier and the middleware to the Configure method.
2. Start the API and Admin projects and click the Users card. You need to remove
the Admin role and add it again to add the Admin claim.
3. Edit the user and remove the tick in the Is Admin checkbox and click the Save
button.
4. Edit the user and add a tick in the Is Admin checkbox and click the Save button.
5. Click the Instructors card; the Index Razor Page displays the same error message
as before above the cards on the Index Razor Page. The reason for this error is
that you are trying to call the API without a JWT token.
563
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
6. Open the AspNetUser table and copy the token from the Token field. Note that
you might have to generate a new token with Postman if it has expired.
7. Open Postman and open a new Get tab and add the URI to the Instructors
controller http://localhost:6600/api/instructors.
8. Add a Content-Type header for application/json.
9. Click the Send button. No data is returned with the response, only a 401
Unauthorized status code, which means that the user is unauthenticated by the
JWT bearer token security in the API.
10. Add an Authorization header and add the word Bearer followed by the token as
its value to send the Bearer authentication token with the request.
Bearer iJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNobW
.IiOiJhQGIuYyIsImVtYWlsIjoiYUBiLmMiLCJqdGkiOiJhYzhkMWQ3MC
.7SRsoF9OLZSFIe9cR9p9I6AglEwdaONaZ10MMderB0M
11. Click the Send button again; now the list of instructors should be returned with
the response and the status code should be 200 OK.
12. Stop the application in Visual Studio.
Let’s add a new service that has three methods for creating, fetching and checking the JWT
token from the Admin application. The CreateTokenAsync will call the API’s CreateToken-
Async to generate a new token for the logged-in user. The GetTokenAsync method will
fetch the token from the logged-in user’s claims and if possible, create an instance of the
TokenDTO with the fetched data, otherwise the GetTokenAsync action in the API will be
called to fetch the user’s token. The CheckTokenAsync method will fetch the user’s token
and check its expiration date; if the token has expired, the CreateTokenAsync method in
the service is called to create a new token.
564
27. JSON Web Tokens (JWT)
user of type LoginUserDTO, uri of type string, serviceName of type string, and
token of type string. The method should return a TokenDTO instance wrapped
in a Task.
Task<TokenDTO> CreateTokenAsync(LoginUserDTO user, string uri,
string serviceName, string token = "");
2. Implement the method in the HttpClientFactoryService class and add the async
keyword to make it asynchronous.
3. Copy the code in the PostAsync method and paste it into the CreateTokenAsync
method.
4. Replace the TRequest and TResponse types with LoginUserDTO and TokenDTO
and the content parameter with user parameter for the PostAsync method call.
5. Save all files.
565
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
2. Implement the method in the HttpClientFactoryService class and add the async
keyword to make it asynchronous.
3. Copy the code in the GetAsync method and paste it into the GetTokenAsync
method.
4. Replace the TResponse and string types that define the GetAsync method with
TokenDTO, LoginUserDTO.
5. Save all files.
566
27. JSON Web Tokens (JWT)
#region Constructor
public JwtTokenService(IHttpClientFactoryService http,
UserManager<VODUser> userManager, IHttpContextAccessor
httpContextAccessor)
{
_http = http;
567
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
_userManager = userManager;
_httpContextAccessor = httpContextAccessor;
}
#endregion
#endregion
}
568
27. JSON Web Tokens (JWT)
The complete code for the CreateTokenAsync method in the JwtTokenService class:
public async Task<TokenDTO> CreateTokenAsync()
{
try
{
var userId = _httpContextAccessor.HttpContext.User
.FindFirst(ClaimTypes.NameIdentifier).Value;
return token;
}
catch
{
return default;
}
}
569
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
The complete code for the GetTokenAsync method in the JwtTokenService class:
public async Task<TokenDTO> GetTokenAsync()
{
try
{
var userId = _httpContextAccessor.HttpContext.User
.FindFirst(ClaimTypes.NameIdentifier).Value;
570
27. JSON Web Tokens (JWT)
c.Type.Equals("TokenExpires")).Value;
DateTime expires;
var succeeded = DateTime.TryParse(date, out expires);
return newToken;
}
catch
{
return default;
}
}
571
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
6. Open the Startup class in the Admin project and add a service configuration for
the IJwtTokenService service.
services.AddScoped<IJwtTokenService, JwtTokenService>();
7. Save all files.
The complete code for the CheckTokenAsync method in the JwtTokenService class:
public async Task<TokenDTO> CheckTokenAsync(TokenDTO token)
{
try
{
if (token.TokenHasExpired)
{
token = await GetTokenAsync();
if (token.TokenHasExpired) token = await CreateTokenAsync();
}
return token;
}
catch
{
return default;
}
}
1. Open the AdminAPIService class in the Services folder in the Common project.
2. Inject the IJwtTokenService into the constructor and store the instance in a
variable named _jwt.
3. Add a TokenDTO variable named token to the class and instantiate it.
TokenDTO token = new TokenDTO();
4. Open the GetAsync method and check the token below the FormatUriWithout-
Ids call by calling the CheckTokenAsync method in the _jwt service. Store the
returned token in the token variable you added to the class.
token = await _jwt.CheckTokenAsync(token);
5. Add the token in the token.Token property to the GetListAsync API method call.
return await _http.GetListAsync<TDestination>($"{uri}?include=
572
27. JSON Web Tokens (JWT)
Summary
In this chapter, you learned how to create JSON Web Tokens (JWTs) and how to secure
the API with JSON Bearer Token authentication and claims based policy authorization
that uses claims from the JWT.
I truly hope that you enjoyed the book and have learned a lot. Please leave a review on
Amazon so that other readers can make an informed decision based on your and other
readers’ thoughts about the book.
Regards,
Jonas Fagerberg
573
ASP.NET Core 2.2 MVC, Razor Pages, API, JSON Web Tokens & HttpClient
574
Other Books and Courses by the Author
575