Monday, July 13, 2020

Developing an Application using ASP.NET Core 3.0, EF Core 3.0, Azure Cosmos DB and Angular.js

Abstract: ASP.NET Core 3.0 with EF Core 3.0 provides a cool mechanism to access Cosmos DB for performing CRUD operations, very similar to a relational database. The built-in Angular template with ASP.NET Core provided a rich experience for front-end development. This tutorial builds an end-to-end app to utilize these conce

For developing modern web applications, a robust application framework, as well as a solid runtime is needed both on the server-side and at the front-end.

ASP.NET Core 3.0 is one such open-source framework which integrates seamlessly with client-side frameworks and libraries, including Blazor, React, Angular, Vue.js etc.

Editorial Note: If you are new to .NET Core 3.0, read What’s New in .NET Core 3.0?

.NET Core 3.0 introduces various new features, some of them being:

  • Single-File Executable
  • Assembly linking
  • Tiered compilation
  • Desktop Applications like WPF and WinForms
  • …and many more

All these new features are useful for modern application development.

Web Applications often have complex requirements now-a-days. Some of these requirements demand that the application must be cross-platform, application data must be stored in relational as well as NoSQL database, the front-end must be modular and highly responsive, and so on.

.NET Core was created to be cross-platform and releases from .NET Core v2.0 onwards, help to design solutions to fulfill most of these requirements.

In ASP.NET Core 2.0 onwards, application templates provide an integration with front-end frameworks like Angular, React, React-Redux. We can make use of these templates to develop applications as per the requirements from users.

Editorial Note: In case you are interested in a Vue.js template, check this tutorial: ASP.NET Core Vue CLI Templates.

Figure 1 shows a projected application implementation we will be building in this tutorial.

aspnetcore-app-efcore-cosmosdb

Figure 1: ASP.NET Core application with EF Core, SQL Server, CosmosDB and Angular

As seen in Figure 1, in .NET Core 2.2, EF core v2.2 was used only as an ORM for relational databases like SQL Server, etc. So, it was necessary for developers to write a separate data access layer for accessing data from Azure Cosmos DB, generally classified as a NoSQL database. This means that our .NET Core 2.2 application would need separate Data Access Layers for relational databases, as well as for a NoSQL one.

Editorial Note: Those new to Cosmos DB can read Azure Cosmos DB – Deep Dive.

In .NET Core 3.0, there is a cool feature provided in EF Core 3.0 which can be used to map the entity classes to a Cosmos DB NoSQL database and generate the database in the traditional code-first approach.

We can make use of DbContext class to map with the Cosmos DB database collection.

.NET Core 3.0 provides the Microsoft.EntityFrameworkCore.Cosmos package which provides the Microsoft.EntityFrameworkCore.Cosmos assembly. This assembly contains the CosmosDbContextOptionsExtensions class with the UseCosmos overloaded method. This is an extension method for the DbContextOptionsBuilder class and this class is used to configure the Cosmos DB database for the application.

The UseCosmos method accepts the following parameters:

  • The Cosmos DB database account endpoint – application can connect to Cosmos DB using this Endpoint
  • The Account key – used for client application authentication
  • Database Name – to which the application is connecting

Using EF Core 3.0, one can directly access the Azure Cosmos DB database and perform CRUD operations. You can use the Code-First approach of EF Core to create a database and collection. Using the ASP.NET Core 3.0 Angular Template and EF Core 3.0 with Cosmos DB, we can modify Figure 1 to the one shown in Figure 2:

efcore-orm-cosmosdb

Figure 2: Using EF Core 3.0 as ORM for Cosmos DB

Developing an application Using ASP.NET Core 3.0, EF Core 3.0 and Azure Cosmos DB

Let’s first create a Cosmos DB database account so that we can have an Endpoint and Key to access Cosmos DB in our application.

Step 1: Open Azure portal using portal.azure.com. Make sure that you have an Azure subscription. Once you login with your credentials, you are inside the portal.

Step 2: In the portal, click on the Create a resource link blade on the top left (see Figure 3). In the search box on this blade, enter Azure Cosmos DB, and the UI will display the Azure Cosmos DB option as shown in Figure 3.

cosmosdb-creation-portal

Figure 3: The Cosmos DB resource option in the portal

Click on the Azure Cosmos DB link that is marked red in the above figure. This will open a new blade for creating an Azure Cosmos DB Account as shown in Figure 4.

 

cosmosdb-creation

Figure 4: Create an Azure Cosmos DB Account

To create an Azure Cosmos DB Account, you need to enter the Azure Subscription and Resource Group (if you have not already created a resource group, it can be created using Create new link provided below the Resource Group combobox).

You can then enter an Account Name as per your choice and then select the Cosmos DB API. In our case, we will be using Core (SQL) which is a JSON document storage. You need to select a Location for the Account and other information as per your requirement. To create the account, click on the Review + create button. Once the Cosmos DB Account is created, we can see its details as shown in Figure 5.

 

cosmosdb-account-details

Figure 5: The Azure Cosmos DB Account

Figure 5 shows the Cosmos DB Account details. We can use Data Explorer (marked red) to view all the databases and their containers. The Keys (marked red) in Figure 5 are authentication keys so that the client application can connect with Cosmos DB and perform operations like create database, create container, etc.

Are you a .NET, C#, Cloud or Web Developer looking for a resource covering New Technologies, in-depth Tutorials and Best Practices?

Well, you are in luck! We at DotNetCurry release a FREE digital magazine once every few months aimed at Developers, Architects and Technical Managers. This magazine covers ASP.NET Core, Xamarin, C#, Patterns and Practices, .NET Core, ASP.NET MVC, Azure, DevOps, ALM, TypeScript, Angular, React, Vuejs and much more.

Subscribe to this magazine for FREE and receive the current and upcoming editions, right in your Inbox. No Spam Policy.

Click here to Download the Latest Edition For Free

Creating an ASP.NET Core 3.0 application with Angular Template

As we have created a Cosmos DB Account, it’s time for us to create an ASP.NET Core 3.0 application with an Angular template. This template was introduced in ASP.NET Core 2.2.

We will create Web APIs using ASP.NET Core 3.0. These Web APIs will access Cosmos DB. The Angular application will be the front-end for our application. We will create an Angular application that will capture the profile information of Students and this profile information will be stored in Cosmos DB as JSON documents. The overall structure of the application is explained in Figure 6.

 

real-aspnetcore-cosmosdb-application

Figure 6: The actual application

Step 1: Open Visual Studio 2019 and create a new ASP.NET Core Web Application. Name this application as ProfileAppNet30. Select the Angular Template for the application. Make sure that you select ASP.NET Core 3.0 as the project version as shown in Figure 7.

 

aspnetcore30-app

Figure 7: The ASP.NET Core 3.0 app with Angular template

Note: Please disable the option “Configure for HTTPS” if you are using Kestrel to avoid CORS errors. Otherwise you will have to change the protocol to https and port to 5001 in the Angular application.

Open the Solution Explorer to see the project structure with references of assemblies targeted to .NET Core 3.0 as shown in Figure 8.

 

aspnetcore30-project-structure

Figure 8: The ASP.NET Core 3.0 Project structure

The ClientApp folder shows the Angular application structure. If you look in the package.json file, you will see that the Angular version supported for this template is Angular 8.0.0.

Step 2: Since we need to access Cosmos DB using EF Core, we need to add EF Core package to the project. (Note that the EF Core package is not present by default in the ASP.NET Core 3.0 Project Template.)

Right click on Dependencies and select Manage NuGet Packages. Search for Microsoft.EntityFrameworkCore.Cosmos package. Once the package is found, install it as shown in Figure 9.

 

adding-nuget-package-efcore-cosmos

Figure 9: Installing Microsoft.EntityFrameworkCore.Cosmos package

Step 3: Modify the appsettings.json file by adding key/value pairs for Cosmos DB settings like EndPoint, AccountKey and DatabaseName. The EndPoint, AccountKey and DatabaseName can be found from the Settings > ConnectionString blade.

"CosmosDbSettings": {
    "EndPoint": "https://COSMOSDB-ACCOUNT-NAME-HERE.documents.azure.com:443/",
    "AccountKey": "YOUR-KEY-HERE",
    "DatabaseName": "ProfilesDatabase"
  }

Listing 1: appsettings.json for Cosmos DB Settings

Step 4: In the project, add a folder named Models and in this folder, add a new class file and name it as ModelClasses.cs. In this class file, add the following code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
 
namespace ProfileAppNet30.Models
{
    public class Education
    {
        [Required(ErrorMessage = "Degree is required")]
        public string Degree { get; set; }
        [Required(ErrorMessage = "Specialization is required")]
        public string Specialization { get; set; }
        [Required(ErrorMessage = "College Or School is required")]
        public string CollegeOrSchool { get; set; }
        [Required(ErrorMessage = "Year Of Admission is required")]
        public int YearOfAdmission { get; set; }
        [Required(ErrorMessage = "Year Of Passing is required")]
        public int YearOfPassing { get; set; }
        [Required(ErrorMessage = "Grade is required")]
        public string Grade { get; set; }
         
    }
    public class WorkExperience
    {
        public string CompanyName { get; set; }
        public string Designation { get; set; }
        public DateTime DateOfJoin { get; set; }
        public DateTime DateOfLeaving { get; set; }
        public int YearsOfExperience { get; set; }
    }
 
    public class ProfileMaster
    {
        public Guid Id { get; set; }
        [Required(ErrorMessage ="FirstName is required")]
        public string FirstName { get; set; }
        public string MiddleName { get; set; }
        [Required(ErrorMessage = "LastName is required")]
        public string LastName { get; set; }
        [Required(ErrorMessage = "Gender is required")]
        public char Gender { get; set; }
        [Required(ErrorMessage = "ContactNumber is required")]
        public int ContactNumber { get; set; }
        [Required(ErrorMessage = "MaritalStatus is required")]
        public string MaritalStatus { get; set; }
        [Required(ErrorMessage = "DateOfBirth is required")]
        public DateTime DateOfBirth { get; set; }
        public List Educations { get; set; }
        public List Experience { get; set; }
    }
}

Listing 2: The Model classes. These classes will be used to map with Cosmos DB to create JSON documents

The Education class contains properties for storing Education details of the end user. The WorkExperience class contains properties to store experience of the end user. The ProfileMaster class contains properties for storing personal information of the end user. This class also contains a list of Education details and WorkExperiences of the end user. This is done for a One-To-Many Relationship between ProfileMaster to Education and WorkExperience classes.

We expect that the collection contains JSON document with collection of Education details and WorkExperiences for a single Profile information.

Step 5: In the Models folder, add a new class file and name it as ProfileDbContext.cs. Add the following code in this file:

using Microsoft.EntityFrameworkCore;
 
namespace ProfileAppNet30.Models
{
    public class ProfileDbContext : DbContext
    {
        public DbSet Profiles { get; set; }
 
        public ProfileDbContext(DbContextOptions options) : base(options)
        {
 
        }
         
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // the container name
            modelBuilder.HasDefaultContainer("Profiles");
            // ProfileMaster has many educations and Many Experiences
            modelBuilder.Entity().OwnsMany(e => e.Educations);
            modelBuilder.Entity().OwnsMany(e=>e.Experience);
        }
    }
}

Listing 3: The ProfileDbContext class contains code for EF Core mapping with Cosmos DB.

If you have used EF Core earlier, then you will find the code familiar. If not, here’s an old albeit useful tutorial.

The ProfileDbContext class is derived from DbContext class. This class is responsible for connection creation and mapping with the database. The class contains a DbSet property for ProfileMaster model class. This will map with the container in Cosmos DB.

The OnModelCreating() method defines the container name as Profiles and defines the strategy of the document creation with relationship between ProfileMaster, Education and WorkExperience class. It contains code for defining a One-To-Many relationship between ProfileMaster, Education and WorkExperience class.

Step 6: In the project, add a new folder and name it as Services. In this folder, add an interface file and name it as ICosmosDbService.cs. Then add a class file, and name this class file as CosmosDbService.cs. Add the following code in ICosmosDbService.cs

using ProfileAppNet30.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
 
namespace ProfileAppNet30.Services
{
    public interface ICosmosDbService where TEntity: class
    {
        Task<IEnumerable> GetAsync();
        Task GetAsync(TPk id);
        Task CreateAsync(TEntity entity);
    }
}

Listing 4: The repository interface.

Add the following code in CosmosDbService.cs file

using Microsoft.EntityFrameworkCore;
using ProfileAppNet30.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
 
namespace ProfileAppNet30.Services
{
    public class CosmosDbService : ICosmosDbService
    {
        private readonly ProfileDbContext ctx;
 
        public CosmosDbService(ProfileDbContext ctx)
        {
            this.ctx = ctx;
            // this will make sure that the database is created
            ctx.Database.EnsureCreated();
        }
 
        public async Task CreateAsync(ProfileMaster entity)
        {
             
           entity.Id = Guid.NewGuid();
           var response = await ctx.Profiles.AddAsync(entity);
           await ctx.SaveChangesAsync();
           return response.Entity;
                               
        }
 
        public async Task<IEnumerable> GetAsync()
        {
              
         var profiles = await ctx.Profiles.ToListAsync();
         return profiles;
             
        }
 
        public async Task GetAsync(string id)
        {
              
         var profile = await ctx.Profiles.FindAsync(id);
         return profile;
              
        }
    }
}

Listing 5: The repository class.

The ICosmosDbService interface is a multi-type generic interface. This interface defines methods for reading and writing data. This interface is implemented by the CosmosDbService class with TEntity parameter as ProfileMaster and TPk parameter as string. The class has a constructor injected with ProfileDbContext class. The constructor contains code to make sure that the database is created in Cosmos DB, if it has not already been created. The other methods of the class contain a familiar code for performing read and write operations against the database using EF Core.

Step 7: Modify Startup.cs file by adding the following code in the ConfigureServices() method of the Startup class

// add this line to make sure that controllers can
            // suppress the naming convention policy
 services.AddControllers().AddJsonOptions(options => {
     options.JsonSerializerOptions.PropertyNamingPolicy = null;
 });
            // register the ProfileDbContext class in DI Container
 services.AddDbContext(options =>
 {
                options.UseCosmos(Configuration["CosmosDbSettings:EndPoint"].ToString(),
                  Configuration["CosmosDbSettings:AccountKey"].ToString(),
                   Configuration["CosmosDbSettings:DatabaseName"].ToString());
            });
              
services.AddScoped<ICosmosDbService,CosmosDbService>();

Listing 6: Registering ProfileDbContext class in DI Container along with CosmosDbService class and code for suppressing the default JSON serialization naming policy

Step 8: In the Controllers folder, add a new empty Web API controller and name it as ProfilesController.cs. In this controller, add code as shown in Listing 7:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ProfileAppNet30.Models;
using ProfileAppNet30.Services;
namespace ProfileAppNet30.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProfilesController : ControllerBase
    {
        private readonly ICosmosDbService service;
        public ProfilesController(ICosmosDbService service)
        {
            this.service = service;
        }
 
        [HttpGet]
        public async Task Get()
        {
            try
            {
                var response = await service.GetAsync();
                return Ok(response);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
 
 
        [HttpGet("{id}")]
        public async Task Get(string id)
        {
            try
            {
                var response = await service.GetAsync(id);
                return Ok(response);
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
 
        [HttpPost]
        public async Task Post(ProfileMaster profile)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    var response = await service.CreateAsync(profile);
                    return Ok(response);
                }
                else
                {
                    return BadRequest(ModelState);
                }
            }
            catch (Exception ex)
            {
                return BadRequest(ex.Message);
            }
        }
 
    }
}

Listing 7: The ProfilesController code

The controller we just saw in Listing 7 has the CosmosDbService class injected in the constructor . The controller contains HTTP Get and Post methods for returning and accepting profile information from an Angular client application.

 

Creating Angular Client Code

Step 1: Expand the ClientApp folder. In this folder, we have the src folder that contains the app sub-folder. In the app folder, add three folders named modelsprofile and services.

Since we will be using Angular material library for a rich UI like dialog-box, we need @angular/cdk and @angular/material package dependencies in the project. Open the Command Prompt and navigate to the ClientApp folder of the current application and run the following command to install these packages.

Note: Node.js must be installed on the machine to run npm command.

npm install --save-dev @angular/cdk @angular/material

Step 2: In the Models folder, add a new TypeScript file and name it as app.constants.ts. This file will contain constant arrays:

export const Degrees = [
  'B.Sc.', 'M.Sc.', 'BCS', 'MCS', 'MCA',
  'B.E.', 'M.E.', 'B.A.', 'M.A.', 'M.Com.',
  'B.Com.', 'MBA', 'MPM'
];
 
export const Specializations = [
  'Computer', 'Mechanical', 'Electrical', 'Civil',
  'Petrochemical', 'Chemical', 'Information Technology',
  'Mathematics', 'Biology', 'Physics', 'Chemistry',
  'Finance', 'Marketing', 'HRD', 'Accounts'
];
 
export const AdmissionYear = [
  1980, 1981, 1982, 1983, 1984, 1985, 1986,
  1987, 1988, 1989, 1990, 1991, 1992, 1993,
  1994, 1995, 1996, 1997, 1998, 1999, 2000,
  2001, 2002, 2003, 2004, 2005, 2006, 2007,
  2008, 2009, 2010, 2011, 2012, 2013, 2014,
  2015, 2016, 2017, 2018, 2019
];
export const PassingYear = [
  1980, 1981, 1982, 1983, 1984, 1985, 1986,
  1987, 1988, 1989, 1990, 1991, 1992, 1993,
  1994, 1995, 1996, 1997, 1998, 1999, 2000,
  2001, 2002, 2003, 2004, 2005, 2006, 2007,
  2008, 2009, 2010, 2011, 2012, 2013, 2014,
  2015, 2016, 2017, 2018, 2019
];
export const Grades = [
  'First', 'Second', 'Third'
];
 
export const Experiences = [1, 2, 4, 5, 6, 7, 8, 9,
  10, 11, 12, 13, 14, 15, 16,
  17, 18, 19, 20, 21, 22, 23,
  24, 25, 26, 27, 28, 29, 30];
 
export const Genders = [
  'M', 'F'
];
export const MaritalStatusInfo = [
  'Married', 'Unmarried'
];

Listing 8: Array constants for showing constant data in Angular View

Listing 8 contains constants for information (e.g. Degree, Specialization, etc.) that we need to capture from the end user.

Step 3: In the Models folder, add a new TypeScript file and name it as app.models.ts. This file will contain TypeScript classes for ProfileMaster, Education and WorkExperience corresponding server-side Models classes in our ASP.NET Core application.

export class Education {
    constructor(
        public Degree: string,
        public Specialization: string,
        public CollegeOrSchool: string,
        public YearOfAdmission: number,
        public YearOfPassing: number,
        public Grade: string,
    ) { }
}
 
export class WorkExperience {
    constructor(
        public CompanyName: string,
        public Designation: string,
        public DateOfJoin: Date,
        public DateOfLeaving: Date,
        public YearsOfExperience: number
    ) { }
}
 
export class ProfileMaster {
    constructor(
        public Id: string,
        public FirstName: string,
        public MiddleName: string,
        public LastName: string,
        public Gender: string,
        public ContactNumber: number,
        public MaritalStatus: string,
        public DateOfBirth: Date,
        public Educations: Array,
        public Experience: Array
    ) { }
}

Listing 9: Model classes on the client

Step 4: We will create an Angular Service to make an HTTP request to the Web API. To do so, in the Services folder, add a new TypeScript file and name it as app.profile.service.ts. In this file, add the code as shown in Listing 10.

Note: Run the application in Kestrel instead of IIS Express (the default used by VS 2019) otherwise, you will get a CORS error. If the application automatically redirects to HTTPS, change the baseUrl value in the code to https://localhost:5001.

import { Injectable} from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ProfileMaster } from '../models/app.models';
 
@Injectable({
    providedIn:'root'
})
export class ProfileService {
   private baseUrl: string
    constructor(private http: HttpClient) {
        this.baseUrl = 'http://localhost:5000';
    }
 
    getProfiles(): Observable {
        let response: Observable = null;
        response = this.http.get(`${this.baseUrl}/api/Profiles`);
        return response;
    }
 
    getProfile(id:string): Observable {
        let response: Observable = null;
        response = this.http.get(`${this.baseUrl}/api/Profiles/${id}`);
        return response;
    }
 
    postProfile(profile: ProfileMaster): Observable {
        let response: Observable = null;
        profile.Id = '00000000-0000-0000-0000-000000000000';
  
        const options = {
            headers: new HttpHeaders({ 'Content-Type':'application/json'})
        };
        response = this.http.post(`${this.baseUrl}/api/Profiles`, profile,options);
        return response;
    }
}

Listing 10: Angular Service to request Web API

Listing 10 contains the ProfileService class decorated as @Injectable. This means that the class will be injected wherever it is required. This class HttpClient is injected in the service class for making an HTTP call to Web API.

Step 5: It’s time for us to create Angular Views and their logic.

To do so, we need to add components in the application. Since we intend to use Angular Material’s dialog boxes for WorkExperience and Educational details, we need to add separate components for a dialog box implementation.

To use a Dialog box in Angular, we need to use the MatDialogRef object. To pass data to this Dialog box, make use of the MAT_DIALOG_DATA object.

This object uses interface to accept data for the dialog box. In our application, we will show dialog boxes for Educational details and Work Experience which will be used by the end user to enter multiple records for Educational details and WorkExperience details.

In the profile folder, add a new TypeScript file and name it as app.educationinfo.dialog.component.ts.

Add the following code in this file as shown in Listing 11:

import { Component, Inject } from '@angular/core';
import { Education } from '../models/app.models';
import { Degrees, Grades, Specializations, AdmissionYear, PassingYear } from '../models/app.constants';
 
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
 
 
export interface EducationDialogData {
  educationInfo: Education;
}
 
@Component({
  selector: 'app-educationinfo-dialog-component',
  templateUrl: 'app.educationinfo.dialog.view.html'
})
export class EducationInfoDialogComponent {
  degrees = Degrees;
  specializations = Specializations;
  yearOfAdmission = AdmissionYear;
  yearOfPassing = PassingYear;
  grades = Grades;
  constructor(
    public dialogRef: MatDialogRef,
    @Inject(MAT_DIALOG_DATA) public educationData: EducationDialogData
  ) { }
 
    cancel(): void {
        this.educationData.educationInfo = new Education('', '', '', 0, 0, '');
    this.dialogRef.close();
  }
}

Listing 11: The Educational Details Dialog box

In Listing 11, the EducationDialogData defines an object of the type Education. This object will be used as MAT_DIALOG_DATA for the dialog box. This dialog box also uses various constant arrays declared in app.constants.ts file. To show user interface for the dialog box, add a new HTML file in the profile folder and name it as app.educationinfo.dialog.view.html.

Add the following markup for this file:

<h2 mat-dialog-title>Educational Information</h2>
<div mat-dialog-content>
    <div class="form-group">
        <label>Degree</label>
        <select matInput [(ngModel)]="educationData.educationInfo.Degree" class="form-control">
        <option>Select Degree</option>
        <option *ngFor="let d of degrees" value="{{d}}">{{d}}</option>
      </select>
    </div>
    <div class="form-group">
        <label>Specialization</label>
        <select matInput [(ngModel)]="educationData.educationInfo.Specialization" class="form-control">
        <option>Select Specialization</option>
        <option *ngFor="let s of specializations" value="{{s}}">{{s}}</option>
      </select>
    </div>
    <div class="form-group">
        <label>College or School</label>
        <input matInput type="text" [(ngModel)]="educationData.educationInfo.CollegeOrSchool" class="form-control">
    </div>
    <div class="form-group">
        <label>Year of Admission</label>
        <select matInput [(ngModel)]="educationData.educationInfo.YearOfAdmission" class="form-control">
        <option>Select Admission Year</option>
        <option *ngFor="let ya of yearOfAdmission" value="{{ya}}">{{ya}}</option>
      </select>
    </div>
    <div class="form-group">
        <label>Year of Passing</label>
        <select matInput [(ngModel)]="educationData.educationInfo.YearOfPassing" class="form-control">
        <option>Select Passing Year</option>
        <option *ngFor="let yp of yearOfPassing" value="{{yp}}">{{yp}}</option>
      </select>
    </div>
    <div class="form-group">
        <label>Grade</label>
        <select matInput [(ngModel)]="educationData.educationInfo.Grade" class="form-control">
        <option>Select Grade</option>
        <option *ngFor="let g of grades" value="{{g}}">{{g}}</option>
      </select>
    </div>
 
</div>
<div mat-dialog-actions>
    <button mat-button [mat-dialog-close]="educationData.educationInfo" (click)="cancel()">Cancel</button>
    <button mat-button [mat-dialog-close]="educationData.educationInfo" cdkFocusInitial>Ok</button>
</div>

Listing 12: The HTML for the Education Dialog box

Editorial Note: A label can be bound to an element either by using the “for” attribute, or by placing the element inside the element. Here the author has skipped binding the label with the element as he won’t be using it anywhere, but nevertheless, it should be used to specify the type of form element a label is bound to.

Listing 12 shows markup that contains the following Material attributes:

· mat-dialog-title – represents title of the dialog box.

· mat-dialog-content – represents UI elements shown on the dialog box.

· matInput – represents the UI element which will be used to capture input from the end user.

· mat-dialog-actions – represents action elements e.g. button on the dialog box.

o The mat-dialog-actions are applied on button elements as mat-dialog-close. So, when the button is clicked, the dialog box will be closed.

Step 6: Similar to dialog boxes for Education details, we need to add a dialog box component for Work Experience also. In the profile folder, add a new TypeScript file and name it as app.workexperience.dialog.component.ts.

Add the following code in this newly added file:

import { Component, Inject } from '@angular/core';
import { WorkExperience } from '../models/app.models';
import { Experiences } from '../models/app.constants';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
 
 
export interface WorkExperienceDialogData {
    experienceInfo: WorkExperience;
}
 
 
@Component({
    selector: 'app-workexperience-dialog',
    templateUrl: 'app.workexperience.dialog.view.html'
})
export class WorkExperienceDialogComponent {
    experiences = Experiences;
    constructor(public dialogRef: MatDialogRef,
        @Inject(MAT_DIALOG_DATA) public workexperienceData: WorkExperienceDialogData) {
        this.workexperienceData.experienceInfo = new WorkExperience('', '', new Date(), new Date(), 0);
    }
   
    cancel(): void {
        this.workexperienceData.experienceInfo = new WorkExperience('', '', new Date(), new Date(), 0);
        this.dialogRef.close();
    }
 
}

Listing 13: The WorkExperience dialog component

To define user interface for the WorkExperience dialog, we need to add a new HTML file in the profile folder and name it as app.workexperience.dialog.view.html. Add the following markup in the HTML file:

<h2 mat-dialog-title>Work Experience Details</h2>
<div mat-dialog-content>
    <div class="form-group">
        <label>Company Name</label>
        <input matInput type="text" [(ngModel)]="workexperienceData.experienceInfo.CompanyName" class="form-control">
 
    </div>
    <div class="form-group">
        <label>Designation</label>
        <input matInput type="text" [(ngModel)]="workexperienceData.experienceInfo.Designation" class="form-control">
 
    </div>
    <div class="form-group">
        <label>Date of Join</label>
        <input matInput type="date" [(ngModel)]="workexperienceData.experienceInfo.DateOfJoin" class="form-control">
    </div>
    <div class="form-group">
        <label>Date of Leaving</label>
        <input matInput type="date" [(ngModel)]="workexperienceData.experienceInfo.DateOfLeaving" class="form-control">
    </div>
 
    <div class="form-group">
      <label>Years of Experience</label>
      <select matInput [(ngModel)]="workexperienceData.experienceInfo.YearsOfExperience" class="form-control">
    <option>Select Experience</option>
    <option *ngFor="let e of experiences" value="{{e}}">{{e}}</option>
  </select>
 
    </div>
 
</div>
<div mat-dialog-actions>
    <button mat-button [mat-dialog-close]="workexperienceData.experienceInfo" (click)="cancel()">Cancel</button>
    <button mat-button [mat-dialog-close]="workexperienceData.experienceInfo" cdkFocusInitial>Ok</button>
</div>

Listing 14: The HTML for WorkExperience

We have added the dialog boxes! Now it’s time for us to define components that will use these dialog boxes and also display a user interface for accepting the profile information.

Step 7: In the profile folder, add a new TypeScript file and name it as app.profile.component.ts. In this file, add the following code

import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Education, WorkExperience, ProfileMaster } from '../models/app.models';
import { EducationInfoDialogComponent } from './app.educationinfo.dialog.component';
 
import {
  Genders,
  Experiences, MaritalStatusInfo
} from '../models/app.constants';
import { WorkExperienceDialogComponent } from './app.workexperience.dialog.component';
import { ProfileService } from '../services/app.profile.service';
 
@Component({
  selector: 'app-profile-component',
  templateUrl: 'app.profile.component.view.html'
})
export class ProfileComponent implements OnInit {
  genders = Genders;
  maritalStatusInfo = MaritalStatusInfo;
 
  education: Education;
    educationDetails: Array;
  educationTableHeaders: Array;
 
  workexperience: WorkExperience;
  workexperienceDetails: Array;
  workexperienceTableHeaders: Array;
 
 
  profile: ProfileMaster;
 
 
  constructor(public dialog: MatDialog, private serv: ProfileService) {
    this.education = new Education('', '', '', 0, 0, '');
    this.educationDetails = new Array();
    this.educationTableHeaders = new Array();
    this.workexperience = new WorkExperience('', '', new Date(), new Date(), 0);
    this.workexperienceDetails = new Array();
    this.workexperienceTableHeaders = new Array();
    this.profile = new ProfileMaster('', '', '', '', '',0, '', new Date(), [], []);
  }
  openEducationDialog(): void {
    this.education = new Education('', '', '', 0, 0, '');
    const educationDialogRef = this.dialog.open(EducationInfoDialogComponent, {
      width: '600px',
      data: { educationInfo: this.education }
    });
 
    educationDialogRef.afterClosed().subscribe(res => {
      if (res !== undefined) {
        console.log(`In If ${JSON.stringify(res)}`);
          this.educationDetails.push(res);
          console.log(JSON.stringify(this.educationDetails));
        this.education = new Education('', '', '', 0, 0, '');
      } else {
        console.log('In Else');
        this.education = new Education('', '', '', 0, 0, '');
      }
    });
  }
  openWorkExperienceDialog(): void {
    this.workexperience = new WorkExperience('', '', new Date(), new Date(), 0);
    const workExperienceDialogRef = this.dialog.open(WorkExperienceDialogComponent, {
      width: '600px',
      data: { experienceInfo: this.workexperience }
    });
 
    workExperienceDialogRef.afterClosed().subscribe(res => {
      if (res !== undefined) {
    
        this.workexperienceDetails.push(res);
        console.log(JSON.stringify(this.workexperienceDetails));
        this.workexperience = new WorkExperience('', '', new Date(), new Date(), 0);
      } else {
   
          this.workexperience = new WorkExperience('', '', new Date(), new Date(), 0);
      }
    });
  }
 
  ngOnInit(): void {
    for (const h in this.education) {
      this.educationTableHeaders.push(h);
    }
 
    for (const h in this.workexperience) {
      this.workexperienceTableHeaders.push(h);
    }
  }
 
    saveProfile(): void {
        this.profile.Educations = this.educationDetails;
        this.profile.Experience = this.workexperienceDetails;
    this.serv.postProfile(this.profile).subscribe(response => {
        console.log(JSON.stringify(response));
    }, (error) => {
            console.log(`${error.status} and ${error.message} ${error.statusText}`);
    });
  }
}

Listing 15: The ProfileComponent

The ProfileComponent uses Education and WorkExperience dialog boxes. This component has ProfileService and MatDialog objects injected in the constructor. Using ProfileService, the component can make HTTP calls to the Web API.

The MatDialog object is used to manage the dialog box. The MatDialog object contains a method to open the dialog box and a method to read data entered in the dialog box after the close event of the dialog box is fired. The saveProfile() method of the component will be used to access postProfile() method of the ProfileService to post the profile information to the server.

In the profile folder, add a new HTML file and name it as app.profile.component.view.html. In this file, add the following markup:

<table class="table table-bordered table-striped">
  <tr>
    <td>
      <label>First Name</label>
      <input type="text" class="form-control" [(ngModel)]="profile.FirstName">
    </td>
    <td>
      <label>Middle Name</label>
      <input type="text" class="form-control" [(ngModel)]="profile.MiddleName">
    </td>
    <td>
      <label>Last Name</label>
      <input type="text" class="form-control" [(ngModel)]="profile.LastName">
    </td>
  </tr>
  <tr>
    <td>
      <label>Gender</label>
      <select class="form-control" [(ngModel)]="profile.Gender">
        <option>Select Gender</option>
        <option *ngFor="let g of genders" value="{{g}}">{{g}}</option>
      </select>
    </td>
    <td>
      <label>Contact Number</label>
      <input type="text" class="form-control" [(ngModel)]="profile.ContactNumber">
    </td>
    <td>
      <label>Marital Status</label>
      <select class="form-control" [(ngModel)]="profile.MaritalStatus">
        <option>Select Marital Status</option>
        <option *ngFor="let m of maritalStatusInfo" value="{{m}}">{{m}}</option>
      </select>
    </td>
  </tr>
  <tr>
    <td colspan="3">
      <label>Date of Birth</label>
      <input type="date" class="form-control" [(ngModel)]="profile.DateOfBirth">
    </td>
  </tr>
  <tr>
    <td colspan="3">
      <h3>Education Details</h3>
      <input type="button" class="btn btn-danger" (click)="openEducationDialog()" value="Click to Add Education">
      <table class="table table-bordered table-striped">
        <thead>
          <tr>
            <td *ngFor="let h of educationTableHeaders">{{h}}</td>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let e of educationDetails">
            <td *ngFor="let h of educationTableHeaders">{{e[h]}}</td>
          </tr>
        </tbody>
      </table>
    </td>
  </tr>
  <tr>
    <td colspan="3">
      <h3>Experience Details</h3>
      <input type="button" class="btn btn-warning" value="Click to Experience Details" (click)="openWorkExperienceDialog()">
      <table class="table table-bordered table-striped">
        <thead>
          <tr>
            <td *ngFor="let h of workexperienceTableHeaders">{{h}}</td>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let e of workexperienceDetails">
            <td *ngFor="let h of workexperienceTableHeaders">{{e[h]}}</td>
          </tr>
        </tbody>
      </table>
    </td>
  </tr>
  <tr>
    <td colspan="3">
      <input type="button" (click)="saveProfile()" class="btn btn-success" value="Save">
    </td>
  </tr>
</table>

Listing 16: The ProfileComponent markup

The markup in Listing 16 contains input elements and select elements for entering the Profile information. These elements are bound with the properties defined in the ProfileMaster class.

We have tables for showing Education details and WorkExperience details. We have button elements on the top of these tables. These buttons are bound with the methods from the component class to open Education and WorkExperience dialog boxes.

In the component class, we have educationTableHeaders and workexperienceTableHeaders arrays. These arrays will be populated from properties of the Education and WorkExperience classes respectively. In the HTML markup, we will be dynamically generating table headers based on these properties.

Step 8: Modify the style.css as shown in the following listing to import @angular/material style to show dialog box.

@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

Listing 17: The material style

Step 9: It’s time for us to update app.module.ts file from the app folder. In this file, we will import all components created for dialog boxes and ProfileComponent. We need to import various material modules so that dialog boxes will be executed successfully. Listing 18 shows the modified app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
 
import { AppComponent } from './app.component';
// removed the default components those are added in project when the
// project is created
import { ProfileComponent } from './profile/app.profile.component';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { EducationInfoDialogComponent } from './profile/app.eductioninfo.dialog.component';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { WorkExperienceDialogComponent } from './profile/app.workexperience.dialog.component';
@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
  // removed some component declaration code here e.g. HomeComponent, CounterComponent etc. for brevity
    ProfileComponent,
    EducationInfoDialogComponent,
    WorkExperienceDialogComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    HttpClientModule,
    MatDialogModule,
    BrowserAnimationsModule,
    MatInputModule,
    MatSelectModule,
    MatNativeDateModule,
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full' },
      { path: 'profile', component: ProfileComponent }
    ])
  ],
  providers: [],
  entryComponents: [ProfileComponent, EducationInfoDialogComponent, WorkExperienceDialogComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

Listing 18: The app.module.ts file with required imports

We have defined the routing for the profile component using RouterModule. We have also imported various Material modules like MatDialogModule, MatInputModule, MatSelectModule, MatNativeModule to execute the MatDialog.

So far, so good! We have completed developing both the Server-Side as well as the Front end.

To test the application, run it using F5.

Note: The Application needs to run on Kestrel hosting environment (not IIS Express) since we are using http with port 5000.

Figure 10 shows the application loaded in the browser:

app-loaded-in-browser

Figure 10: Application loaded in the browser

Update nav-menu.component.html to add the navigation for the profile page as shown in listing 19.

<li class="nav-item" [routerLinkActive]="['link-active']">
    <a class="nav-link text-dark" [routerLink]="['/profile']">
    Profile
    </a>
</li>

Listing 19: The navigation link for profile

Click on the Profile link on the top right, the following profile page as shown in Figure 11, will be displayed.

aspnet-core-profile-page

Figure 11: The Profile Page

Enter details like the First Name, Middle Name, Last Name etc. and click on the Click to Add Education button. This brings up the Dialog Box shown in Figure 12.

education-details-dialog-box

Figure 12: The dialog box

Click on the OK button, and the education details as shown in Figure 13 will be displayed in the table.

education-details-dialog-box-modified

Figure 13: The Education details table

Now click on the Click to Experience details button to add experience details. A dialog box will be displayed where you can enter the experience details. Once these details are added, click on the Save button to add the profile and save the information in Cosmos DB. This will create a container named as Profiles and the document is added in it.

Figure 14 shows the data added in Cosmos DB by creating Profiles

data-cosmosdb

Figure 14: The data added in Cosmos DB by creating Profiles

Conclusion: In this tutorial, we saw how ASP.NET Core 3.0 with EF Core 3.0 provides a cool mechanism to access Cosmos DB (classified as a NoSQL database ) for performing CRUD operations, very similar to a relational database.

Additionally, the built-in Angular template with ASP.NET Core provided a rich experience for front-end development. ASP.NET Core provides a unified platform for building web UI and web APIs using a Relational or a NoSQL Database.

Download the entire source code from GitHub.

No comments:

Post a Comment

Free hosting web sites and features -2024

  Interesting  summary about hosting and their offers. I still host my web site https://talash.azurewebsites.net with zero cost on Azure as ...