Introduction


SignalR is a library for ASP.NET developers to simplify the process of adding real-time web functionality to applications. Real-time web functionality is the ability to have server code push content to connected clients instantly as it becomes available, rather than having the server wait for a client to request new data. Chat application is often used as SignalR example, but here, we will create an employee application in Angular 11 along with .NET 5 backend to describe real-time features. We will create a .NET 5 application with SQL server database to insert, edit and delete employee data. We will also create an Angular 11 application as front-end. When we add a new employee data or update or delete the data, we will get broadcasted message from SignalR hub in the Angular application and immediately show the modified data in all connected client browsers. We will also display the notification instantly, as a bell icon in the menu bar. User can click in the notification bell icon and see all the notification history. We will also provide the option to delete all notification history from database.

Create .NET 5 Web API application in Visual Studio 2019


We can create a new Web API with .NET 5 SDK in Visual Studio 2019. We will choose the ASP.NET Core Web API template. We will also choose the default “Enable Open API support” option. This feature will help us to enable swagger API documentation in our application.
We can create a “Models” folder and create two classes “Employee” and “Notification”.
Employee.cs
  1. namespace NET5SignalR.Models
  2. {
  3. public class Employee
  4. {
  5. public string Id { get; set; }
  6. public string Name { get; set; }
  7. public string Designation { get; set; }
  8. public string Company { get; set; }
  9. public string Cityname { get; set; }
  10. public string Address { get; set; }
  11. public string Gender { get; set; }
  12. }
  13. }
Notification.cs
  1. using System.ComponentModel.DataAnnotations.Schema;
  2. namespace NET5SignalR.Models
  3. {
  4. public class Notification
  5. {
  6. [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  7. public int Id { get; set; }
  8. public string EmployeeName { get; set; }
  9. public string TranType { get; set; }
  10. }
  11. }
We can create our Employee API controller using scaffolding feature in Visual Studio. Please note that, we are using entity framework code first approach in this application.
We have chosen Employee model from the class list and chose a new name for Db context class.
Scaffolding feature will add the required NuGet packages to the application and will create an API controller with all CRUD default methods.
We can see that a new class “MyDbContext” is created inside the “Data” folder automatically. This class is created with DbSet property of Employee class. This will be used to create an Employee table while Db migration time. We can add one more DbSet property for Notification table.
MyDbContext.cs
  1. using Microsoft.EntityFrameworkCore;
  2. using NET5SignalR.Models;
  3. namespace NET5SignalR.Data
  4. {
  5. public class MyDbContext : DbContext
  6. {
  7. public MyDbContext (DbContextOptions<MyDbContext> options)
  8. : base(options)
  9. {
  10. }
  11. public DbSet<Employee> Employee { get; set; }
  12. public DbSet<Notification> Notification { get; set; }
  13. }
  14. }
Scaffolding has also created a connection string in appsettings.json file for database connectivity. Now we can use “Package Manager Console” to create database and tables.
Above migration command will create a migration script inside the “Migrations” folder. We can use below command to create database and tables using above script.
If you look at the SQL server object explorer in Visual Studio, you can see that the new database is created with two tables.
We can install the NuGet package “Microsoft.AspNet.SignalR” now.
We will create an interface “IHubClient” followed by a class “BroadcastHub” inside the “Models” folder.
IHubClient.cs
  1. using System.Threading.Tasks;
  2. namespace NET5SignalR.Models
  3. {
  4. public interface IHubClient
  5. {
  6. Task BroadcastMessage();
  7. }
  8. }
BroadcastHub.cs
  1. using Microsoft.AspNetCore.SignalR;
  2. namespace NET5SignalR.Models
  3. {
  4. public class BroadcastHub : Hub<IHubClient>
  5. {
  6. }
  7. }
Both interface and class will be used to broadcast real-time messages to Angular application.
We must add below changes in the Startup class to broadcast messages from SignalR to Angular client.
Startup.cs
  1. using Microsoft.AspNetCore.Builder;
  2. using Microsoft.AspNetCore.Hosting;
  3. using Microsoft.EntityFrameworkCore;
  4. using Microsoft.Extensions.Configuration;
  5. using Microsoft.Extensions.DependencyInjection;
  6. using Microsoft.Extensions.Hosting;
  7. using Microsoft.OpenApi.Models;
  8. using NET5SignalR.Data;
  9. using NET5SignalR.Models;
  10. namespace NET5SignalR
  11. {
  12. public class Startup
  13. {
  14. public Startup(IConfiguration configuration)
  15. {
  16. Configuration = configuration;
  17. }
  18. public IConfiguration Configuration { get; }
  19. public void ConfigureServices(IServiceCollection services)
  20. {
  21. services.AddControllers();
  22. services.AddSwaggerGen(c =>
  23. {
  24. c.SwaggerDoc("v1", new OpenApiInfo { Title = "NET5SignalR", Version = "v1" });
  25. });
  26. services.AddCors(o => o.AddPolicy("CorsPolicy", builder => {
  27. builder
  28. .AllowAnyMethod()
  29. .AllowAnyHeader()
  30. .AllowCredentials()
  31. .WithOrigins("http://localhost:4200");
  32. }));
  33. services.AddSignalR();
  34. services.AddDbContext<MyDbContext>(options =>
  35. options.UseSqlServer(Configuration.GetConnectionString("MyDbContext")));
  36. }
  37. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  38. {
  39. if (env.IsDevelopment())
  40. {
  41. app.UseDeveloperExceptionPage();
  42. app.UseSwagger();
  43. app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "NET5SignalR v1"));
  44. }
  45. app.UseRouting();
  46. app.UseAuthorization();
  47. app.UseCors("CorsPolicy");
  48. app.UseEndpoints(endpoints =>
  49. {
  50. endpoints.MapHub<BroadcastHub>("/notify");
  51. });
  52. app.UseEndpoints(endpoints =>
  53. {
  54. endpoints.MapControllers();
  55. });
  56. }
  57. }
  58. }
We must modify the default Employee controller with below code changes.
EmployeesController.cs
  1. using Microsoft.AspNetCore.Mvc;
  2. using Microsoft.AspNetCore.SignalR;
  3. using Microsoft.EntityFrameworkCore;
  4. using NET5SignalR.Data;
  5. using NET5SignalR.Models;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Linq;
  9. using System.Threading.Tasks;
  10. namespace NET5SignalR.Controllers
  11. {
  12. [Route("api/[controller]")]
  13. [ApiController]
  14. public class EmployeesController : ControllerBase
  15. {
  16. private readonly MyDbContext _context;
  17. private readonly IHubContext<BroadcastHub, IHubClient> _hubContext;
  18. public EmployeesController(MyDbContext context, IHubContext<BroadcastHub, IHubClient> hubContext)
  19. {
  20. _context = context;
  21. _hubContext = hubContext;
  22. }
  23. // GET: api/Employees
  24. [HttpGet]
  25. public async Task<ActionResult<IEnumerable<Employee>>> GetEmployee()
  26. {
  27. return await _context.Employee.ToListAsync();
  28. }
  29. // GET: api/Employees/5
  30. [HttpGet("{id}")]
  31. public async Task<ActionResult<Employee>> GetEmployee(string id)
  32. {
  33. var employee = await _context.Employee.FindAsync(id);
  34. if (employee == null)
  35. {
  36. return NotFound();
  37. }
  38. return employee;
  39. }
  40. // PUT: api/Employees/5
  41. // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
  42. [HttpPut("{id}")]
  43. public async Task<IActionResult> PutEmployee(string id, Employee employee)
  44. {
  45. if (id != employee.Id)
  46. {
  47. return BadRequest();
  48. }
  49. _context.Entry(employee).State = EntityState.Modified;
  50. Notification notification = new Notification()
  51. {
  52. EmployeeName = employee.Name,
  53. TranType = "Edit"
  54. };
  55. _context.Notification.Add(notification);
  56. try
  57. {
  58. await _context.SaveChangesAsync();
  59. await _hubContext.Clients.All.BroadcastMessage();
  60. }
  61. catch (DbUpdateConcurrencyException)
  62. {
  63. if (!EmployeeExists(id))
  64. {
  65. return NotFound();
  66. }
  67. else
  68. {
  69. throw;
  70. }
  71. }
  72. return NoContent();
  73. }
  74. // POST: api/Employees
  75. // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
  76. [HttpPost]
  77. public async Task<ActionResult<Employee>> PostEmployee(Employee employee)
  78. {
  79. employee.Id = Guid.NewGuid().ToString();
  80. _context.Employee.Add(employee);
  81. Notification notification = new Notification()
  82. {
  83. EmployeeName = employee.Name,
  84. TranType = "Add"
  85. };
  86. _context.Notification.Add(notification);
  87. try
  88. {
  89. await _context.SaveChangesAsync();
  90. await _hubContext.Clients.All.BroadcastMessage();
  91. }
  92. catch (DbUpdateException)
  93. {
  94. if (EmployeeExists(employee.Id))
  95. {
  96. return Conflict();
  97. }
  98. else
  99. {
  100. throw;
  101. }
  102. }
  103. return CreatedAtAction("GetEmployee", new { id = employee.Id }, employee);
  104. }
  105. // DELETE: api/Employees/5
  106. [HttpDelete("{id}")]
  107. public async Task<IActionResult> DeleteEmployee(string id)
  108. {
  109. var employee = await _context.Employee.FindAsync(id);
  110. if (employee == null)
  111. {
  112. return NotFound();
  113. }
  114. Notification notification = new Notification()
  115. {
  116. EmployeeName = employee.Name,
  117. TranType = "Delete"
  118. };
  119. _context.Employee.Remove(employee);
  120. _context.Notification.Add(notification);
  121. await _context.SaveChangesAsync();
  122. await _hubContext.Clients.All.BroadcastMessage();
  123. return NoContent();
  124. }
  125. private bool EmployeeExists(string id)
  126. {
  127. return _context.Employee.Any(e => e.Id == id);
  128. }
  129. }
  130. }
We will be adding a new record to Notification table after adding, editing or deleting a record in the Employee table inside the respective web methods. Also notice that, we have broadcasted the message to all connected clients inside these web methods.
We must create below models for notification count and notification result.
NotificationCountResult.cs
  1. namespace NET5SignalR.Models
  2. {
  3. public class NotificationCountResult
  4. {
  5. public int Count { get; set; }
  6. }
  7. }
NotificationResult.cs
  1. namespace NET5SignalR.Models
  2. {
  3. public class NotificationResult
  4. {
  5. public string EmployeeName { get; set; }
  6. public string TranType { get; set; }
  7. }
  8. }
We can create new API class “NotificationsController” to get details from Notification table. This controller will also use to delete entire records from Notification table.
NotificationsController.cs
  1. using Microsoft.AspNetCore.Mvc;
  2. using Microsoft.AspNetCore.SignalR;
  3. using Microsoft.EntityFrameworkCore;
  4. using NET5SignalR.Data;
  5. using NET5SignalR.Models;
  6. using System.Collections.Generic;
  7. using System.Linq;
  8. using System.Threading.Tasks;
  9. namespace NET5SignalR.Controllers
  10. {
  11. [Route("api/[controller]")]
  12. [ApiController]
  13. public class NotificationsController : ControllerBase
  14. {
  15. private readonly MyDbContext _context;
  16. private readonly IHubContext<BroadcastHub, IHubClient> _hubContext;
  17. public NotificationsController(MyDbContext context, IHubContext<BroadcastHub, IHubClient> hubContext)
  18. {
  19. _context = context;
  20. _hubContext = hubContext;
  21. }
  22. // GET: api/Notifications/notificationcount
  23. [Route("notificationcount")]
  24. [HttpGet]
  25. public async Task<ActionResult<NotificationCountResult>> GetNotificationCount()
  26. {
  27. var count = (from not in _context.Notification
  28. select not).CountAsync();
  29. NotificationCountResult result = new NotificationCountResult
  30. {
  31. Count = await count
  32. };
  33. return result;
  34. }
  35. // GET: api/Notifications/notificationresult
  36. [Route("notificationresult")]
  37. [HttpGet]
  38. public async Task<ActionResult<List<NotificationResult>>> GetNotificationMessage()
  39. {
  40. var results = from message in _context.Notification
  41. orderby message.Id descending
  42. select new NotificationResult
  43. {
  44. EmployeeName = message.EmployeeName,
  45. TranType = message.TranType
  46. };
  47. return await results.ToListAsync();
  48. }
  49. // DELETE: api/Notifications/deletenotifications
  50. [HttpDelete]
  51. [Route("deletenotifications")]
  52. public async Task<IActionResult> DeleteNotifications()
  53. {
  54. await _context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE Notification");
  55. await _context.SaveChangesAsync();
  56. await _hubContext.Clients.All.BroadcastMessage();
  57. return NoContent();
  58. }
  59. }
  60. }
Above controller has three web methods. “GetNotificationCount” is used to get total notification count and “GetNotificationMessage” is used to get all notification details (Employee name and Transaction type). “DeleteNotifications” is used to delete entire records from Notification table.
We have completed all API side code. If needed, you can check all the web methods using Swagger or Postman tool.

Create Angular 11 application using CLI


We can create the Angular 11 application using Angular CLI. We will create all the services and components step by step.
Create a new Angular application using below command.
ng new AngularSignalR
We can choose the option to create Routing. (Be default, it is false)
It will take some time to install all the node packages. We can install below three packages using npm command.
npm install @microsoft/signalr
npm install bootstrap
npm install font-awesome
We have now installed the SignalR client, bootstrap and font-awesome packages in our Angular 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";
Create an environment variable inside environment class for baseUrl. This will be used across the application.
environment.ts
  1. // This file can be replaced during build by using the `fileReplacements` array.
  2. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
  3. // The list of file replacements can be found in `angular.json`.
  4. export const environment = {
  5. production: false,
  6. baseUrl: 'http://localhost:62769/'
  7. };
  8. /*
  9. * For easier debugging in development mode, you can import the following file
  10. * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
  11. *
  12. * This import should be commented out in production mode because it will have a negative impact
  13. * on performance if an error is thrown.
  14. */
  15. // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
Please replace the API end point with your API end point port number.
We can create an employee class now.
ng g class employee\employee
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 now.
ng g service employee\employee
employee.service.ts
  1. import { Injectable } 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. import { environment } from 'src/environments/environment';
  7. @Injectable({
  8. providedIn: 'root'
  9. })
  10. export class EmployeeService {
  11. private employeesUrl = environment.baseUrl + 'api/employees';
  12. constructor(private http: HttpClient) { }
  13. getEmployees(): Observable<Employee[]> {
  14. return this.http.get<Employee[]>(this.employeesUrl)
  15. .pipe(
  16. catchError(this.handleError)
  17. );
  18. }
  19. getEmployee(id: string): Observable<Employee> {
  20. if (id === '') {
  21. return of(this.initializeEmployee());
  22. }
  23. const url = `${this.employeesUrl}/${id}`;
  24. return this.http.get<Employee>(url)
  25. .pipe(
  26. catchError(this.handleError)
  27. );
  28. }
  29. createEmployee(employee: Employee): Observable<Employee> {
  30. const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
  31. return this.http.post<Employee>(this.employeesUrl, employee, { headers: headers })
  32. .pipe(
  33. catchError(this.handleError)
  34. );
  35. }
  36. deleteEmployee(id: string): Observable<{}> {
  37. const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
  38. const url = `${this.employeesUrl}/${id}`;
  39. return this.http.delete<Employee>(url, { headers: headers })
  40. .pipe(
  41. catchError(this.handleError)
  42. );
  43. }
  44. updateEmployee(employee: Employee): Observable<Employee> {
  45. debugger
  46. const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
  47. const url = `${this.employeesUrl}/${employee.id}`;
  48. return this.http.put<Employee>(url, employee, { headers: headers })
  49. .pipe(
  50. map(() => employee),
  51. catchError(this.handleError)
  52. );
  53. }
  54. private handleError(err) {
  55. let errorMessage: string;
  56. if (err.error instanceof ErrorEvent) {
  57. errorMessage = `An error occurred: ${err.error.message}`;
  58. } else {
  59. errorMessage = `Backend returned code ${err.status}: ${err.body.error}`;
  60. }
  61. console.error(err);
  62. return throwError(errorMessage);
  63. }
  64. private initializeEmployee(): Employee {
  65. return {
  66. id: null,
  67. name: null,
  68. address: null,
  69. gender: null,
  70. company: null,
  71. designation: null,
  72. cityname: null
  73. };
  74. }
  75. }
We can create employee list component. This component will be used to display all the employee information. This component also uses to edit and delete employee data.
ng g component employee\EmployeeList
We can modify the class file with below code.
employee-list.component.ts
  1. import { Component, OnInit } from '@angular/core';
  2. import { Employee } from '../employee';
  3. import { EmployeeService } from '../employee.service';
  4. import * as signalR from '@microsoft/signalr';
  5. import { environment } from 'src/environments/environment';
  6. @Component({
  7. selector: 'app-employee-list',
  8. templateUrl: './employee-list.component.html',
  9. styleUrls: ['./employee-list.component.css']
  10. })
  11. export class EmployeeListComponent implements OnInit {
  12. pageTitle = 'Employee List';
  13. filteredEmployees: Employee[] = [];
  14. employees: Employee[] = [];
  15. errorMessage = '';
  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. constructor(private employeeService: EmployeeService) { }
  25. performFilter(filterBy: string): Employee[] {
  26. filterBy = filterBy.toLocaleLowerCase();
  27. return this.employees.filter((employee: Employee) =>
  28. employee.name.toLocaleLowerCase().indexOf(filterBy) !== -1);
  29. }
  30. ngOnInit(): void {
  31. this.getEmployeeData();
  32. const connection = new signalR.HubConnectionBuilder()
  33. .configureLogging(signalR.LogLevel.Information)
  34. .withUrl(environment.baseUrl + 'notify')
  35. .build();
  36. connection.start().then(function () {
  37. console.log('SignalR Connected!');
  38. }).catch(function (err) {
  39. return console.error(err.toString());
  40. });
  41. connection.on("BroadcastMessage", () => {
  42. this.getEmployeeData();
  43. });
  44. }
  45. getEmployeeData() {
  46. this.employeeService.getEmployees().subscribe(
  47. employees => {
  48. this.employees = employees;
  49. this.filteredEmployees = this.employees;
  50. },
  51. error => this.errorMessage = <any>error
  52. );
  53. }
  54. deleteEmployee(id: string, name: string): void {
  55. if (id === '') {
  56. this.onSaveComplete();
  57. } else {
  58. if (confirm(`Are you sure want to delete this Employee: ${name}?`)) {
  59. this.employeeService.deleteEmployee(id)
  60. .subscribe(
  61. () => this.onSaveComplete(),
  62. (error: any) => this.errorMessage = <any>error
  63. );
  64. }
  65. }
  66. }
  67. onSaveComplete(): void {
  68. this.employeeService.getEmployees().subscribe(
  69. employees => {
  70. this.employees = employees;
  71. this.filteredEmployees = this.employees;
  72. },
  73. error => this.errorMessage = <any>error
  74. );
  75. }
  76. }
If you look at the code, you can see that inside ngOnInit method, I have created a constant variable with signalR hub connection builder and also started the connection. This connection will be listening to the messages from SignlarR hub from backend Web API. Whenever, backend sends a message, getEmployeeData method will be triggered automatically.
We can modify the template and style files also.
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 with below command
ng g component employee\EmployeeEdit
Modify the class file with below code.
employee-edit.component.ts
  1. import { Component, OnInit, OnDestroy, ElementRef, ViewChildren } from '@angular/core';
  2. import { FormControlName, FormGroup, FormBuilder, Validators } from '@angular/forms';
  3. import { Subscription } from 'rxjs';
  4. import { ActivatedRoute, Router } from '@angular/router';
  5. import { Employee } from '../employee';
  6. import { EmployeeService } from '../employee.service';
  7. @Component({
  8. selector: 'app-employee-edit',
  9. templateUrl: './employee-edit.component.html',
  10. styleUrls: ['./employee-edit.component.css']
  11. })
  12. export class EmployeeEditComponent implements OnInit, OnDestroy {
  13. @ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];
  14. pageTitle = 'Employee Edit';
  15. errorMessage: string;
  16. employeeForm: FormGroup;
  17. tranMode: string;
  18. employee: Employee;
  19. private sub: Subscription;
  20. displayMessage: { [key: string]: string } = {};
  21. private validationMessages: { [key: string]: { [key: string]: string } };
  22. constructor(private fb: FormBuilder,
  23. private route: ActivatedRoute,
  24. private router: Router,
  25. private employeeService: EmployeeService) {
  26. this.validationMessages = {
  27. name: {
  28. required: 'Employee name is required.',
  29. minlength: 'Employee name must be at least three characters.',
  30. maxlength: 'Employee name cannot exceed 50 characters.'
  31. },
  32. cityname: {
  33. required: 'Employee city name is required.',
  34. }
  35. };
  36. }
  37. ngOnInit() {
  38. this.tranMode = "new";
  39. this.employeeForm = this.fb.group({
  40. name: ['', [Validators.required,
  41. Validators.minLength(3),
  42. Validators.maxLength(50)
  43. ]],
  44. address: '',
  45. cityname: ['', [Validators.required]],
  46. gender: '',
  47. company: '',
  48. designation: '',
  49. });
  50. this.sub = this.route.paramMap.subscribe(
  51. params => {
  52. const id = params.get('id');
  53. const cityname = params.get('cityname');
  54. if (id == '0') {
  55. const employee: Employee = { id: "0", name: "", address: "", gender: "", company: "", designation: "", cityname: "" };
  56. this.displayEmployee(employee);
  57. }
  58. else {
  59. this.getEmployee(id);
  60. }
  61. }
  62. );
  63. }
  64. ngOnDestroy(): void {
  65. this.sub.unsubscribe();
  66. }
  67. getEmployee(id: string): void {
  68. this.employeeService.getEmployee(id)
  69. .subscribe(
  70. (employee: Employee) => this.displayEmployee(employee),
  71. (error: any) => this.errorMessage = <any>error
  72. );
  73. }
  74. displayEmployee(employee: Employee): void {
  75. if (this.employeeForm) {
  76. this.employeeForm.reset();
  77. }
  78. this.employee = employee;
  79. if (this.employee.id == '0') {
  80. this.pageTitle = 'Add Employee';
  81. } else {
  82. this.pageTitle = `Edit Employee: ${this.employee.name}`;
  83. }
  84. this.employeeForm.patchValue({
  85. name: this.employee.name,
  86. address: this.employee.address,
  87. gender: this.employee.gender,
  88. company: this.employee.company,
  89. designation: this.employee.designation,
  90. cityname: this.employee.cityname
  91. });
  92. }
  93. deleteEmployee(): void {
  94. if (this.employee.id == '0') {
  95. this.onSaveComplete();
  96. } else {
  97. if (confirm(`Are you sure want to delete this Employee: ${this.employee.name}?`)) {
  98. this.employeeService.deleteEmployee(this.employee.id)
  99. .subscribe(
  100. () => this.onSaveComplete(),
  101. (error: any) => this.errorMessage = <any>error
  102. );
  103. }
  104. }
  105. }
  106. saveEmployee(): void {
  107. if (this.employeeForm.valid) {
  108. if (this.employeeForm.dirty) {
  109. const p = { ...this.employee, ...this.employeeForm.value };
  110. if (p.id === '0') {
  111. this.employeeService.createEmployee(p)
  112. .subscribe(
  113. () => this.onSaveComplete(),
  114. (error: any) => this.errorMessage = <any>error
  115. );
  116. } else {
  117. this.employeeService.updateEmployee(p)
  118. .subscribe(
  119. () => this.onSaveComplete(),
  120. (error: any) => this.errorMessage = <any>error
  121. );
  122. }
  123. } else {
  124. this.onSaveComplete();
  125. }
  126. } else {
  127. this.errorMessage = 'Please correct the validation errors.';
  128. }
  129. }
  130. onSaveComplete(): void {
  131. this.employeeForm.reset();
  132. this.router.navigate(['/employees']);
  133. }
  134. }
We can modify the template file also.
employee-edit.component.html
  1. <div class="card">
  2. <div class="card-header">
  3. {{pageTitle}}
  4. </div>
  5. <div class="card-body">
  6. <form novalidate
  7. (ngSubmit)="saveEmployee()"
  8. [formGroup]="employeeForm">
  9. <div class="form-group row mb-2">
  10. <label class="col-md-3 col-form-label"
  11. for="employeeNameId">Employee Name</label>
  12. <div class="col-md-7">
  13. <input class="form-control"
  14. id="employeeNameId"
  15. type="text"
  16. placeholder="Name (required)"
  17. formControlName="name"
  18. [ngClass]="{'is-invalid': displayMessage.name }" />
  19. <span class="invalid-feedback">
  20. {{displayMessage.name}}
  21. </span>
  22. </div>
  23. </div>
  24. <div class="form-group row mb-2">
  25. <label class="col-md-3 col-form-label"
  26. for="citynameId">City</label>
  27. <div class="col-md-7">
  28. <input class="form-control"
  29. id="citynameid"
  30. type="text"
  31. placeholder="Cityname (required)"
  32. formControlName="cityname"
  33. [ngClass]="{'is-invalid': displayMessage.cityname}" />
  34. <span class="invalid-feedback">
  35. {{displayMessage.cityname}}
  36. </span>
  37. </div>
  38. </div>
  39. <div class="form-group row mb-2">
  40. <label class="col-md-3 col-form-label"
  41. for="addressId">Address</label>
  42. <div class="col-md-7">
  43. <input class="form-control"
  44. id="addressId"
  45. type="text"
  46. placeholder="Address"
  47. formControlName="address" />
  48. </div>
  49. </div>
  50. <div class="form-group row mb-2">
  51. <label class="col-md-3 col-form-label"
  52. for="genderId">Gender</label>
  53. <div class="col-md-7">
  54. <select id="genderId" formControlName="gender" class="form-control">
  55. <option value="" disabled selected>Select an Option</option>
  56. <option value="Male">Male</option>
  57. <option value="Female">Female</option>
  58. </select>
  59. </div>
  60. </div>
  61. <div class="form-group row mb-2">
  62. <label class="col-md-3 col-form-label"
  63. for="companyId">Company</label>
  64. <div class="col-md-7">
  65. <input class="form-control"
  66. id="companyId"
  67. type="text"
  68. placeholder="Company"
  69. formControlName="company" />
  70. </div>
  71. </div>
  72. <div class="form-group row mb-2">
  73. <label class="col-md-3 col-form-label"
  74. for="designationId">Designation</label>
  75. <div class="col-md-7">
  76. <input class="form-control"
  77. id="designationId"
  78. type="text"
  79. placeholder="Designation"
  80. formControlName="designation" />
  81. </div>
  82. </div>
  83. <div class="form-group row mb-2">
  84. <div class="offset-md-2 col-md-6">
  85. <button class="btn btn-primary mr-3"
  86. style="width:80px;"
  87. type="submit"
  88. [title]="employeeForm.valid ? 'Save your entered data' : 'Disabled until the form data is valid'"
  89. [disabled]="!employeeForm.valid">
  90. Save
  91. </button>
  92. <button class="btn btn-outline-secondary mr-3"
  93. style="width:80px;"
  94. type="button"
  95. title="Cancel your edits"
  96. [routerLink]="['/employees']">
  97. Cancel
  98. </button>
  99. <button class="btn btn-outline-warning" *ngIf="pageTitle != 'Add Employee'"
  100. style="width:80px"
  101. type="button"
  102. title="Delete this product"
  103. (click)="deleteEmployee()">
  104. Delete
  105. </button>
  106. </div>
  107. </div>
  108. </form>
  109. </div>
  110. <div class="alert alert-danger"
  111. *ngIf="errorMessage">{{errorMessage}}
  112. </div>
  113. </div>
We need one more component to display the employee details in a separate window. We can create now.
ng g component employee\EmployeeDetail
We can modify the class file with below code.
employee-detail.component.ts
  1. import { Component, OnInit } from '@angular/core';
  2. import { ActivatedRoute, Router } from '@angular/router';
  3. import { Employee } from '../employee';
  4. import { EmployeeService } from '../employee.service';
  5. @Component({
  6. selector: 'app-employee-detail',
  7. templateUrl: './employee-detail.component.html',
  8. styleUrls: ['./employee-detail.component.css']
  9. })
  10. export class EmployeeDetailComponent implements OnInit {
  11. pageTitle = 'Employee Detail';
  12. errorMessage = '';
  13. employee: Employee | undefined;
  14. constructor(private route: ActivatedRoute,
  15. private router: Router,
  16. private employeeService: EmployeeService) { }
  17. ngOnInit() {
  18. const id = this.route.snapshot.paramMap.get('id');
  19. if (id) {
  20. this.getEmployee(id);
  21. }
  22. }
  23. getEmployee(id: string) {
  24. this.employeeService.getEmployee(id).subscribe(
  25. employee => this.employee = employee,
  26. error => this.errorMessage = <any>error);
  27. }
  28. onBack(): void {
  29. this.router.navigate(['/employees']);
  30. }
  31. }
Modify the template file with below code.
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,'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 need a modal popup window to display the notification messages. As I mentioned earlier, application will create new record into notification table for each transaction like Add/Edit/Delete.
We can create modal service first.
ng g service modal\modal
Modify the service class with below code.
modal.service.ts
  1. import { Injectable } from '@angular/core';
  2. @Injectable({
  3. providedIn: 'root'
  4. })
  5. export class ModalService {
  6. constructor() { }
  7. private modals: any[] = [];
  8. add(modal: any) {
  9. this.modals.push(modal);
  10. }
  11. remove(id: string) {
  12. this.modals = this.modals.filter(x => x.id !== id);
  13. }
  14. open(id: string) {
  15. const modal = this.modals.find(x => x.id === id);
  16. modal.open();
  17. }
  18. close(id: string) {
  19. const modal = this.modals.find(x => x.id === id);
  20. modal.close();
  21. }
  22. }
Now, we can create the component using below command.
ng g component modal
Modify the class file with below code.
modal.component.ts
  1. import { Component, ElementRef, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
  2. import { ModalService } from './modal.service';
  3. @Component({
  4. selector: 'app-modal',
  5. templateUrl: './modal.component.html',
  6. styleUrls: ['./modal.component.less'],
  7. encapsulation: ViewEncapsulation.None
  8. })
  9. export class ModalComponent implements OnInit, OnDestroy {
  10. @Input() id: string;
  11. private element: any;
  12. constructor(private modalService: ModalService, private el: ElementRef) {
  13. this.element = el.nativeElement;
  14. }
  15. ngOnInit() {
  16. if (!this.id) {
  17. console.error('modal must have an id');
  18. return;
  19. }
  20. document.body.appendChild(this.element);
  21. this.element.addEventListener('click', el => {
  22. if (el.target.className === 'app-modal') {
  23. this.close();
  24. }
  25. });
  26. this.modalService.add(this);
  27. }
  28. ngOnDestroy(): void {
  29. this.modalService.remove(this.id);
  30. this.element.remove();
  31. }
  32. open(): void {
  33. this.element.style.display = 'block';
  34. document.body.classList.add('app-modal-open');
  35. }
  36. close(): void {
  37. this.element.style.display = 'none';
  38. document.body.classList.remove('app-modal-open');
  39. }
  40. }
Please note that, we are using the “less” stylesheet instead of default “css” for this component.
modal.component.less
  1. app-modal {
  2. display: none;
  3. .app-modal {
  4. position: fixed;
  5. top: 1%;
  6. right: 0;
  7. bottom: 0;
  8. left: 25%;
  9. z-index: 1000;
  10. overflow: auto;
  11. .app-modal-body {
  12. padding: 10px;
  13. background: #fff;
  14. overflow-y: auto;
  15. margin-top: 40px;
  16. width: 700px;
  17. height: 450px;
  18. }
  19. }
  20. .app-modal-background {
  21. position: fixed;
  22. top: 0;
  23. right: 0;
  24. bottom: 0;
  25. left: 0;
  26. background-color: #000;
  27. opacity: 0.75;
  28. z-index: 900;
  29. }
  30. }
  31. body.app-modal-open {
  32. overflow: hidden;
  33. }
“less” stylesheet files allow us the flexibility to add some logical coding inside the stylesheet.
We can modify the html template with below code.
modal.component.html
  1. <div class="app-modal">
  2. <div class="app-modal-body">
  3. <ng-content></ng-content>
  4. </div>
  5. </div>
  6. <div class="app-modal-background"></div>
We can create a notification class file and add two models “NotificationCountResult” and “NotificationResult” inside it.
ng g class notification\notification
notification.ts
  1. export class NotificationCountResult {
  2. count: number;
  3. }
  4. export class NotificationResult {
  5. employeeName: string;
  6. tranType: string;
  7. }
We can create the notification service now.
ng g service notification\notification
Replace service with below code.
notification.service.ts
  1. import { HttpClient, HttpHeaders } from '@angular/common/http';
  2. import { Injectable } from '@angular/core';
  3. import { Observable, throwError } from 'rxjs';
  4. import { catchError } from 'rxjs/operators';
  5. import { environment } from 'src/environments/environment';
  6. import { NotificationCountResult, NotificationResult } from './notification';
  7. @Injectable({
  8. providedIn: 'root'
  9. })
  10. export class NotificationService {
  11. private notificationsUrl = environment.baseUrl +'api/notifications';
  12. constructor(private http: HttpClient) { }
  13. getNotificationCount(): Observable<NotificationCountResult> {
  14. const url = `${this.notificationsUrl}/notificationcount`;
  15. return this.http.get<NotificationCountResult>(url)
  16. .pipe(
  17. catchError(this.handleError)
  18. );
  19. }
  20. getNotificationMessage(): Observable<Array<NotificationResult>> {
  21. const url = `${this.notificationsUrl}/notificationresult`;
  22. return this.http.get<Array<NotificationResult>>(url)
  23. .pipe(
  24. catchError(this.handleError)
  25. );
  26. }
  27. deleteNotifications(): Observable<{}> {
  28. const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
  29. const url = `${this.notificationsUrl}/deletenotifications`;
  30. return this.http.delete(url, { headers: headers })
  31. .pipe(
  32. catchError(this.handleError)
  33. );
  34. }
  35. private handleError(err) {
  36. let errorMessage: string;
  37. if (err.error instanceof ErrorEvent) {
  38. errorMessage = `An error occurred: ${err.error.message}`;
  39. } else {
  40. errorMessage = `Backend returned code ${err.status}: ${err.body.error}`;
  41. }
  42. console.error(err);
  43. return throwError(errorMessage);
  44. }
  45. }
Create the navigation menu component now.
ng g component NavMenu
Modify the class file with below code.
nav-menu.component.ts
  1. import { Component, OnInit } from '@angular/core';
  2. import { ModalService } from '../modal/modal.service';
  3. import * as signalR from '@microsoft/signalr';
  4. import { NotificationCountResult, NotificationResult } from '../Notification/notification';
  5. import { NotificationService } from '../Notification/notification.service';
  6. import { environment } from 'src/environments/environment';
  7. @Component({
  8. selector: 'app-nav-menu',
  9. templateUrl: './nav-menu.component.html',
  10. styleUrls: ['./nav-menu.component.css']
  11. })
  12. export class NavMenuComponent implements OnInit {
  13. notification: NotificationCountResult;
  14. messages: Array<NotificationResult>;
  15. errorMessage = '';
  16. constructor(private notificationService: NotificationService, private modalService: ModalService) { }
  17. isExpanded = false;
  18. ngOnInit() {
  19. this.getNotificationCount();
  20. const connection = new signalR.HubConnectionBuilder()
  21. .configureLogging(signalR.LogLevel.Information)
  22. .withUrl(environment.baseUrl + 'notify')
  23. .build();
  24. connection.start().then(function () {
  25. console.log('SignalR Connected!');
  26. }).catch(function (err) {
  27. return console.error(err.toString());
  28. });
  29. connection.on("BroadcastMessage", () => {
  30. this.getNotificationCount();
  31. });
  32. }
  33. collapse() {
  34. this.isExpanded = false;
  35. }
  36. toggle() {
  37. this.isExpanded = !this.isExpanded;
  38. }
  39. getNotificationCount() {
  40. this.notificationService.getNotificationCount().subscribe(
  41. notification => {
  42. this.notification = notification;
  43. },
  44. error => this.errorMessage = <any>error
  45. );
  46. }
  47. getNotificationMessage() {
  48. this.notificationService.getNotificationMessage().subscribe(
  49. messages => {
  50. this.messages = messages;
  51. },
  52. error => this.errorMessage = <any>error
  53. );
  54. }
  55. deleteNotifications(): void {
  56. if (confirm(`Are you sure want to delete all notifications?`)) {
  57. this.notificationService.deleteNotifications()
  58. .subscribe(
  59. () => {
  60. this.closeModal();
  61. },
  62. (error: any) => this.errorMessage = <any>error
  63. );
  64. }
  65. }
  66. openModal() {
  67. this.getNotificationMessage();
  68. this.modalService.open('custom-modal');
  69. }
  70. closeModal() {
  71. this.modalService.close('custom-modal');
  72. }
  73. }
Like the employee list component, we will add the singalR connection here as well.
Whenever, user adds a new employee or edit or delete data, notification will be shown in the connected client browsers immediately.
We can modify the template file with below code.
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"
  6. aria-label="Toggle navigation" [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. <i class="fa fa-bell has-badge" style="cursor: pointer;" (click)="openModal()"></i>
  18. <div class="numberCircle" *ngIf="notification && notification?.count>0" style="cursor: pointer;"
  19. (click)="openModal()">
  20. {{notification?.count}}</div>
  21. </ul>
  22. </div>
  23. </div>
  24. </nav>
  25. </header>
  26. <footer>
  27. <nav class="navbar navbar-light bg-white mt-5 fixed-bottom">
  28. <div class="navbar-expand m-auto navbar-text">
  29. Developed with <i class="fa fa-heart"></i> by <b>Sarathlal
  30. Saseendran</b>
  31. </div>
  32. </nav>
  33. </footer>
  34. <app-modal id="custom-modal">
  35. <button class="btn btn-primary" (click)="deleteNotifications();" style="margin-right: 10px;" [disabled]="notification?.count==0">Delete all Notifications</button>
  36. <button class="btn btn-secondary" (click)="closeModal();">Close</button>
  37. <div style="margin-bottom: 10px;"></div>
  38. <div *ngFor="let msg of messages" [ngSwitch]="msg.tranType">
  39. <h6 *ngSwitchCase="'Add'"><span class="badge badge-success">New employee '{{msg.employeeName}}' added</span></h6>
  40. <h6 *ngSwitchCase="'Edit'"><span class="badge badge-info">Employee '{{msg.employeeName}}' edited</span></h6>
  41. <h6 *ngSwitchCase="'Delete'"><span class="badge badge-warning">Employee '{{msg.employeeName}}' deleted</span></h6>
  42. </div>
  43. </app-modal>
We can also modify the stylesheet file with below code.
nav-menu.component.css
  1. a.navbar-brand {
  2. white-space: normal;
  3. text-align: center;
  4. word-break: break-all;
  5. }
  6. html {
  7. font-size: 14px;
  8. }
  9. @media (min-width: 768px) {
  10. html {
  11. font-size: 16px;
  12. }
  13. }
  14. .box-shadow {
  15. box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
  16. }
  17. .fa-heart {
  18. color: hotpink;
  19. }
  20. .fa-bell {
  21. padding-top: 10px;
  22. color: red;
  23. }
  24. .numberCircle {
  25. border-radius: 50%;
  26. width: 21px;
  27. height: 21px;
  28. padding: 4px;
  29. background: #fff;
  30. border: 1px solid darkgrey;
  31. color:red;
  32. text-align: center;
  33. margin-left: -7px;
  34. font: 10px Arial, sans-serif;
  35. }
Create the final home component now.
ng g component home
There is no code change for the class file. We can modify the html template file with below code.
home.component.html
  1. <div style="text-align:center;">
  2. <h3>Real-time Angular 11 Application with SignalR and .NET 5</h3>
  3. <p>Welcome to our new single-page Employee application, built with below technologies:</p>
  4. <img src="../../assets/AngularSignalR.png" width="700px">
  5. </div>
We must add reference for below modules in app.module class.
app.module.ts
  1. import { BrowserModule } from '@angular/platform-browser';
  2. import { NgModule } from '@angular/core';
  3. import { AppRoutingModule } from './app-routing.module';
  4. import { FormsModule, ReactiveFormsModule } from '@angular/forms';
  5. import { HttpClientModule } from '@angular/common/http';
  6. import { AppComponent } from './app.component';
  7. import { EmployeeListComponent } from './employee/employee-list/employee-list.component';
  8. import { EmployeeEditComponent } from './employee/employee-edit/employee-edit.component';
  9. import { EmployeeDetailComponent } from './employee/employee-detail/employee-detail.component';
  10. import { ModalComponent } from './modal/modal.component';
  11. import { NavMenuComponent } from './nav-menu/nav-menu.component';
  12. import { HomeComponent } from './home/home.component';
  13. @NgModule({
  14. declarations: [
  15. AppComponent,
  16. EmployeeListComponent,
  17. EmployeeEditComponent,
  18. EmployeeDetailComponent,
  19. ModalComponent,
  20. NavMenuComponent,
  21. HomeComponent
  22. ],
  23. imports: [
  24. BrowserModule,
  25. AppRoutingModule,
  26. ReactiveFormsModule,
  27. FormsModule,
  28. HttpClientModule,
  29. ],
  30. providers: [],
  31. bootstrap: [AppComponent]
  32. })
  33. export class AppModule { }
We must add below route values in the app-routing.module class as well.
app-routing.module.ts
  1. import { NgModule } from '@angular/core';
  2. import { Routes, RouterModule } from '@angular/router';
  3. import { EmployeeDetailComponent } from './employee/employee-detail/employee-detail.component';
  4. import { EmployeeEditComponent } from './employee/employee-edit/employee-edit.component';
  5. import { EmployeeListComponent } from './employee/employee-list/employee-list.component';
  6. import { HomeComponent } from './home/home.component';
  7. const routes: Routes = [
  8. { path: '', component: HomeComponent, pathMatch: 'full' },
  9. {
  10. path: 'employees',
  11. component: EmployeeListComponent
  12. },
  13. {
  14. path: 'employees/:id',
  15. component: EmployeeDetailComponent
  16. },
  17. {
  18. path: 'employees/:id/edit',
  19. component: EmployeeEditComponent
  20. },
  21. ]
  22. @NgModule({
  23. imports: [RouterModule.forRoot(routes)],
  24. exports: [RouterModule]
  25. })
  26. export class AppRoutingModule { }
We can modify the app.component.html with below code.
app.component.html
  1. <body>
  2. <app-nav-menu></app-nav-menu>
  3. <div class="container">
  4. <router-outlet></router-outlet>
  5. </div>
  6. </body>
We have completed entire coding part. We are ready to run our application. We can run both API and Angular app together.
Currently, we have no employee data in database. There is no notification also. We can create new employee record. At the same, we can open the application in another Edge browser as well.
After clicking the Save button, you can notice that one notification is displayed in the bell icon on the menu bar. This notification is not only displayed in this browser, it is also displayed instantaneously in the other Edge browser also.
Now, we can edit the employee record from Edge browser.
If you check the other Chrome browser, you can see that the employee data is updated instantly along with notification on bell icon.
You can click the bell icon, and see the entire notifications inside a popup window.

Conclusion


In this post, we have seen how to create a real-time application with Angular 11, SignalR and .NET 5. We have created an employee app which allow us to add, edit and delete employee data. We have seen how the data is displayed in other connected client browsers instantly. You can download entire source and check it from your end. I have used entity framework for database connectivity. I have already added the migration script along with source code. You can simply create the database using migration command. Please feel free to give your valuable comments after checking the application.