Easily Create A Real-time Application With Blazor And SignalR

Introduction

SignalR is a library for ASP.NET developers to simplify the process of adding real-time web functionality to applications. Real-time web functionality is the ability to have server code push content to connected clients instantly as it becomes available, rather than having the server wait for a client to request new data. The chat application is often used as a SignalR example, but here we will see a small book application, where we can have all the CRUD operations.

Blazor is a framework built by Microsoft for creating interactive client-side web UI with a .NET codebase. We can write both client-side and server-side code in C#.NET itself. There are two hosting models available for Blazor. Blazor Server and Blazor WebAssembly. Blazor Server for production was already available. Recently Microsoft released the production version of Blazor WebAssembly also.

Create Blazor WebAssembly App

We can create a web application with Visual Studio 2019 using the Blazor WebAssembly template. Please select the “ASP.NET Core hosted” option also.

Create Blazor WebAssembly app

If you look at the solution structure, we can see 3 different projects are created by default.

Solution structure

As we are creating a Book app, add a “Book” class in the “Shared” project.

Book. cs

namespace BlazorSignalR.Shared
{
    public class Book
    {
        public string Id { get; set; }
        public string Isbn { get; set; }
        public string Name { get; set; }
        public string Author { get; set; }
        public double Price { get; set; }
    }
}

We have five properties in the Book class.

We can install the “Microsoft.AspNetCore.SignalR.Client” library using NuGet package manager in “Client” project. This project library is already added as a dependency in “Server” project. Hence, we can use SignalR library in the Server project as well.

We must register the SignalR component inside the “ConfigureServices” method of Startup class (Server project).

Configure Services

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddSignalR();
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
}

Create a “BroadcastHub” class inside a new “Hubs” folder in Server project and inherit “Hub“ class from SignalR library.

BroadcastHub.cs

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace BlazorSignalR.Server.Hubs
{
    public class BroadcastHub : Hub
    {
        public async Task SendMessage()
        {
            await Clients.All.SendAsync("ReceiveMessage");
        }
    }
}

We have added a “SendMessage” method in the above class. You can give any name for this method. It will be used to send and receive push notification using SignalR hub.

We can add the endpoints for the BroadcastHub class in the Configure method of Startup class. We named it as “broadcastHub”. This will be used in our Razor components later in Client project.

BroadcastHub class

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseWebAssemblyDebugging();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseBlazorFrameworkFiles();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllers();
        endpoints.MapHub<BroadcastHub>("/broadcastHub");
        endpoints.MapFallbackToFile("index.html");
    });
}

We can create a “BooksController” class for CRUD operations under Controllers folder using Scaffolding template.

Books Controller 

We have used scaffolding with entity framework template. So that, all the methods for CRUD operations has been created automatically. But we have slightly changed the “PostBook” method for our purpose. Please use the below code.

BooksController.cs

using BlazorSignalR.Server.Data;
using BlazorSignalR.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorSignalR.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly BooksDbContext _context;

        public BooksController(BooksDbContext context)
        {
            _context = context;
        }

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

        // GET: api/Books/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Book>> GetBook(string id)
        {
            var book = await _context.Book.FindAsync(id);

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

            return book;
        }

        // PUT: api/Books/5
        // To protect from overposting attacks, enable the specific properties you want to bind to, for 
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutBook(string id, Book book)
        {
            if (id != book.Id)
            {
                return BadRequest();
            }

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

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

            return NoContent();
        }

        // POST: api/Books
        // To protect from overposting attacks, enable the specific properties you want to bind to, for 
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPost]
        public async Task<ActionResult> PostBook(Book book)
        {
            book.Id = Guid.NewGuid().ToString();
            _context.Book.Add(book);
            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateException)
            {
                if (BookExists(book.Id))
                {
                    return Conflict();
                }
                else
                {
                    throw;
                }
            }

            return Ok();
        }

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

            _context.Book.Remove(book);
            await _context.SaveChangesAsync();

            return book;
        }

        private bool BookExists(string id)
        {
            return _context.Book.Any(e => e.Id == id);
        }
    }
}

We can notice that a new connection string is created in the appsettings.json file and registered the database connection context in “ConfigureServices” method of Startup class.

Open Package Manager Console from “Tools” -> “NuGet Package Manager” and use below NuGet command to create a migration script. We are using Entity framework code first approach in this application.

Add-migration Init

Above command will create a new class with current timestamp (Suffix as “_Init”), and it will be used for our data migration.

Use the NuGet command to update the database

Update-database

If you look at the SQL Server Object Explorer, you can see that a new database is created with a “Book” table.

SQL Server

Create Razor Components in Client project

We can create the razor components for CRUD operations in Client project.

Add “ListBooks” component inside the “Pages” folder to display all book details from database using API get method.

ListBooks.razor

@page "/listbooks"

@using BlazorSignalR.Shared
@using Microsoft.AspNetCore.SignalR.Client

@inject NavigationManager NavigationManager
@inject HttpClient Http

<h2>Book Details</h2>
<p>
    <a href="/addbook">Create New Book</a>
</p>
@if (books == null)
{
    <p>Loading...</p>
}
else
{
    <table class='table'>
        <thead>
            <tr>
                <th>Name</th>
                <th>ISBN</th>
                <th>Author</th>
                <th>Price</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in books)
            {
                <tr>
                    <td>@book.Name</td>
                    <td>@book.Isbn</td>
                    <td>@book.Author</td>
                    <td>@book.Price</td>
                    <td>
                        <a href='/editbook/@book.Id'>Edit</a>
                        <a href='/deletebook/@book.Id'>Delete</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    Book[] books;
    private HubConnection hubConnection;

    protected override async Task OnInitializedAsync()
    {

        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/broadcastHub"))
            .Build();

        hubConnection.On("ReceiveMessage", () =>
        {
            CallLoadData();
            StateHasChanged();
        });

        await hubConnection.StartAsync();

        await LoadData();
    }

    private void CallLoadData()
    {
        Task.Run(async () =>
        {
            await LoadData();
        });
    }

    protected async Task LoadData()
    {
        books = await Http.GetFromJsonAsync<Book[]>("api/books");
        StateHasChanged();
    }

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public void Dispose()
    {
        _ = hubConnection.DisposeAsync();
    }
}

If you look at the code, you can see that we have initialized a SignalR hub connection inside the “OnInitializedAsync” method and also navigated to the “broadcastHub” endpoint, that we have already registered in the Startup class.

OnInitializedAsync

Hub connection is listening for a new push message from Hub server and it will call the “CallLoadData” method. This method will again call the LoadData method and will get new or modified Book data from database using API get method. Whenever, we add or change a book record in another web client, it will be automatically reflected in this component. Hence, we will get real-time data.

Create a new razor component “AddBook” and add below code inside the component file.

AddBook.razor

@page "/addbook"

@using Microsoft.AspNetCore.SignalR.Client
@using BlazorSignalR.Shared

@inject HttpClient Http
@inject NavigationManager NavigationManager

<h2>Create Book</h2>
<hr />
<form>
    <div class="row">
        <div class="col-md-8">
            <div class="form-group">
                <label for="Name" class="control-label">Name</label>
                <input id="Name" class="form-control" @bind="@book.Name" />
            </div>
            <div class="form-group">
                <label for="Department" class="control-label">ISBN</label>
                <input id="Department" class="form-control" @bind="@book.Isbn" />
            </div>
            <div class="form-group">
                <label for="Designation" class="control-label">Author</label>
                <input id="Designation" class="form-control" @bind="@book.Author" />
            </div>
            <div class="form-group">
                <label for="Company" class="control-label">Price</label>
                <input id="Company" class="form-control" @bind="@book.Price" />
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-4">
            <div class="form-group">
                <input type="button" class="btn btn-primary" @onclick="@CreateBook" value="Save" />
                <input type="button" class="btn" @onclick="@Cancel" value="Cancel" />
            </div>
        </div>
    </div>
</form>

@code {

    private HubConnection hubConnection;
    Book book = new Book();

    protected override async Task OnInitializedAsync()
    {
        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/broadcastHub"))
            .Build();

        await hubConnection.StartAsync();
    }

    protected async Task CreateBook()
    {
        await Http.PostAsJsonAsync("api/books", book);
        if (IsConnected) await SendMessage();
        NavigationManager.NavigateTo("listbooks");
    }

    Task SendMessage() => hubConnection.SendAsync("SendMessage");

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public void Dispose()
    {
        _ = hubConnection.DisposeAsync();
    }

    void Cancel()
    {
        NavigationManager.NavigateTo("listbooks");
    }
}

Hub connection

Here also, we have initialized the hub connection inside the “OnInitializedAsync” method. If you look at the “CreateBook” method, you can notice that we have sent a notification to hub server after saving the data to database using API post method. Currently, we are not sending any additional message with this notification. We can send any object along with hub notification, but in our application, we have not implemented that. Whenever, we send a push notification from here, all the other open clients will receive the push notification and inside the ListBooks component. We have already implemented the code to load the data from database after receiving new notification.

We can create a new razor component “EditBook” and add below code inside the component file.

EditBook.razor

@page "/editbook/{id}"

@using Microsoft.AspNetCore.SignalR.Client
@using BlazorSignalR.Shared

@inject HttpClient Http
@inject NavigationManager NavigationManager

<h2>Edit Book</h2>
<hr />
<form>
    <div class="row">
        <div class="col-md-8">
            <div class="form-group">
                <label for="Name" class="control-label">Name</label>
                <input id="Name" class="form-control" @bind="@book.Name" />
            </div>
            <div class="form-group">
                <label for="Department" class="control-label">ISBN</label>
                <input id="Department" class="form-control" @bind="@book.Isbn" />
            </div>
            <div class="form-group">
                <label for="Designation" class="control-label">Author</label>
                <input id="Designation" class="form-control" @bind="@book.Author" />
            </div>
            <div class="form-group">
                <label for="Company" class="control-label">Price</label>
                <input id="Company" class="form-control" @bind="@book.Price" />
            </div>
        </div>
    </div>
    <div class="row">
        <div class="form-group">
            <input type="button" class="btn btn-primary" @onclick="@UpdateBook" value="Update" />
            <input type="button" class="btn" @onclick="@Cancel" value="Cancel" />
        </div>
    </div>
</form>

@code {

    private HubConnection hubConnection;
    [Parameter]
    public string id { get; set; }

    Book book = new Book();

    protected override async Task OnInitializedAsync()
    {
        book = await Http.GetFromJsonAsync<Book>("api/books/" + id);

        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/broadcastHub"))
            .Build();

        await hubConnection.StartAsync();
    }

    protected async Task UpdateBook()
    {
        await Http.PutAsJsonAsync("api/books/" + id, book);
        if (IsConnected) await SendMessage();
        NavigationManager.NavigateTo("listbooks");
    }

    Task SendMessage() => hubConnection.SendAsync("SendMessage");

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public void Dispose()
    {
        _ = hubConnection.DisposeAsync();
    }

    void Cancel()
    {
        NavigationManager.NavigateTo("listbooks");
    }
}

Literally, we have used the same approach here also like AddBook component to send a push notification. After updating the data, hub will send a notification to all open clients and client will immediately refresh the data (in ListBooks component) after receiving the notification.

We can create a DeleteBook component and use below code.

DeleteBook.razor

@page "/deletebook/{id}"

@using Microsoft.AspNetCore.SignalR.Client
@using BlazorSignalR.Shared

@inject HttpClient Http
@inject NavigationManager NavigationManager

<h2>Delete</h2>
<p>Are you sure you want to delete this Book with Id: <b>@id</b></p>
<br />

<div class="col-md-4">
    <table class="table">
        <tr>
            <td>Name</td>
            <td>@book.Name</td>
        </tr>
        <tr>
            <td>ISBN</td>
            <td>@book.Isbn</td>
        </tr>
        <tr>
            <td>Author</td>
            <td>@book.Author</td>
        </tr>
        <tr>
            <td>Price</td>
            <td>@book.Price</td>
        </tr>
    </table>
    <div class="form-group">
        <input type="button" value="Delete" @onclick="@Delete" class="btn btn-primary" />
        <input type="button" value="Cancel" @onclick="@Cancel" class="btn" />
    </div>
</div>

@code {

    [Parameter]
    public string id { get; set; }

    Book book = new Book();
    private HubConnection hubConnection;

    protected override async Task OnInitializedAsync()
    {
        book = await Http.GetFromJsonAsync<Book>("api/books/" + id);

        hubConnection = new HubConnectionBuilder()
            .WithUrl(NavigationManager.ToAbsoluteUri("/broadcastHub"))
            .Build();

        await hubConnection.StartAsync();
    }

    Task SendMessage() => hubConnection.SendAsync("SendMessage");

    public bool IsConnected =>
        hubConnection.State == HubConnectionState.Connected;

    public void Dispose()
    {
        _ = hubConnection.DisposeAsync();
    }

    protected async Task Delete()
    {
        await Http.DeleteAsync("api/books/" + id);
        if (IsConnected) await SendMessage();
        NavigationManager.NavigateTo("listbooks");
    }

    void Cancel()
    {
        NavigationManager.NavigateTo("listbooks");
    }
}

We have sent the push notification after deleting the data from database using API delete method.

We can add the navigation menu for Books data inside the “NavMenu” shared component.

NavMenu.razor

<div class="top-row pl-4 navbar navbar-dark">  
    <a class="navbar-brand" href="">BlazorSignalR</a>  
    <button class="navbar-toggler" @onclick="ToggleNavMenu">  
        <span class="navbar-toggler-icon"></span>  
    </button>  
</div>  

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">  
    <ul class="nav flex-column">  
        <li class="nav-item px-3">  
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">  
                <span class="oi oi-home" aria-hidden="true"></span> Home  
            </NavLink>  
        </li>  
        <li class="nav-item px-3">  
            <NavLink class="nav-link" href="counter">  
                <span class="oi oi-plus" aria-hidden="true"></span> Counter  
            </NavLink>  
        </li>  
        <li class="nav-item px-3">  
            <NavLink class="nav-link" href="fetchdata">  
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data  
            </NavLink>  
        </li>  
        <li class="nav-item px-3">  
            <NavLink class="nav-link" href="listbooks">  
                <span class="oi oi-list-rich" aria-hidden="true"></span> Book Details  
            </NavLink>  
        </li>  
    </ul>  
</div>  

@code {  
    private bool collapseNavMenu = true;  

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;  

    private void ToggleNavMenu()  
    {  
        collapseNavMenu = !collapseNavMenu;  
    }  
}

We have completed the entire coding part. We can run the application.

Click the “Books Details” navigation button and add a new book detail. You can open the same application inside a new browser or tab in the same time and notice that the browser will be automatically refreshed after saving the data in current browser.

New book detail

We can edit or delete the book data as well.

Book landing page

Conclusion

In this post, we have seen how to create a Real-time web application with Blazor WebAssembly and SignalR. We have created a simple Book data application with all CRUD operations and see that how the data is updated (automatically refreshed) in different browsers in real-time. We can see more exciting features of Blazor WebAssembly in upcoming articles. Please feel free to share your valuable feedback.


Similar Articles