Single Page Application With Blazor And CosmosDB

In this article, we will create one single-page application with the default Blazor (ASP. Net Core Hosted) template. We will manage a book entry in this project and save the data to CosmosDB. We will use “Microsoft.Azure.DocumentDB.Core” NuGet package to perform CRUD actions with CosmosDB.

As per Microsoft’s documentation about Blazor, it is an experimental .NET web framework using C#/Razor and HTML that runs in the browser with WebAssembly. Blazor provides all the benefits of a client-side web UI framework using .NET on the client and optionally on the server.

Web Assembly is a low-level assembly language with a binary format that provides a way to run code written in different languages like C# in the browser with native speed.

The major difference between Blazor and Razor is traditional Razor view engines always post back to the server in each request but Blazor never does it. It always works like any other client-side framework or library.

Prerequisites for Blazor application development.

To create a Blazor application you need .NET Core SDK 2.1 or higher. Please download the latest SDK from here.

You must have Visual Studio 2017 (I am using the free Community edition) to leverage the Blazortemplate benefits. You can use Visual Studio Code too (without template features). Please download Visual Studio 2017 from here.

The most important thing - if you are using Visual Studio 2017 you must add Blazor Language Services extension to Visual Studio to get the Blazor language templates.

Let’s start with application development.

Please open Visual Studio choose “Create New project” and select ASP.NET Core Web Application. Please give a valid name to your project.

Blazor Application Development

Currently, there are three types of Blazor templates available. We are going with the Blazor (ASP.NET Core hosted) template.

Blazor templates

Our project will be created in a moment. If look at the solution structure, we can see there are three projects created under our solution.

Solution structure

“BlazorCosmosDBSPA.Client” contains all our client pages (Razor files and all other client-side script files.) and the “BlazorCosmosDBSPA.Server” project contains the Web API controllers and other services. “BlazorCosmosDBSPA.Shared” project mainly contains the commonly shared class files for both Client and Server projects.

By default, our application contains one Counter page and another Fetch Data page. These are automatically created from the Blazor template. We can remove these pages from the Client project and remove the “SampleDataController.cs” file from the Server project and the “WeatherForeCast.cs” model class from a Shared project.

We can add “Microsoft.Azure.DocumentDB.Core” NuGet package to our Server project.

NuGet package

As I stated earlier, we will create a Book management application. We can create a “Book” model in the “Shared” project. (Inside the new “Models” folder).

Please add the “Newtonsoft.Json” NuGet package to the Shared project.

Blazor Application Development

Book. cs

using Newtonsoft.Json;

namespace BlazorCosmosDBSPA.Shared.Models 
{   
    public class Book 
    {   
        [JsonProperty(PropertyName = "id")]
        public string Id { get; set; }
        
        [JsonProperty(PropertyName = "name")]
        public string Name { get; set; }
        
        [JsonProperty(PropertyName = "isbn")]
        public string ISBN { get; set; }
        
        [JsonProperty(PropertyName = "author")]
        public string Author { get; set; }
        
        [JsonProperty(PropertyName = "price")]
        public decimal Price { get; set; }
    }   
}

We use five properties in this Book model.

We can create our “CosmosDBRepository” inside the “DataAccess” folder. This repository will provide all the CRUD actions for our Web API controller.

Please note, that in this article, we are not using an actual Azure ComosDB account. Instead, we use the local Azure CosmosDB emulator provided by Microsoft. It will create a local CosmosDB account for us and we can run our application with this database.

Please refer to this article for more about CosmosDB Emulator and download it to your Windows machine.

Run this emulator and copy the “Endpoint” and “Key” from the emulator. We will use these values inside our “CosmosDBRepository” class.

CosmosDBRepository

CosmosDBRepository.cs

using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.Documents.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorCosmosDBSPA.Server.DataAccess 
{   
    public static class CosmosDBRepository<T> where T : class 
    {   
        private static readonly string Endpoint = "https://localhost:8081/";
        private static readonly string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
        private static readonly string DatabaseId = "SarathCosmosDB";
        private static readonly string BookCollectionId = "Books";
        private static DocumentClient client;

        public static void Initialize() 
        {   
            client = new DocumentClient(new Uri(Endpoint), Key, new ConnectionPolicy 
            {   
                EnableEndpointDiscovery = false 
            });
            CreateDatabaseIfNotExistsAsync().Wait();
            CreateCollectionIfNotExistsAsync(BookCollectionId).Wait();
        }

        private static async Task CreateDatabaseIfNotExistsAsync() 
        {   
            try 
            {   
                await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));
            } 
            catch (DocumentClientException e) 
            {   
                if (e.StatusCode == System.Net.HttpStatusCode.NotFound) 
                {   
                    await client.CreateDatabaseAsync(new Database 
                    {   
                        Id = DatabaseId 
                    });
                } 
                else 
                {   
                    throw;
                }
            }
        }

        private static async Task CreateCollectionIfNotExistsAsync(string collectionId) 
        {   
            try 
            {   
                await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId));
            } 
            catch (DocumentClientException e) 
            {   
                if (e.StatusCode == System.Net.HttpStatusCode.NotFound) 
                {   
                    await client.CreateDocumentCollectionAsync(UriFactory.CreateDatabaseUri(DatabaseId), new DocumentCollection 
                    {   
                        Id = collectionId 
                    }, new RequestOptions 
                    {   
                        OfferThroughput = 1000 
                    });
                } 
                else 
                {   
                    throw;
                }
            }
        }

        public static async Task<T> GetSingleItemAsync(string id, string collectionId) 
        {   
            try 
            {   
                Document document = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id));
                return (T)(dynamic) document;
            } 
            catch (DocumentClientException e) 
            {   
                if (e.StatusCode == System.Net.HttpStatusCode.NotFound) 
                {   
                    return null;
                } 
                else 
                {   
                    throw;
                }
            }
        }

        public static async Task<IEnumerable<T>> GetItemsAsync(string collectionId) 
        {   
            IDocumentQuery<T> query = client.CreateDocumentQuery<T>(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId), new FeedOptions 
            {   
                MaxItemCount = -1 
            }).AsDocumentQuery();
            List<T> results = new List<T>();
            while (query.HasMoreResults) 
            {   
                results.AddRange(await query.ExecuteNextAsync<T>());
            }
            return results;
        }

        public static async Task<Document> CreateItemAsync(T item, string collectionId) 
        {   
            return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId), item);
        }

        public static async Task<Document> UpdateItemAsync(string id, T item, string collectionId) 
        {   
            return await client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id), item);
        }

        public static async Task DeleteItemAsync(string id, string collectionId) 
        {   
            await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id));
        }
    }
}

Inside this “CosmosDBRepository”, we have the below methods available.

Methods

We must call the “Initialize” method from the “Startup” class. This method will create a CosmosDB database and a Collection if it does not exist. We are using other five methods to provide the CRUD actions.

We can create our “BooksController” controller from the “Add New Item” option.

 Books Controller

Please add the below code to the BooksController.cs file.

BooksController.cs

using BlazorCosmosDBSPA.Server.DataAccess;
using BlazorCosmosDBSPA.Shared.Models;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace BlazorCosmosDBSPA.Server.Controllers 
{   
    public class BooksController : Controller 
    {   
        private static readonly string CollectionId = "Books";

        [HttpGet]
        [Route("api/Books/Get")]
        public async Task<IEnumerable<Book>> Get() 
        {   
            var result = await CosmosDBRepository<Book>.GetItemsAsync(CollectionId);
            return result;
        }

        [HttpPost]
        [Route("api/Books/Create")]
        public async Task CreateAsync([FromBody] Book book) 
        {   
            if (ModelState.IsValid) 
            {   
                await CosmosDBRepository<Book>.CreateItemAsync(book, CollectionId);
            }
        }

        [HttpGet]
        [Route("api/Books/Details/{id}")]
        public async Task<Book> Details(string id) 
        {   
            var result = await CosmosDBRepository<Book>.GetSingleItemAsync(id, CollectionId);
            return result;
        }

        [HttpPut]
        [Route("api/Books/Edit")]
        public async Task EditAsync([FromBody] Book book) 
        {   
            if (ModelState.IsValid) 
            {   
                await CosmosDBRepository<Book>.UpdateItemAsync(book.Id, book, CollectionId);
            }
        }

        [HttpDelete]
        [Route("api/Books/Delete/{id}")]
        public async Task DeleteConfirmedAsync(string id) 
        {   
            await CosmosDBRepository<Book>.DeleteItemAsync(id, CollectionId);
        }
    }
}

Initialize the CosmosDBRepository from the “Startup. cs” class.

Startup. cs

Our “Server” and “Shared” projects are ready now.

We can now move to our “Client” project. We must add four Razor Views to the Client Project. “ListBooks.cshtml”, “AddBook.cshtml”, “EditBook.cshtml” and “DeleteBook.cshtml”.

We must change the existing “NavMenu.cshtml” View under Shared folder too. This is the navigation menu used for adding menus to our application.

NavMenu.cshtml

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">Books App</a>
    <button class="navbar-toggler" onclick=@ToggleNavMenu>
        <span class="navbar-toggler-icon"></span>
    </button>
</div>
<div class=@(collapseNavMenu ? "collapse" : null) 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="/listbooks">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Books Details </NavLink>
        </li>
    </ul>
</div> 
@functions {
    bool collapseNavMenu = true;
    void ToggleNavMenu() {
        collapseNavMenu = !collapseNavMenu;
    }
}

We have added new navigation to this file. When we click this link, it will open the page. Blazor uses a special kind of routing mechanism to control the navigation.

Now, we can add above mentioned four Razor Views to our application.

Please choose “Add New Item” and choose “Razor View” under the “Asp.Net Core -> Web” tab.

Blazor Application Development

Please add the below codes to this file.

ListBooks.cshtml

@using BlazorCosmosDBSPA.Shared.Models
@page "/listbooks"
@inject HttpClient Http

<h1>Books Details</h1>
<p>
    <a href="/addbook">Create New Book</a>
</p>

@if (bookList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class='table'>
        <thead>
            <tr>
                <th>Name</th>
                <th>ISBN</th>
                <th>Author</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in bookList)
            {
                <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>
}

@functions {
    Book[] bookList;

    protected override async Task OnInitAsync()
    {
        bookList = await Http.GetJsonAsync<Book[]>("/api/Books/Get");
    }
}

Inside this file, we use a “@page” directive. This will control the routing of our application. We injected the “HttpClient” service with the “@inject” directive.

We also use the “@functions” directive to declare our C# code inside this Razor View. One “OnInitAsync” method will be automatically invoked while the page loads and we can write the code inside this method to control the page load event.

Please add the other three Razor Views in the same way and add the below codes to these files respectively.

AddBook.cshtml

@using BlazorCosmosDBSPA.Shared.Models
@page "/addbook"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper

<h2>Create Book</h2>
<hr />

<div class="row">
    <div class="col-md-4">
        <form>
            <div class="form-group">
                <label for="Name" class="control-label">Name</label>
                <input for="Name" class="form-control" bind="@book.Name" />
            </div>
            <div class="form-group">
                <label for="ISBN" class="control-label">ISBN</label>
                <input for="ISBN" class="form-control" bind="@book.ISBN" />
            </div>
            <div class="form-group">
                <label for="Author" class="control-label">Author</label>
                <input for="Author" class="form-control" bind="@book.Author" />
            </div>
            <div class="form-group">
                <label for="Price" class="control-label">Price</label>
                <input for="Price" class="form-control" bind="@book.Price" />
            </div>
            <div class="form-group">
                <input type="button" class="btn btn-default" onclick="@(async () => await CreateBook())" value="Save" />
                <input type="button" class="btn" onclick="@Cancel" value="Cancel" />
            </div>
        </form>
    </div>
</div>

@functions {
    Book book = new Book();

    protected async Task CreateBook()
    {
        await Http.SendJsonAsync(HttpMethod.Post, "/api/Books/Create", book);
        UriHelper.NavigateTo("/listbooks");
    }

    void Cancel()
    {
        UriHelper.NavigateTo("/listbooks");
    }
}

EditBook.cshtml

@using BlazorCosmosDBSPA.Shared.Models
@page "/editbook/{bookId}"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper

<h2>Edit</h2>
<h4>Book</h4>
<hr />

<div class="row">
    <div class="col-md-4">
        <form>
            <div class="form-group">
                <label for="Name" class="control-label">Name</label>
                <input for="Name" class="form-control" bind="@book.Name" />
            </div>
            <div class="form-group">
                <label for="ISBN" class="control-label">ISBN</label>
                <input for="ISBN" class="form-control" bind="@book.ISBN" />
            </div>
            <div class="form-group">
                <label for="Author" class="control-label">Author</label>
                <input for="Author" class="form-control" bind="@book.Author" />
            </div>
            <div class="form-group">
                <label for="City" class="control-label">Price</label>
                <input for="City" class="form-control" bind="@book.Price" />
            </div>
            <div class="form-group">
                <input type="button" value="Save" onclick="@(async () => await UpdateBook())" class="btn btn-default" />
                <input type="button" value="Cancel" onclick="@Cancel" class="btn" />
            </div>
        </form>
    </div>
</div>

@functions {
    [Parameter]
    string bookId { get; set; }

    Book book = new Book();

    protected override async Task OnInitAsync()
    {
        book = await Http.GetJsonAsync<Book>("/api/Books/Details/" + bookId);
    }

    protected async Task UpdateBook()
    {
        await Http.SendJsonAsync(HttpMethod.Put, "api/Books/Edit", book);
        UriHelper.NavigateTo("/listbooks");
    }

    void Cancel()
    {
        UriHelper.NavigateTo("/listbooks");
    }
}

DeleteBook.cshtml

@using BlazorCosmosDBSPA.Shared.Models
@page "/deletebook/{bookId}"
@inject HttpClient Http
@inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper

<h2>Delete</h2>
<p>Are you sure you want to delete this book with id: <b>@bookId</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="@(async () => await Delete())" class="btn btn-default" />
        <input type="button" value="Cancel" onclick="@Cancel" class="btn" />
    </div>
</div>

@functions {
    [Parameter]
    string bookId { get; set; }

    Book book = new Book();

    protected override async Task OnInitAsync()
    {
        book = await Http.GetJsonAsync<Book>("/api/Books/Details/" + bookId);
    }

    protected async Task Delete()
    {
        await Http.DeleteAsync("api/Books/Delete/" + bookId);
        UriHelper.NavigateTo("/listbooks");
    }

    void Cancel()
    {
        UriHelper.NavigateTo("/listbooks");
    }
}

Our application is ready and now, we can check the functionalities one by one.

When we click the Books Details button in the menu bar, it will list all book details. The first time, we do not have any data available.

Books Details

Create a new book by clicking the above hyperlink and adding book details.

Create book

If you want to edit the book details, please click the “Edit” hyperlink. It will show the existing data and after modifying the data you can click the “Save” button.

Edit book

If you check the CosmosDB emulator, you can see the new document there.

CosmosDB emulator

Now we can delete this document by clicking the “Delete” hyperlink on the book details page

Delete

If you click the “Delete” button, the document will be deleted from CosmosDB.

In this article, we created a simple Books app with the help of the Blazor (ASP.NET Core hosted) template. We saw all four CRUD actions in this Books app. We used the CosmosDB database to store our data. (In CosmosDB, data is called documents.) We also saw how to create Web API controllers in Asp.Net Core and we used CosmosDBRepositry to provide the CRUD actions in Web API controller.

We can create more Blazor applications in the coming articles with other exciting features.

Important References


Similar Articles