Client-Side Caching In Angular 8 Using HTTP Interceptor

In this post, we will see how to achieve client-side caching in Angular 8 application using HTTP Interceptor. We will create an ASP.NET Core 3.0 application with SQL server as backend to save and retrieve the employee data.

Introduction


Caching is a very handy feature to reduce the server-side network traffic. Caching is a temporary storage and can reduce the server requests by saving and retrieving the data in client-side itself. Hence, we will get more performance in client application. In this post, we will achieve the client-side caching through HTTP interceptors. We will create a simple HTTP interceptor in Angular application and intercept all the requests to server. When we request to server first time, interceptor will save the response data in client side as a cache and when user requests the same URL again, it will not go to server again. Instead, the client data will be served from cache. But when we modify the data, interceptor will invalidate the client-side cache and send request to server and response data will be saved to cache again. We will create an ASP.NET Core 3.0 application with entity framework to save and retrieve the employee data and create an Angular 8 application to perform all CRUD actions. We can see all the actions step by step.

Create ASP.NET Core 3.0 application


It is very easy to create a Web API application in ASP.NET Core 3.0 with Visual Studio 2019. Open Visual Studio and choose “ASP.NET Core Web Application” template and choose a valid name to the solution. Choose “ASP.NET Core 3.0” version and “API” template and click “Create” button.
 
 
 
Install “Newtonsoft.Json” NuGet package to the application.
 
 
 
We can add an “Employee” class now.
 
Employee.cs
  1. using Newtonsoft.Json;  
  2.   
  3. namespace Server  
  4. {  
  5.     public class Employee  
  6.     {  
  7.         [JsonProperty(PropertyName = "id")]  
  8.         public string Id { getset; }  
  9.         [JsonProperty(PropertyName = "name")]  
  10.         public string Name { getset; }  
  11.         [JsonProperty(PropertyName = "address")]  
  12.         public string Address { getset; }  
  13.         [JsonProperty(PropertyName = "gender")]  
  14.         public string Gender { getset; }  
  15.         [JsonProperty(PropertyName = "company")]  
  16.         public string Company { getset; }  
  17.         [JsonProperty(PropertyName = "designation")]  
  18.         public string Designation { getset; }  
  19.     }  
  20. }  
We can create a new API controller using scaffolding template. Right click Controller folder and add “New Scaffolded Item”.
 
 
We can choose the “API Controller with action, using Entity Framework” template. Choose “Employee” class as model and create a new data context class also.
 
 
 
 
Web API controller will be created shortly. We can make a small change in the “PostEmployee” method to add a “Guid” as Id for a new employee.
  1. [HttpPost]  
  2. public async Task<ActionResult<Employee>> PostEmployee(Employee employee)  
  3. {  
  4.     employee.Id = Guid.NewGuid().ToString();  
  5.     _context.Employee.Add(employee);  
  6.     try  
  7.     {  
  8.         await _context.SaveChangesAsync();  
  9.     }  
  10.     catch (DbUpdateException)  
  11.     {  
  12.         if (EmployeeExists(employee.Id))  
  13.         {  
  14.             return Conflict();  
  15.         }  
  16.         else  
  17.         {  
  18.             throw;  
  19.         }  
  20.     }  
  21.   
  22.     return CreatedAtAction("GetEmployee"new { id = employee.Id }, employee);  
  23. }  
We can modify the “Startup.cs” class to add CORS headers.
 
Modify the “ConfigureServices” method.
  1. public void ConfigureServices(IServiceCollection services)  
  2. {  
  3.     services.AddControllers();  
  4.   
  5.     services.AddDbContext<EmployeeDbContext>(options =>  
  6.         options.UseSqlServer(Configuration.GetConnectionString("EmployeeDbContext")));  
  7.   
  8.     string corsDomains = "http://localhost:4200";  
  9.     string[] domains = corsDomains.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);  
  10.   
  11.     services.AddCors(o => o.AddPolicy("CORSPolicy", builder =>  
  12.     {  
  13.         builder.AllowAnyOrigin()  
  14.                .AllowAnyMethod()  
  15.                .AllowAnyHeader()  
  16.                .AllowCredentials()  
  17.                .WithOrigins(domains);  
  18.         }));  
  19. }  
We can modify the “Configure” method also.
  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
  2. {  
  3.     if (env.IsDevelopment())  
  4.     {  
  5.         app.UseDeveloperExceptionPage();  
  6.     }  
  7.   
  8.     app.UseRouting();  
  9.   
  10.     app.UseAuthorization();  
  11.   
  12.     app.UseCors("CORSPolicy");   
  13.   
  14.     app.UseEndpoints(endpoints =>  
  15.     {  
  16.         endpoints.MapControllers();  
  17.     });  
  18. }  

Create SQL database and table using DB migration


If you check the “appsettings.json” file, you can see that a connection string is automatically created while creating the API controller using scaffolding template. Visual Studio themselves have a local SQL server and can be used for testing application. We can use DB migration steps to create the database and table automatically in code first approach.
 
Open “Package Manager Console” from Tools -> NuGet Package Manager menu.
 
 
 
 
 
You can use below command to create a Migration.
 
add-migration MyFirstMigration
 
This will create a new migration file under “Migrations” folder with current timestamp.
 
Use below command to create database and table.
 
Update-Database
 
If you look at the SQL server object explorer window, you can see the newly created database and table.
 
 
We have successfully completed the API service. If needed, you can check all methods using Postman or any other tool.
 

Create Angular 8 application using CLI


We can create Angular 8 application and required components, using CLI.
 
ng new Client
 
It will take some time to create all node modules for our Angular application. Once, our application is ready, we can install below node packages into our application.
 
npm install bootstrap
npm install font-awesome
 
We have installed bootstrap and font-awesome packages in our application.
 
We must modify styles.css file in the root folder with below changes to access these packages globally in the application without further references.
 
styles.css
  1. @import "~bootstrap/dist/css/bootstrap.css";      
  2. @import "~font-awesome/css/font-awesome.css";      
Use below command to create a new “Home” component.
 
ng g component home
 
Modify the html template file with below code.
 
home.component.html
  1. <div style="text-align:center">    
  2.     <h1>    
  3.       Welcome to Employee Application!    
  4.     </h1>    
  5.     <img width="200" alt="Angular Logo"    
  6.       src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">    
  7.       
  8.     <div class="jumbotron">    
  9.       <h5>    
  10.         Client side caching in Angular 8 using HttpInterceptor  
  11.       </h5>    
  12.     </div>    
  13.   </div>    
We can create a common header component for our application.
 
ng g c ui\header
 
The above command will create a new header component inside the new “ui” folder.
 
Copy the below code and paste it inside the header.component.html file.
 
header.component.html
  1. <nav class="navbar navbar-dark bg-dark mb-5">  
  2.     <a class="navbar-brand" href="/">Employee App</a>  
  3.     <div class="navbar-expand mr-auto">  
  4.         <div class="navbar-nav">  
  5.             <a class="nav-item nav-link active" routerLink="home" routerLinkActive="active">Home</a>  
  6.             <a class="nav-item nav-link" routerLink="employees">Employee</a>  
  7.         </div>  
  8.     </div>  
  9.     <div class="navbar-expand ml-auto navbar-nav">  
  10.         <div class="navbar-nav">  
  11.             <a class="nav-item nav-link" href="https://github.com/sarathlalsaseendran" target="_blank">  
  12.                 <i class="fa fa-github"></i>  
  13.             </a>  
  14.             <a class="nav-item nav-link" href="https://twitter.com/sarathlalsasee1" target="_blank">  
  15.                 <i class="fa fa-twitter"></i>  
  16.             </a>  
  17.             <a class="nav-item nav-link" href="https://codewithsarath.com" target="_blank">  
  18.                 <i class="fa fa-wordpress"></i>  
  19.             </a>  
  20.         </div>  
  21.     </div>  
  22. </nav>  
We can create a common footer component in the same way.
 
ng g c ui\footer
 
Copy the below code and paste inside the footer.component.html file.
 
footer.component.html
  1. <nav class="navbar navbar-dark bg-dark mt-5 fixed-bottom">  
  2.     <div class="navbar-expand m-auto navbar-text">  
  3.         Developed with <i class="fa fa-heart"></i> by <a href="https://codewithsarath.com" target="_blank">Sarathlal</a>  
  4.     </div>  
  5. </nav>  
We can create Layout component now.
 
ng g c ui\layout
 
Copy below code and paste inside the layout.component.html file.
 
layout.component.html
  1. <app-header></app-header>  
  2. <div class="container">  
  3.     <ng-content></ng-content>  
  4. </div>  
  5. <app-footer></app-footer>  
We have added header and footer components tags inside this file along with a container to display remaining application parts.
 
We will use a generic validation class to validate the employee name. We can create this class now.
 
ng g class shared\GenericValidator
 
The above command will create an empty class inside the shared folder. Copy the below code and paste it inside this class file.
 
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 it is a FormGroup, process its child controls.    
  14.                 if (c instanceof FormGroup) {  
  15.                     const childMessages = this.processMessages(c);  
  16.                     Object.assign(messages, childMessages);  
  17.                 } else {  
  18.                     // Only validate if there are validation messages for the control    
  19.                     if (this.validationMessages[controlKey]) {  
  20.                         messages[controlKey] = '';  
  21.                         if ((c.dirty || c.touched) && c.errors) {  
  22.                             Object.keys(c.errors).map(messageKey => {  
  23.                                 if (this.validationMessages[controlKey][messageKey]) {  
  24.                                     messages[controlKey] += this.validationMessages[controlKey][messageKey] + ' ';  
  25.                                 }  
  26.                             });  
  27.                         }  
  28.                     }  
  29.                 }  
  30.             }  
  31.         }  
  32.         return messages;  
  33.     }  
  34.   
  35.     getErrorCount(container: FormGroup): number {  
  36.         let errorCount = 0;  
  37.         for (const controlKey in container.controls) {  
  38.             if (container.controls.hasOwnProperty(controlKey)) {  
  39.                 if (container.controls[controlKey].errors) {  
  40.                     errorCount += Object.keys(container.controls[controlKey].errors).length;  
  41.                     console.log(errorCount);  
  42.                 }  
  43.             }  
  44.         }  
  45.         return errorCount;  
  46.     }  
  47. }    
We can create an employee interface using below command.
 
ng g interface employee\Employee
 
The above command will create an empty interface inside the employee folder. Copy the below code and paste to this file.
 
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. }    
We can create employee service now.
 
ng g service employee\Employee
 
Copy the below code to this service class.
 
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 './employee';  
  6.   
  7. @Injectable({  
  8.   providedIn: 'root',  
  9.  })  
  10. export class EmployeeService {  
  11.   private employeesUrl = 'http://localhost:3333/api/employees';  
  12.   
  13.   constructor(private http: HttpClient) { }  
  14.   
  15.   getEmployees(): Observable<Employee[]> {  
  16.     return this.http.get<Employee[]>(this.employeesUrl)  
  17.       .pipe(  
  18.         catchError(this.handleError)  
  19.       );  
  20.   }  
  21.   
  22.   getEmployee(id: string): Observable<Employee> {  
  23.     if (id === '') {  
  24.       return of(this.initializeEmployee());  
  25.     }  
  26.     const url = `${this.employeesUrl}/${id}`;  
  27.     return this.http.get<Employee>(url)  
  28.       .pipe(  
  29.         catchError(this.handleError)  
  30.       );  
  31.   }  
  32.   
  33.   createEmployee(employee: Employee): Observable<Employee> {  
  34.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  35.     return this.http.post<Employee>(this.employeesUrl, employee, { headers: headers })  
  36.       .pipe(  
  37.         catchError(this.handleError)  
  38.       );  
  39.   }  
  40.   
  41.   deleteEmployee(id: string): Observable<{}> {  
  42.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  43.     const url = `${this.employeesUrl}/${id}`;  
  44.     return this.http.delete<Employee>(url, { headers: headers })  
  45.       .pipe(  
  46.         catchError(this.handleError)  
  47.       );  
  48.   }  
  49.   
  50.   updateEmployee(id: string, employee: Employee): Observable<Employee> {  
  51.     const headers = new HttpHeaders({ 'Content-Type''application/json' });  
  52.     const url = `${this.employeesUrl}/${id}`;  
  53.     return this.http.put<Employee>(url, employee, { headers: headers })  
  54.       .pipe(  
  55.         map(() => employee),  
  56.         catchError(this.handleError)  
  57.       );  
  58.   }  
  59.   
  60.   private handleError(err) {  
  61.     let errorMessage: string;  
  62.     if (err.error instanceof ErrorEvent) {  
  63.       errorMessage = `An error occurred: ${err.error.message}`;  
  64.     } else {  
  65.       errorMessage = `Backend returned code ${err.status}: ${err.body.error}`;  
  66.     }  
  67.     console.error(err);  
  68.     return throwError(errorMessage);  
  69.   }  
  70.   
  71.   private initializeEmployee(): Employee {  
  72.     return {  
  73.       id: null,  
  74.       name: null,  
  75.       address: null,  
  76.       gender: null,  
  77.       company: null,  
  78.       designation: null,  
  79.     };  
  80.   }  
  81. }      
We have added all CRUD actions logic inside above service class.
 
We can add employee list component to list all employee information.
 
ng g component employee\EmployeeList
 
Copy the below code and paste inside the component class file.
 
employee-list.component.ts
  1. import { Component, OnInit } from '@angular/core';    
  2. import { Employee } from '../employee';    
  3. import { EmployeeService } from '../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): void {    
  44.     if (id === '') {    
  45.       // Don't delete, it was never saved.    
  46.       this.onSaveComplete();    
  47.     } else {    
  48.       if (confirm(`Are you sure want to delete this Employee: ${name}?`)) {    
  49.         this.employeeService.deleteEmployee(id)    
  50.           .subscribe(    
  51.             () => this.onSaveComplete(),    
  52.             (error: any) => this.errorMessage = <any>error    
  53.           );    
  54.       }    
  55.     }    
  56.   }    
  57.     
  58.   onSaveComplete(): void {    
  59.     this.employeeService.getEmployees().subscribe(    
  60.       employees => {    
  61.         this.employees = employees;    
  62.         this.filteredEmployees = this.employees;    
  63.       },    
  64.       error => this.errorMessage = <any>error    
  65.     );    
  66.   }    
  67.     
  68. }  
Also, copy the corresponding HTML and CSS files.
 
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-2"></div>    
  12.             <div class="col-md-4">    
  13.                 <button class="btn btn-primary mr-3" [routerLink]="['/employees/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]">    
  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"    
  49.                                 [routerLink]="['/employees', employee.id, 'edit']">    
  50.                                 Edit    
  51.                             </button>    
  52.                         </td>    
  53.                         <td>    
  54.                             <button class="btn btn-outline-warning btn-sm"    
  55.                                 (click)="deleteEmployee(employee.id,  employee.name);">    
  56.                                 Delete    
  57.                             </button>    
  58.                         </td>    
  59.                     </tr>    
  60.                 </tbody>    
  61.             </table>    
  62.         </div>    
  63.     </div>    
  64. </div>    
  65. <div *ngIf="errorMessage" class="alert alert-danger">    
  66.     Error: {{ errorMessage }}    
  67. </div>    
employee-list.component.css
  1. thead {  
  2.     color#337AB7;  
  3. }  
We can create employee edit component in the same way.
 
ng g component employee\EmployeeEdit
 
Copy the below code and paste to component class and HTML files.
 
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 { Employee } from '../employee';    
  5. import { EmployeeService } from '../employee.service';    
  6. import { ActivatedRoute, Router } from '@angular/router';    
  7. import { debounceTime } from 'rxjs/operators';    
  8. import { GenericValidator } from 'src/app/shared/generic-validator';    
  9.     
  10. @Component({    
  11.   selector: 'app-employee-edit',    
  12.   templateUrl: './employee-edit.component.html',    
  13.   styleUrls: ['./employee-edit.component.css']    
  14. })    
  15. export class EmployeeEditComponent implements OnInit, AfterViewInit, OnDestroy {    
  16.   @ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];    
  17.   pageTitle = 'Employee Edit';    
  18.   errorMessage: string;    
  19.   employeeForm: FormGroup;    
  20.     
  21.   employee: Employee;    
  22.   private sub: Subscription;    
  23.     
  24.   displayMessage: { [key: string]: string } = {};    
  25.   private validationMessages: { [key: string]: { [key: string]: string } };    
  26.   private genericValidator: GenericValidator;    
  27.     
  28.     
  29.   constructor(private fb: FormBuilder,    
  30.     private route: ActivatedRoute,    
  31.     private router: Router,    
  32.     private employeeService: EmployeeService) {    
  33.     
  34.     this.validationMessages = {    
  35.       name: {    
  36.         required: 'Employee name is required.',    
  37.         minlength: 'Employee name must be at least three characters.',    
  38.         maxlength: 'Employee name cannot exceed 50 characters.'    
  39.       },    
  40.       address: {    
  41.         required: 'Employee address is required.',    
  42.       }    
  43.     };    
  44.     
  45.     this.genericValidator = new GenericValidator(this.validationMessages);    
  46.   }    
  47.     
  48.   ngOnInit() {    
  49.     this.employeeForm = this.fb.group({    
  50.       name: ['', [Validators.required    
  51.     ]],    
  52.       address: ['', [Validators.required]],    
  53.       gender: '',    
  54.       company: '',    
  55.       designation: ''    
  56.     });    
  57.     
  58.     this.sub = this.route.paramMap.subscribe(    
  59.       params => {    
  60.         const id = params.get('id');    
  61.         if (id == '0') {    
  62.           const employee: Employee = { id: "0", name: "", address: "", gender: "", company: "", designation: "" };    
  63.           this.displayEmployee(employee);    
  64.         }    
  65.         else {    
  66.           this.getEmployee(id);    
  67.         }    
  68.       }    
  69.     );    
  70.   }    
  71.     
  72.   ngOnDestroy(): void {    
  73.     this.sub.unsubscribe();    
  74.   }    
  75.     
  76.   ngAfterViewInit(): void {    
  77.     const controlBlurs: Observable<any>[] = this.formInputElements    
  78.       .map((formControl: ElementRef) => fromEvent(formControl.nativeElement, 'blur'));    
  79.     
  80.     merge(this.employeeForm.valueChanges, ...controlBlurs).pipe(    
  81.       debounceTime(800)    
  82.     ).subscribe(value => {    
  83.       this.displayMessage = this.genericValidator.processMessages(this.employeeForm);    
  84.     });    
  85.   }    
  86.     
  87.     
  88.   getEmployee(id: string): void {    
  89.     this.employeeService.getEmployee(id)    
  90.       .subscribe(    
  91.         (employee: Employee) => this.displayEmployee(employee),    
  92.         (error: any) => this.errorMessage = <any>error    
  93.       );    
  94.   }    
  95.     
  96.   displayEmployee(employee: Employee): void {    
  97.     if (this.employeeForm) {    
  98.       this.employeeForm.reset();    
  99.     }    
  100.     this.employee = employee;    
  101.     
  102.     if (this.employee.id == '0') {    
  103.       this.pageTitle = 'Add Employee';    
  104.     } else {    
  105.       this.pageTitle = `Edit Employee: ${this.employee.name}`;    
  106.     }    
  107.     
  108.     this.employeeForm.patchValue({    
  109.       name: this.employee.name,    
  110.       address: this.employee.address,    
  111.       gender: this.employee.gender,    
  112.       company: this.employee.company,    
  113.       designation: this.employee.designation    
  114.     });    
  115.   }    
  116.     
  117.   deleteEmployee(): void {    
  118.     if (this.employee.id == '0') {    
  119.       this.onSaveComplete();    
  120.     } else {    
  121.       if (confirm(`Are you sure want to delete this Employee: ${this.employee.name}?`)) {    
  122.         this.employeeService.deleteEmployee(this.employee.id)    
  123.           .subscribe(    
  124.             () => this.onSaveComplete(),    
  125.             (error: any) => this.errorMessage = <any>error    
  126.           );    
  127.       }    
  128.     }    
  129.   }    
  130.     
  131.   saveEmployee(): void {    
  132.     if (this.employeeForm.valid) {    
  133.       if (this.employeeForm.dirty) {    
  134.         const p = { ...this.employee, ...this.employeeForm.value };    
  135.         if (p.id === '0') {    
  136.           this.employeeService.createEmployee(p)    
  137.             .subscribe(    
  138.               () => this.onSaveComplete(),    
  139.               (error: any) => this.errorMessage = <any>error    
  140.             );    
  141.         } else {    
  142.           this.employeeService.updateEmployee(p.id,p)    
  143.             .subscribe(    
  144.               () => this.onSaveComplete(),    
  145.               (error: any) => this.errorMessage = <any>error    
  146.             );    
  147.         }    
  148.       } else {    
  149.         this.onSaveComplete();    
  150.       }    
  151.     } else {    
  152.       this.errorMessage = 'Please correct the validation errors.';    
  153.     }    
  154.   }    
  155.     
  156.     
  157.   onSaveComplete(): void {    
  158.     this.employeeForm.reset();    
  159.     this.router.navigate(['/employees']);    
  160.   }    
  161. }    
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 (ngSubmit)="saveEmployee()" [formGroup]="employeeForm">  
  8.   
  9.             <div class="form-group row mb-2">  
  10.                 <label class="col-md-3 col-form-label" for="employeeNameId">Employee Name</label>  
  11.                 <div class="col-md-7">  
  12.                     <input class="form-control" id="employeeNameId" type="text" placeholder="Name (required)"  
  13.                         formControlName="name" [ngClass]="{'is-invalid': displayMessage.name }" />  
  14.                     <span class="invalid-feedback">  
  15.                         {{displayMessage.name}}  
  16.                     </span>  
  17.                 </div>  
  18.             </div>  
  19.   
  20.             <div class="form-group row mb-2">  
  21.                 <label class="col-md-3 col-form-label" for="addressId">Address</label>  
  22.                 <div class="col-md-7">  
  23.                     <input class="form-control" id="addressId" type="text" placeholder="Address"  
  24.                         formControlName="address" />  
  25.                 </div>  
  26.             </div>  
  27.   
  28.             <div class="form-group row mb-2">  
  29.                 <label class="col-md-3 col-form-label" for="genderId">Gender</label>  
  30.                 <div class="col-md-7">  
  31.                     <select id="genderId" formControlName="gender" class="form-control">  
  32.                         <option value="" disabled selected>Select an Option</option>  
  33.                         <option value="Male">Male</option>  
  34.                         <option value="Female">Female</option>  
  35.                     </select>  
  36.   
  37.                 </div>  
  38.             </div>  
  39.   
  40.             <div class="form-group row mb-2">  
  41.                 <label class="col-md-3 col-form-label" for="companyId">Company</label>  
  42.                 <div class="col-md-7">  
  43.                     <input class="form-control" id="companyId" type="text" placeholder="Company"  
  44.                         formControlName="company" />  
  45.                 </div>  
  46.             </div>  
  47.   
  48.             <div class="form-group row mb-2">  
  49.                 <label class="col-md-3 col-form-label" for="designationId">Designation</label>  
  50.                 <div class="col-md-7">  
  51.                     <input class="form-control" id="designationId" type="text" placeholder="Designation"  
  52.                         formControlName="designation" />  
  53.                 </div>  
  54.             </div>  
  55.   
  56.             <div class="form-group row mb-2">  
  57.                 <div class="offset-md-2 col-md-6">  
  58.                     <button class="btn btn-primary mr-3" style="width:80px;" type="submit"  
  59.                         [title]="employeeForm.valid ? 'Save your entered data' : 'Disabled until the form data is valid'"  
  60.                         [disabled]="!employeeForm.valid">  
  61.                         Save  
  62.                     </button>  
  63.                     <button class="btn btn-outline-secondary mr-3" style="width:80px;" type="button"  
  64.                         title="Cancel your edits" [routerLink]="['/employees']">  
  65.                         Cancel  
  66.                     </button>  
  67.                     <button class="btn btn-outline-warning" *ngIf="pageTitle != 'Add Employee'" style="width:80px"  
  68.                         type="button" title="Delete this product" (click)="deleteEmployee()">  
  69.                         Delete  
  70.                     </button>  
  71.                 </div>  
  72.             </div>  
  73.         </form>  
  74.     </div>  
  75.   
  76.     <div class="alert alert-danger" *ngIf="errorMessage">{{errorMessage}}  
  77.     </div>  
  78. </div>  
We can create a guard to prevent data loss before saving the data.
 
ng g guard employee\EmployeeEdit
 
Copy the below code and paste to guard class file.
 
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/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 employee detail component.
 
ng g component employee\EmployeeDetail
 
Copy the below code and paste to component class and HTML files.
 
employee-detail.component.ts
  1. import { Component, OnInit } from '@angular/core';    
  2. import { Employee } from '../employee';    
  3. import { ActivatedRoute, Router } from '@angular/router';    
  4. import { EmployeeService } from '../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 param = this.route.snapshot.paramMap.get('id');    
  22.     if (param) {    
  23.       const id = param;    
  24.       this.getEmployee(id);    
  25.     }    
  26.   }    
  27.     
  28.   getEmployee(id: string) {    
  29.     this.employeeService.getEmployee(id).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" *ngIf="employee">  
  3.         {{pageTitle + ": " + employee.name}}  
  4.     </div>  
  5.     <div class="card-body" *ngIf="employee">  
  6.         <div class="row">  
  7.             <div class="col-md-8">  
  8.                 <div class="row">  
  9.                     <div class="col-md-3">Name:</div>  
  10.                     <div class="col-md-6">{{employee.name}}</div>  
  11.                 </div>  
  12.                 <div class="row">  
  13.                     <div class="col-md-3">Address:</div>  
  14.                     <div class="col-md-6">{{employee.address}}</div>  
  15.                 </div>  
  16.                 <div class="row">  
  17.                     <div class="col-md-3">Gender:</div>  
  18.                     <div class="col-md-6">{{employee.gender}}</div>  
  19.                 </div>  
  20.                 <div class="row">  
  21.                     <div class="col-md-3">Company:</div>  
  22.                     <div class="col-md-6">{{employee.company}}</div>  
  23.                 </div>  
  24.                 <div class="row">  
  25.                     <div class="col-md-3">Designation:</div>  
  26.                     <div class="col-md-6">{{employee.designation}}</div>  
  27.                 </div>  
  28.             </div>  
  29.         </div>  
  30.         <div class="row mt-4">  
  31.             <div class="col-md-4">  
  32.                 <button class="btn btn-outline-secondary mr-3" style="width:80px" (click)="onBack()">  
  33.                     <i class="fa fa-chevron-left"></i> Back  
  34.                 </button>  
  35.                 <button class="btn btn-outline-primary" style="width:80px"  
  36.                     [routerLink]="['/employees', employee.id, 'edit']">  
  37.                     Edit  
  38.                 </button>  
  39.             </div>  
  40.         </div>  
  41.     </div>  
  42.     <div class="alert alert-danger" *ngIf="errorMessage">  
  43.         {{errorMessage}}  
  44.     </div>  
  45. </div>  

Implement Client-Side caching using HTTP Interceptor


We have created all the components. We can create a HttpCache service to implement client-side caching.
 
ng g service shared\HttpCache
 
Copy below code and paste to http-cache.service.ts file.
 
http-cache.service.ts
  1. import { Injectable } from '@angular/core';  
  2. import { HttpResponse } from '@angular/common/http';  
  3.   
  4. @Injectable({  
  5.   providedIn: 'root'  
  6. })  
  7. export class HttpCacheService {  
  8.   
  9.   private requests: any = { };  
  10.   
  11.   constructor() { }  
  12.   
  13.   put(url: string, response: HttpResponse<any>): void {  
  14.     this.requests[url] = response;  
  15.   }  
  16.   
  17.   get(url: string): HttpResponse<any> | undefined {  
  18.     return this.requests[url];  
  19.   }  
  20.   
  21.   invalidateCache(): void {  
  22.     this.requests = { };  
  23.   }  
  24. }  
We have added “put”, “get”, and “invalidateCache” methods inside above service. “put” method will add http response to a local variable. “get” method will get the previously saved response value from cache. “invalidateCache” method will destroy the cache value from variable. We will call this cache service from http interceptor.
 
Currently Angular CLI is not supporting generate command to create an interceptor. We can create the interceptor manually and copy below code.
 
cache.interceptor.ts
  1. import { Injectable } from '@angular/core';  
  2. import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';  
  3. import { Observable, of } from 'rxjs';  
  4. import { tap } from 'rxjs/operators';  
  5. import { HttpCacheService } from './http-cache.service';  
  6.   
  7.   
  8. @Injectable()  
  9. export class CacheInterceptor implements HttpInterceptor {  
  10.   
  11.   constructor(private cacheService: HttpCacheService) { }  
  12.   
  13.   intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
  14.   
  15.     // pass along non-cacheable requests and invalidate cache  
  16.     if(req.method !== 'GET') {  
  17.       console.log(`Invalidating cache: ${req.method} ${req.url}`);  
  18.       this.cacheService.invalidateCache();  
  19.       return next.handle(req);  
  20.     }  
  21.   
  22.     // attempt to retrieve a cached response  
  23.     const cachedResponse: HttpResponse<any> = this.cacheService.get(req.url);  
  24.   
  25.     // return cached response  
  26.     if (cachedResponse) {  
  27.       console.log(`Returning a cached response: ${cachedResponse.url}`);  
  28.       console.log(cachedResponse);  
  29.       return of(cachedResponse);  
  30.     }      
  31.   
  32.     // send request to server and add response to cache  
  33.     return next.handle(req)  
  34.       .pipe(  
  35.         tap(event => {  
  36.           if (event instanceof HttpResponse) {  
  37.             console.log(`Adding item to cache: ${req.url}`);  
  38.             this.cacheService.put(req.url, event);  
  39.           }  
  40.         })  
  41.       );  
  42.   
  43.   }  
  44. }  
We have implemented “HttpInterceptor” interface inside this interceptor class. This interface has a single “intercept” method. This will be used to check each request from browser. If the request is not a Get method, this will invalidate the existing cache. When we request the same URL again, interceptor will get the value from cache and return to response. This way, we can reduce the request to server.
 
We can create a route config file now.
 
ng g class RouterConfig
 
Copy the below code and paste inside the router constant file.
 
router-config.ts
  1. import { Routes } from '@angular/router';    
  2. import { HomeComponent } from './home/home.component';    
  3. import { EmployeeListComponent } from './employee/employee-list/employee-list.component';    
  4. import { EmployeeEditComponent } from './employee/employee-edit/employee-edit.component';    
  5. import { EmployeeEditGuard } from './employee/employee-edit.guard';    
  6. import { EmployeeDetailComponent } from './employee/employee-detail/employee-detail.component';    
  7.     
  8. export const routes: Routes = [    
  9.     {    
  10.         path: 'home',    
  11.         component: HomeComponent    
  12.     },    
  13.     {    
  14.         path: 'employees',    
  15.         component: EmployeeListComponent    
  16.     },    
  17.     {    
  18.         path: 'employees/:id',    
  19.         component: EmployeeDetailComponent    
  20.     },    
  21.     {    
  22.         path: 'employees/:id/edit',    
  23.         canDeactivate: [EmployeeEditGuard],    
  24.         component: EmployeeEditComponent    
  25.     },    
  26.     {    
  27.         path: '',    
  28.         redirectTo: 'home',    
  29.         pathMatch: 'full'    
  30.     },    
  31.     {    
  32.         path: '**',    
  33.         redirectTo: 'home',    
  34.         pathMatch: 'full'    
  35.     }    
  36. ];    
We can modify the app.module.ts file with below code.
 
app.module.ts
  1. import { BrowserModule } from '@angular/platform-browser';  
  2. import { NgModule } from '@angular/core';  
  3.   
  4. import { AppComponent } from './app.component';  
  5. import { HomeComponent } from './home/home.component';  
  6. import { HeaderComponent } from './ui/header/header.component';  
  7. import { FooterComponent } from './ui/footer/footer.component';  
  8. import { LayoutComponent } from './ui/layout/layout.component';  
  9. import { EmployeeListComponent } from './employee/employee-list/employee-list.component';  
  10. import { EmployeeEditComponent } from './employee/employee-edit/employee-edit.component';  
  11. import { EmployeeDetailComponent } from './employee/employee-detail/employee-detail.component';  
  12. import { RouterModule } from '@angular/router';  
  13. import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';  
  14. import { routes } from './router-config';  
  15. import { ReactiveFormsModule, FormsModule } from '@angular/forms';  
  16. import { CacheInterceptor } from './shared/cache.interceptor';  
  17.   
  18. @NgModule({  
  19.   declarations: [  
  20.     AppComponent,  
  21.     HomeComponent,  
  22.     HeaderComponent,  
  23.     FooterComponent,  
  24.     LayoutComponent,  
  25.     EmployeeListComponent,  
  26.     EmployeeEditComponent,  
  27.     EmployeeDetailComponent  
  28.   ],  
  29.   imports: [  
  30.     BrowserModule,  
  31.     FormsModule,   
  32.     ReactiveFormsModule,   
  33.     RouterModule,    
  34.     HttpClientModule,    
  35.     RouterModule.forRoot(routes),    
  36.   ],  
  37.   providers: [  
  38.     { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true }  
  39.   ],  
  40.   bootstrap: [AppComponent]  
  41. })  
  42. export class AppModule { }  
We have provided the interceptor token and class name inside above module.
 
 
 
We can modify app.component.html file
 
app.component.html
  1. <app-layout>      
  2.   <router-outlet></router-outlet>      
  3. </app-layout>   
We have completed entire Angular application. We can run both ASP.NET Core and Angular application now.
 
 
 
We can add a new employee now.  
 
 
 
When you click the Employee menu again, you can see that, the response is getting from cache. Not from server.
 
 
You will not see any new request on Network tab.
 
 
 
 
 

Conclusion


In this post, we have created an ASP.NET Core 3.0 application with entity framework to save and retrieve employee data. Later, we have created an Angular 8 application to perform all CRUD actions from client side. We have successfully implemented an http interceptor for caching all get requests to server.