Showing posts with label ASP.NET MVC. Show all posts
Showing posts with label ASP.NET MVC. Show all posts

Thursday, December 31, 2020

Electron.NET with ASP.NET MVC & EF

In this tutorial I will show you how to develop a simple cross-platform Electron application that retrieves data from the Northwind database and renders results in a chart. The solution also allows you to do the following:
  • export data to a CSV file
  • setup the solution as a separate desktop application

What is Electron?

Electron is a framework that supports development of apps using web technologies such as Chromium rendering engine and Node.js runtime. The platform supports Windows, MacOS and Linux. Some very popular applications that run on Electron are Visual Studio Code, Discord, Skype, GitHub Desktop and many others. The official site for Electron is https://www.electronjs.org/.

What is Electron.NET?

Electron.NET is a wrapper around Electron that allows .NET web developers to invoke native Electron APIs using C#. To develop with Electron.NET, you need Node.js & Npm installed on your computer. In addition, you must have .NET Core 3.1 or later. The official site for Electron.NET open source project is https://github.com/electronnet/electron.net/.

Running a docker container with SQL-Server Northwind sample database

I will use a docker image that contains the SQL-Server Northwind database. Credit goes to kcornwall for creating this docker image.

To pull & run the Northwind database in a docker container, run the following command in a terminal window:

docker run -d --name nw -p 1444:1433 kcornwall/sqlnorthwind

The above command does the following:

Docker image: kcornwall/sqlnorthwind
Container Name
(--name):
 nw
Ports (-p): Port 1433 in container is exposed as port 1444 on the host computer
Password: The sa password is Passw0rd2018. This was determined from the Docker Hub page for the image.
-d: Starts the container in detached mode


This is what I experienced after I ran the above command:
docker run
Let us make sure that the container is running. Execute this command to ensure that the container is running OK.
docker ps

The following confirms that the container is indeed running:
docker ps

Setup our application

At the time of writing this article, I was using .NET version 5.0.101 on a Windows 10 computer running version 1909

Let us create an ASP.NET MVC app named ElectronEF with the following terminal window commands:

mkdir ElectronEf
cd ElectronEf
dotnet new mvc

We need three .NET tools. Run the following commands from within a terminal window to install ElectronNET.CLI , dotnet-aspnet-codegenerator and dotnet-ef.

dotnet tool install –g ElectronNET.CLI
dotnet tool install -g dotnet-aspnet-codegenerator
dotnet tool install –g dotnet-ef

Continue by adding these packages to your project:

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package ElectronNET.API
dotnet add package C1.AspNetCore.Mvc

ElectronNET.API is the Electron.NET package and C1.AspNetCore.Mvc is a package from a company named ComponentOne that provides  components that we will use (under a short trial license) for creating a visual chart.

Finally, let's open our project in VS Code. To do that, simply execute the following command from the same terminal window:

code .

Open Program.cs in the editor and add the following statements to the CreateHostBuilder() method right before webBuilder.UseStartup<Startup>() : 

webBuilder.UseElectron(args);
webBuilder.UseEnvironment("Development");

Next, open Startup.cs in the editor and add the following statement to the bottom of the Configure() method:

// Open the Electron-Window here
Task.Run (async () => {
  await Electron.WindowManager.CreateWindowAsync ();
});

That's it. Your ASP.NET application is now electron-ized. To see the fruits of your labor, type the following command in the terminal window:

electronize init
electronize start

electronize init is a one-time command that creates a manifest file named electron.manifest.json and adds it to your project. 

electronize start launches the Electron app. Note that it takes a little longer the first time and the content now appears in an application window, not a browser.


Note that you can still run your application as a web app by simply stopping the Electron app (with File >> Exit from the app's menu system) and running the web app with: dotnet run.

Interacting with the Northwind database

Let us reverse engineer the database with the following command so that it generates a DbContext class and classes representing the Category & Product database entities in a folder named NW:

dotnet-ef dbcontext scaffold "Data Source=localhost,1444;Initial Catalog=Northwind;Persist Security Info=True;User ID=sa;Password=Passw0rd2018" Microsoft.EntityFrameworkCore.SqlServer -c NorthwindContext -o NW --table Products --table Categories

Add the following connection string to the top of appsettings.json just before "Logging":

"ConnectionStrings": {
    "NW": "Data Source=localhost,1444;Initial Catalog=Northwind;Persist Security Info=True;User ID=sa;Password=Passw0rd2018"
},

Open NW/NorthwindContext.cs and delete the OnConfiguring() method so that we do not have confidential connection string information embedded in source code.

Add the following to ConfigureServices() method in Startup.cs:

services.AddDbContext<NorthwindContext>(options => options.UseSqlServer(Configuration.GetConnectionString("NW")));

Rendering a chart 

Add the following instance variable to Controllers/HomeController.cs:

private readonly NorthwindContext _context;

Replace the HomeController constructor with this code:

public HomeController(ILogger<HomeController> logger, NorthwindContext context) {
    _logger = logger;
    _context = context;
}

Add the following helper method named getProductsByCategory() that returns a count of products by category from the Northwind database:

private List<object> getProductsByCategory () {
  var query = _context.Products
    .Include (c => c.Category)
    .GroupBy (p => p.Category.CategoryName)
    .Select (g => new {
        Name = g.Key,
        Count = g.Count ()
    })
    .OrderByDescending (cp => cp.Count);

  return query.ToList<object> ();
}

Add a ProductsByCategory() action method to HomeController.cs:

public IActionResult Chart() {
  ViewBag.CategoryProduct = this.getProductsByCategory ();
  return View ();
} 

To make available the char control to all views, add the following to Views/_ViewImports.cshtml:

@addTagHelper *, C1.AspNetCore.Mvc

We need a view to render the chart. Therefore, execute the following command to create /Views/Home/Chart.cshtml:

dotnet aspnet-codegenerator view Chart Empty -outDir Views/Home –udl

Replace Views/Home/Chart.cshtml with following code:

@{
  ViewData["Title"] = "Number of products by category";
}
<br />
<h1>@ViewData["Title"]</h1>

<div>
  <c1-flex-chart binding-x="Name" chart-type="Bar" legend-position="None">
    <c1-items-source source-collection="@ViewBag.CategoryProduct"></c1-items-source>
    <c1-flex-chart-series binding="Count" name="Count" />
    <c1-flex-chart-axis c1-property="AxisX" position="None" />
    <c1-flex-chart-axis c1-property="AxisY" reversed="true" />
  </c1-flex-chart>
</div>

Add these styles to Views/Shared/_Layout.cshtml just before </head>:

<c1-styles />
<c1-scripts>
   <c1-basic-scripts />
</c1-scripts>

Also in _Layout.cshtml, add the following menu item  at around line 35:

<li class="nav-item">
  <a class="nav-link text-dark" asp-area="" asp-controller="Home"
    asp-action="Chart">Chart</a>
</li>

Run the application by typing the following command in the terminal window:

electronize start

You should see the following output:

Save data to file system as CSV file

Add an action method named SaveAs() to Controllers/HomeController.cs with the following code:

public async Task<IActionResult> SaveAs (string path) {
  System.IO.StringWriter writer = new System.IO.StringWriter ();
  writer.WriteLine ("Name,Count");

  var query = this.getProductsByCategory ();
  query.ForEach (item => {
    writer.Write (item.GetType ().GetProperty ("Name").GetValue (item));
    writer.Write (",");
    writer.WriteLine (item.GetType ().GetProperty ("Count").GetValue (item));
  });

  await System.IO.File.WriteAllTextAsync (path, writer.ToString ());
  return RedirectToAction ("Index");
}

Menu customization

Electron.NET provides a default application menu. Note that there are differences between macOS and other platforms. On macOS, applications have their own menu to the left of the standard File/Edit/View menus.

Add the following using statements at the top of Startup.cs:

using ElectronNET.API.Entities;
using System.Runtime.InteropServices;

Add this CreateMenu() method to Startup.cs:

private void CreateMenu () {
  bool isMac = RuntimeInformation.IsOSPlatform (OSPlatform.OSX);
  MenuItem[] menu = null;

  MenuItem[] appMenu = new MenuItem[] {
    new MenuItem { Role = MenuRole.about },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.services },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.hide },
    new MenuItem { Role = MenuRole.hideothers },
    new MenuItem { Role = MenuRole.unhide },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.quit }
  };

  MenuItem[] fileMenu = new MenuItem[] {
    new MenuItem {
      Label = "Save As...", Type = MenuType.normal, Click = async () => {
        var mainWindow = Electron.WindowManager.BrowserWindows.First ();
        var options = new SaveDialogOptions () {
          Filters = new FileFilter[] {
            new FileFilter { Name = "CSV Files", Extensions = new string[] { "csv" } }
          }
        };
        string result = await Electron.Dialog.ShowSaveDialogAsync (mainWindow, options);
        if (!string.IsNullOrEmpty (result)) {
          string url = $"http://localhost:{BridgeSettings.WebPort}/Home/SaveAs?path={result}";
          mainWindow.LoadURL (url);
        }
      }
    },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = isMac ? MenuRole.close : MenuRole.quit }
  };

  MenuItem[] viewMenu = new MenuItem[] {
    new MenuItem { Role = MenuRole.reload },
    new MenuItem { Role = MenuRole.forcereload },
    new MenuItem { Role = MenuRole.toggledevtools },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.resetzoom },
    new MenuItem { Role = MenuRole.zoomin },
    new MenuItem { Role = MenuRole.zoomout },
    new MenuItem { Type = MenuType.separator },
    new MenuItem { Role = MenuRole.togglefullscreen }
  };

  if (isMac) {
    menu = new MenuItem[] {
      new MenuItem { Label = "Electron", Type = MenuType.submenu, Submenu = appMenu },
      new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
      new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
    };
  } else {
    menu = new MenuItem[] {
      new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },
      new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }
    };
  }

  Electron.Menu.SetApplicationMenu (menu);
}

Add following statement in Configure() method of Startup.cs just before await Electron.WindowManager.CreateWindowAsync():

CreateMenu();

Test the save-as functionality by starting the Electron app with the following terminal-window command:

electronize start

Click on File >> Save As ...

Select a location and give the export file a name (like data), then click on save. The content of data.csv should look like this:

Build for specific platform:

You can produce a setup application for Windows, macOS & Linux. To generate the setup application for Windows, execute the following command from a terminal window:

electronize build /target win /PublishReadyToRun false 

The result is a setup application located in bin/Desktop that you can distribute. Be patient because it takes time to generate.


If you run the setup exe file, it will install a desktop application on your computer that you can easily uninstall.

I hope you found this article useful and hope you build great Electron.NET apps.

Reference:
    https://www.grapecity.com/blogs/building-cross-platform-desktop-apps-with-electron-dot-net


Saturday, December 12, 2020

Exploring GitHub Codespaces

In this tutorial I will introduce you to GitHub Codespaces. We will first create an ASP.NET Core MVC application on your local computer. We will then push the application to GitHub. Once the application source code is on GitHub, we will use Visual Studio Code in GitHub Codespaces to modify the app and test it out - all in the cloud.

Companion video: https://youtu.be/DsGNAx_kJ3g

What is GitHub codespaces?

GitHub codespaces is an online development environment, hosted by GitHub and powered by Visual Studio Code. It allows you to develop entirely in the cloud. Codespaces is currently in limited public beta and subject to change.

You can signup for access to GitHub Codespaces at: https://github.com/features/codespaces/signup

Let's get started.

1) Create a repository in GitHub. I named mine MvcOnCodespaces.

2) Create an ASP.NET Core MVC application on your local computer. These are the commands I used to create the application named MvcOnCodespaces.

Create a directory for your application

mkdir MvcOnCodespaces

Change to the directory you just created.

cd MvcOnCodespaces

At the moment, the default version of .NET Core that is available on GitHub Codespaces is version 3.1. Therefore, to ensure that we create an application that uses .NET Core 3.1, we will create a global.json file specifying .NET Core version as follows:

dotnet new globaljson --sdk-version 3.1.401

NOTE: Find out the version of .NET Core 3.1 that exists on your computer using command:

dotnet --list-sdks 

Use the appropriate version in the 'dotnet new globaljson ..." command.

This was necessary for me to do because the default version of .NET Core on my computer was 5.0 at the time of writing this post. 

Now we can create an ASP.NET Core MVC 3.1 app with:

dotnet new mvc

If you inspect your .csproj file, you will find that it, indeed, targets .NET Core 3.1 (netcoreapp3.1).

netcoreapp3.1
At this point, you can go ahead and delete global.json because it served its purpose and we do not need it anymore.

3) Before we push our ASP.NET Core MVC application to GitHub, we need to have a .gitignore file. To create an appropriate .gitignore file, enter the following command in a terminal window:

dotnet new gitignore

Thereafter, create a local git repository, add all your source code files to it and commit your changes with these commands:

git init
git add .
git commit -m "1st commit"

4) Push your source-code to GitHub with the instructions on your repository for pushing existing code:
an existing repository from the command line

4) Create a Codespace. In your GitHub repository, click on Code followed by "Open with Codespaces".

Open with Codespaces

On the next dialog, click on the "+ New codespace" button.

+ New codespace

At the top right side you will see a progress bar that indicates that a Codespace is being prepared for you.

preparing your codespace
Click on Yes when you see this dialog:
required assets to build and debug
You will find yourself in familiar territory with VS Code running in your browser. Wait until all the activity in the lower pane settles down and you see a Finished statement.
Online VS Code

Querying the .NET environment in your Codespace

Let us query the .NET Core environment in a terminal window. Click on the TERMINAL tab.
Terminal
In the terminal window, type:
dotnet --list-sdks
The list of SDKs at the time of writing this article were as shown below:

dotnet --list-sdks

Build & run your web app

You can also go ahead and build with: dotnet build
dotnet build
To run your application, hit CTRL F5. In the "DEBUG CONSOLE" pane, do a "CTRL Click" on the https://localhost:5001 link.
Ctrl Click
Port forwarding happens and the web app opens in a separate tab in your browser.
Port forwarding in codespaces

Let's make a change to our application. Edit Views/Shared/_Layout.cshtml in the Codespace. Around line 32, add the following style to the main <div> tag to change the background color to gold:

style="background-color: gold;"

css style

Stop and restart the application. This is done by clicking on the stop button first.
Stop application
Thereafter, hit CTRL F5. After the application restarts, go to the other tab that has theweb app and refresh the page. You will see the style change that we made.

CSS style change

Syncing source code

Git reminds us that there are three changes that happened to our code.

Git changes

Stage changes with the following:
Stage all changes

Next, let us commit staged changes:
Commit stages

Enter a message:
git commit message
Finally, push the changes:
git push

Debugging

Let us see if we can debug the application in GitHub Codespaces. Stop the application. Open Controllers/HomeController.cs in the online VS Code editor. Add some code to the Index() action method as follows:

breakpoint
Add a breakpoint on the line with statement 'return View()'.

Run your application in Debug mode by hitting F5. If you refresh the web app in the other tab, the app will stop at the breakpoint, as expected.

stop at breakpoint

You can use the debug controls to: Continue, Step Over, Step Into, Step Out, Restart and Stop

debug controls

Cleanup

Delete the codespace you created once you determine that you do not need it anymore. Click on the Codespaces tab, click the ... (three dots) on the right side of the codepace,  then choose delete.

delete github codespace

Conclusion

I hope this journey through the world of GitHub Codespaces gave you a good understanding of what is possible with this new cloud service.

Monday, April 20, 2020

Working with JSON APIs from ASP.NET MVC

Companion Video: https://youtu.be/r8stP_6V0OY
Source Code: https://github.com/medhatelmasry/ConsumeStudentsAPI

APIs can be consumed from any type of application. In your career you will consume APIs mostly from JavaScript. We can, however, consume APIs from an ASP.NET MVC application too. This is what we will be doing today. There is an online Students API that we will be working with. This API works with the following HTTP methods:
POST
insert
PUT
update
GET
read
DELETE
delete
The Students API has the following columns:
studentId
string
firstName
string
lastName
string
school
string
Let us first create an ASP.NET Core MVC application. Go into your working directory in a terminal window and execute this command:
dotnet new mvc -o ConsumeStudentsAPI
The above command will create an ASP.NET MVC web application in a directory called ConsumeStudentsAPI. Next, also in a terminal window, change directory with:
cd ConsumeStudentsAPI
We will need to use a package named Newtonsoft.Json. Therefore, execute the following command in a terminal window to add this package:
dotnet add package Newtonsoft.Json
You can continue either with Visual Studio 2019 or Visual Studio Code. It is really up to you.
Add to the Models folder a class file named Student.cs with the following class definition:
public class Student {

  [Display(Name = "ID")]
  [Key]
  public string studentId { get; set; }

  [Required]
  [Display(Name = "First Name")]
  public string firstName { get; set; }
 
  [Required]
  [Display(Name = "Last Name")]
  public string lastName { get; set; }

  [Required]
  [Display(Name = "School")]
  public string school { get; set; }
}
Notice these annotations:
·       Display allows you to have an alternative display name for a property in the model
·       Key sets studentId as the primary key
·       Required makes sure that the user enters a value for this property.
We will be using the IHttpClientFactory factory class to make HTTP requests to the API. Therefore, we need to add a singleton object into the application. Add this code to the ConfigureServices() method in Startup.cs:
services.AddHttpClient();
Next, add to the Controllers folder a file named StudentsController.cs with the following class definitions:
public class StudentsController : Controller {
  const string BASE_URL = "https://api.azurewebsites.net/";
  private readonly ILogger<StudentsController> _logger;
  private readonly IHttpClientFactory _clientFactory;
  public IEnumerable<Student> Students { get; set; }
  public bool GetStudentsError { get; private set; }
 
  public StudentsController(ILogger<StudentsController> logger, IHttpClientFactory clientFactory) {
    _logger = logger;
    _clientFactory = clientFactory;
  }

  public async Task<IActionResult> Index() {
    var message = new HttpRequestMessage();
    message.Method = HttpMethod.Get;
    message.RequestUri = new Uri($"{BASE_URL}api/students");
    message.Headers.Add("Accept", "application/json");
 
    var client = _clientFactory.CreateClient();

    var response = await client.SendAsync(message);

    if (response.IsSuccessStatusCode) {
        using var responseStream = await response.Content.ReadAsStreamAsync();
        Students = await JsonSerializer.DeserializeAsync<IEnumerable<Student>>(responseStream);
    } else {
        GetStudentsError = true;
        Students = Array.Empty<Student>();
    }

    return View(Students);
  }

  public async Task<IActionResult> Details(string id) {
    if (id == null)
      return NotFound();

    var message = new HttpRequestMessage();
    message.Method = HttpMethod.Get;
    message.RequestUri = new Uri($"{BASE_URL}api/students/{id}");
    message.Headers.Add("Accept", "application/json");

    var client = _clientFactory.CreateClient();

    var response = await client.SendAsync(message);

    Student student = null;

    if (response.IsSuccessStatusCode) {
      using var responseStream = await response.Content.ReadAsStreamAsync();
      student = await JsonSerializer.DeserializeAsync<Student>(responseStream);
    } else {
      GetStudentsError = true;
    }

    if (student == null)
      return NotFound();

    return View(student);

  }

  public IActionResult Create() {
    return View();
  }

  [HttpPost]
  [ValidateAntiForgeryToken]
  public async Task<IActionResult> Create([Bind("studentId,firstName,lastName,school")] Student student)
  {
    if (ModelState.IsValid) {
      HttpContent httpContent = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(student), Encoding.UTF8);
      httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");

      var message = new HttpRequestMessage();
      message.Content = httpContent;
      message.Method = HttpMethod.Post;
      message.RequestUri = new Uri($"{BASE_URL}api/students");

      HttpClient client = _clientFactory.CreateClient();
      HttpResponseMessage response = await client.SendAsync(message);

      var result = await response.Content.ReadAsStringAsync();

      return RedirectToAction(nameof(Index));
    }

    return View(student);
  }

  public async Task<IActionResult> Edit(string id) {
    if (id == null)
      return NotFound();

    var message = new HttpRequestMessage();
    message.Method = HttpMethod.Get;
    message.RequestUri = new Uri($"{BASE_URL}api/students/{id}");
    message.Headers.Add("Accept", "application/json");

    var client = _clientFactory.CreateClient();

    var response = await client.SendAsync(message);

    Student student = null;

    if (response.IsSuccessStatusCode) {
      using var responseStream = await response.Content.ReadAsStreamAsync();
      student = await JsonSerializer.DeserializeAsync<Student>(responseStream);
    } else {
      GetStudentsError = true;
    }

    if (student == null)
      return NotFound();

    return View(student);

  }

  [HttpPost]
  [ValidateAntiForgeryToken]
  public async Task<IActionResult> Edit(string id, [Bind("studentId,firstName,lastName,school")] Student student)
  {
    if (id != student.studentId)
      return NotFound();

    if (ModelState.IsValid) {
      HttpContent httpContent = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(student), Encoding.UTF8);
      httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");

      var message = new HttpRequestMessage();
      message.Content = httpContent;
      message.Method = HttpMethod.Put;
      message.RequestUri = new Uri($"{BASE_URL}api/students/{id}");

      HttpClient client = _clientFactory.CreateClient();
      HttpResponseMessage response = await client.SendAsync(message);

      var result = await response.Content.ReadAsStringAsync();

      return RedirectToAction(nameof(Index));
    }

    return View(student);
  }

  public async Task<IActionResult> Delete(string id) {
    if (id == null)
      return NotFound();

    var message = new HttpRequestMessage();
    message.Method = HttpMethod.Get;
    message.RequestUri = new Uri($"{BASE_URL}api/students/{id}");
    message.Headers.Add("Accept", "application/json");

    var client = _clientFactory.CreateClient();

    var response = await client.SendAsync(message);

    Student student = null;

    if (response.IsSuccessStatusCode) {
      using var responseStream = await response.Content.ReadAsStreamAsync();
      student = await JsonSerializer.DeserializeAsync<Student>(responseStream);
    } else {
        GetStudentsError = true;
    }

    if (student == null)
      return NotFound();

    return View(student);

  }

  [HttpPost, ActionName("Delete")]
  [ValidateAntiForgeryToken]
  public async Task<IActionResult> DeleteConfirmed(string id) {
    var message = new HttpRequestMessage();
    message.Method = HttpMethod.Delete;
    message.RequestUri = new Uri($"{BASE_URL}api/students/{id}");

    HttpClient client = _clientFactory.CreateClient();
    HttpResponseMessage response = await client.SendAsync(message);

    var result = await response.Content.ReadAsStringAsync();

    return RedirectToAction(nameof(Index));
  }
}
The above code represents a controller that has action methods to list, add, edit and delete data. We will need to have views for the action methods in StudentsContrller. Therefore, in the Views folder, create another folder named Students. Inside of the Views/Students folder add these Create.cshtml, Delete.cshtml, Details.cshtml, Edit.cshtml and Index.cshtml files:

Create.cshtml

@model ConsumeStudentsAPI.Models.Student

@{
  ViewData["Title"] = "Add Student";
}

<h1>@ViewData["Title"]</h1>

<hr />
<div class="row">
  <div class="col-md-4">
    <form asp-action="Create">
      <div asp-validation-summary="ModelOnly" class="text-danger"></div>

      <div class="form-group">
        <label asp-for="studentId" class="control-label"></label>
        <input asp-for="studentId" class="form-control" />
        <span asp-validation-for="studentId" class="text-danger"></span>
      </div>

      <div class="form-group">
        <label asp-for="firstName" class="control-label"></label>
        <input asp-for="firstName" class="form-control" />
        <span asp-validation-for="firstName" class="text-danger"></span>
      </div>

      <div class="form-group">
        <label asp-for="lastName" class="control-label"></label>
        <input asp-for="lastName" class="form-control" />
        <span asp-validation-for="lastName" class="text-danger"></span>
      </div>

      <div class="form-group">
        <label asp-for="school" class="control-label"></label>
        <input asp-for="school" class="form-control" />
        <span asp-validation-for="school" class="text-danger"></span>
      </div>

      <input type="submit" value="Create" class="btn btn-success" />
      <a asp-action="Index" class="btn btn-primary">&lt;&lt; Back to List</a>
    </form>
  </div>
</div>

Delete.cshtml

@model ConsumeStudentsAPI.Models.Student
@{
    ViewData["Title"] = "Delete Student";
}
 
<h1>@ViewData["Title"]</h1>
<h3>Are you sure you want to delete this?</h3>
<div>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.studentId)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.studentId)
        </dd> 
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.firstName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.firstName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.lastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.lastName)
        </dd>
 
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.school)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.school)
        </dd>
    </dl>   
    <form asp-action="Delete">
        <input type="hidden" asp-for="studentId" />
        <input type="submit" value="Delete" class="btn btn-danger" /> 
        <a asp-action="Index" class="btn btn-primary">&lt;&lt; Back to List</a>
    </form>
</div>

Details.cshtml

@model ConsumeStudentsAPI.Models.Student

@{
  ViewData["Title"] = "Student Details";
}

<h1>@ViewData["Title"]</h1>
<div>
  <hr />
  <dl class="row">
    <dt class="col-sm-2">
        @Html.DisplayNameFor(model => model.studentId)
    </dt>
    <dd class="col-sm-10">
      @Html.DisplayFor(model => model.studentId)
    </dd>

    <dt class="col-sm-2">
      @Html.DisplayNameFor(model => model.firstName)
    </dt>
    <dd class="col-sm-10">
      @Html.DisplayFor(model => model.firstName)
    </dd>

    <dt class="col-sm-2">
      @Html.DisplayNameFor(model => model.lastName)
    </dt>
    <dd class="col-sm-10">
      @Html.DisplayFor(model => model.lastName)
    </dd>

    <dt class="col-sm-2">
          @Html.DisplayNameFor(model => model.school)
    </dt>
    <dd class="col-sm-10">
      @Html.DisplayFor(model => model.school)
    </dd>
  </dl>
</div>
<div>
  <a asp-action="Edit" asp-route-id="@Model.studentId" class="btn btn-warning">Edit</a> 
  <a asp-action="Index" class="btn btn-primary">&lt;&lt; Back to List</a>
</div>


Edit.cshtml



@model ConsumeStudentsAPI.Models.Student

@{
  ViewData["Title"] = "Edit Student";
}

<h1>@ViewData["Title"]</h1>
<hr />
<div class="row">
  <div class="col-md-4">
    <form asp-action="Edit">
      <div asp-validation-summary="ModelOnly" class="text-danger"></div>
      <input type="hidden" asp-for="studentId" />
      <div class="form-group">
        <label asp-for="firstName" class="control-label"></label>
        <input asp-for="firstName" class="form-control" />
        <span asp-validation-for="firstName" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="lastName" class="control-label"></label>
        <input asp-for="lastName" class="form-control" />
        <span asp-validation-for="lastName" class="text-danger"></span>
      </div>
      <div class="form-group">
        <label asp-for="school" class="control-label"></label>
        <input asp-for="school" class="form-control" />
        <span asp-validation-for="school" class="text-danger"></span>
      </div>            
      <div class="form-group">
        <input type="submit" value="Save" class="btn btn-warning" />
        <a asp-action="Index" class="btn btn-primary">&lt;&lt; Back to List</a>
      </div>
    </form>
  </div>
</div>

Index.cshtml

@model IEnumerable<ConsumeStudentsAPI.Models.Student>

@{
  ViewData["Title"] = "List Students";
}

<div>
  <h1 class="display-4">@ViewData["Title"]</h1>

  <p>
      <a asp-action="Create" class="btn btn-sm btn-success">Create New</a>
  </p>

  <table class="table table-striped table-bordered">
    <tr>
      <th>@Html.DisplayNameFor(model => model.studentId)</th>
      <th>@Html.DisplayNameFor(model => model.firstName)</th>
      <th>@Html.DisplayNameFor(model => model.lastName)</th>
      <th>@Html.DisplayNameFor(model => model.school)</th>
      <th></th>
    </tr>
    @foreach (var item in Model)
    {
      <tr>
        <td>@item.studentId</td>
        <td>@item.firstName</td>
        <td>@item.lastName</td>
        <td>@item.school</td>
        <td style="text-align: center;">
          <a asp-action="Edit" asp-route-id="@item.studentId" class="btn btn-sm btn-warning">Edit</a>
          <a asp-action="Details" asp-route-id="@item.studentId" class="btn btn-sm btn-info">Details</a>
          <a asp-action="Delete" asp-route-id="@item.studentId" class="btn btn-sm btn-danger">Delete</a>
        </td>
      </tr>
      }
  </table>
</div>
We need to add the Students link to the main menu. Therefore add this <li> tag to Views/Shared/_Layout.cshtml after around line 26:
<li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-action="Index">Students</a> </li>
Let us run the application and see what we have. The home page looks like this:
Click on Students. A list of students in the database will be shown:
You can try adding, editing, displaying and deleting data.