Showing posts with label SignalR. Show all posts
Showing posts with label SignalR. Show all posts

Monday, November 6, 2023

Server-side Blazor 7.0 APP with CRUD Operations and SQLite

In this post, we will build a Server-side Blazor app talks directly to the SQLite database. This is a very realistic option since both blazor and the database server run on the server. 

Source Code: https://github.com/medhatelmasry/ServerBlazorEF7

Overview

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

Architecture

The architecture of the application that we will be building will look like this:




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. The Blazor application on the server connects directly to the database using Entity Framework Core.

What are we doing in this tutorial?

In this tutorial I will show you how to build a server-side Blazor application that connects directly with SQLite database using Entity Framework Core.

Let's start coding

1) In a terminal window, go to your working directory. Enter the following command to create a Server-Side Blazor application inside a directory called ServerBlazorEF

dotnet new blazorserver -f net7.0 -o ServerBlazorEF

2) Open the ServerBlazorEF folder in Visual Studio Code.

3) For a Blazor server-side project, the IDE requests that you add assets to build and debug the project. Select Yes.

4) Hit Ctrl F5 (or dotnet watch in a terminal windowto run the application. Your default browser will load a page that looks like this: 


Our objective is to extend the above application so that it talks to SQLite using Entity Framework Core. To this end, we will be dealing with a very simple student model. Therefore, add a Student.cs class file in a folder named Models with the following content: 

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

Since we will be using SQLite, we will need to add the appropriate packages. Therefore, from within a terminal window at the root of your ServerBlazorEF project, run the following commands that will add the appropriate database related packages: 

dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package CsvHelper 

We need to add a connection string for the database. Add the following to the appsettings.json file: 

"ConnectionStrings": {
  "DefaultConnection": "DataSource=college.db;Cache=Shared"
}
 
We will be using the Entity Framework Code First approach. 

Developers prefer having sample data when building data driven applications. Therefore, we will create some sample data to ensure that our application behaves as expected. Copy the following data and save it in a text file named students.csv in the wwwroot folder:

StudentId,FirstName,LastName,School
1,Tom,Max,Nursing
2,Ann,Fay,Mining
3,Joe,Sun,Nursing
4,Sue,Fox,Computing
5,Ben,Ray,Mining
6,Zoe,Cox,Business
7,Sam,Ray,Mining
8,Dan,Ash,Medicine
9,Pat,Lee,Computing
10,Kim,Day,Nursing
11,Tim,Rex,Computing
12,Rob,Ram,Business
13,Jan,Fry,Mining
14,Jim,Tex,Nursing
15,Ben,Kid,Business
16,Mia,Chu,Medicine
17,Ted,Tao,Computing
18,Amy,Day,Business
19,Ian,Roy,Nursing
20,Liz,Kit,Nursing
21,Mat,Tan,Medicine
22,Deb,Roy,Medicine
23,Ana,Ray,Mining
24,Lyn,Poe,Computing
25,Amy,Raj,Nursing
26,Kim,Ash,Mining
27,Bec,Kid,Nursing
28,Eva,Fry,Computing
29,Eli,Lap,Business
30,Sam,Yim,Nursing
31,Joe,Hui,Mining
32,Liz,Jin,Nursing
33,Ric,Kuo,Business
34,Pam,Mak,Computing
35,Cat,Yao,Medicine
36,Lou,Zhu,Mining
37,Tom,Dag,Business
38,Stu,Day,Business
39,Tom,Gad,Mining
40,Bob,Bee,Business
41,Jim,Ots,Business
42,Tom,Mag,Business
43,Hal,Doe,Mining
44,Roy,Kim,Mining
45,Vis,Cox,Nursing
46,Kay,Aga,Nursing
47,Reo,Hui,Nursing
48,Bob,Roe,Mining
49,Jay,Eff,Computing
50,Eva,Chu,Business
51,Lex,Rae,Nursing
52,Lin,Dex,Mining
53,Tom,Dag,Business
54,Ben,Shy,Computing
55,Rob,Bos,Nursing
56,Ali,Mac,Business
57,Ken,Sim,Medicine

The starting point is to create a database context class. Add a C# class file named SchoolDbContext.cs in the Data folder with the following class code: 

public class SchoolDbContext : DbContext {
  public DbSet<Student> Students => Set<Student>();

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

  protected override void OnModelCreating(ModelBuilder modelBuilder) {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Student>().HasData(GetStudents());
  }
  
  private static IEnumerable<Student> GetStudents() {
    string[] p = { Directory.GetCurrentDirectory(), "wwwroot", "students.csv" };
    var csvFilePath = Path.Combine(p);

    var config = new CsvConfiguration(CultureInfo.InvariantCulture) {
      PrepareHeaderForMatch = args => args.Header.ToLower(),
    };

    var data = new List<Student>().AsEnumerable();
    using (var reader = new StreamReader(csvFilePath)) {
      using (var csvReader = new CsvReader(reader, config)) {
        data = csvReader.GetRecords<Student>().ToList();
      }
    }
    return data;
  }
}

Notice the above code is adding the contents of the wwwroot/students.csv file as seed data into the database.

In the Program.cs file, just before ‘var app = builder.Build();’, add the following code so that our application can use SQLite:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<SchoolDbContext>(
    options => options.UseSqlite(connectionString)
);
 
We are now ready to apply Entity Framework migrations, create the database and seed some sample data. If you have not done so already, you will need to globally install the Entity Framework CLI tool. This tooling is installed globally on your computer by running the following command in a terminal window:

dotnet tool install --global dotnet-ef

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

dotnet ef migrations add M1 -o Data/Migrations

 You should get no errors and this results in the creation of a migration file ending with the name ....M1.cs in the Migrations folder which contains commands for inserting sample data.

The next step is to create the SQLite college.db database file. This is done by running the following command from inside a terminal window at the root folder of the application. 

dotnet ef database update

If no errors are encountered, you can assume that the database was created and properly seeded with data.

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

public class StudentService {
  private SchoolDbContext _context;
  
  public StudentService(SchoolDbContext context) {
    _context = context;
  }

  public async Task<List<Student>> GetStudentsAsync() {
   return await  _context.Students.ToListAsync();
  }

  public async Task<Student?> GetStudentByIdAsync(int id) {
    return await _context.Students.FindAsync(id) ?? null;
  }

  public async Task<Student?> InsertStudentAsync(Student student) {
    _context.Students.Add(student);
    await _context!.SaveChangesAsync();

    return student;
  }

  public async Task<Student> UpdateStudentAsync(int id, Student s) {
    var student = await _context.Students!.FindAsync(id);

    if (student == null)
      return null!;

    student.FirstName = s.FirstName;
    student.LastName = s.LastName;
    student.School = s.School;

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

    return student!;
  }

  public async Task<Student> DeleteStudentAsync(int id) {
    var student = await _context.Students!.FindAsync(id);

    if (student == null)
      return null!;

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

    return student!;
  }

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

The above StudentService class provides all the necessary methods for CRUD operations involving data retrieval, insertion, update and deletion.

We need to configure the StudentService class as a scoped service so that we can use dependency injection. Scoped lifetime services are created once per client request (connection). Add the following statement to the Program.cs just before ‘var app = builder.Build()’: 

builder.Services.AddScoped<StudentService>();

Close all the files in your editor. Rename FetchData.razor file in the Pages folder to Students.razor. Replace its contents with the following code: 

@page "/students"
@using ServerBlazorEF.Data
@using ServerBlazorEF.Models
@inject StudentService studentService
<h1>Students</h1>

@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>
}


@code {
  List<Student>? students;

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

}

Since we will be using the Student class in multiple razor pages, move the @using ServerBlazorEF.Models statement on line 3 in the above code to _Imports.razor.

Let us focus on the @code block. The OnInitAsyns() method is called when the page gets loaded. It makes a call to the student service which loads a list of students from the database. The remaining HTML/Razor code simply displays the data in a table.

Let's modify the menu item on the left navigation of our application. Open Shared/NavMenu.razor in the editor and change the link for “Fetch data” so it looks like this:

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

You must be eager to test out the server-side Blazor project. Run your app and select the “Get Students” link on the left navigation, this is what the output will look like: 

Adding data

Our Blazor app is not complete without add, edit and delete functionality. We shall start with adding data. 

Let us re-purpose Counter.razor so that it becomes our page for adding data. Rename Counter.razor to AddStudent.razor.

Replace AddStudent.razor with the following code:

@page "/addstudent"
@using ServerBlazorEF.Models
@inject ServerBlazorEF.Data.StudentService studentService
@inject NavigationManager NavManager

<PageTitle>Add Student</PageTitle>

<h1>Add Student</h1>

<EditForm Model="@student" OnValidSubmit="HandleValidSubmit">
  <DataAnnotationsValidator />
  <ValidationSummary />

  <div class="form-group">
    <label for="FirstName">First Name:</label>
    <InputText id="FirstName" class="form-control" @bind-Value="student.FirstName" />
  </div>

  <div class="form-group">
    <label for="LastName">Last Name:</label>
    <InputText id="LastName" class="form-control" @bind-Value="student.LastName" />
  </div>

  <div class="form-group">
    <label for="School">School:</label>
    <InputText id="School" class="form-control" @bind-Value="student.School" />
  </div>

  <button type="submit" class="btn btn-primary">Submit</button>
</EditForm>

@code {
  private Student student = new Student();

  private async Task HandleValidSubmit() {
    await studentService.InsertStudentAsync(student);
    NavManager.NavigateTo("/students");
  }
}

Open Shared/NavMenu.razor in the editor and change the link for “Counter” so it looks like this:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="addstudent">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Add Student
    </NavLink>
</div>

Run the Blazor server-side project and select Add Student on the left navigation menu. This is what it should look like: 


I entered Bob, Smith and Travel for data and when I clicked on the Submit button I got the following data inserted into the database:


Update & Delete data using PUT & DELETE methods

We want to be able to select a row of data and update or delete  it. Add the following additional cells to the table row in Students.razor

<td><a class="btn btn-success btn-sm" href="/updel/@item.StudentId/edit">edit</a></td>

<td><a class="btn btn-danger btn-sm" href="/updel/@item.StudentId/del">del</a></td> 

The above would pass the appropriate studentId and mode parameters to another page with route /updel.

Create a text file named UpdateDelete.razor in the Pages folder with the following content:

@page "/updel/{id}/{mode}"
@using ServerBlazorEF.Models
@inject ServerBlazorEF.Data.StudentService studentService
@inject NavigationManager NavManager

<style>
    fieldset {
        border: 2px solid #000;
        padding-left: 20px;
        margin-bottom: 20px;
    }
</style>

<PageTitle>Update/Delete Student</PageTitle>

@if (student != null && Mode == "edit") // Update
{
    <p>Update Student with ID == @Id</p>
    <EditForm Model="@student" OnValidSubmit="HandleValidSubmit">
        <DataAnnotationsValidator />
        <ValidationSummary />

        <div class="form-group">
            <label for="FirstName">First Name:</label>
            <InputText id="FirstName" class="form-control" @bind-Value="student.FirstName" />
        </div>

        <div class="form-group">
            <label for="LastName">Last Name:</label>
            <InputText id="LastName" class="form-control" @bind-Value="student.LastName" />
        </div>

        <div class="form-group">
            <label for="School">School:</label>
            <InputText id="School" class="form-control" @bind-Value="student.School" />
        </div>

        <button type="submit" class="btn btn-primary">Update</button>
    </EditForm>

    @code {
        private async Task HandleValidSubmit()
        {
            await studentService.UpdateStudentAsync(student!.StudentId, student);
            NavManager.NavigateTo("/students");
        }
    }
}
else if (student != null && Mode == "del")
{ // Delete
    // display student details
    <fieldset>
        <legend>Student Information</legend>
        <p>Student ID: @Id</p>
        <p>First Name: @student.FirstName</p>
        <p>Last Name: @student.LastName</p>
        <p>School: @student.School</p>
    </fieldset>
    <p>Delete Student with ID == @Id</p>
    <p>Are you sure?</p>
    <button type="button" class="btn btn-danger" @onclick="HandleDeleteStudent">Delete</button>
    @code {
    private async Task HandleDeleteStudent()
    {
        await studentService.DeleteStudentAsync(student!.StudentId);
        NavManager.NavigateTo("/students");
    }
}
}
else
{
    <p>Student with ID == @Id not found</p>
}

@code {
    [Parameter]
    public string? Id { get; set; }
    [Parameter]
    public string? Mode { get; set; }
    private Student? student = new Student();

    protected override async Task OnInitializedAsync()
    {
        int intId = Convert.ToInt32(Id);
        student = await studentService.GetStudentByIdAsync(intId);
    }
}

Note how parameters are passed from one page to another.

1) {id}/{mode} are defined in the route
2) In the @code section, the following parameters are defined:

[Parameter]
public string? Id { get; set; }
[Parameter]
public string? Mode { get; set; }

CRUD Experience

    










Component CSS

In the UpdateDeleter.razor page, notice that we have some CSS in the <style> . . .  </style> block. We can move this into a CSS file that only serves the UpdateDelete.razor component. 

In the Pages foldr, create a text file name UpdateDelete.razor.css and add to it the following CSS:

fieldset {
  border: 2px solid #000;
  padding-left: 20px;
  margin-bottom: 20px;
}

Meantime, delete the entire <style> . . . . </style> block in UpdateDeleter.razor. The page should behave just like it did before when you view the delete page.

SignalR

Server-side Blazor uses SignalR to keep a copy of the DOM on the server and to only update changes on the client. Open your Chrome browser development settings and look at the blazor line in the Network tab. This gives you an indication that websockets are used to transmit data between clent and server.


Also, look at the negotiate line in the Network tab.


I hope you learned something new in this tutorial and trust that you will build much more sophisticated Blazor apps.

Sunday, February 16, 2020

Build a simple Sketchpad app with SignalR in ASP.NET Core 3.1

Companion Video: https://youtu.be/ktPTkISof4k
Source Code: https://github.com/medhatelmasry/SignalrSketchpad

What is SignalR

ASP.NET SignalR is a library to add real-time web functionality to applications. Real-time web functionality is the ability to have server-side code push content to the connected clients as it happens, in real-time. It essentially allows your server-side C# code to invoke client-side JavaScript functions and vice-versa.

SignalR takes advantage of several transports, automatically selecting the best available transport given the client's and server's capabilities. SignalR takes advantage of WebSockets, an HTML5 API that enables bi-directional communication between the browser and server. SignalR will use WebSockets under the covers when it's available, and gracefully fall back to other techniques and technologies when it is not, while the application code remains the same.

SignalR also provides a simple, high-level API for doing server-to-client RPC (call JavaScript functions in a client's browser from server-side .NET code) in an ASP.NET application, as well as adding useful hooks for connection management, such as connect/disconnect events, grouping connections, authorization, etc.

Assumptions

It is assumed that you have .NET Core 3.1 and VS Code installed on your computer. The following walkthrough works well on Linux, Mac and Windows 10.

The Mission

We will build an ASP.NET Code 3.1 application that allows multiple users, using multiple browsers, to share and scribble on the same canvas.

Getting Started

Let's get started. Go into a terminal windows at suitable workspace folder on your computer and create an ASP.NET Core 3.1 web app as follows:
mkdir SignalrSketchpad
cd SignalrSketchpad
dotnet new webapp --no-https

We will use Library Manager (LibMan) to get the client library from unpkg. unpkg is a content delivery network (CDN)) that can deliver anything found in npm, the Node.js package manager.
Run the following command if you do not already have LibMan installed on your computer:
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
Run the following command in the root folder of the SignalrSketchpad project to get the SignalR JavaScript client library by using LibMan. You might have to wait a few seconds before seeing output.
libman install @aspnet/signalr -p unpkg -d wwwroot/lib/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js
The above parameters specify the following options:
- Use the unpkg provider.
- Copy files to the wwwroot/lib/signalr destination.
- Copy only the specified files.

In the SignalrSketchpad project folder, create a Hubs folder. In the Hubs folder, create a DrawDotHub.cs file with the following code:
public class DrawDotHub: Hub {
   public async Task UpdateCanvas(int x, int y) {
      await Clients.All.SendAsync("updateDot",x, y);
   }

   public async Task ClearCanvas() {
      await Clients.All.SendAsync("clearCanvas");
   }
}

The DrawDotHub class inherits from the SignalR Hub class. The Hub class manages connections, groups, and messaging.

The UpdateCanvas and ClearCanvas methods can be called by a connected JavaScript client to draw and clear drawings respectively on all clients.

Configure SignalR

Append this code to the ConfigureServices() method in Startup.cs:
services.AddSignalR();
Add this code to the Configure() method in Startup.cs. Put it inside the app.UseEndpoints() block:
endpoints.MapHub<DrawDotHub>("/drawDotHub");

Add SignalR client code

Replace contents of Pages\Index.cshtml with the following code:
@page
<style>
        /* Some CSS styling */
        .rightside {
            float: left;
            margin-left: 10px;
        }

        #sketchpad {
            float: left;
            height: 300px;
            width: 600px;
            border: 2px solid #888;
            border-radius: 4px;
            position: relative; /* Necessary for correct mouse co-ords in Firefox */
        }

        #clear_button, #save_button {
            float: left;
            font-size: 15px;
            padding: 10px;
            -webkit-appearance: none;
            background: #feee;
            border: 1px solid #888;
            margin-bottom: 5px;
        }
</style
<h1>SignalR Sketchpad</h1
<div id="sketchpadapp">
        <div class="rightside">
            <button id="clear_button" onclick="tellServerToClear()">Clear Canvas</button>
            <br />
            <canvas id="sketchpad" width="600" height="300"></canvas>
        </div
</div>

<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/draw.js"></script>

The preceding code:
- Creates a drawing canvas with id = sketchpad.
- Creates a “Clear Canvas” button right above the canvas that calls a function named tellServerToClear().
- Includes script references to SignalR and the draw.js application code that will be created in the next step.
- JavaScript files signalr.js and draw.js are loaded

We will not need Pages/Index.cshtml.cs so you can go ahead and delete it. 

In the wwwroot/js folder, create a file named draw.js with the following code: 

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/drawDotHub").build();

connection.on("updateDot", function (x, y) {
    drawDot(x, y, 8);
});

connection.on("clearCanvas", function () {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
});

connection.start().then(function () {
    // nothing here
}).catch(function (err) {
    return console.error(err.toString());
});

function tellServerToClear() {
    connection.invoke("ClearCanvas").catch(function (err) {
        return console.error(err.toString());
    });
}
//////////////////////////////////////////////////////
// Variables for referencing the canvas and 2dcanvas context
var canvas, ctx;
// Variables to keep track of the mouse position and left-button status
var mouseX, mouseY, mouseDown = 0;
// Draws a dot at a specific position on the supplied canvas name
// Parameters are: A canvas context, the x position, the y position, the size of the dot
function drawDot(x, y, size) {
    // Let's use black by setting RGB values to 0, and 255 alpha (completely opaque)
    var r = 0;
    var g = 0;
    var b = 0;
    var a = 255;
    // Select a fill style
    ctx.fillStyle = "rgba(" + r + "," + g + "," + b + "," + (a / 255) + ")";
    // Draw a filled circle
    ctx.beginPath();
    ctx.arc(x, y, size, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
}

// Keep track of the mouse button being pressed and draw a dot at current location
function sketchpad_mouseDown() {
    mouseDown = 1;
    drawDot(mouseX, mouseY, 8);

    connection.invoke("UpdateCanvas", mouseX, mouseY).catch(function (err) {
        return console.error(err.toString());
    });
}

// Keep track of the mouse button being released
function sketchpad_mouseUp() {
    mouseDown = 0;
}

// Keep track of the mouse position and draw a dot if mouse button is currently pressed
function sketchpad_mouseMove(e) {
    // Update the mouse co-ordinates when moved
    getMousePos(e);
    // Draw a dot if the mouse button is currently being pressed
    if (mouseDown == 1) {
        drawDot(mouseX, mouseY, 8);
        connection.invoke("UpdateCanvas", mouseX, mouseY).catch(function (err) {
            return console.error(err.toString());
        });
    }
}

// Get the current mouse position relative to the top-left of the canvas
function getMousePos(e) {
    if (!e)
        var e = event;
    if (e.offsetX) {
        mouseX = e.offsetX;
        mouseY = e.offsetY;
    }
    else if (e.layerX) {
        mouseX = e.layerX;
        mouseY = e.layerY;
    }
}

// Set-up the canvas and add our event handlers after the page has loaded
// Get the specific canvas element from the HTML document
canvas = document.getElementById('sketchpad');
// If the browser supports the canvas tag, get the 2d drawing context for this canvas
if (canvas.getContext)
    ctx = canvas.getContext('2d');

// Check that we have a valid context to draw on/with before adding event handlers
if (ctx) {
    // React to mouse events on the canvas, and mouseup on the entire document
    canvas.addEventListener('mousedown', sketchpad_mouseDown, false);
    canvas.addEventListener('mousemove', sketchpad_mouseMove, false);
    window.addEventListener('mouseup', sketchpad_mouseUp, false);
} else {
    document.write("Browser not supported!!");
}

The preceding code:
- gets a 2d handle to the canvas element in the page
- sets event listeners for mousedown, mousemove and mouseup events
- a connection is established with the server-side hub at endpoint /drawDotHub
- whenever the server invokes a function named updateDot() on the client, then the drawDot() JavaScript function is called
- whenever the server invokes a function named clearCanvas() on the client, then the clearCanvas() JavaScript statement is executed
- connection.start() establishes a live connection with the server
- JavaScript function tellServerToClear() invokes method ClearCanvas() on the server
- JavaScript function sketchpad_mouseDown() and sketchpad_mouseMove() invoke the UpdateCanvas() methods on the server.

Run the app by typing “dotnet run” in a terminal window inside the project root folder. Point your browser to http://localhost:5000. 
Open the same page in a different browser then put both browsers side-by-side. When you scribble on one canvas you will see the same sketch on the other browser.
Use the concept to build much more sophisticated SignalR apps.


Thursday, July 4, 2019

Blazor server-side app with CRUD operations that talk directly to SQL Server & built with VS Code on Windows 10

Source code for this tutorial can be found at: https://github.com/medhatelmasry/ServerBlazorEF.git
Companion Video Tutorial: https://youtu.be/wDVLRg_HCE4

In a previous post, I discussed a Blazor server-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 the Server-side Blazor app talks directly to SQL Server Express. This is a very realistic option since both blazor and the database server run on the server.

Overview

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

Architecture

The architecture of the application that we will be building will look like this:



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. The Blazor application on the server connects directly to the database using Entity Framework Core.

What are we going to do in this Tutorial?

In this tutorial I will show you how to build a server-side Blazor application that connects directly with SQL Server Express using Entity Framework Core.

Perquisites

This tutorial was written while Blazor was in preview mode. The specific preview version of .NET Core 3.0 used in this tutorial is 3.0.100-preview6-012264. I used the following site for setting up my environment: https://docs.microsoft.com/en-us/aspnet/core/blazor/get-started

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
- Visual Studio Code
- The latest C# for Visual Studio Code extension

Let's start coding

1) In a terminal window, go to your working directory. Enter the following command to create a Server-Side Blazor application inside a directory called ServerBlazorEF:
dotnet new blazorserverside -o ServerBlazorEF
2) Open the ServerBlazorEF folder in Visual Studio Code.

3) For a Blazor server-side project, the IDE requests that you add assets to build and debug the project. Select Yes.

4) Hit Ctrl F5 to run the application. Your default browser will load a page that looks like this:



Our objective is to extend the above application so that it talks to SQL Server Express using Entity Framework Core. To this end, we will be dealing with a very simple student model. Therefore, add a Student.cs class file in a folder named Models with the following  content:
using System.ComponentModel.DataAnnotations;

namespace ServerBlazorEF.Models {
  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; }
  }
}
Since we will be using SQL Server, we will need to add the appropriate Entity Framework packages. From within a terminal window at the root of your ServerBlazorEF project,  run the following commands that will add the appropriate database related packages:
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.SqlServer.Design
We need to add a connection string for the database. Add the following to the top of the appsettings.json file:
"ConnectionStrings": {
  "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CollegeDB;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 in the Data folder with the following class code:
using System;
using Microsoft.EntityFrameworkCore;

namespace ServerBlazorEF.Models {
  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"
        }
      );
    }
  }
}
Notice the above code is adding four records of seed data into the database.
In the Startup.cs file, 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. If you have not done so already, you will need to globally install the Entity Framework CLI tool. This tooling has 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 in the Models folder that helps read configuration settings from appsettings.json:
using Microsoft.Extensions.Configuration;

namespace ServerBlazorEF.Models {
  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 in the Data folder. This file extends from IDesignTimeDbContextFactory<T> and will be used to create the database context:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace ServerBlazorEF.Models {
  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);
    }
  }
}
After builing your application, 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:
using Microsoft.EntityFrameworkCore.Migrations;

namespace ServerBlazorEF.Migrations {
  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[,] {
          { "22ced2e2-ff02-4fdb-b690-292df3eecef7", "Jane", "Smith", "Medicine" },
          { "733548c4-aa07-40e7-bb79-737fb91a32fd", "John", "Fisher", "Engineering" },
          { "48c713d5-0609-47ea-a0f8-7a962ffc0232", "Pamela", "Baker", "Food Science" },
          { "48eb4c23-c8af-420c-950d-f74be1a43b98", "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 CollegeDB database in SQL Server. This is done by running the following command from inside a terminal window at the root folder of the application.
dotnet-ef database update
If no errors are encountered, you can assume that the database was created and properly seeded with data.

Add a class file named StudentService.cs in the Data folder. Replace the class with the following code:
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ServerBlazorEF.Models {
  public class StudentService {
      SchoolDbContext _context;
      public StudentService(SchoolDbContext context) {
        _context = context;
      }

      public async Task<List<Student>> GetStudentsAsync() {
        return await _context.Students.ToListAsync();
      }

      public async Task<Student> GetStudentByIdAsync(string id) {
        return await _context.Students.FindAsync(id);
      }

      public async Task<Student> InsertStudentAsync(Student student) {
        _context.Students.Add(student);
        await _context.SaveChangesAsync();

        return student;
      }

      public async Task<Student> UpdateStudentAsync(string id, Student s) {
        var student = await _context.Students.FindAsync(id);
        
        if (student == null)
          return null;

        student.FirstName = s.FirstName;
        student.LastName = s.LastName;
        student.School = s.School;

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

        return student;
      }

      public async Task<Student> DeleteStudentAsync(string id)
      {
        var student = await _context.Students.FindAsync(id);
        
        if (student == null)
          return null;

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

        return student;
      }

      private bool StudentExists(string id) {
        return _context.Students.Any(e => e.StudentId == id);
      }
  }
}
The above StudentService class provides all the necessary methods for CRUD operations involving data retrieval, insertion, update and delettion.

We need to configure the StudentService class as a scoped service so that we can use dependency injection. Scoped lifetime services are created once per client request (connection).
Add the following statement to the ConfigureServices() method in Startup.cs:
services.AddScoped<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 ServerBlazorEF.Models
@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 {
  List<Student> students;

  protected override async Task OnInitAsync() {
    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 list of students array with data from the database. 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, hit CTRL F5 to run the application without debugging. If you select the Students link on the left-side navigation, this is what the output will look like:

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 left-side navigation menu. This is what it should look like:


I entered Harry, Green and Agriculture for data and when I clicked on the Insert button 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 37, change the @if (students != null) statement to: 
@if (students != null && mode==MODE.Add) // Insert form 
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 service and displays it in the Update/Delete form:
protected async Task Show(string id) {
  student = await studentService.GetStudentByIdAsync(id);
  studentId = student.StudentId;
  firstName = student.FirstName;
  lastName = student.LastName;
  school = student.School;
  mode = MODE.EditDelete;
}
Let us test our app by 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 Delete, Harry Green got removed.


I hope you learned something new in this tutorial and trust that you will build much more sophisticated Blazor apps than this.

Good Luck!