Thursday, June 6, 2019

Blazor server-side app with CRUD operations against a Web API / EF / SQL Server endpoint

In a previous post, I discussed a Blazor client-side app with CRUD operations against a Web API endpoint. In this post, I will build a Blazor application with similar functionality. However, this time we will use the Server-side Blazor template.

Overview

Blazor is a framework for developing interactive client-side web applications using C# instead of JavaScript.

Source code for this tutorial can be found at: https://github.com/medhatelmasry/ServerSideBlazor

Companion video tutorial at: https://youtu.be/ErKfJwIyp_A

Client-Side Blazor

In the client side blazor model, the Blazor app + its dependencies +  .NET runtime are downloaded to the browser. The app is then executed directly on the browser UI thread as shown below:

Server-Side Blazor

ASP.NET Core hosts the server-side app and sets up SignalR endpoint where clients connect. SignalR is responsible for updating the DOM on the client with any changes.

What are we going to do in this Tutorial?

In this tutorial I will show you how to build a server-side Blazor application with the following structure:


1) The class library will have model definitions that are shared between the Web API and Blazor Visual Studio projects. The model that we will create in this tutorial is a simple C# Student class that looks like this:

public class Student {
  public string StudentId { get; set; }
  [Required]
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
  [Required]
  public string School { get; set; }
}

2) The ASP.NET Core Web API app will provide the REST endpoints for a students service that the Blazor client-side application will consume. It will use the GET, POST, PUT and DELETE methods to carry out CRUD operations with the API service. Entity Framework will be used to save data in SQL Server using the "Code First" approach.

3) The Server-side Blazor app will update the DOM on the client using SignalR.

Perquisites

This tutorial was written while Blazor was in preview mode. I used the following site for setting up my environment:


This is how I setup my development environment:

- .NET Core 3.0 Preview SDK installed from https://dotnet.microsoft.com/download/dotnet-core/3.0

- Installed Blazor templates by running the following command in a terminal window:

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview5-19227-01

- Visual Studio 2019 preview version installed from: https://visualstudio.com/preview

- Latest Blazor extension for Visual Studio 2019 installed from https://go.microsoft.com/fwlink/?linkid=870389.

Creating the class library

Create a new project in Visual Studio 2019.
Choose "Class Library (.NET Standard)" then click Next:

Set these values on the next dialog:

Project name: SchoolLibrary
Solution name: School

Click on Create.

We need to install a package to the class library project. From within a terminal window at the main SchoolLibrary folder, run the following command:

dotnet add package System.ComponentModel.Annotations


Delete Class1.cs, then create a new Student.cs class file and add to it the following code:

public class Student {
  public string StudentId { get; set; }
  [Required]
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
  [Required]
  public string School { get; set; }
}

Resolve "Required" by adding the following using statement at the top of Student.cs:

using System.ComponentModel.DataAnnotations;

Creating the ASP.NET Core Web API student service application

Right-click on the main solution node in Solution Explorer and select: Add >> New Project. Choose the "ASP.NET Core Web Application" template:

Name the Project name: SchoolAPI

Click on Create.

On the next dialog, select ASP.NET Core 3.0, choose the API template then click on Create:

Our Web API project needs to use the Student class in the SchoolLibrary project. Therefore, we will make a reference from the SchoolAPI project into the SchoolLibrary project. Right-click on the SchoolAPI project node then: Add >> Reference... >> Projects >> Solution >> check SchoolLibrary. Then click on OK.

Since we will be using SQL Server, we will need to add the appropriate Entity Framework packages and tooling. From within a terminal window at the root of your StudentAPI project,  run the following commands that will add the appropriate database related packages:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools

We need to add a connection string to the database. Add the following to the top of the appsettings.json file in project SchoolAPI:
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlazorDB;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

We will be using the Entity Framework Code First approach. The starting point is to create a database context class. Add a C#  class file named SchoolDbContext.cs with the following class code:
public class SchoolDbContext : DbContext {
  public DbSet<Student> Students { get; set; }

  public SchoolDbContext(DbContextOptions<SchoolDbContext> options) : base(options) { }

  protected override void OnModelCreating(ModelBuilder builder) {
    base.OnModelCreating(builder);
    
    builder.Entity<Student>().HasData(
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "Jane", LastName = "Smith", School = "Medicine" },
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "John", LastName = "Fisher", School = "Engineering" },
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "Pamela", LastName = "Baker", School = "Food Science" },
      new { StudentId = Guid.NewGuid().ToString(), 
            FirstName = "Peter", LastName = "Taylor", School = "Mining" }
    );
  }
}

You will need to resolve some of the unrecognized namespaces.

Notice the above code is adding four records of seed data into the database.

In the Startup.cs file in the SchoolAPI project, add the following code to the ConfigureServices() method so that our application can use SQL Server:

services.AddDbContext<SchoolDbContext>(
  option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

We are now ready to apply Entity Framework migrations, create the database and seed data. We need to globally install the Entity Framework CLI tool. This tooling is changed in .NET Core 3.0 and is installed globally on your computer by running the following command in a terminal window:

dotnet tool install --global dotnet-ef --version 3.0.0-*

Remember to build your entire solution before proceeding. Then, from within a terminal window in the SchoolAPI root directory, run the following command to create migrations:

dotnet-ef migrations add initial_migration

I experienced the following error:

Unable to create an object of type 'SchoolDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

I followed the suggested link which obliged me to create DbContextFactory class. This class needs to read the connection string from appsettings.json without dependency injection. For this purpose, I added this helper class that will help read configuration settings from appsettings.json:

public class ConfigurationHelper  {
  public static string GetCurrentSettings(string key) {
    var builder = new ConfigurationBuilder()
      .SetBasePath(System.IO.Directory.GetCurrentDirectory())
      .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
      .AddEnvironmentVariables();

    IConfigurationRoot configuration = builder.Build();

    return configuration.GetValue<string>(key);
  }
}

Next, create another class file named SchoolDbContextFactory.cs. This file extends from IDesignTimeDbContextFactory<T> and will be used to create the database context:

public class SchoolDbContextFactory : IDesignTimeDbContextFactory<SchoolDbContext> {
  public SchoolDbContext CreateDbContext(string[] args) {
    var optionsBuilder = new DbContextOptionsBuilder<SchoolDbContext>();
    var connStr = ConfigurationHelper.GetCurrentSettings("ConnectionStrings:DefaultConnection");
    optionsBuilder.UseSqlServer(connStr);
    return new SchoolDbContext(optionsBuilder.Options);
  }
}

Execute the following terminal command again:

dotnet-ef migrations add initial_migration

You should get no errors and this results in the creation of a migration file ending with the name ....initial_migration.cs in the Migrations folder. In my case, this file looked like this:

public partial class initial_migration : Migration {
  protected override void Up(MigrationBuilder migrationBuilder) {
    migrationBuilder.CreateTable(
      name: "Students",
      columns: table => new {
        StudentId = table.Column<string>(nullable: false),
        FirstName = table.Column<string>(nullable: false),
        LastName = table.Column<string>(nullable: false),
        School = table.Column<string>(nullable: false)
      },
      constraints: table => {
          table.PrimaryKey("PK_Students", x => x.StudentId);
      });

    migrationBuilder.InsertData(
      table: "Students",
      columns: new[] { "StudentId", "FirstName", "LastName", "School" },
      values: new object[,] {
        { "02445eaf-4b31-4266-9234-43a8facdd457", "Jane", "Smith", "Medicine" },
        { "76cc1e56-7aa3-40aa-b1e7-e2dfa3d46489", "John", "Fisher", "Engineering" },
        { "07f046c0-95fb-4425-b52a-4222a71c3f46", "Pamela", "Baker", "Food Science" },
        { "8a4df7f5-2c6a-44d4-9ec1-7824c896c20e", "Peter", "Taylor", "Mining" }
      });
  }

  protected override void Down(MigrationBuilder migrationBuilder) {
    migrationBuilder.DropTable(name: "Students");
  }
}

Note that the above code also includes commands for inserting sample data.

The next step is to create the BlazorDB database in SQL Server. This is done by running the following command from inside a terminal window at the SchoolAPI folder.

dotnet-ef database update

If no errors are encountered, we can assume that the database was created and properly seeded with data. Let us create an API controller so that we can see the data that is in our database.  

Right-click on the Controllers node in the SchoolAPI project then: Add >> Controller...

Under Installed >> Common, choose the API tab, highlight "API Controller with actions, using Entity Framework" then click Add.


On the next dialog, make the following choices:

Model class: Student (SchoolLibrary)
Data context class: SchoolDbContext (SchoolAPI)
Controller name: StudentsController


Once you click on Add, the StudentsController.cs file is created in the Controllers folder. Here is my code for StudentsController.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SchoolAPI;
using SchoolLibrary;

namespace SchoolAPI.Controllers {
  [Route("api/[controller]")]
  [ApiController]
  public class StudentsController : ControllerBase {
    private readonly SchoolDbContext _context;

    public StudentsController(SchoolDbContext context) {
      _context = context;
    }

    // GET: api/Students
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Student>>> GetStudents() {
      return await _context.Students.ToListAsync();
    }

    // GET: api/Students/5
    [HttpGet("{id}")]
    public async Task<ActionResult<Student>> GetStudent(string id) {
      var student = await _context.Students.FindAsync(id);

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

      return student;
    }

    // PUT: api/Students/5
    [HttpPut("{id}")]
    public async Task<IActionResult> PutStudent(string id, Student student) {
      if (id != student.StudentId) {
        return BadRequest();
      }

      _context.Entry(student).State = EntityState.Modified;

      try {
        await _context.SaveChangesAsync();
      } catch (DbUpdateConcurrencyException) {
        if (!StudentExists(id)) {
          return NotFound();
        } else {
          throw;
        }
      }

      return NoContent();
    }

    // POST: api/Students
    [HttpPost]
    public async Task<ActionResult<Student>> PostStudent(Student student) {
      _context.Students.Add(student);
      await _context.SaveChangesAsync();

      return CreatedAtAction("GetStudent", new { id = student.StudentId }, student);
    }

    // DELETE: api/Students/5
    [HttpDelete("{id}")]
    public async Task<ActionResult<Student>> DeleteStudent(string id) {
      var student = await _context.Students.FindAsync(id);
      if (student == null) {
        return NotFound();
      }

      _context.Students.Remove(student);
      await _context.SaveChangesAsync();

      return student;
    }

    private bool StudentExists(string id) {
      return _context.Students.Any(e => e.StudentId == id);
    }
  }
}

 To load the students controller every time you run the SchoolAPI project, edit the launchSettings.json file under the Properties node. Change every instance of "api/values" to "api/students". Now you can run the application by hitting CTRL F5 on your keyboard. This is what you should see in your browser:

[{"studentId":"7721c620-a82b-4204-80af-6c7636efc81e",
  "firstName":"Pamela","lastName":"Baker","school":"Food Science"},
{"studentId":"8734ce0b-4337-487a-bd86-102130d47f8d",
  "firstName":"Jane","lastName":"Smith","school":"Medicine"},
{"studentId":"9a85f51f-160e-4003-9de9-b01354e180a3",
  "firstName":"Peter","lastName":"Taylor","school":"Mining"},
{"studentId":"db356c3a-d745-4f65-9aac-234f706859c2",
  "firstName":"John","lastName":"Fisher","school":"Engineering"}]

Even though our API app seems to be working fine, there is one more thing we need to do. We need to enable CORS (Cross Origin Resource Sharing) so that the service can be accessed from other domains. Add the following code to the ConfigureServices() method in the Startup.cs file found in the SchoolAPI project:

// Add Cors
services.AddCors(o => o.AddPolicy("Policy", builder => {
  builder.AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader();
}));

Add this statement at the top of the Configure() method, also in Startup.cs:

            app.UseCors("Policy");

We now have an API and data that we can work with. Let's see how we can use this data in a server-side Blazor app.

Creating our Server-Side Blazor app

Add a server-side Blazor project to your solution. In Solution Explorer, right-click on the solution node then: Add >> New Project...

Choose the "ASP.NET Core Web Application" template then click on Next.
Name the project ServerBlazor, then click on Create.

Choose "ASP.NET Core 3.0" and the "Blazor (server-side)" template then click on Create.


Let us run our Blazor application to see what we have out of the box . Run the ServerBlazor project by hitting CTRL F5 on your keyboard. You will see a UI that looks like this:



Let's find out more about how the the app works. Click on Counter on the left navigation.

We need the Newtonsoft.Json package for handling json objects. Therefore, run the following command from a terminal window in the ServerBlazor folder:

dotnet add package Newtonsoft.Json

We will add a Students razor page and menu item to this server-side Blazor project.

Our Blazor project needs to use the Student class in the class library. Therefore, make a reference from the Blazor ServerBlazor project into the class library project SchoolLibrary. Right-click on the ServerBlazor project node then: Add >> Reference... >> Projects >> Solution >> check SchoolLibrary. Then click OK.


Open the _Imports.razor file in the editor and add the following using statement to the bottom of the content so that the Student class is available to a  views:

@using SchoolLibrary

Add a class file named StudentService.cs in the Data folder. Replace the class with the following code:

public class StudentService {
  string baseUrl = "https://localhost:44318/";

  public async Task<Student[]> GetStudentsAsync() {
    HttpClient http = new HttpClient();
    var json = await http.GetStringAsync($"{baseUrl}api/students");
    return JsonConvert.DeserializeObject<Student[]>(json);
  }

  public async Task<Student> GetStudentsByIdAsync(string id) {
    HttpClient http = new HttpClient();
    var json = await http.GetStringAsync($"{baseUrl}api/students/{id}");
    return JsonConvert.DeserializeObject<Student>(json);
  }

  public async Task<HttpResponseMessage> InsertStudentAsync(Student student) {
    var client = new HttpClient();
    return await client.PostAsync($"{baseUrl}api/students", getStringContentFromObject(student));
  }

  public async Task<HttpResponseMessage> UpdateStudentAsync(string id, Student student) {
    var client = new HttpClient();
    return await client.PutAsync($"{baseUrl}api/students/{id}", getStringContentFromObject(student));
  }

  public async Task<HttpResponseMessage> DeleteStudentAsync(string id) {
    var client = new HttpClient();
    return await client.DeleteAsync($"{baseUrl}api/students/{id}");
  }

  private StringContent getStringContentFromObject(object obj) {
    var serialized = JsonConvert.SerializeObject(obj);
    var stringContent = new StringContent(serialized, Encoding.UTF8, "application/json");
    return stringContent;
  }
}

You will need to resolve some of the missing namespaces. Also, make sure you adjust the value of baseUrl to match the URL of your SchoolAPI service.

The above StudentService class provides all the necessary methods for making HTTP requests to the API service with GET, POST, PUT and DELETE methods so that CRUD operations can be processed against data.

We need to configure the StudentService class as a singleton so that we can use dependency injection. Add the following statement to the ConfigureServices() method in Startup.cs:

services.AddSingleton<StudentService>();

Make a duplicate copy of the FetchData.razor file in the Pages node and name the new file Students.razor. Replace its contents with the following code:

@page "/students"
@using ServerBlazor.Data
@inject StudentService studentService

<h1>Students</h1>

<p>This component demonstrates managing students data.</p>

@if (students == null) {
  <p><em>Loading...</em></p>
} else {
  <table class='table table-hover'>
    <thead>
      <tr>
        <th>ID</th>
        <th>First Name</th>
        <th>Last Name</th>
        <th>School</th>
      </tr>
    </thead>
    <tbody>
      @foreach (var item in students)
      {
        <tr>
            <td>@item.StudentId</td>
            <td>@item.FirstName</td>
            <td>@item.LastName</td>
            <td>@item.School</td>
            </tr>
        }
    </tbody>
  </table>
}


@functions {
  Student[] students;

  protected override async Task OnInitializedAsync() {
    await load();
  }

  protected async Task load() {
    students = await studentService.GetStudentsAsync();
  }
}

Let us focus on the @functions block. The OnInitAsyns() method is called when the page gets loaded. It calls a local load() method. The load() method makes a call to the student service which loads a students array with data from our API service. The remaining HTML/Razor code simply displays the data in a table.

Let's add a menu item to the left-side navigation of our application. Open Shared/NavMenu.razor in the editor and add the following <li> to the <ul> block (around line 24):

<li class="nav-item px-3">
  <NavLink class="nav-link" href="students">
    <span class="oi oi-list-rich" aria-hidden="true"></span> Students
  </NavLink>
</li>

You must be eager to test out the server-side Blazor project. To run your app, highlight the SchoolAPI node then hit CTRL F5 to run the server-side application without debugging. This starts the API service.

Next, we will run the server-side Blazor application. Highlight the ServerBlazor node then hit CTRL F5 to run the server-side Blazor app. This is what it should look like when you click on Students:

Adding data 

Our Blazor app is not complete without add, edit and delete functionality. We shall start with adding data. Place the following instance variables just above the OnInitAsyc() method:

string studentId;
string firstName;
string lastName;
string school;

We need an HTML form to add data. Add the following RAZOR code just before @functions:

@if (students != null) // Insert form 
{
  <input placeholder="First Name" bind="@firstName" /><br />
  <input placeholder="Last Name" bind="@lastName" /><br />
  <input placeholder="School" bind="@school" /><br />
  <button onclick="@Insert" class="btn btn-warning">Insert</button>
}

When the Insert button is clicked, an Insert() method is called. Add the following Insert() method inside the @functions() block:

protected async Task Insert() {

  Student s = new Student() {
    StudentId = Guid.NewGuid().ToString(),
    FirstName = firstName,
    LastName = lastName,
    School = school
  };

  await studentService.InsertStudentAsync(s);
  ClearFields();
  await load();
}

After data is inserted, the above code clears the fields then loads the data again into an HTML table. Add the following ClearFields() method:

protected void ClearFields() {
  studentId = string.Empty;
  firstName = string.Empty;
  lastName = string.Empty;
  school = string.Empty;
}

Run the Blazor server-side project and select Students from the navigation menu. This is what it should look like:



I entered Harry, Green and Agriculture for data and when I clicked on Insert I got the following data inserted into the database:

Updating & Deleting data

To distinguish between INSERT and EDIT/DELETE mode, we shall add an enum declaration to our code. Add the following to the list of instance variables:

private enum MODE { None, Add, EditDelete };
MODE mode = MODE.None;
Student student;

We will add a button at the top of our table for adding data. Add the following markup just above the opening <table> tag:

<button onclick="@Add"  class="btn btn-success">Add</button>

Here is the Add() method that is called when the above button is clicked:

protected void Add() {
  ClearFields();
  mode = MODE.Add;
}

Around line 39, change the @if (students != null) statement to:

@if (students != null && mode==MODE.Add)

Run the server-side Blazor project. The form for inserting data only displays when the Add button is clicked:


After we successfully insert data, we want the insert form to disappear. Add the following code to the bottom of the Insert() method:

mode = MODE.None;

Let us now add a form that only appears when we wish to update or delete data. Add the following just before @functions:

@if (students != null && mode==MODE.EditDelete) // Update & Delete form
{
  <input type="hidden" bind="@studentId" /><br />
  <input placeholder="First Name" bind="@firstName" /><br />
  <input placeholder="Last Name" bind="@lastName" /><br />
  <input placeholder="School" bind="@school" /><br />
  <button onclick="@Update" class="btn btn-primary">Update</button>
  <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
  <button onclick="@Delete" class="btn btn-danger">Delete</button>
}

Add these Update() and Delete() methods as shown below:

protected async Task Update() {

  Student s = new Student() {
    StudentId = studentId,
    FirstName = firstName,
    LastName = lastName,
    School = school
  };

  await studentService.UpdateStudentAsync(studentId, s);
  ClearFields();
  await load();
  mode = MODE.None;
}

protected async Task Delete() {
  await studentService.DeleteStudentAsync(studentId);
  ClearFields();
  await load();
  mode = MODE.None;
}

We want to be able to select a row of data and update or delete it. We will add an onclick handler to a row. In the HTML table, replace the opening <tr> tag, under the @foreach statement, with this:

<tr onclick="@(() => Show(item.StudentId))">

The above would pass the appropriate studentId to a method named Show() whenever a row is clicked. Add the following Show() method that retrieves a student from the API service and displays it in the Update/Delete form:

protected async Task Show(string id) {
  student = await studentService.GetStudentsByIdAsync(id);
  studentId = student.StudentId;
  firstName = student.FirstName;
  lastName = student.LastName;
  school = student.School;
  mode = MODE.EditDelete;
}

Let us test our app for adding, updating and deleting data. Run the application:


Click on the Add button to insert data.


Enter data then click on the Insert button. The data should get added to the database:


To update the data, click on a row. The selected data should display in the update/delete form.


I selected James Bond and changed Bond to Gardner.


 After I clicked on Update, last name was successfully changed to Gardner.

Lastly, let us delete data. I clicked on the Harry Green row. Data was displayed in the Update/Delete form.

When I clicked on DeleteHarry Green  get removed.

We are told that SignalR is using web sockets to update the DOM. Let us look into this further. I am using Chrome. I hit F12 in Chrome and went to the Network tab and clicked on WS.


Refresh the page in your browser then click on the resource as shown below:


This will show the web socket traffic.

When you click on any button in the UI, the numbers will change indicating that data is being updated using web sockets.

I thank you for coming this far in the tutorial and wish you much luck in your Blazor adventure

3 comments:

  1. Positive site, where did u come up with the information on this posting? I'm pleased I discovered it though, ill be checking back soon to find out what additional posts you include. latest web series

    ReplyDelete
  2. I learned about Blazor from attending various presentations by industry experts on this topic.

    ReplyDelete