Single Page Application With Blazor And CosmosDB

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 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 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 and 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 Blazor (ASP.NET Core hosted) template.

Blazor Application Development 
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.

Blazor Application Development 

“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 and remove “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.

Blazor Application Development 

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.

Blazor Application Development 

Book.cs

  1. using Newtonsoft.Json;  
  2. namespace BlazorCosmosDBSPA.Shared.Models {  
  3.     public class Book {  
  4.         [JsonProperty(PropertyName = "id")]  
  5.         public string Id {  
  6.             get;  
  7.             set;  
  8.         }  
  9.         [JsonProperty(PropertyName = "name")]  
  10.         public string Name {  
  11.             get;  
  12.             set;  
  13.         }  
  14.         [JsonProperty(PropertyName = "isbn")]  
  15.         public string ISBN {  
  16.             get;  
  17.             set;  
  18.         }  
  19.         [JsonProperty(PropertyName = "author")]  
  20.         public string Author {  
  21.             get;  
  22.             set;  
  23.         }  
  24.         [JsonProperty(PropertyName = "price")]  
  25.         public decimal Price {  
  26.             get;  
  27.             set;  
  28.         }  
  29.     }  
  30. }  
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 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 “CosmosDBRepositoryclass.

Blazor Application Development

CosmosDBRepository.cs
  1. using Microsoft.Azure.Documents;  
  2. using Microsoft.Azure.Documents.Client;  
  3. using Microsoft.Azure.Documents.Linq;  
  4. using System;  
  5. using System.Collections.Generic;  
  6. using System.Threading.Tasks;  
  7. namespace BlazorCosmosDBSPA.Server.DataAccess {  
  8.     public static class CosmosDBRepository < T > where T: class {  
  9.         private static readonly string Endpoint = "https://localhost:8081/";  
  10.         private static readonly string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";  
  11.         private static readonly string DatabaseId = "SarathCosmosDB";  
  12.         private static readonly string BookCollectionId = "Books";  
  13.         private static DocumentClient client;  
  14.         public static void Initialize() {  
  15.             client = new DocumentClient(new Uri(Endpoint), Key, new ConnectionPolicy {  
  16.                 EnableEndpointDiscovery = false  
  17.             });  
  18.             CreateDatabaseIfNotExistsAsync().Wait();  
  19.             CreateCollectionIfNotExistsAsync(BookCollectionId).Wait();  
  20.         }  
  21.         private static async Task CreateDatabaseIfNotExistsAsync() {  
  22.             try {  
  23.                 await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));  
  24.             } catch (DocumentClientException e) {  
  25.                 if (e.StatusCode == System.Net.HttpStatusCode.NotFound) {  
  26.                     await client.CreateDatabaseAsync(new Database {  
  27.                         Id = DatabaseId  
  28.                     });  
  29.                 } else {  
  30.                     throw;  
  31.                 }  
  32.             }  
  33.         }  
  34.         private static async Task CreateCollectionIfNotExistsAsync(string collectionId) {  
  35.             try {  
  36.                 await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId));  
  37.             } catch (DocumentClientException e) {  
  38.                 if (e.StatusCode == System.Net.HttpStatusCode.NotFound) {  
  39.                     await client.CreateDocumentCollectionAsync(UriFactory.CreateDatabaseUri(DatabaseId), new DocumentCollection {  
  40.                         Id = collectionId  
  41.                     }, new RequestOptions {  
  42.                         OfferThroughput = 1000  
  43.                     });  
  44.                 } else {  
  45.                     throw;  
  46.                 }  
  47.             }  
  48.         }  
  49.         public static async Task < T > GetSingleItemAsync(string id, string collectionId) {  
  50.             try {  
  51.                 Document document = await client.ReadDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id));  
  52.                 return (T)(dynamic) document;  
  53.             } catch (DocumentClientException e) {  
  54.                 if (e.StatusCode == System.Net.HttpStatusCode.NotFound) {  
  55.                     return null;  
  56.                 } else {  
  57.                     throw;  
  58.                 }  
  59.             }  
  60.         }  
  61.         public static async Task < IEnumerable < T >> GetItemsAsync(string collectionId) {  
  62.             IDocumentQuery < T > query = client.CreateDocumentQuery < T > (UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId), new FeedOptions {  
  63.                 MaxItemCount = -1  
  64.             }).AsDocumentQuery();  
  65.             List < T > results = new List < T > ();  
  66.             while (query.HasMoreResults) {  
  67.                 results.AddRange(await query.ExecuteNextAsync < T > ());  
  68.             }  
  69.             return results;  
  70.         }  
  71.         public static async Task < Document > CreateItemAsync(T item, string collectionId) {  
  72.             return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId), item);  
  73.         }  
  74.         public static async Task < Document > UpdateItemAsync(string id, T item, string collectionId) {  
  75.             return await client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id), item);  
  76.         }  
  77.         public static async Task DeleteItemAsync(string id, string collectionId) {  
  78.             await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id));  
  79.         }  
  80.     }  
  81. }  
Inside this “CosmosDBRepository”, we have the below methods available.

Blazor Application Development 

We must call “Initialize” method from “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 “Add New Item” option.

 Blazor Application Development

Please add the below code to BooksController.cs file.

BooksController.cs
  1. using BlazorCosmosDBSPA.Server.DataAccess;  
  2. using BlazorCosmosDBSPA.Shared.Models;  
  3. using Microsoft.AspNetCore.Mvc;  
  4. using System.Collections.Generic;  
  5. using System.Threading.Tasks;  
  6. namespace BlazorCosmosDBSPA.Server.Controllers {  
  7.     public class BooksController: Controller {  
  8.         private static readonly string CollectionId = "Books";  
  9.         [HttpGet]  
  10.         [Route("api/Books/Get")]  
  11.         public async Task < IEnumerable < Book >> Get() {  
  12.                 var result = await CosmosDBRepository < Book > .GetItemsAsync(CollectionId);  
  13.                 return result;  
  14.             }  
  15.             [HttpPost]  
  16.             [Route("api/Books/Create")]  
  17.         public async Task CreateAsync([FromBody] Book book) {  
  18.                 if (ModelState.IsValid) {  
  19.                     await CosmosDBRepository < Book > .CreateItemAsync(book, CollectionId);  
  20.                 }  
  21.             }  
  22.             [HttpGet]  
  23.             [Route("api/Books/Details/{id}")]  
  24.         public async Task < Book > Details(string id) {  
  25.                 var result = await CosmosDBRepository < Book > .GetSingleItemAsync(id, CollectionId);  
  26.                 return result;  
  27.             }  
  28.             [HttpPut]  
  29.             [Route("api/Books/Edit")]  
  30.         public async Task EditAsync([FromBody] Book book) {  
  31.                 if (ModelState.IsValid) {  
  32.                     await CosmosDBRepository < Book > .UpdateItemAsync(book.Id, book, CollectionId);  
  33.                 }  
  34.             }  
  35.             [HttpDelete]  
  36.             [Route("api/Books/Delete/{id}")]  
  37.         public async Task DeleteConfirmedAsync(string id) {  
  38.             await CosmosDBRepository < Book > .DeleteItemAsync(id, CollectionId);  
  39.         }  
  40.     }  
  41. }  
Initialize the CosmosDBRepository from “Startup.cs” class.

Blazor Application Development 

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
  1. <div class="top-row pl-4 navbar navbar-dark">  
  2.     <a class="navbar-brand" href="">Books App</a>  
  3.     <button class="navbar-toggler" onclick=@ToggleNavMenu>  
  4.         <span class="navbar-toggler-icon"></span>  
  5.     </button>  
  6. </div>  
  7. <div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>  
  8.     <ul class="nav flex-column">  
  9.         <li class="nav-item px-3">  
  10.             <NavLink class="nav-link" href="" Match=NavLinkMatch.All>  
  11.                 <span class="oi oi-home" aria-hidden="true"></span> Home </NavLink>  
  12.         </li>  
  13.         <li class="nav-item px-3">  
  14.             <NavLink class="nav-link" href="/listbooks">  
  15.                 <span class="oi oi-list-rich" aria-hidden="true"></span> Books Details </NavLink>  
  16.         </li>  
  17.     </ul>  
  18. </div> @functions { bool collapseNavMenu = truevoid 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.

Blazor Application Development 

Please add the below codes to this file.

ListBooks.cshtml
 
  1. @using BlazorCosmosDBSPA.Shared.Models  
  2. @page "/listbooks"  
  3. @inject HttpClient Http  
  4. <h1>Books Details</h1>  
  5. <p>  
  6. <a href="/addbook">Create New Book</a>  
  7. </p>  
  8. @if (bookList == null)  
  9. {  
  10. <p><em>Loading...</em></p>  
  11. }  
  12. else  
  13. {  
  14. <table class='table'>  
  15.     <thead>  
  16.         <tr>  
  17.             <th>Name</th>  
  18.             <th>ISBN</th>  
  19.             <th>Author</th>  
  20.             <th>Price</th>  
  21.         </tr>  
  22.     </thead>  
  23.     <tbody> @foreach (var book in bookList) { <tr>  
  24.             <td>@book.Name</td>  
  25.             <td>@book.ISBN</td>  
  26.             <td>@book.Author</td>  
  27.             <td>@book.Price</td>  
  28.             <td>  
  29.                 <a href='/editbook/@book.Id'>Edit</a> | <a href='/deletebook/@book.Id'>Delete</a>  
  30.             </td>  
  31.         </tr> } </tbody>  
  32. </table>  
  33. }  
  34. @functions {  
  35.     Book[] bookList;  
  36.     protected override async Task OnInitAsync() {  
  37.         bookList = await Http.GetJsonAsync < Book[] > ("/api/Books/Get");  
  38.     }  
  39. }  

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
  1. @using BlazorCosmosDBSPA.Shared.Models  
  2. @page "/addbook"  
  3. @inject HttpClient Http  
  4. @inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper  
  5. <h2>Create Book</h2>  
  6. <hr />  
  7. <div class="row">  
  8.     <div class="col-md-4">  
  9.         <form>  
  10.             <div class="form-group">  
  11.                 <label for="Name" class="control-label">Name</label>  
  12.                 <input for="Name" class="form-control" bind="@book.Name" />  
  13.             </div>  
  14.             <div class="form-group">  
  15.                 <label for="ISBN" class="control-label">ISBN</label>  
  16.                 <input for="ISBN" class="form-control" bind="@book.ISBN" />  
  17.             </div>  
  18.             <div class="form-group">  
  19.                 <label for="Author" class="control-label">Author</label>  
  20.                 <input for="Author" class="form-control" bind="@book.Author" />  
  21.             </div>  
  22.             <div class="form-group">  
  23.                 <label for="Price" class="control-label">Price</label>  
  24.                 <input for="Price" class="form-control" bind="@book.Price" />  
  25.             </div>  
  26.             <div class="form-group">  
  27.                 <input type="button" class="btn btn-default" onclick="@(async () => await CreateBook())" value="Save" />  
  28.                 <input type="button" class="btn" onclick="@Cancel" value="Cancel" />  
  29.             </div>  
  30.         </form>  
  31.     </div>  
  32. </div>  
  33. @functions {  
  34.     Book book = new Book();  
  35.     protected async Task CreateBook() {  
  36.         await Http.SendJsonAsync(HttpMethod.Post, "/api/Books/Create", book);  
  37.         UriHelper.NavigateTo("/listbooks");  
  38.     }  
  39.     void Cancel() {  
  40.         UriHelper.NavigateTo("/listbooks");  
  41.     }  
  42. }  
EditBook.cshtml
  1. @using BlazorCosmosDBSPA.Shared.Models  
  2. @page "/editbook/{bookId}"  
  3. @inject HttpClient Http  
  4. @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  
  5. for = "Name"  
  6. class = "control-label" > Name < /label> < input  
  7. for = "Name"  
  8. class = "form-control"  
  9. bind = "@book.Name" / > < /div> < div class = "form-group" > < label  
  10. for = "ISBN"  
  11. class = "control-label" > ISBN < /label> < input  
  12. for = "ISBN"  
  13. class = "form-control"  
  14. bind = "@book.ISBN" / > < /div> < div class = "form-group" > < label  
  15. for = "Author"  
  16. class = "control-label" > Author < /label> < input  
  17. for = "Author"  
  18. class = "form-control"  
  19. bind = "@book.Author" / > < /div> < div class = " form-group" > < label  
  20. for = "City"  
  21. class = "control-label" > Price < /label> < input  
  22. for = "City"  
  23. class = "form-control"  
  24. bind = "@book.Price" / > < /div> < div class = "form-group" > < input type = "button"  
  25. value = "Save"  
  26. onclick = "@(async () => await UpdateBook())"  
  27. class = "btn btn-default" / > < input type = "button"  
  28. value = "Cancel"  
  29. onclick = "@Cancel"  
  30. class = "btn" / > < /div> < /form> < /div> < /div>  
  31. @functions {  
  32.     [Parameter]  
  33.     string bookId {  
  34.         get;  
  35.         set;  
  36.     }  
  37.     Book book = new Book();  
  38.     protected override async Task OnInitAsync() {  
  39.         book = await Http.GetJsonAsync < Book > ("/api/Books/Details/" + bookId);  
  40.     }  
  41.     protected async Task UpdateBook() {  
  42.         await Http.SendJsonAsync(HttpMethod.Put, "api/Books/Edit", book);  
  43.         UriHelper.NavigateTo("/listbooks");  
  44.     }  
  45.     void Cancel() {  
  46.         UriHelper.NavigateTo("/listbooks");  
  47.     }  
  48. }  
DeleteBook.cshtml
  1. @using BlazorCosmosDBSPA.Shared.Models  
  2. @page "/deletebook/{bookId}"  
  3. @inject HttpClient Http  
  4. @inject Microsoft.AspNetCore.Blazor.Services.IUriHelper UriHelper  
  5. <h2>Delete</h2>  
  6.    <p>Are you sure you want to delete this book with id :<b> @bookId</b></p>  
  7.       <br />  
  8.   <div class="col-md-4">  
  9. <table class="table">  
  10.    <tr>  
  11.       <td>Name</td>  
  12.       <td>@book.Name</td>  
  13.    </tr>  
  14.    <tr>  
  15.       <td>ISBN</td>  
  16.       <td>@book.ISBN</td>  
  17.    </tr>  
  18.    <tr>  
  19.       <td>Author</td>  
  20.       <td>@book.Author</td>  
  21.    </tr>  
  22.    <tr>  
  23.       <td>Price</td>  
  24.       <td>@book.Price</td>  
  25.    </tr>  
  26. </table>  
  27. <div class="form-group">  
  28.    <input type="button" value="Delete" onclick="@(async () => await Delete())" class="btn btn-default" />  
  29.    <input type="button" value="Cancel" onclick="@Cancel" class="btn" />  
  30. </div>  
  31. </div>  
  32. @functions {  
  33. [Parameter]  
  34. string bookId { get; set; }  
  35. Book book = new Book();  
  36. protected override async Task OnInitAsync()  
  37.    {  
  38.    book = await Http.GetJsonAsync<Book>  
  39.    ("/api/Books/Details/" + bookId);  
  40.    }  
  41.    protected async Task Delete()  
  42.    {  
  43.       await Http.DeleteAsync("api/Books/Delete/" + bookId);  
  44.       UriHelper.NavigateTo("/listbooks");  
  45.    }  
  46. void Cancel()  
  47.    {  
  48.       UriHelper.NavigateTo("/listbooks");  
  49.    }  
  50. }  

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.

Blazor Application Development 
Create a new book by clicking the above hyper link and add book details.

Blazor Application Development 

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.

Blazor Application Development 

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

Blazor Application Development 

Now we can delete this document by clicking the “Delete” hyper click in book details page

Blazor Application Development

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 Blazor (ASP.NET Core hosted) template. We saw all the four CRUD actions in this Books app. We used CosmosDB database to store our data. (In CosmosDB, data is called as 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.