Single Page Application with Blazor and Cosmos DB (SQL API)

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

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 free Community edition) to leverage the Blazor template 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 and choose “Create New project” and select ASP.NET Core Web Application. Please give a valid name to your project.
Currently, there are three types of Blazor templates available. We are going with Blazor (ASP.NET Core hosted) template.
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.

“BlazorCosmosDBSPA.Client” contains all our client pages (Razor files and all other client-side script files.) and “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 Blazor template. We can remove these pages from Client project andremove “SampleDataController.cs” file from Server project and “WeatherForeCast.cs” model class from a Shared project.

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

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

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

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 “DataAccess” folder. This repository will provide all the CRUD actions for our Web API controller.

Please note, in this article we are not using actual Azure Comos DB account. Instead, we use the local Azure Cosmos DB emulator provided by Microsoft. It will create a local Cosmos DB 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.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.

We must call “Initialize” method from “Startup” class. This method will create a Cosmos DB 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 “Add New Item” option.

Please add the below code to 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 “Startup.cs” class.

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

We can now move to our “Client” project. We must add four Razor Views to 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 is using a special kind for 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 “
Asp.Net Core -> Web” tab.
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 “HttpClient” service with “@inject” directive.

We also use “@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 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 books details. The first time, we do not have any data available.
Create a new book by clicking the above hyper link and add book details.
If you want to edit the book details, please click “Edit” hyper link. It will show the existing data and after modifying the data you can click the “Save” button.
If you check the Cosmos DB emulator, you can see the new document there.
Now we can delete this document by clicking the “Delete” hyper click in book details page
If you click the “Delete” button, the document will be deleted from Cosmos DB.

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

We can create more Blazor applications in upcoming articles with other exciting features.
Source Code can be downloaded from Github
Please follow and like me:

1 thought on “Single Page Application with Blazor and Cosmos DB (SQL API)

  1. foloren torium Reply

    I just couldn’t depart your site prior to suggesting that I actually loved the usual info an individual provide in your guests? Is going to be again ceaselessly in order to investigate cross-check new posts

Leave a Reply

Your email address will not be published. Required fields are marked *