NubeSync: Create a Blazor WebAssembly Client

Introduction

To complement our mobile app we will create a NubeSync compatible Blazor WebAssembly application.

The Blazor client does not implement offline sync, but will work in a way that ensures that offline sync on the mobile counterparts will work flawlessly.

The Blazor client will communicate with the server via standard REST calls, the server will then process the database operations in a way that ensures that incremental sync works for the offline mobile clients by creating the operation history.

This opens up the possibility to implement all kinds of other clients, like reporting services or IOT devices – since all the client has to be capable of is making http calls!

 

Server Side

It is assumed that you already integrated NubeSync into an ASP.NET core WebApi project, if not please have a look at https://github.com/stefffdev/NubeSync/wiki/Creating-a-ASP.NET-Core-Backend.

 

Register the IChangeTracker service

In the Startup.cs file register the following service from the namespace NubeSync.Core:

services.AddTransient<IChangeTracker, ChangeTracker>();

 

Implement CRUD in the TodoItemsController

The idea here is that the server exposes standard CRUD methods to the client and internally translates create/update/delete requests to operations and process them via the OperationService – just like with operations that come from a offline client. This ensures that all the operation history necessary for offline clients to be fully functional are generated.

Although this technically introduces a performance hit it creates the possibility to work online and offline on the same records with automatic conflict resolution.

  1. Inject a IChangeTracker in the TodoItemsController, this is necessary for generating the operations for a create/update/delete request:
private readonly DataContext _context;
private readonly IAuthentication _authentication;
private readonly IOperationService _operationService;
private readonly IChangeTracker _changeTracker;

public TodoItemsController(
    IAuthentication authentication,
    IOperationService operationService,
    IChangeTracker changeTracker,
    DataContext context)
{
    _authentication = authentication;
    _operationService = operationService;
    _changeTracker = changeTracker;
    _context = context;
}
  1. Add the following code to the file TodoItemsController.cs:

The get item by id is straightforward, read the requested item directly from the database and return it:

[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> Get(string id)
{
    try
    {
        if (string.IsNullOrWhiteSpace(id))
        {
            return NotFound();
        }

        return Ok(await _context.TodoItems.FindAsync(id));
    }
    catch (Exception)
    {
        // log exception here
        return StatusCode(500);
    }
}

When a item is added, the corresponding operations are created and processed by the OperationService, which will manipulate the database accordingly:

[HttpPost]
public async Task<IActionResult> Add(TodoItem item)
{
    string userId = _authentication.GetUserIdentifier(User);
    string installationId = Request.GetInstallationId();

    try
    {
        var operations = await _changeTracker.TrackAddAsync(item);
        await _operationService.ProcessOperationsAsync(_context, operations, userId, installationId);

        return Ok();
    }
    catch (Exception)
    {
        // log exception here
        return StatusCode(500);
    }
}

Updating a item works in the same manner, but additionally the existing item has to be loaded from the database, so the ChangeTracker can find out what has been changed:

[HttpPut]
public async Task<IActionResult> Update(TodoItem item)
{
    string userId = _authentication.GetUserIdentifier(User);
    string installationId = Request.GetInstallationId();

    try
    {
        var currentItem = await _context.TodoItems.FindAsync(item.Id);
        List<NubeOperation> operations = await _changeTracker.TrackModifyAsync(currentItem, item);
        await _operationService.ProcessOperationsAsync(_context, operations, userId, installationId);

        return Ok();
    }
    catch (Exception)
    {
        // log exception here
        return StatusCode(500);
    }
}

Deleting a record:

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(string id)
{
    string userId = _authentication.GetUserIdentifier(User);
    string installationId = Request.GetInstallationId();

    try
    {
        var item = await _context.TodoItems.FindAsync(id);
        if (item != null)
        {
            List<NubeOperation> operations = await _changeTracker.TrackDeleteAsync(item);
            await _operationService.ProcessOperationsAsync(_context, operations, userId, installationId);
        }
        return Ok();
    }
    catch (Exception)
    {
        // log exception here
        return StatusCode(500);
    }
}

 

Client Side

Create a new Blazor Wasm app:

dotnet new blazorwasm

No special packages have to be installed, since the Blazor client will communicate with the server via standard REST calls.

Insert the URL to your webapi in the Program.cs file:

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("<enter the url to your webapi>") });

 

Create the DTO

Similar to the server project the DTO (Data Transfer Object) contains the structure of the records for communicating with the server.

Create a file TodoItem.cs with the following content:

public class TodoItem
{
    public string Id { get; set; }

    public string Name { get; set; }

    public bool IsChecked { get; set; }
}
Add the TodoItems component

Create a new razor component TodoItems.razor in the pages folder, with the following content:

@page "/todoitems"
@using System.Text.Json;

@inject HttpClient Http

<h3>Todo Items</h3>

<button @onclick="AddButtonClicked">Add Item</button>
<span> </span>
<button @onclick="ModifyButtonClicked">Modify First Item</button>
<span> </span>
<button @onclick="DeleteButtonClicked">Delete Last Item</button>
<span> </span>
<br />
<br />

@if (Items == null)
{
    <p>Loading...</p> 
}
else
{
    <ul>
        @foreach (var item in Items)
        {
            <li>@item.Name @item.IsChecked</li>
        }
    </ul>
}

@code { 
    
    private List<TodoItem> Items { get; set; }

    private async Task GetItems()
    {
        var result = await Http.GetAsync("todoitems");
        if (result.IsSuccessStatusCode)
        {
            var content = await result.Content.ReadAsStringAsync();
            Items = JsonSerializer.Deserialize<List<TodoItem>>(content,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
        }
    }

    protected override async Task OnParametersSetAsync()
    {
        await GetItems();
    }

    private async Task ModifyButtonClicked()
    {
        if (Items != null && Items.Count > 0)
        {
            var item = Items.First();
            item.Name = DateTime.Now.ToLongTimeString();

            await Http.PutAsJsonAsync("todoitems", item);
        }

        await GetItems();
    }

    private async Task AddButtonClicked()
    {
        var item = new TodoItem();
        item.Id = Guid.NewGuid().ToString();
        item.Name = "New Item";

        await Http.PostAsJsonAsync("todoitems", item);

        await GetItems();
    }

    private async Task DeleteButtonClicked()
    {
        if (Items != null && Items.Count > 0)
        {
            var customer = Items.Last();
            await Http.DeleteAsync($"todoitems/{customer.Id}");
        }

        await GetItems();
    } 
}

 

Add a navigation item

Add the todoitems page to the navigation bar in the file Shared\NavMenu.razor:

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

Now you should be able to create and edit items in your brand new Blazor app and after syncing in your mobile app those changes should be reflected there.

 

Sample

A working sample for this can be found under https://github.com/stefffdev/NubeSync/tree/master/samples

 

Troubleshooting