CRUD With Blazor Using Google Cloud Firestore

We will create a Single Page Application (SPA) using Blazor and Google cloud Firestore. We will perform CRUD operations on it to understand the data manipulation by Firestore.

Introduction

In this article, we will create a Blazor application using Google Firestore as a database provider. We will create a Single Page Application (SPA) and perform CRUD operations on it. We will use Bootstrap 4 to display a modal popup for handling user inputs. The form also has a dropdown list which will bind to a collection in our database. We will also implement a client-side search functionality to search the employee list by employee name.

Take a look at the final application.

Prerequisites

  • Install the .NET Core 2.1 SDK from here.
  • Install the latest version of Visual Studio 2017 from here.
  • Install ASP.NET Core Blazor Language Services extension from here.

Source Code

Get the source code from GitHub.

Configuring Cloud Firestore

The first step is to create a project in Google Firebase console. Navigate to https://console.firebase.google.com and sign-in with your Google account. Click on Add Project link. A pop-up window will open as shown in the image below. Provide your project name and click on the "Create project" button at the bottom.

 

Note the project id here. Firebase project ids are globally unique. You can edit your project id while creating a new project. Once the project is created you cannot change your project id. We will use this project id in the next section while initializing our application.

Click on the project you just created. A "Project Overview" page will open. Select “Database” from the left menu. Then, click the “Create database” button. A pop-up window will open asking you to select the “Security rules for Cloud Firestore”. Select “Start in locked mode” and click on "Enable".

Refer to the image below.

 

This will enable the database for your project. Firebase project has two options for the database – Real-time Database and Cloud Firestore. For this application, we will use “Cloud Firestore” database. Click on the “Database” dropdown at the top of the page and select “Cloud Firestore”.

 
We will create a cities collection to store the city name for employees. We will also bind this collection to a dropdown list in our web application from which the user will select the desired city. Click on “Add collection”. Set the collection ID as “cities”. Click on “Next”. Refer to the image below.
 
 
Put the field value as “CityName”, select string from the Type dropdown, and fill the value with city name as “Mumbai”. Click on "Save".
 
 

This will create the “cities” collection and insert the first document in it. Similarly, create four more documents inside this collection and put the “CityName” value as Chennai, New Delhi, Bengaluru, and Hyderabad.

We will use the “employees” collection to store employee data, but we will not create it manually. We will create an “employees” collection while adding the first employee data from the application.

Configuring Google Application Credentials

To access the database from our project, we need to set the GOOGLE_APPLICATION_CREDENTIALS environment variable to point to a JSON service account key file. This will set an authentication pipeline from our application to cloud Firestore.

To generate the service account key file, follow the steps mentioned below.

Step 1

Navigate here to log in with the same Google account that you have used to create Firestore DB.

Step 2

Select Project from the dropdown in the menu bar at the top.

Step 3

Select “Service accounts” from the left menu. Select the service account for which you want to create the key. Click on more button in the “Actions” column in that row, and then click "Create key".

Refer to the image below.

 

Step 4

A popup modal will open asking you to select the key type. Select “JSON” and click on the "Create" button. This will create a private key for accessing your Firestore account and download a JSON key file to your machine. We will use this file to set the  GOOGLE_APPLICATION_CREDENTIALS environment variable in the later part of this article.
 

Create Blazor Web Application

Open Visual Studio and select File >> New >> Project. After selecting the project, a “New Project” dialog will open. Select .NET Core inside the Visual C# menu from the left panel. Then, select “ASP.NET Core Web Application” from available project types. Put the name of the project as BlazorWithFirestore and press OK.

 
After clicking on OK, a new dialog will open asking you to select the project template. You can observe two drop-down menus at the top left of the template window. Select “.NET Core” and “ASP.NET Core 2.1” from these dropdowns. Then, select “Blazor (ASP .NET Core hosted)” template and press OK.
 
 

Now, our Blazor solution will be created. You can observe that we have three project files created in this solution.

  1. BlazorWithFirestore.Client – It has the client side code and contains the pages that will be rendered on the browser.
  2. BlazorWithFirestore.Server – It has the server side codes such as data access layer and web API.
  3. BlazorWithFirestore.Shared – It contains the shared code that can be accessed by both client and server. It contains our Model classes.

Adding Package reference for Firestore

We need to add the package reference for Google cloud Firestore, which will allow us to access our DB from the Blazor application. Right click on BlazorWithFirestore.Shared project. Select “Edit BlazorWithFirestore.Shared.csproj”. It will open the BlazorWithFirestore.Shared.csproj file. Add the following lines inside it.

  1. <ItemGroup>
  2. <PackageReference Include="Google.Cloud.Firestore" Version="1.0.0-beta14" />
  3. </ItemGroup>

Similarly, add these lines to BlazorWithFirestore.Server.csproj file also.

Creating the Model

We will create our model class in BlazorWithFirestore.Shared project. Right-click on BlazorWithFirestore.Shared and select Add >> New Folder. Name the folder as Models. Again, right click on Models folder and select Add >> Class to add a new class file. Put the name of your class as Employee.cs and click Add.

Open the Employee.cs class and put the following code into it.

  1. using System;  
  2. using Google.Cloud.Firestore;  
  3.   
  4. namespace BlazorWithFirestore.Shared.Models  
  5. {  
  6.     [FirestoreData]  
  7.     public class Employee  
  8.     {  
  9.         public string EmployeeId { getset; }  
  10.         public DateTime date { getset; }  
  11.         [FirestoreProperty]  
  12.         public string EmployeeName { getset; }  
  13.         [FirestoreProperty]  
  14.         public string CityName { getset; }  
  15.         [FirestoreProperty]  
  16.         public string Designation { getset; }  
  17.         [FirestoreProperty]  
  18.         public string Gender { getset; }  
  19.     }  
  20. }  

We have decorated the class with [FirestoreData] attribute. This will allow us to map this class object to Firestore collection. Only those class properties, which are marked with [FirestoreProperty] attribute, are considered when we are saving the document to our collection. We do not need to save EmployeeId to our database as it is generated automatically. While fetching the data, we will bind the auto generated document id to the EmployeeId property. Similarly, we will use date property to bind the created date of collection while fetching the record. We will use this date property to sort the list of employees by created date. Hence, we have not applied [FirestoreProperty] attribute to these two properties.

Similarly, create a class file Cities.cs and put the following code into it.

  1. using System;  
  2. using Google.Cloud.Firestore;  
  3.   
  4. namespace BlazorWithFirestore.Shared.Models  
  5. {  
  6.     [FirestoreData]  
  7.     public class Cities  
  8.     {  
  9.         public string CityName { getset; }  
  10.     }  
  11. }  

Creating Data Access Layer for the Application

Right-click on the BlazorWithFirestore.Server project and then select Add >> New Folder and name the folder as DataAccess. We will be adding our class to handle the database related operations inside this folder only. Right-click on the DataAccess folder and select Add >> Class. Name your class EmployeeDataAccessLayer.cs.

Put the following code inside this class.

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Threading.Tasks;  
  5. using BlazorWithFirestore.Shared.Models;  
  6. using Google.Cloud.Firestore;  
  7. using Newtonsoft.Json;  
  8.   
  9. namespace BlazorWithFirestore.Server.DataAccess  
  10. {  
  11.     public class EmployeeDataAccessLayer  
  12.     {  
  13.         string projectId;  
  14.         FirestoreDb fireStoreDb;  
  15.         public EmployeeDataAccessLayer()  
  16.         {  
  17.             string filepath = "C:\\FirestoreAPIKey\\blazorwithfirestore-6d0a096b0174.json";  
  18.             Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", filepath);  
  19.             projectId = "blazorwithfirestore";  
  20.             fireStoreDb = FirestoreDb.Create(projectId);  
  21.         }  
  22.         public async Task<List<Employee>> GetAllEmployees()  
  23.         {  
  24.             try  
  25.             {  
  26.                 Query employeeQuery = fireStoreDb.Collection("employees");  
  27.                 QuerySnapshot employeeQuerySnapshot = await employeeQuery.GetSnapshotAsync();  
  28.                 List<Employee> lstEmployee = new List<Employee>();  
  29.   
  30.                 foreach (DocumentSnapshot documentSnapshot in employeeQuerySnapshot.Documents)  
  31.                 {  
  32.                     if (documentSnapshot.Exists)  
  33.                     {  
  34.                         Dictionary<stringobject> city = documentSnapshot.ToDictionary();  
  35.                         string json = JsonConvert.SerializeObject(city);  
  36.                         Employee newuser = JsonConvert.DeserializeObject<Employee>(json);  
  37.                         newuser.EmployeeId = documentSnapshot.Id;  
  38.                         newuser.date = documentSnapshot.CreateTime.Value.ToDateTime();  
  39.                         lstEmployee.Add(newuser);  
  40.                     }  
  41.                 }  
  42.   
  43.                 List<Employee> sortedEmployeeList = lstEmployee.OrderBy(x => x.date).ToList();  
  44.                 return sortedEmployeeList;  
  45.             }  
  46.             catch  
  47.             {  
  48.                 throw;  
  49.             }  
  50.         }  
  51.         public async void AddEmployee(Employee employee)  
  52.         {  
  53.             try  
  54.             {  
  55.                 CollectionReference colRef = fireStoreDb.Collection("employees");  
  56.                 await colRef.AddAsync(employee);  
  57.             }  
  58.             catch  
  59.             {  
  60.                 throw;  
  61.             }  
  62.         }  
  63.         public async void UpdateEmployee(Employee employee)  
  64.         {  
  65.             try  
  66.             {  
  67.                 DocumentReference empRef = fireStoreDb.Collection("employees").Document(employee.EmployeeId);  
  68.                 await empRef.SetAsync(employee, SetOptions.Overwrite);  
  69.             }  
  70.             catch  
  71.             {  
  72.                 throw;  
  73.             }  
  74.         }  
  75.         public async Task<Employee> GetEmployeeData(string id)  
  76.         {  
  77.             try  
  78.             {  
  79.                 DocumentReference docRef = fireStoreDb.Collection("employees").Document(id);  
  80.                 DocumentSnapshot snapshot = await docRef.GetSnapshotAsync();  
  81.   
  82.                 if (snapshot.Exists)  
  83.                 {  
  84.                     Employee emp = snapshot.ConvertTo<Employee>();  
  85.                     emp.EmployeeId = snapshot.Id;  
  86.                     return emp;  
  87.                 }  
  88.                 else  
  89.                 {  
  90.                     return new Employee();  
  91.                 }  
  92.             }  
  93.             catch  
  94.             {  
  95.                 throw;  
  96.             }  
  97.         }  
  98.         public async void DeleteEmployee(string id)  
  99.         {  
  100.             try  
  101.             {  
  102.                 DocumentReference empRef = fireStoreDb.Collection("employees").Document(id);  
  103.                 await empRef.DeleteAsync();  
  104.             }  
  105.             catch  
  106.             {  
  107.                 throw;  
  108.             }  
  109.         }  
  110.         public async Task<List<Cities>> GetCityData()  
  111.         {  
  112.             try  
  113.             {  
  114.                 Query citiesQuery = fireStoreDb.Collection("cities");  
  115.                 QuerySnapshot citiesQuerySnapshot = await citiesQuery.GetSnapshotAsync();  
  116.                 List<Cities> lstCity = new List<Cities>();  
  117.   
  118.                 foreach (DocumentSnapshot documentSnapshot in citiesQuerySnapshot.Documents)  
  119.                 {  
  120.                     if (documentSnapshot.Exists)  
  121.                     {  
  122.                         Dictionary<stringobject> city = documentSnapshot.ToDictionary();  
  123.                         string json = JsonConvert.SerializeObject(city);  
  124.                         Cities newCity = JsonConvert.DeserializeObject<Cities>(json);  
  125.                         lstCity.Add(newCity);  
  126.                     }  
  127.                 }  
  128.                 return lstCity;  
  129.             }  
  130.             catch  
  131.             {  
  132.                 throw;  
  133.             }  
  134.         }  
  135.     }  
  136. }  

In the constructor of this class, we are setting the GOOGLE_APPLICATION_CREDENTIALS environment variable. You need to set the value of the filepath variable to the path where the JSON service account key file is located in your machine. Remember we downloaded this file in the previous section. The projectId variable should be set to the project id of your Firebase project.

We have also defined the methods for performing CRUD operations. The GetAllEmployees method will fetch the list of all employee document from our “employees” collection. It will return the employee list sorted by document creation date.

The AddEmployee method will add a new employee document to our “employees” collection. If the collection does not exist, it will create the collection first then insert a new document in it.

The UpdateEmployee method will update the field values of an already existing employee document, based on the employee id passed to it. We are binding the document id to the employeeId property, hence we can easily manipulate the documents.

The GetEmployeeData method will fetch a single employee document from our “employees” collection based on the employee id.

The DeleteEmployee method will delete the document for a particular employee from the “employees” collection.

GetCityData method will return the list of cities from “cities” collection.

Adding the web API Controller to the Application

Right-click on BlazorWithFirestore.Server/Controllers folder and select Add >> New Item. An “Add New Item” dialog box will open. Select Web from the left panel, then select “API Controller Class” from templates panel and put the name as EmployeeController.cs. Click Add.

This will create our API EmployeeController class. We will call the methods of EmployeeDataAccessLayer class to fetch data and pass on the data to the client side.

Open EmployeeController.cs file and put the following code into it.

  1. using System;    
  2. using System.Collections.Generic;    
  3. using System.Threading.Tasks;    
  4. using BlazorWithFirestore.Server.DataAccess;    
  5. using BlazorWithFirestore.Shared.Models;    
  6. using Microsoft.AspNetCore.Mvc;    
  7.     
  8. namespace BlazorWithFirestore.Server.Controllers    
  9. {    
  10.     [Route("api/[controller]")]    
  11.     public class EmployeeController : Controller    
  12.     {    
  13.         EmployeeDataAccessLayer objemployee = new EmployeeDataAccessLayer();    
  14.     
  15.         [HttpGet]    
  16.         public Task<List<Employee>> Get()    
  17.         {    
  18.             return objemployee.GetAllEmployees();    
  19.         }    
  20.     
  21.         [HttpGet("{id}")]    
  22.         public Task<Employee> Get(string id)    
  23.         {    
  24.             return objemployee.GetEmployeeData(id);    
  25.         }    
  26.     
  27.         [HttpPost]    
  28.         public void Post([FromBody] Employee employee)    
  29.         {    
  30.             objemployee.AddEmployee(employee);    
  31.         }    
  32.     
  33.         [HttpPut]    
  34.         public void Put([FromBody]Employee employee)    
  35.         {    
  36.             objemployee.UpdateEmployee(employee);    
  37.         }    
  38.     
  39.         [HttpDelete("{id}")]    
  40.         public void Delete(string id)    
  41.         {    
  42.             objemployee.DeleteEmployee(id);    
  43.     
  44.         }    
  45.         [HttpGet("GetCities")]    
  46.         public Task<List<Cities>> GetCities()    
  47.         {    
  48.             return objemployee.GetCityData();    
  49.         }    
  50.     }    
  51. }    

Creating the Blazor component

We will create the component in the "BlazorWithFirestore.Client/Pages" folder. The application template provides the Counter and fetches data files by default in this folder. Before adding our own component file, we will delete these two default files to make our solution cleaner. Right-click on "BlazorWithFirestore.Client/Pages" folder and then select Add >> New Item. An “Add New Item” dialog box will open, select “ASP.NET Core” from the left panel, then select “Razor Page” from templates panel and name it EmployeeData.cshtml. Click Add. Refer to the image below,

 

This will add an EmployeeData.cshtml page to our BlazorSPA.Client/Pages folder. This razor page will have two files – EmployeeData.cshtml and EmployeeData.cshtml.cs.

Adding references for JS Interop

We will be using a bootstrap modal dialog in our application. We will also include a few Font Awesome icons for styling in the application.

To be able to use these two libraries, we need to add the CDN references to allow the JS interop,
  1. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">  
  2. <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>  
  3. <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>  
  4. <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>  

Here, we have included the CDN references, which will allow us to use the bootstrap modal dialog and Font Awesome icons in our applications. Now, we will add codes to our view files.

EmployeeData.cshtml.cs

Open EmployeeData.cshtml.cs and put the following code into it.

  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Net.Http;  
  5. using System.Threading.Tasks;  
  6. using BlazorWithFirestore.Shared.Models;  
  7. using Microsoft.AspNetCore.Blazor;  
  8. using Microsoft.AspNetCore.Blazor.Components;  
  9.   
  10. namespace BlazorWithFirestore.Client.Pages  
  11. {  
  12.     public class EmployeeDataModel : BlazorComponent  
  13.     {  
  14.         [Inject]  
  15.         protected HttpClient Http { getset; }  
  16.         protected List<Employee> empList = new List<Employee>();  
  17.         protected List<Cities> cityList = new List<Cities>();  
  18.         protected Employee emp = new Employee();  
  19.         protected string modalTitle { getset; }  
  20.         protected string searchString { getset; }  
  21.   
  22.         protected override async Task OnInitAsync()  
  23.         {  
  24.             await GetCityList();  
  25.             await GetEmployeeList();  
  26.         }  
  27.         protected async Task GetCityList()  
  28.         {  
  29.             cityList = await Http.GetJsonAsync<List<Cities>>("api/Employee/GetCities");  
  30.         }  
  31.         protected async Task GetEmployeeList()  
  32.         {  
  33.             empList = await Http.GetJsonAsync<List<Employee>>("api/Employee");  
  34.         }  
  35.         protected void AddEmployee()  
  36.         {  
  37.             emp = new Employee();  
  38.             modalTitle = "Add Employee";  
  39.         }  
  40.         protected async Task EditEmployee(string empID)  
  41.         {  
  42.             emp = await Http.GetJsonAsync<Employee>("/api/Employee/" + empID);  
  43.             modalTitle = "Edit Employee";  
  44.         }  
  45.         protected async Task SaveEmployee()  
  46.         {  
  47.             if (emp.EmployeeId != null)  
  48.             {  
  49.                 await Http.SendJsonAsync(HttpMethod.Put, "api/Employee/", emp);  
  50.             }  
  51.             else  
  52.             {  
  53.                 await Http.SendJsonAsync(HttpMethod.Post, "/api/Employee/", emp);  
  54.             }  
  55.             await GetEmployeeList();  
  56.         }  
  57.         protected async Task DeleteConfirm(string empID)  
  58.         {  
  59.             emp = await Http.GetJsonAsync<Employee>("/api/Employee/" + empID);  
  60.         }  
  61.         protected async Task DeleteEmployee(string empID)  
  62.         {  
  63.             Console.WriteLine(empID);  
  64.             await Http.DeleteAsync("api/Employee/" + empID);  
  65.             await GetEmployeeList();  
  66.         }  
  67.         protected async Task SearchEmployee()  
  68.         {  
  69.             await GetEmployeeList();  
  70.             if (searchString != "")  
  71.             {  
  72.                 empList = empList.Where(  
  73.                 x => x.EmployeeName.IndexOf(searchString,  
  74.                 StringComparison.OrdinalIgnoreCase) != -1).ToList();  
  75.             }  
  76.         }  
  77.     }  
  78. }  

Here, we have defined the EmployeeDataModel class, which is inheriting from BlazorComponent. This allows the EmployeeDataModel class to act as a Blazor component.

We are also injecting the HttpClient service to enable the web API calls to our EmployeeController API.

We will use the two variables – empList and cityList to hold the data of our Employee and Cities collections respectively. The modalTitle property, which is of type string, is used to hold the title that will be displayed in the modal dialog. The value provided in the search box is stored in the searchString property which is also of type string.

The GetCityList method will make a call to our web API GetCities method to fetch the list of city data from the cities collection. The GetEmployeeList method will send a GET request to our web API to fetch the list of Employee Data from the Employee table.

We are invoking these two methods inside the OnInitAsync method, to ensure that the Employee Data and the cities data will be available as the page loads.

The AddEmployee method will initialize an empty instance of the Employee object and set the modalTitle property, which will display the title message on the Add modal popup.

The EditEmployee method will accept the employee ID as the parameter. It will send a GET request to our web API to fetch the record of the employee corresponding to the employee ID supplied to it.

We will use the SaveEmployee method to save the record of the employee for both the Add request and Edit request. To differentiate between the Add and the Edit requests, we will use the EmployeeId property of the Employee object. If an Edit request is made, then the EmployeeId property contains a string value, and we will send a PUT request to our web API, which will update the record of the employee. Otherwise, if we make an Add request, then the EmployeeId property is not initialized, and hence it will be null. In this case, we need to send a POST request to our web API, which will create a new employee record.

The DeleteConfirm method will accept the employee ID as the parameter. It will fetch the Employee Data corresponding to the employee ID supplied to it.

The DeleteEmployee method will send a delete request to our API and pass the employee ID as the parameter. It will then call the GetEmployeeList method to refresh the view with the updated list of Employee Data.

The SearchEmployee method is used to implement the search by the employee name functionality. We will return all the records of the employee, which will match the search criteria either fully or partially. To make the search more effective, we will ignore the text case of the search string. This means the search result will be same whether the search text is in uppercase or in lowercase.

EmployeeData.cshtml

Open EmployeeData.cshtml page and put the following code into it.

  1. @page "/employeerecords"  
  2. @inherits EmployeeDataModel  
  3.   
  4. <h1>Employee Data</h1>  
  5.   
  6. <div class="container">  
  7.     <div class="row">  
  8.         <div class="col-xs-3">  
  9.             <button class="btn btn-primary" data-toggle="modal" data-target="#AddEditEmpModal" onclick="@AddEmployee">  
  10.                 <i class="fa fa-user-plus"></i>  
  11.                 Add Employee  
  12.             </button>  
  13.         </div>  
  14.         <div class="input-group col-md-4 offset-md-5">  
  15.             <input type="text" class="form-control" placeholder="Search Employee" bind="@searchString" />  
  16.             <div class="input-group-append">  
  17.                 <button class="btn btn-info" onclick="@SearchEmployee">  
  18.                     <i class="fa fa-search"></i>  
  19.                 </button>  
  20.             </div>  
  21.         </div>  
  22.     </div>  
  23. </div>  
  24. <br />  
  25. @if (empList == null)  
  26. {  
  27.     <p><em>Loading...</em></p>  
  28. }  
  29. else  
  30. {  
  31.     <table class='table'>  
  32.         <thead>  
  33.             <tr>  
  34.                 <th>Name</th>  
  35.                 <th>Gender</th>  
  36.                 <th>Designation</th>  
  37.                 <th>City</th>  
  38.             </tr>  
  39.         </thead>  
  40.         <tbody>  
  41.             @foreach (var emp in empList)  
  42.             {  
  43.                 <tr>  
  44.                     <td>@emp.EmployeeName</td>  
  45.                     <td>@emp.Gender</td>  
  46.                     <td>@emp.Designation</td>  
  47.                     <td>@emp.CityName</td>  
  48.                     <td>  
  49.                         <button class="btn btn-outline-dark" data-toggle="modal" data-target="#AddEditEmpModal"  
  50.                                 onclick="@(async () => await EditEmployee(@emp.EmployeeId))">  
  51.                             <i class="fa fa-pencil-square-o"></i>  
  52.                             Edit  
  53.                         </button>  
  54.                         <button class="btn btn-outline-danger" data-toggle="modal" data-target="#deleteEmpModal"  
  55.                                 onclick="@(async () => await DeleteConfirm(@emp.EmployeeId))">  
  56.                             <i class="fa fa-trash-o"></i>  
  57.                             Delete  
  58.                         </button>  
  59.                     </td>  
  60.                 </tr>  
  61.             }  
  62.         </tbody>  
  63.     </table>  
  64. }  
  65. <div class="modal fade" id="AddEditEmpModal">  
  66.     <div class="modal-dialog">  
  67.         <div class="modal-content">  
  68.             <div class="modal-header">  
  69.                 <h3 class="modal-title">@modalTitle</h3>  
  70.                 <button type="button" class="close" data-dismiss="modal">  
  71.                     <span aria-hidden="true">X</span>  
  72.                 </button>  
  73.             </div>  
  74.             <div class="modal-body">  
  75.                 <form>  
  76.                     <div class="form-group">  
  77.                         <label class="control-label">Name</label>  
  78.                         <input class="form-control" bind="@emp.EmployeeName" />  
  79.                     </div>  
  80.                     <div class="form-group">  
  81.                         <label class="control-label">Gender</label>  
  82.                         <select class="form-control" bind="@emp.Gender">  
  83.                             <option value="">-- Select Gender --</option>  
  84.                             <option value="Male">Male</option>  
  85.                             <option value="Female">Female</option>  
  86.                         </select>  
  87.                     </div>  
  88.                     <div class="form-group">  
  89.                         <label class="control-label">Designation</label>  
  90.                         <input class="form-control" bind="@emp.Designation" />  
  91.                     </div>  
  92.                     <div class="form-group">  
  93.                         <label class="control-label">City</label>  
  94.                         <select class="form-control" bind="@emp.CityName">  
  95.                             <option value="-- Select City --">-- Select City --</option>  
  96.                             @foreach (var city in cityList)  
  97.                             {  
  98.                                 <option value="@city.CityName">@city.CityName</option>  
  99.                             }  
  100.                         </select>  
  101.                     </div>  
  102.                 </form>  
  103.             </div>  
  104.             <div class="modal-footer">  
  105.                 <button class="btn btn-block btn-success"  
  106.                         onclick="@(async () => await SaveEmployee())" data-dismiss="modal">  
  107.                     Save  
  108.                 </button>  
  109.             </div>  
  110.         </div>  
  111.     </div>  
  112. </div>  
  113. <div class="modal fade" id="deleteEmpModal">  
  114.     <div class="modal-dialog">  
  115.         <div class="modal-content">  
  116.             <div class="modal-header">  
  117.                 <h3 class="modal-title">Confirm Delete !!!</h3>  
  118.                 <button type="button" class="close" data-dismiss="modal">  
  119.                     <span aria-hidden="true">X</span>  
  120.                 </button>  
  121.             </div>  
  122.             <div class="modal-body">  
  123.                 <table class="table">  
  124.                     <tr>  
  125.                         <td>Name</td>  
  126.                         <td>@emp.EmployeeName</td>  
  127.                     </tr>  
  128.                     <tr>  
  129.                         <td>Gender</td>  
  130.                         <td>@emp.Gender</td>  
  131.                     </tr>  
  132.                     <tr>  
  133.                         <td>Designation</td>  
  134.                         <td>@emp.Designation</td>  
  135.                     </tr>  
  136.                     <tr>  
  137.                         <td>City</td>  
  138.                         <td>@emp.CityName</td>  
  139.                     </tr>  
  140.                 </table>  
  141.             </div>  
  142.             <div class="modal-footer">  
  143.                 <button class="btn btn-danger" data-dismiss="modal"  
  144.                         onclick="@(async () => await DeleteEmployee(@emp.EmployeeId))">  
  145.                     Delete  
  146.                 </button>  
  147.                 <button data-dismiss="modal" class="btn">Cancel</button>  
  148.             </div>  
  149.         </div>  
  150.     </div>  
  151. </div>  

The route for our component is defined at the top as “/employeerecords”. To use the methods defined in the EmployeeDataModel class, we will inherit it using the @inherits directive.

We have defined an Add Employee button. Upon clicking, this button will invoke the AddEmployee method and open a modal dialog, which allows the user to fill out the new Employee Data in a form.

We have also defined our search box and a corresponding search button. The search box will bind the value to the searchString property. On clicking the search button, SearchEmployee method will be invoked, which will return the filtered list of data as per the search text. If the empList property is not null, we will bind the Employee Data to a table to display it on the web page. Each employee record has the following two action buttons corresponding to it:

  • Edit
    This button will perform two tasks. It will invoke the EditEmployee method and open the edit employee modal dialog for editing the employee record.

  • Delete
    This button will also perform two tasks. It will invoke the DeleteConfirm method and open a delete confirm modal dialog, asking the user to confirm the deletion of the employee’s record.

We have defined a form inside the bootstrap modal to accept user inputs for the employee records. The input fields of this form will bind to the properties of the employee class. The City field is a drop-down list, which will bind to the cities collection of the database with the help of the cityList variable. When we click on the save button, the SaveEmployee method will be invoked and the modal dialog will be closed.

When the user clicks on the "Delete" button corresponding to an employee record, another bootstrap modal dialog will be displayed. This modal will show the Employee Data in a table and ask the user to confirm the deletion. Clicking on the Delete button inside this modal dialog will invoke the DeleteEmployee method and close the modal. Clicking on the Cancel button will close the modal without performing any action on the data.

Adding the navigation link to our component

Before executing the application, we will add the navigation link to our component in the navigation menu.

Open the "BlazorWithFirestore.Client/Shared/NavMenu.cshtml" page and add the following navigation link,

  1. <li class="nav-item px-3">  
  2.   <NavLink class="nav-link" href="employeerecords">  
  3.     <span class="oi oi-list-rich" aria-hidden="true"></span> Employee Data  
  4.   </NavLink>  
  5. </li>  

Hence, we have successfully created a Single Page Application (SPA) using Blazor with the help of cloud Firestore as database provider.

Execution Demo

Press F5 to launch the application.

A web page will open as shown in the image below. The navigation menu on the left is showing the navigation link for Employee data page.

You can perform the CRUD operations on this application as shown in the GIF image at the start of this article.

Conclusion

We have created a Single Page Application (SPA) using Blazor with the help of Google cloud Firestore as a database provider. We have created a sample employee record management system and performed CRUD operations on it. Firestore is a NoSQL database, which allows us to store data in form of collections and documents. We have also used a bootstrap modal popup to handle user inputs. We have also implemented a search box to search the employee list by employee name.

Please get the source code from GitHub and play around to get a better understanding.

You can also read my other articles on my blog here.
 

See Also