Building A Simple Web App Using Razor Pages

Introduction

In this article, you will learn how to build a simple CRUD web app using Razor Pages, which is new in ASP.NET Core 2.0. This web app is about the management of the students, and all operation of the database will be through the API building by NancyFx.

What is Razor Pages

Razor Pages is a new feature of ASP.NET Core MVC that makes coding page-focused scenarios easier and more productive. 

Let's take a look at the 2.0.0 Roadmap of ASP.NET Core about Razor Pages.

Adding a page-focused programming model to MVC, built on the existing primitives but optimized for typical scenarios and patterns seen in server-side HTML rendering.

You can visit the following link to find out more information about Razor Pages.

https://github.com/aspnet/Home/wiki/Roadmap#razor-pages

Environment

Here is the environment of my computer.

  • VS for Mac Community
  • .NET Core 2.0

You can also use VS 2017 or VS Code as your IDE but the .NET Core 2.0 SDK and Runtime is required !!

Technologies I used are as follow -

  • Razor Pages(build the website)
  • Dapper(database operation )
  • SQLite(store the data)
  • NancyFx(build the API)

Now, let's begin.

Step #1 Set up the project structure

Create an ASP.NET Core Web App(Razor Pages) project which is used to build the website.

ASP.NET Core

Note: Don't select the traditional ASP.NET Core Web App with MVC!! 

Create an ASP.NET Core Empty project to handle the API.

ASP.NET Core

When you click the Next button, it will ask you to choose the target framework of your project! At this step, we choose the .NET Core 2.0.

ASP.NET Core

Create a .NET Standard Library to store the Entities.

ASP.NET Core

When you click the next button, it will ask you to choose the target framework of your project as well! Of course, we choose the .NET Core 2.0 again!

Step #2 Create database/tables and the entities

Create an SQLite table with the following SQL query.

  1. CREATE TABLE [students] (  
  2.     [id] integer NOT NULL PRIMARY KEY AUTOINCREMENT,   
  3.     [no] nvarchar(254),   
  4.     [name] nvarchar(254),   
  5.     [gender] nvarchar(254)  
  6. );  

Create the Student Model.

  1. public class Student  
  2. {  
  3.     public int Id { get; set; }  
  4.   
  5.     [Display(Name = "No of student")]  
  6.     [Required(ErrorMessage ="No is required")]  
  7.     [StringLength(maximumLength: 6, MinimumLength = 2)]  
  8.     public string No { get; set; }  
  9.   
  10.     [Display(Name = "Name of student")]  
  11.     [Required(ErrorMessage = "Name is required")]          
  12.     [MaxLength(20)]  
  13.     [MinLength(3)]  
  14.     public string Name { get; set; }  
  15.   
  16.     [Display(Name = "Gender of student")]  
  17.     [Required(ErrorMessage = "Name is required")]  
  18.     [StringLength(maximumLength:6,MinimumLength =1)]  
  19.     public string Gender { get; set; }          
  20. }  
  21.   
  22. public class StudentCreateVM  
  23. {  
  24.     [Display(Name = "No")]  
  25.     [Required(ErrorMessage = "No is required")]  
  26.     [StringLength(maximumLength: 6, MinimumLength = 2)]  
  27.     public string No { get; set; }  
  28.   
  29.     [Display(Name = "Name")]  
  30.     [Required(ErrorMessage = "Name is required")]  
  31.     [MaxLength(20)]  
  32.     [MinLength(3)]  
  33.     public string Name { get; set; }  
  34.   
  35.     [Display(Name = "Gender")]  
  36.     [Required(ErrorMessage = "Name is required")]  
  37.     [StringLength(maximumLength: 6, MinimumLength = 1)]  
  38.     public string Gender { get; set; }  
  39. }   

As you can see, I added some attributes on the properties which are used for the display and the validation of the forms.

This usage is similar to what we always use in ASP.NET MVC.

However, what we need to be cautious about is that we need to add the reference of System.ComponentModel.DataAnnotations !!

Step #3 Build the RESTful API 

Here, we use NancyFx to build our RESTful API.

  1. public class StudentModule : NancyModule  
  2. {  
  3.     private string _connStr;  
  4.     public StudentModule() : base("api/students")  
  5.     {  
  6.         this._connStr = $"Data Source ={Path.Combine(Directory.GetCurrentDirectory(), "rpdemo.db")}";  
  7.   
  8.         Get("/", async (_, ct) =>  
  9.         {  
  10.             using (IDbConnection conn = new SqliteConnection(_connStr))  
  11.             {  
  12.                 conn.Open();  
  13.                 var sql = "select s.id,s.no,s.name,s.gender from students s";  
  14.                 var res = await conn.QueryAsync<Student>(sql);  
  15.   
  16.                 return Negotiate  
  17.                         .WithMediaRangeModel(new MediaRange("application/json"), res)  
  18.                         .WithMediaRangeModel(new MediaRange("application/xml"), res);  
  19.             }  
  20.         });  
  21.   
  22.         Get("/{id}", async (_, ct) => { // Code removed for brevity. });  
  23.   
  24.         Post("/", async (_, ct) =>  
  25.         {  
  26.             var student = this.Bind<Student>();  
  27.             using (IDbConnection conn = new SqliteConnection(_connStr))  
  28.             {  
  29.                 conn.Open();  
  30.                 var sql = @"insert into students(no,name,gender)  
  31.                             values (@no, @name, @gender)";  
  32.                 var res = await conn.ExecuteAsync(sql, new  
  33.                 {  
  34.                     no = student.No,  
  35.                     name = student.Name,  
  36.                     gender = student.Gender  
  37.                 });  
  38.   
  39.                 if (res > 0)  
  40.                     return HttpStatusCode.Created;                         
  41.                 else  
  42.                     return HttpStatusCode.InternalServerError;  
  43.             }  
  44.         });  
  45.   
  46.         Put("/", async (_, ct) => { // Code removed for brevity. });  
  47.   
  48.         Delete("/{id}", async (_, ct) => { // Code removed for brevity. });  
  49.     }  
  50. }  

As you can see, all the methods are asynchronous, which you may use in MVC like this.

  1. [HttpGet]  
  2. public async Task<IActionResult> Index()  
  3. {  
  4.     await xxxx;  
  5.     return xxx;  
  6. }  
After finishing the above job, we should ensure that the API is accessible. So, we need to run this project by the .NET CLI.   

ASP.NET Core

When the terminal tells us that application has started, it means that the API project is running now.

Step #4 Build the Razor Pages

Add a new folder named Services in the website project which is used to handle the request of the APIs. 

The following code shows the IStudentService interface.

  1. public interface IStudentService  
  2. {          
  3.     Task<IEnumerable<Student>> GetStudentListAsync();  
  4.     Task<Student> GetStudentByIdAsync(int id);  
  5.     Task<bool> AddStudentAsync(StudentCreateVM student);  
  6.     Task<bool> UpdateStudentAsync(Student student);  
  7.     Task<bool> DeleteStudentByIdAsync(int id);  
  8. }   

The implementation of the above interface is as follows.

  1. public class StudentService : IStudentService  
  2. {  
  3.     private HttpClient _client;  
  4.     public StudentService()  
  5.     {  
  6.         _client = new HttpClient();  
  7.         _client.BaseAddress = new Uri("http://localhost:59309/api/students");  
  8.         _client.DefaultRequestHeaders.Clear();  
  9.         _client.DefaultRequestHeaders.Add("Accept""application/json");  
  10.     }  
  11.   
  12.     public async Task<IEnumerable<Student>> GetStudentListAsync()  
  13.     {  
  14.         var resStr = await _client.GetStringAsync("");  
  15.         return JsonConvert.DeserializeObject<IEnumerable<Student>>(resStr);  
  16. }  
  17.   
  18.     public async Task<bool> AddStudentAsync(StudentCreateVM student)  
  19.     {  
  20.         var pairs = new List<KeyValuePair<string, string>>  
  21.         {  
  22.             new KeyValuePair<string, string>("No", student.No),  
  23.             new KeyValuePair<string, string>("Name", student.Name),  
  24.             new KeyValuePair<string, string>("Gender", student.Gender),  
  25.         };  
  26.         var content = new FormUrlEncodedContent(pairs);  
  27.         var res = await _client.PostAsync("", content);  
  28.         return res.IsSuccessStatusCode;  
  29. }  
  30.   
  31.     public async Task<Student> GetStudentByIdAsync(int id) { ... }  
  32.     public async Task<bool> UpdateStudentAsync(Student student) { ... }  
  33.     public async Task<bool> DeleteStudentByIdAsync(int id) { ... }  
  34.   
  35.     ~ StudentService()  
  36.     {  
  37.         if (_client != null)  
  38.         {  
  39.             _client.Dispose();  
  40.         }  
  41.     }  
  42. }  

Using the HttpClient to send requests to our API.

Remember to register the services in the Startup class; this is an important step.

  1. public void ConfigureServices(IServiceCollection services)  
  2. {  
  3.     services.AddSingleton(typeof(IStudentService),typeof(StudentService));  
  4.     services.AddMvc();  
  5. }  

Now add a sub-folder named Students in the Pages folder.

Create a Razor Page to show all of the students.

ASP.NET Core

Note

The Razor Page is similar to the web form, both of them are code-behind. But they are not the same! In Razor Page, we can use lots of features that we use in ASP.NET Core MVC, such as Model Binding, Tag Helper .etc. 

Here is the code of Index.cshtml .

  1. @page  
  2. @model Pages.Students.IndexModel  
  3. @{  
  4.     ViewData["Title"] = "Index Page";  
  5. }  
  6. <h2>Student List Here</h2>  
  7. <hr />  
  8. <a asp-page="./Create">Create a new student</a>       
  9. <table class="table">  
  10.     <thead>  
  11.         <tr>  
  12.             <th>Id</th>  
  13.             <th>No</th>  
  14.             <th>Name</th>  
  15.             <th>Gender</th>  
  16.         </tr>  
  17.     </thead>  
  18.     <tbody>  
  19.         @foreach (var item in Model.StudentList)  
  20.         {  
  21.             <tr>  
  22.                 <td>@item.Id</td>  
  23.                 <td>@item.No</td>  
  24.                 <td>@item.Name</td>  
  25.                 <td>@item.Gender</td>  
  26.                 <td>                                     
  27.                     <a asp-page="./Details" asp-route-id="@item.Id">Details</a> |                   
  28.                     <a asp-page="./Edit" asp-route-id="@item.Id" >Edit</a>    
  29.                 </td>  
  30.             </tr>  
  31.         }          
  32. </tbody>  
  33. </table>  

There are many common points between MVC View Page and Razor Page. Both of them are based on Razor.

Razor Page must start with @page !

The Tag Helper asp-page is special in Razor, it will generate a URL based on the rule of Razor Pages.

Turn to the Index.cshtml.cs file.

  1. public class IndexModel : PageModel  
  2. {  
  3.     private readonly IStudentService _studentService;  
  4.     public IndexModel(IStudentService studentService)  
  5.     {  
  6.         this._studentService = studentService;  
  7.     }  
  8.   
  9.     public IEnumerable<Student> StudentList;  
  10.   
  11.     public async Task OnGetAsync()  
  12.     {  
  13.         var _studentList = await _studentService.GetStudentListAsync();  
  14.         this.StudentList = _studentList;  
  15.           
  16.         //the same as the following code  
  17.         //this.StudentList = await _studentService.GetStudentListAsync();  
  18.     }  
  19. }  

As you can see , the IndexModel inherits from the PageModel which plays an important role in the Razor Pages.

Normally,  this file has some methods that start with On+Http Method, such as OnGet, OnPost. And some of them end with Async, which means this is an asynchronous method. 

For my above sample, I used an asynchronous method to load the student list when we open this page.

At this time, we have finished the index page of the Student. When running this project and entering http://yourdomain.com/Students on your browser, you may get a result like the following screenshot.

ASP.NET Core
Then, we will add a Create Page to create some new students. 

Here is the code of Create.cshtml.

  1. @page  
  2. @model Pages.Students.CreateModel  
  3.   
  4. @{  
  5.     ViewData["Title"] = "Create Page";  
  6. }  
  7. <h2>Create</h2>  
  8. <h4>Student</h4><hr /><div class="row">  
  9.     <div class="col-md-4">  
  10.         <form method="post">  
  11.             <div asp-validation-summary="ModelOnly" class="text-danger"></div>              
  12.             <div class="form-group">  
  13.                 <label asp-for="Student.No" class="control-label"></label>  
  14.                 <input asp-for="Student.No" class="form-control" />  
  15.                 <span asp-validation-for="Student.No" class="text-danger"></span>  
  16.             </div>  
  17.             <div class="form-group">  
  18.                 <label asp-for="Student.Name" class="control-label"></label>  
  19.                 <input asp-for="Student.Name" class="form-control" />  
  20.                 <span asp-validation-for="Student.Name" class="text-danger"></span>  
  21.             </div>  
  22.             <div class="form-group">  
  23.                 <label asp-for="Student.Gender" class="control-label"></label>  
  24.                 <input asp-for="Student.Gender" class="form-control" />  
  25.                 <span asp-validation-for="Student.Gender" class="text-danger"></span>  
  26.             </div>  
  27.             <div class="form-group">  
  28.                 <button type="submit" class="btn btn-default">Save</button>  
  29.             </div>  
  30.         </form>  
  31.     </div></div>  
  32. <div>  
  33.     <a asp-page="Index">Back to List</a></div>  
  34.   
  35. @section Scripts {  
  36.     @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}  
  37. }   

This is the code of the Create.cshtml.cs file.

  1. public class CreateModel : PageModel  
  2. {  
  3.     private readonly IStudentService _studentService;  
  4.     public CreateModel(IStudentService studentService)  
  5.     {  
  6.         this._studentService = studentService;  
  7.     }  
  8.   
  9.     [BindProperty]  
  10.     public StudentCreateVM Student { get; set; }  
  11.   
  12.     public void OnGet()  
  13.     {                      
  14.     }  
  15.   
  16.     public async Task<IActionResult> OnPostAsync()  
  17.     {  
  18.         if (!ModelState.IsValid)  
  19.             return Page();  
  20.   
  21.         var res = await _studentService.AddStudentAsync(this.Student);  
  22.   
  23.         if (res)  
  24.             return RedirectToPage("./Index");  
  25.         else  
  26.             return Page();  
  27.     }  
  28. }  

We do nothing in the OnGet method because when we open this page, it only show us a static page with a form that we can create a new student.

When we submit the form, it will call the OnPostAsync method in the cs file , because we set the create form with the post method!!

If we create the student successfully, it will redirect to the index page. Otherwise, it will keep stay in the create page.

Because we use the DataAnnotations in our model, when the input is not valid , it will show us the error message we define in the class.

ASP.NET Core

The Student property uses [BindProperty] attribute to opt-in to model binding.

Now, we will turn to the edit page.

The code of the Edit.cshtml file.

  1. @page "{id:int}"  
  2. @model Pages.Students.EditModel  
  3. @{  
  4.     ViewData["Title"] = "Edit Page";  
  5. }  
  6. <h2>Edit</h2>  
  7. <h4>Student</h4><hr /><div class="row">  
  8.     <div class="col-md-4">  
  9.         <form method="post">  
  10.             <div asp-validation-summary="ModelOnly" class="text-danger"></div>  
  11.             <input type="hidden" asp-for="Student.Id" />  
  12.             <div class="form-group">  
  13.                 <label asp-for="Student.No" class="control-label"></label>  
  14.                 <input asp-for="Student.No" class="form-control" />  
  15.                 <span asp-validation-for="Student.No" class="text-danger"></span>  
  16.             </div>  
  17.             <div class="form-group">  
  18.                 <label asp-for="Student.Name" class="control-label"></label>  
  19.                 <input asp-for="Student.Name" class="form-control" />  
  20.                 <span asp-validation-for="Student.Name" class="text-danger"></span>  
  21.             </div>  
  22.             <div class="form-group">  
  23.                 <label asp-for="Student.Gender" class="control-label"></label>  
  24.                 <input asp-for="Student.Gender" class="form-control" />  
  25.                 <span asp-validation-for="Student.Gender" class="text-danger"></span>  
  26.             </div>              
  27.             <div class="form-group">  
  28.                 <button type="submit" class="btn btn-default">Save</button>  
  29.             </div>  
  30.         </form>  
  31.     </div></div>  
  32. <div>  
  33.     <a asp-page="./Index">Back to List</a></div>  
  34.   
  35. @section Scripts {  
  36.     @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}  
  37. }   

You will find something new after the @page in the first line.

@page "{id:int}" means that the page accepts requests that contain int route data.

What @page "{id:int}" does is similar with [Route("users/{id:int}"] which you can see in the following code.

  1. [Route("users/{id:int}"]public ActionResult GetUserById(int id) { ... }   

The code of Edit.cshtml.cs file is similar with Create.cshtml.cs.

Because we add a constraint on the page, we need to add a parameter to the OnGetAsync method so that it can receive the parameter from the route.

  1. public async Task<IActionResult> OnGetAsync(int? id) {...}  

Summary

This article shows you a easy sample to learn Razor Pages in ASP.NET Core 2.0. However, there are lots of things I didn't mention in this article such as @functions , @namespace, multiple handlers and so on.

For more information on the Razor Pages, you can visit their official documentation at https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/