In Focus

Angular App With ASP.NET Core And Cosmos DB

In this post, we will see how to create an Angular application in Visual Studio 2017 with ASP.NET Core template and we will connect this app with Cosmos DB database. For testing purposes, we are using Cosmos DB free emulator service.

Introduction

Cosmos DB is one of the most demanding No SQL databases in recent days. This is a multi-model (Currently available with SQL, Mongo DB, Cassandra, Table, and Graph APIs) and horizontally scalable database. You can enable geo-replication in Cosmos DB which will reduce the latency between different regions. You can refer this document to get more details about Cosmos DB.

Install Cosmos DB Emulator and create database locally

 
For testing purposes, I am using Cosmos DB local emulator. This is a free, fully functional database service. You can run the emulator on your Windows machine easily. You can download the Emulator MSI setup from this URL.
 
After the download and installation of the emulator, you can run it and create a new database and a collection for your application. Since we are creating an Employee application, we need to create a collection named “Employee”. (In Azure Cosmos DB, collections are now called as containers)
 
Angular App With ASP.NET Core And Cosmos DB
 
You can see a URI and Primary key in this emulator. We will use these values later with our application to connect Cosmos DB. You can click the Explorer tab and create a new database and a collection.
 
Angular App With ASP.NET Core And Cosmos DB

You can give a name to your database and collection id. You must give a Partition key. A Partition key is very important in Cosmos DB in order to store the data.

Please note, you can’t change the partition key once you created the collection. You must be very careful to choose the correct partition key.
 
Unlike SQL database, there is no need to create other schemas in design time. Data will be saved in JSON format with appropriate field names in run time.
 

Create Angular Application in Visual Studio with ASP.NET Core

 
We can create a web application using ASP.NET Core and Angular template in Visual Studio 2017. You can use Visual Studio 2019 also.
 
Angular App With ASP.NET Core And Cosmos DB
 
Please note, I have chosen ASP.NET Core 2.2 version and Angular template. It will take some time to create the web application and load all .NET Core dependencies. If you check the project structure, you can see that a “ClientApp” folder is created under “wwwroot” and contains all the basic files for Angular application. As you all know, every angular project contains a “node_modules” folder when we install npm packages. Here this folder will be created automatically once we build our project (first-time build).
 
Angular App With ASP.NET Core And Cosmos DB
 
Our default application is ready now. If needed, you can run the application and check whether all the functionalities are working properly or not.
 
You may get the below error message sometimes while running the application. Don’t panic about this message. Try to refresh the screen and this error will automatically disappear and you will see the actual page now.
 
Angular App With ASP.NET Core And Cosmos DB

Create Cosmos DB API Service with Employees Controller

 
We can create the Web API service for our angular application in ASP.NET core with Cosmos DB database. Since we are creating Employee application, we can create an “Employee” model class first.
 
Create a “Models” folder in the root and create “Employee” class inside it. You can copy the below code and paste to this class file.
 
Employee.cs
  1. using Newtonsoft.Json;  
  2.   
  3. namespace AngularCoreCosmos.Models  
  4. {  
  5.     public class Employee  
  6.     {  
  7.         [JsonProperty(PropertyName = "id")]  
  8.         public string Id { getset; }  
  9.         public string Name { getset; }  
  10.         public string Address { getset; }  
  11.         public string Gender { getset; }  
  12.         public string Company { getset; }  
  13.         public string Designation { getset; }  
  14.         public string Cityname { getset; }  
  15.     }  
  16. }  

I have added all the field names required for our Cosmos DB collection. Also note that I have added a “JsonProperty” attribute for “Id” property. Because, Cosmos DB automatically creates an “id” field for each record.

We can install the “Microsoft.Azure.DocumentDB.Core” NuGet package in our project. This will be used to connect with Cosmos DB. You can install the latest version.
 
Angular App With ASP.NET Core And Cosmos DB

We can create a “Data” folder and create an “IDocumentDBRepository” interface inside it. This interface contains all the method names for our Cosmos DB repository. We will implement this interface in the “DocumentDBRepository” class.

IDocumentDBRepository.cs
  1. using Microsoft.Azure.Documents;  
  2. using System;  
  3. using System.Collections.Generic;  
  4. using System.Linq.Expressions;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace AngularCoreCosmos.Data  
  8. {  
  9.     public interface IDocumentDBRepository<T> where T : class  
  10.     {  
  11.         Task<Document> CreateItemAsync(T item, string collectionId);  
  12.         Task DeleteItemAsync(string id, string collectionId, string partitionKey);  
  13.         Task<IEnumerable<T>> GetItemsAsync(Expression<Func<T, bool>> predicate, string collectionId);  
  14.         Task<IEnumerable<T>> GetItemsAsync(string collectionId);  
  15.         Task<Document> UpdateItemAsync(string id, T item, string collectionId);  
  16.     }  
  17. }  

I have added all the methods declaration for CRUD actions for Web API controller in the above interface.

We can implement this interface in the “DocumentDBRepository” class.
 
DocumentDBRepository.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.Linq;  
  7. using System.Linq.Expressions;  
  8. using System.Threading.Tasks;  
  9.   
  10. namespace AngularCoreCosmos.Data  
  11. {  
  12.     public class DocumentDBRepository<T> : IDocumentDBRepository<T> where T : class  
  13.     {  
  14.   
  15.         private readonly string Endpoint = "https://localhost:8081/";  
  16.         private readonly string Key = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";  
  17.         private readonly string DatabaseId = "SarathCosmosDB";  
  18.         private DocumentClient client;  
  19.   
  20.         public DocumentDBRepository()  
  21.         {  
  22.             client = new DocumentClient(new Uri(Endpoint), Key);  
  23.         }  
  24.   
  25.         public async Task<IEnumerable<T>> GetItemsAsync(Expression<Func<T, bool>> predicate, string collectionId)  
  26.         {  
  27.             IDocumentQuery<T> query = client.CreateDocumentQuery<T>(  
  28.                 UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId),  
  29.                 new FeedOptions { MaxItemCount = -1 })  
  30.                 .Where(predicate)  
  31.                 .AsDocumentQuery();  
  32.   
  33.             List<T> results = new List<T>();  
  34.             while (query.HasMoreResults)  
  35.             {  
  36.                 results.AddRange(await query.ExecuteNextAsync<T>());  
  37.             }  
  38.   
  39.             return results;  
  40.         }  
  41.   
  42.         public async Task<IEnumerable<T>> GetItemsAsync(string collectionId)  
  43.         {  
  44.             IDocumentQuery<T> query = client.CreateDocumentQuery<T>(  
  45.                 UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId),  
  46.                 new FeedOptions { MaxItemCount = -1 })  
  47.                 .AsDocumentQuery();  
  48.   
  49.             List<T> results = new List<T>();  
  50.             while (query.HasMoreResults)  
  51.             {  
  52.                 results.AddRange(await query.ExecuteNextAsync<T>());  
  53.             }  
  54.   
  55.             return results;  
  56.         }  
  57.   
  58.         public async Task<Document> CreateItemAsync(T item, string collectionId)  
  59.         {  
  60.             return await client.CreateDocumentAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId), item);  
  61.         }  
  62.   
  63.         public async Task<Document> UpdateItemAsync(string id, T item, string collectionId)  
  64.         {  
  65.             return await client.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id), item);  
  66.         }  
  67.   
  68.         public async Task DeleteItemAsync(string id, string collectionId, string partitionKey)  
  69.         {  
  70.             await client.DeleteDocumentAsync(UriFactory.CreateDocumentUri(DatabaseId, collectionId, id),  
  71.             new RequestOptions() { PartitionKey = new PartitionKey(partitionKey) });  
  72.         }  
  73.   
  74.         private async Task CreateDatabaseIfNotExistsAsync()  
  75.         {  
  76.             try  
  77.             {  
  78.                 await client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(DatabaseId));  
  79.             }  
  80.             catch (DocumentClientException e)  
  81.             {  
  82.                 if (e.StatusCode == System.Net.HttpStatusCode.NotFound)  
  83.                 {  
  84.                     await client.CreateDatabaseAsync(new Database { Id = DatabaseId });  
  85.                 }  
  86.                 else  
  87.                 {  
  88.                     throw;  
  89.                 }  
  90.             }  
  91.         }  
  92.   
  93.         private async Task CreateCollectionIfNotExistsAsync(string collectionId)  
  94.         {  
  95.             try  
  96.             {  
  97.                 await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(DatabaseId, collectionId));  
  98.             }  
  99.             catch (DocumentClientException e)  
  100.             {  
  101.                 if (e.StatusCode == System.Net.HttpStatusCode.NotFound)  
  102.                 {  
  103.                     await client.CreateDocumentCollectionAsync(  
  104.                         UriFactory.CreateDatabaseUri(DatabaseId),  
  105.                         new DocumentCollection { Id = collectionId },  
  106.                         new RequestOptions { OfferThroughput = 1000 });  
  107.                 }  
  108.                 else  
  109.                 {  
  110.                     throw;  
  111.                 }  
  112.             }  
  113.         }  
  114.     }  
  115. }  

I have implemented all the CRUD actions inside the above class. We can use these methods in our Web API controller.

We can create “EmployeesController” controller class now.
 
EmployeesController.cs
  1. using AngularCoreCosmos.Data;  
  2. using AngularCoreCosmos.Models;  
  3. using Microsoft.AspNetCore.Mvc;  
  4. using System.Collections.Generic;  
  5. using System.Threading.Tasks;  
  6.   
  7. namespace AngularCoreCosmos.Controllers  
  8. {  
  9.     [Route("api/[controller]")]  
  10.     [ApiController]  
  11.     public class EmployeesController : ControllerBase  
  12.     {  
  13.         private readonly IDocumentDBRepository<Employee> Respository;  
  14.         private readonly string CollectionId;  
  15.         public EmployeesController(IDocumentDBRepository<Employee> Respository)  
  16.         {  
  17.             this.Respository = Respository;  
  18.             CollectionId = "Employee";  
  19.         }  
  20.   
  21.         [HttpGet]  
  22.         public async Task<IEnumerable<Employee>> Get()  
  23.         {  
  24.             return await Respository.GetItemsAsync(CollectionId);  
  25.         }  
  26.   
  27.         [HttpGet("{id}/{cityname}")]  
  28.         public async Task<Employee> Get(string id, string cityname)  
  29.         {  
  30.             var employees = await Respository.GetItemsAsync(d => d.Id == id && d.Cityname == cityname, CollectionId);  
  31.             Employee employee = new Employee();  
  32.             foreach (var emp in employees)  
  33.             {  
  34.                 employee = emp;  
  35.                 break;  
  36.             }  
  37.             return employee;  
  38.         }  
  39.   
  40.         [HttpPost]  
  41.         public async Task<bool> Post([FromBody]Employee employee)  
  42.         {  
  43.             try  
  44.             {  
  45.                 if (ModelState.IsValid)  
  46.                 {  
  47.                     employee.Id = null;  
  48.                     await Respository.CreateItemAsync(employee, CollectionId);  
  49.                 }  
  50.                 return true;  
  51.             }  
  52.             catch  
  53.             {  
  54.                 return false;  
  55.             }  
  56.   
  57.         }  
  58.   
  59.         [HttpPut]  
  60.         public async Task<bool> Put([FromBody]Employee employee)  
  61.         {  
  62.             try  
  63.             {  
  64.                 if (ModelState.IsValid)  
  65.                 {  
  66.                     await Respository.UpdateItemAsync(employee.Id, employee, CollectionId);  
  67.                 }  
  68.                 return true;  
  69.             }  
  70.             catch  
  71.             {  
  72.                 return false;  
  73.             }  
  74.         }  
  75.   
  76.         [HttpDelete("{id}/{cityname}")]  
  77.         public async Task<bool> Delete(string id, string cityname)  
  78.         {  
  79.             try  
  80.             {  
  81.                 await Respository.DeleteItemAsync(id, CollectionId, cityname);  
  82.                 return true;  
  83.             }  
  84.             catch  
  85.             {  
  86.                 return false;  
  87.             }  
  88.         }  
  89.     }  
  90. }  

I have implemented all the action methods inside this controller class using DocumentDBRepository class.

We can inject the dependency to DocumentDBRepositoryservice inside Startup class using a singleton pattern.
 
Angular App With ASP.NET Core And Cosmos DB

We have successfully created our Web API service. If needed, you can check the API with Postman or any other tool.

Add Employee components in Angular

 
We can add employee app related components in the Angular part of this project.
 
Before that modify the home component HTML file inside the home folder.
 
home.component.html
  1. <div style="text-align:center;">  
  2.   <h1>Angular App with ASP.NET Core and Cosmos DB</h1>  
  3.   <p>Welcome to our new single-page application, built with below technologies:</p>  
  4.   <img src="../../assets/angular-asp-core-cosmos.png" style="width:700px;" />  
  5. </div>  

I have added a beautiful image as background in this file.

We can modify the navigation menu as well.
 
nav-menu.component.html
  1. <header>  
  2.   <nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'>  
  3.     <div class="container">  
  4.       <a class="navbar-brand" [routerLink]='["/"]'>Employee App</a>  
  5.       <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-label="Toggle navigation"  
  6.               [attr.aria-expanded]="isExpanded" (click)="toggle()">  
  7.         <span class="navbar-toggler-icon"></span>  
  8.       </button>  
  9.       <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'>  
  10.         <ul class="navbar-nav flex-grow">  
  11.           <li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'>  
  12.             <a class="nav-link text-dark" [routerLink]='["/"]'>Home</a>  
  13.           </li>  
  14.           <li class="nav-item" [routerLinkActive]='["link-active"]'>  
  15.             <a class="nav-link text-dark" [routerLink]='["/employees"]'>Employees</a>  
  16.           </li>  
  17.         </ul>  
  18.       </div>  
  19.     </div>  
  20.   </nav>  
  21. </header>  
  22. <footer>  
  23.   <nav class="navbar navbar-light bg-white mt-5 fixed-bottom">  
  24.     <div class="navbar-expand m-auto navbar-text">  
  25.       Developed with <i class="fa fa-heart"></i> by <a href="https://codewithsarath.com" target="_blank"><b>Sarathlal Saseendran</b></a>  
  26.     </div>  
  27.   </nav>  
  28. </footer>  

This is the navigation part of the application. I have added an Employee menu in this file.

We can create a generic validator for validating employee name and employee city in the employee edit screen. Both these fields are mandatory. So, we must validate these fields.
 
Create a “shared” folder and create a “GenericValidator” class inside it.
 
generic-validator.ts
  1. import { FormGroup } from '@angular/forms';  
  2.   
  3. export class GenericValidator {  
  4.   
  5.   constructor(private validationMessages: { [key: string]: { [key: string]: string } }) {  
  6.   }  
  7.   
  8.   processMessages(container: FormGroup): { [key: string]: string } {  
  9.     const messages = {};  
  10.     for (const controlKey in container.controls) {  
  11.       if (container.controls.hasOwnProperty(controlKey)) {  
  12.         const c = container.controls[controlKey];  
  13.         if (c instanceof FormGroup) {  
  14.           const childMessages = this.processMessages(c);  
  15.           Object.assign(messages, childMessages);  
  16.         } else {  
  17.           if (this.validationMessages[controlKey]) {  
  18.             messages[controlKey] = '';  
  19.             if ((c.dirty || c.touched) && c.errors) {  
  20.               Object.keys(c.errors).map(messageKey => {  
  21.                 if (this.validationMessages[controlKey][messageKey]) {  
  22.                   messages[controlKey] += this.validationMessages[controlKey][messageKey] + ' ';  
  23.                 }  
  24.               });  
  25.             }  
  26.           }  
  27.         }  
  28.       }  
  29.     }  
  30.     return messages;  
  31.   }  
  32. }  

We can create an “employee” model inside “data-models” and define all the employee properties inside this class. We are creating all employee related components and service inside the “employees” folder.

employee.ts
  1. export interface Employee {  
  2.   id: string,  
  3.   name: string,  
  4.   address: string,  
  5.   gender: string,  
  6.   company: string,  
  7.   designation: string,  
  8.   cityname: string  
  9. }  

We can create an employee service inside the services folder. We will write all the methods for CRUD operations inside this service. The methods in this service will be invoked from various components later.

employee-service.ts
  1. import { Injectable, Inject } from '@angular/core';  
  2. import { HttpClient, HttpHeaders } from '@angular/common/http';  
  3. import { Observable, throwError, of } from 'rxjs';  
  4. import { catchError, map } from 'rxjs/operators';  
  5. import { Employee } from '../data-models/employee';  
  6.   
  7. @Injectable()  
  8. export class EmployeeService {  
  9.   private employeesUrl = this.baseUrl + 'api/employees';  
  10.   
  11.   constructor(private http: HttpClient, @Inject('BASE_URL'private baseUrl: string) { }  
  12.   
  13.   getEmployees(): Observable<Employee[]> {  
  14.     return this.http.get<Employee[]>(this.employeesUrl)  
  15.       .pipe(  
  16.         catchError(this.handleError)  
  17.       );  
  18.   }  
  19.   
  20.   getEmployee(id: string, cityName: string): Observable<Employee> {  
  21.     if (id === '') {  
  22.       return of(this.initializeEmployee());  
  23.     }  
  24.     const url = `${this.employeesUrl}/${id}/${cityName}`;  
  25.     return this.http.get<Employee>(url)  
  26.       .pipe(  
  27.         catchError(this.handleError)  
  28.       );  
  29.   }  
  30.   
  31.   createEmployee(employee: Employee): Observable<Employee> {  
  32.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  33.     return this.http.post<Employee>(this.employeesUrl, employee, { headers: headers })  
  34.       .pipe(  
  35.         catchError(this.handleError)  
  36.       );  
  37.   }  
  38.   
  39.   deleteEmployee(id: string, cityname: string): Observable<{}> {  
  40.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  41.     const url = `${this.employeesUrl}/${id}/${cityname}`;  
  42.     return this.http.delete<Employee>(url, { headers: headers })  
  43.       .pipe(  
  44.         catchError(this.handleError)  
  45.       );  
  46.   }  
  47.   
  48.   updateEmployee(employee: Employee): Observable<Employee> {  
  49.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  50.     const url = this.employeesUrl;  
  51.     return this.http.put<Employee>(url, employee, { headers: headers })  
  52.       .pipe(  
  53.         map(() => employee),  
  54.         catchError(this.handleError)  
  55.       );  
  56.   }  
  57.   
  58.   private handleError(err) {  
  59.     let errorMessage: string;  
  60.     if (err.error instanceof ErrorEvent) {  
  61.       errorMessage = `An error occurred: ${err.error.message}`;  
  62.     } else {  
  63.       errorMessage = `Backend returned code ${err.status}: ${err.body.error}`;  
  64.     }  
  65.     console.error(err);  
  66.     return throwError(errorMessage);  
  67.   }  
  68.   
  69.   private initializeEmployee(): Employee {  
  70.     return {  
  71.       id: null,  
  72.       name: null,  
  73.       address: null,  
  74.       gender: null,  
  75.       company: null,  
  76.       designation: null,  
  77.       cityname: null  
  78.     };  
  79.   }  
  80. }  

We can create employee list component to list all entire employee data in a grid.

We will create this component under “employee-list” folder.
 
employee-list.component.ts
  1. import { Component, OnInit } from '@angular/core';  
  2. import { Employee } from '../data-models/employee';  
  3. import { EmployeeService } from '../services/employee-service';  
  4.   
  5. @Component({  
  6.   selector: 'app-employee-list',  
  7.   templateUrl: './employee-list.component.html',  
  8.   styleUrls: ['./employee-list.component.css']  
  9. })  
  10. export class EmployeeListComponent implements OnInit {  
  11.   pageTitle = 'Employee List';  
  12.   filteredEmployees: Employee[] = [];  
  13.   employees: Employee[] = [];  
  14.   errorMessage = '';  
  15.   
  16.   _listFilter = '';  
  17.   get listFilter(): string {  
  18.     return this._listFilter;  
  19.   }  
  20.   set listFilter(value: string) {  
  21.     this._listFilter = value;  
  22.     this.filteredEmployees = this.listFilter ? this.performFilter(this.listFilter) : this.employees;  
  23.   }  
  24.   
  25.   constructor(private employeeService: EmployeeService) { }  
  26.   
  27.   performFilter(filterBy: string): Employee[] {  
  28.     filterBy = filterBy.toLocaleLowerCase();  
  29.     return this.employees.filter((employee: Employee) =>  
  30.       employee.name.toLocaleLowerCase().indexOf(filterBy) !== -1);  
  31.   }  
  32.   
  33.   ngOnInit(): void {  
  34.     this.employeeService.getEmployees().subscribe(  
  35.       employees => {  
  36.         this.employees = employees;  
  37.         this.filteredEmployees = this.employees;  
  38.       },  
  39.       error => this.errorMessage = <any>error  
  40.     );  
  41.   }  
  42.   
  43.   deleteEmployee(id: string, name: string, cityname: string): void {  
  44.     if (id === '') {  
  45.       this.onSaveComplete();  
  46.     } else {  
  47.       if (confirm(`Are you sure want to delete this Employee: ${name}?`)) {  
  48.         this.employeeService.deleteEmployee(id, cityname)  
  49.           .subscribe(  
  50.             () => this.onSaveComplete(),  
  51.             (error: any) => this.errorMessage = <any>error  
  52.           );  
  53.       }  
  54.     }  
  55.   }  
  56.   
  57.   onSaveComplete(): void {  
  58.     this.employeeService.getEmployees().subscribe(  
  59.       employees => {  
  60.         this.employees = employees;  
  61.         this.filteredEmployees = this.employees;  
  62.       },  
  63.       error => this.errorMessage = <any>error  
  64.     );  
  65.   }  
  66.   
  67. }  

employee-list.component.html

  1. <div class="card">  
  2.   <div class="card-header">  
  3.     {{pageTitle}}  
  4.   </div>  
  5.   <div class="card-body">  
  6.     <div class="row" style="margin-bottom:15px;">  
  7.       <div class="col-md-2">Filter by:</div>  
  8.       <div class="col-md-4">  
  9.         <input type="text" [(ngModel)]="listFilter" />  
  10.       </div>  
  11.       <div class="col-md-4"></div>  
  12.       <div class="col-md-2">  
  13.         <button class="btn btn-primary mr-3" [routerLink]="['/employees/0/0/edit']">  
  14.           New Employee  
  15.         </button>  
  16.       </div>  
  17.     </div>  
  18.     <div class="row" *ngIf="listFilter">  
  19.       <div class="col-md-6">  
  20.         <h4>Filtered by: {{listFilter}}</h4>  
  21.       </div>  
  22.     </div>  
  23.     <div class="table-responsive">  
  24.       <table class="table mb-0" *ngIf="employees && employees.length">  
  25.         <thead>  
  26.           <tr>  
  27.             <th>Name</th>  
  28.             <th>Address</th>  
  29.             <th>Gender</th>  
  30.             <th>Company</th>  
  31.             <th>Designation</th>  
  32.             <th></th>  
  33.             <th></th>  
  34.           </tr>  
  35.         </thead>  
  36.         <tbody>  
  37.           <tr *ngFor="let employee of filteredEmployees">  
  38.             <td>  
  39.               <a [routerLink]="['/employees', employee.id,employee.cityname]">  
  40.                 {{ employee.name }}  
  41.               </a>  
  42.             </td>  
  43.             <td>{{ employee.address }}</td>  
  44.             <td>{{ employee.gender }}</td>  
  45.             <td>{{ employee.company }}</td>  
  46.             <td>{{ employee.designation}} </td>  
  47.             <td>  
  48.               <button class="btn btn-outline-primary btn-sm" [routerLink]="['/employees', employee.id, employee.cityname, 'edit']">  
  49.                 Edit  
  50.               </button>  
  51.             </td>  
  52.             <td>  
  53.               <button class="btn btn-outline-warning btn-sm" (click)="deleteEmployee(employee.id,  employee.name,employee.cityname);">  
  54.                 Delete  
  55.               </button>  
  56.             </td>  
  57.           </tr>  
  58.         </tbody>  
  59.       </table>  
  60.     </div>  
  61.   </div>  
  62. </div>  
  63. <div *ngIf="errorMessage" class="alert alert-danger">  
  64.   Error: {{ errorMessage }}  
  65. </div>  

employee-list.component.css

  1. thead {  
  2.   color#337AB7;  
  3. }  

We have added TS, HTML, and CSS files for employee list component. We can create employee edit component in the same way. This component will be created under the “employee-edit” folder.

employee-edit.component.ts
  1. import { Component, OnInit, AfterViewInit, OnDestroy, ElementRef, ViewChildren } from '@angular/core';  
  2. import { FormControlName, FormGroup, FormBuilder, Validators } from '@angular/forms';  
  3. import { Subscription, Observable, fromEvent, merge } from 'rxjs';  
  4. import { ActivatedRoute, Router } from '@angular/router';  
  5. import { Employee } from '../data-models/employee';  
  6. import { EmployeeService } from '../services/employee-service';  
  7. import { GenericValidator } from 'src/app/shared/generic-validator';  
  8.   
  9. @Component({  
  10.   selector: 'app-employee-edit',  
  11.   templateUrl: './employee-edit.component.html',  
  12.   styleUrls: ['./employee-edit.component.css']  
  13. })  
  14. export class EmployeeEditComponent implements OnInit, OnDestroy {  
  15.   @ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];  
  16.   pageTitle = 'Employee Edit';  
  17.   errorMessage: string;  
  18.   employeeForm: FormGroup;  
  19.   tranMode: string;  
  20.   employee: Employee;  
  21.   private sub: Subscription;  
  22.   
  23.   displayMessage: { [key: string]: string } = {};  
  24.   private validationMessages: { [key: string]: { [key: string]: string } };  
  25.   private genericValidator: GenericValidator;  
  26.   
  27.   
  28.   constructor(private fb: FormBuilder,  
  29.     private route: ActivatedRoute,  
  30.     private router: Router,  
  31.     private employeeService: EmployeeService) {  
  32.   
  33.     this.validationMessages = {  
  34.       name: {  
  35.         required: 'Employee name is required.',  
  36.         minlength: 'Employee name must be at least three characters.',  
  37.         maxlength: 'Employee name cannot exceed 50 characters.'  
  38.       },  
  39.       cityname: {  
  40.         required: 'Employee city name is required.',  
  41.       }  
  42.     };  
  43.     this.genericValidator = new GenericValidator(this.validationMessages);  
  44.   }  
  45.   
  46.   ngOnInit() {  
  47.     this.tranMode = "new";  
  48.     this.employeeForm = this.fb.group({  
  49.       name: ['', [Validators.required,  
  50.       Validators.minLength(3),  
  51.       Validators.maxLength(50)  
  52.       ]],  
  53.       address: '',  
  54.       cityname: ['', [Validators.required]],  
  55.       gender: '',  
  56.       company: '',  
  57.       designation: '',  
  58.     });  
  59.   
  60.     this.sub = this.route.paramMap.subscribe(  
  61.       params => {  
  62.         const id = params.get('id');  
  63.         const cityname = params.get('cityname');  
  64.         if (id == '0') {  
  65.           const employee: Employee = { id: "0", name: "", address: "", gender: "", company: "", designation: "", cityname: "" };  
  66.           this.displayEmployee(employee);  
  67.         }  
  68.         else {  
  69.           this.getEmployee(id, cityname);  
  70.         }  
  71.       }  
  72.     );  
  73.   }  
  74.   
  75.   ngOnDestroy(): void {  
  76.     this.sub.unsubscribe();  
  77.   }  
  78.   
  79.   getEmployee(id: string, cityname: string): void {  
  80.     this.employeeService.getEmployee(id, cityname)  
  81.       .subscribe(  
  82.         (employee: Employee) => this.displayEmployee(employee),  
  83.         (error: any) => this.errorMessage = <any>error  
  84.       );  
  85.   }  
  86.   
  87.   displayEmployee(employee: Employee): void {  
  88.     if (this.employeeForm) {  
  89.       this.employeeForm.reset();  
  90.     }  
  91.     this.employee = employee;  
  92.     if (this.employee.id == '0') {  
  93.       this.pageTitle = 'Add Employee';  
  94.     } else {  
  95.       this.pageTitle = `Edit Employee: ${this.employee.name}`;  
  96.     }  
  97.     this.employeeForm.patchValue({  
  98.       name: this.employee.name,  
  99.       address: this.employee.address,  
  100.       gender: this.employee.gender,  
  101.       company: this.employee.company,  
  102.       designation: this.employee.designation,  
  103.       cityname: this.employee.cityname  
  104.     });  
  105.   }  
  106.   
  107.   deleteEmployee(): void {  
  108.     if (this.employee.id == '0') {  
  109.       this.onSaveComplete();  
  110.     } else {  
  111.       if (confirm(`Are you sure want to delete this Employee: ${this.employee.name}?`)) {  
  112.         this.employeeService.deleteEmployee(this.employee.id, this.employee.cityname)  
  113.           .subscribe(  
  114.             () => this.onSaveComplete(),  
  115.             (error: any) => this.errorMessage = <any>error  
  116.           );  
  117.       }  
  118.     }  
  119.   }  
  120.   
  121.   saveEmployee(): void {  
  122.     if (this.employeeForm.valid) {  
  123.       if (this.employeeForm.dirty) {  
  124.         const p = { ...this.employee, ...this.employeeForm.value };  
  125.         if (p.id === '0') {  
  126.           this.employeeService.createEmployee(p)  
  127.             .subscribe(  
  128.               () => this.onSaveComplete(),  
  129.               (error: any) => this.errorMessage = <any>error  
  130.             );  
  131.         } else {  
  132.           this.employeeService.updateEmployee(p)  
  133.             .subscribe(  
  134.               () => this.onSaveComplete(),  
  135.               (error: any) => this.errorMessage = <any>error  
  136.             );  
  137.         }  
  138.       } else {  
  139.         this.onSaveComplete();  
  140.       }  
  141.     } else {  
  142.       this.errorMessage = 'Please correct the validation errors.';  
  143.     }  
  144.   }  
  145.   
  146.   onSaveComplete(): void {  
  147.     this.employeeForm.reset();  
  148.     this.router.navigate(['/employees']);  
  149.   }  
  150. }  

employee-edit.component.html

  1. <div class="card">  
  2.   <div class="card-header">  
  3.     {{pageTitle}}  
  4.   </div>  
  5.   
  6.   <div class="card-body">  
  7.     <form novalidate  
  8.           (ngSubmit)="saveEmployee()"  
  9.           [formGroup]="employeeForm">  
  10.   
  11.       <div class="form-group row mb-2">  
  12.         <label class="col-md-2 col-form-label"  
  13.                for="employeeNameId">Employee Name</label>  
  14.         <div class="col-md-8">  
  15.           <input class="form-control"  
  16.                  id="employeeNameId"  
  17.                  type="text"  
  18.                  placeholder="Name (required)"  
  19.                  formControlName="name"  
  20.                  [ngClass]="{'is-invalid': displayMessage.name }" />  
  21.           <span class="invalid-feedback">  
  22.             {{displayMessage.name}}  
  23.           </span>  
  24.         </div>  
  25.       </div>  
  26.   
  27.       <div class="form-group row mb-2">  
  28.         <label class="col-md-2 col-form-label"  
  29.                for="citynameId">City</label>  
  30.         <div class="col-md-8">  
  31.           <input class="form-control"  
  32.                  id="citynameid"  
  33.                  type="text"  
  34.                  placeholder="Cityname (required)"  
  35.                  formControlName="cityname"  
  36.                  [ngClass]="{'is-invalid': displayMessage.cityname}" />  
  37.           <span class="invalid-feedback">  
  38.             {{displayMessage.cityname}}  
  39.           </span>  
  40.         </div>  
  41.       </div>  
  42.   
  43.       <div class="form-group row mb-2">  
  44.         <label class="col-md-2 col-form-label"  
  45.                for="addressId">Address</label>  
  46.         <div class="col-md-8">  
  47.           <input class="form-control"  
  48.                  id="addressId"  
  49.                  type="text"  
  50.                  placeholder="Address"  
  51.                  formControlName="address" />  
  52.         </div>  
  53.       </div>  
  54.   
  55.       <div class="form-group row mb-2">  
  56.         <label class="col-md-2 col-form-label"  
  57.                for="genderId">Gender</label>  
  58.         <div class="col-md-8">  
  59.           <select id="genderId" formControlName="gender" class="form-control">  
  60.             <option value="" disabled selected>Select an Option</option>  
  61.             <option value="Male">Male</option>  
  62.             <option value="Female">Female</option>  
  63.           </select>  
  64.   
  65.         </div>  
  66.       </div>  
  67.   
  68.       <div class="form-group row mb-2">  
  69.         <label class="col-md-2 col-form-label"  
  70.                for="companyId">Company</label>  
  71.         <div class="col-md-8">  
  72.           <input class="form-control"  
  73.                  id="companyId"  
  74.                  type="text"  
  75.                  placeholder="Company"  
  76.                  formControlName="company" />  
  77.         </div>  
  78.       </div>  
  79.   
  80.       <div class="form-group row mb-2">  
  81.         <label class="col-md-2 col-form-label"  
  82.                for="designationId">Designation</label>  
  83.         <div class="col-md-8">  
  84.           <input class="form-control"  
  85.                  id="designationId"  
  86.                  type="text"  
  87.                  placeholder="Designation"  
  88.                  formControlName="designation" />  
  89.         </div>  
  90.       </div>  
  91.   
  92.       <div class="form-group row mb-2">  
  93.         <div class="offset-md-2 col-md-4">  
  94.           <button class="btn btn-primary mr-3"  
  95.                   style="width:80px;"  
  96.                   type="submit"  
  97.                   [title]="employeeForm.valid ? 'Save your entered data' : 'Disabled until the form data is valid'"  
  98.                   [disabled]="!employeeForm.valid">  
  99.             Save  
  100.           </button>  
  101.           <button class="btn btn-outline-secondary mr-3"  
  102.                   style="width:80px;"  
  103.                   type="button"  
  104.                   title="Cancel your edits"  
  105.                   [routerLink]="['/employees']">  
  106.             Cancel  
  107.           </button>  
  108.           <button class="btn btn-outline-warning" *ngIf="pageTitle != 'Add Employee'"  
  109.                   style="width:80px"  
  110.                   type="button"  
  111.                   title="Delete this product"  
  112.                   (click)="deleteEmployee()">  
  113.             Delete  
  114.           </button>  
  115.         </div>  
  116.       </div>  
  117.     </form>  
  118.   </div>  
  119.   
  120.   <div class="alert alert-danger"  
  121.        *ngIf="errorMessage">{{errorMessage}}  
  122.   </div>  
  123. </div>  

We can also create an “EmployeeEditGuard” guard which will be used to prevent the screen from closing before saving the data. It will ask a confirmation before leaving the screen without saving the data.

employee-edit.guard.ts
  1. import { Injectable } from '@angular/core';  
  2. import { CanDeactivate } from '@angular/router';  
  3. import { Observable } from 'rxjs';  
  4. import { EmployeeEditComponent } from './employee-edit.component';  
  5.   
  6.   
  7. @Injectable({  
  8.   providedIn: 'root'  
  9. })  
  10. export class EmployeeEditGuard implements CanDeactivate<EmployeeEditComponent> {  
  11.   canDeactivate(component: EmployeeEditComponent): Observable<boolean> | Promise<boolean> | boolean {  
  12.     if (component.employeeForm.dirty) {  
  13.       const name = component.employeeForm.get('name').value || 'New Employee';  
  14.       return confirm(`Navigate away and lose all changes to ${name}?`);  
  15.     }  
  16.     return true;  
  17.   }  
  18. }   

We can create an employee detail component inside “employee-detail” folder in the same way.

employee-detail.component.ts
  1. import { Component, OnInit } from '@angular/core';  
  2. import { ActivatedRoute, Router } from '@angular/router';  
  3. import { Employee } from '../data-models/employee';  
  4. import { EmployeeService } from '../services/employee-service';  
  5.   
  6. @Component({  
  7.   selector: 'app-employee-detail',  
  8.   templateUrl: './employee-detail.component.html',  
  9.   styleUrls: ['./employee-detail.component.css']  
  10. })  
  11. export class EmployeeDetailComponent implements OnInit {  
  12.   pageTitle = 'Employee Detail';  
  13.   errorMessage = '';  
  14.   employee: Employee | undefined;  
  15.   
  16.   constructor(private route: ActivatedRoute,  
  17.     private router: Router,  
  18.     private employeeService: EmployeeService) { }  
  19.   
  20.   ngOnInit() {  
  21.     const id = this.route.snapshot.paramMap.get('id');  
  22.     const cityname = this.route.snapshot.paramMap.get('cityname');  
  23.     if (id && cityname) {  
  24.       this.getEmployee(id, cityname);  
  25.     }  
  26.   }  
  27.   
  28.   getEmployee(id: string, cityName: string) {  
  29.     this.employeeService.getEmployee(id, cityName).subscribe(  
  30.       employee => this.employee = employee,  
  31.       error => this.errorMessage = <any>error);  
  32.   }  
  33.   
  34.   onBack(): void {  
  35.     this.router.navigate(['/employees']);  
  36.   }  
  37. }  

employee-detail.component.html

  1. <div class="card">  
  2.   <div class="card-header"  
  3.        *ngIf="employee">  
  4.     {{pageTitle + ": " + employee.name}}  
  5.   </div>  
  6.   <div class="card-body"  
  7.        *ngIf="employee">  
  8.     <div class="row">  
  9.       <div class="col-md-8">  
  10.         <div class="row">  
  11.           <div class="col-md-3">Name:</div>  
  12.           <div class="col-md-6">{{employee.name}}</div>  
  13.         </div>  
  14.         <div class="row">  
  15.           <div class="col-md-3">City:</div>  
  16.           <div class="col-md-6">{{employee.cityname}}</div>  
  17.         </div>  
  18.         <div class="row">  
  19.           <div class="col-md-3">Address:</div>  
  20.           <div class="col-md-6">{{employee.address}}</div>  
  21.         </div>  
  22.         <div class="row">  
  23.           <div class="col-md-3">Gender:</div>  
  24.           <div class="col-md-6">{{employee.gender}}</div>  
  25.         </div>  
  26.         <div class="row">  
  27.           <div class="col-md-3">Company:</div>  
  28.           <div class="col-md-6">{{employee.company}}</div>  
  29.         </div>  
  30.         <div class="row">  
  31.           <div class="col-md-3">Designation:</div>  
  32.           <div class="col-md-6">{{employee.designation}}</div>  
  33.         </div>  
  34.       </div>  
  35.     </div>  
  36.     <div class="row mt-4">  
  37.       <div class="col-md-4">  
  38.         <button class="btn btn-outline-secondary mr-3"  
  39.                 style="width:80px"  
  40.                 (click)="onBack()">  
  41.           <i class="fa fa-chevron-left"></i> Back  
  42.         </button>  
  43.         <button class="btn btn-outline-primary"  
  44.                 style="width:80px"  
  45.                 [routerLink]="['/employees', employee.id, employee.cityname, 'edit']">  
  46.           Edit  
  47.         </button>  
  48.       </div>  
  49.     </div>  
  50.   </div>  
  51.   <div class="alert alert-danger"  
  52.        *ngIf="errorMessage">  
  53.     {{errorMessage}}  
  54.   </div>  
  55. </div>  

We can modify the “app.module.ts” file. We must add all the declarations for components and service inside this file. We can also add the routing details into this file.

We have completed the coding part. We can execute the application now.
 
Angular App With ASP.NET Core And Cosmos DB

We can click the “Employees” menu and add a new employee detail.

Angular App With ASP.NET Core And Cosmos DB

If you click the “Cancel” button before saving the data, you will get below message.

Angular App With ASP.NET Core And Cosmos DB

This is due to the edit guard implemented with employee edit component.

We can add one more employee detail and see the data in the grid. You can even search the employee name in Filter textbox.
 
Angular App With ASP.NET Core And Cosmos DB
 
You can click the employee name and see the employee detail in read-only mode.
 
Angular App With ASP.NET Core And Cosmos DB
 
You can click the edit button to modify the existing employee data. Here, I have changed the employee name.
 
Angular App With ASP.NET Core And Cosmos DB
 
You can click the Delete button to remove the employee record. It will ask for confirmation before deleting the data.
 
Angular App With ASP.NET Core And Cosmos DB
 
We have seen all the CRUD actions with this simple employee app.
 

Conclusion

 
We have created a web application using ASP.NET Core and Angular template in Visual Studio 2017. We have then created a new database and collection in Cosmos DB. We have used Cosmos DB local emulator service. We have added all components and service for an employee application in Angular. We have seen all CRUD actions with this application. We have also seen the edit guard to protect data loss when we close or cancel the edit screen before saving the information. Please give your valuable comments on this post, so that I can improve my future articles with your valuable input.