Introduction
A single-page application (SPA) is a web application or website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of a web browser loading entire new pages. The goal is faster transitions that make the website feel more like a native app.
In an SPA, a page refresh never occurs; instead, all necessary HTML, JavaScript, and CSS code is either retrieved by the browser with a single page load, or the proper resources are dynamically loaded and added to the page as necessary, usually in response to user actions.
Single-page applications perform well compared to traditional web applications. We can create SPAs in Angular or React very easily.
When you are creating an Angular or React application, you need to create a backend application. Usually, we use .NET, Node, Java, PHP or Python as backend applications. We must create and run separate backend applications. Visual Studio supports creating both Angular (or react) and .NET Core in a single application. The advantage is that we can host(publish) both these applications in a single domain as a single application.
Visual Studio 2022 latest version (17.1.0 as of 6th March 2022) allows us to create a single-page application with .NET 6.0 and Angular 13. (Earlier versions of Visual Studio 2022 create Angular 12 version by default)
We can see all the steps to create an SPA and we will also see how to publish this application into Azure.
Create ASP.NET Core and Angular application in Visual Studio 2022
Currently Visual Studio has two templates for Angular applications.
We can choose the first template ASP.NET Core with Angular. The second template is used to create standalone Angular applications.
We can give a valid file name and click the next button.
I have unchecked the HTTPS configuration. You can choose HTTPS if needed.
Our project will be created in a few moments.
We can see the structure of the entire SPA application.
If you open the project file (.csproj) you will see the details below.
By default, Angular 13 application is created inside a ClientApp folder. It is set inside the SpaRoot property. If you rename the ClientApp folder, you must change the property value in this project file as well. Angular running port is set inside the SpaProxyServerUrl property. If you want to change the Angular running port number, you can change inside this property. At the same time, you must change the package.json file of Angular application also.
We can see the Angular app structure.
As we discussed earlier, Angular app is created under the ClientApp folder. The project structure is like a normal Angular 13 project, but it has two modules. It also has a new file named proxy.conf.js. This is a configuration file which has the ASP.NET backend controller names. We will discuss more about this file later.
We will be deploying this application to Azure. Hence, we can create a simple working application instead of default template. We will be creating an application to analyze the C# Corner posts (articles/blogs) details of an author.
I have already written two detailed articles about this topic and published. Please read the below articles.
Easily Do Web Scraping In .NET Core 6.0
Easily Create Charts In Angular 13 with Dynamic Data
Complete the Backend ASP.NET Core 6.0 application
We must install the libraries below using NuGet package manger.
- HtmlAgilityPack
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
We are using Entity framework in this application.
We can add database connection string and parallel task counts inside the appsettings.json file.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"ConnStr": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=AnalyticsDB;Integrated Security=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
},
"ParallelTasksCount": 20
}
Database connection string will be used by entity framework to connect SQL database and parallel task counts will be used by web scraping parallel foreach code.
We can create a Feed class inside a Models folder. This class will be used to get required information from C# Corner RSS feeds.
Feed.cs
namespace Angular13ASP.NET6SPA.Models
{
public class Feed
{
public string Link { get; set; }
public string Title { get; set; }
public string FeedType { get; set; }
public string Author { get; set; }
public string Content { get; set; }
public DateTime PubDate { get; set; }
public Feed()
{
Link = "";
Title = "";
FeedType = "";
Author = "";
Content = "";
PubDate = DateTime.Today;
}
}
}
We can create an ArticleMatrix class inside the Models folder. This class will be used to get information for each article/blog once we get it after web scraping.
ArticleMatrix.cs
using System.ComponentModel.DataAnnotations.Schema;
namespace Angular13ASP.NET6SPA.Models
{
public class ArticleMatrix
{
public int Id { get; set; }
public string? AuthorId { get; set; }
public string? Author { get; set; }
public string? Link { get; set; }
public string? Title { get; set; }
public string? Type { get; set; }
public string? Category { get; set; }
public string? Views { get; set; }
[Column(TypeName = "decimal(18,4)")]
public decimal ViewsCount { get; set; }
public int Likes { get; set; }
public DateTime PubDate { get; set; }
}
}
We can create an Authors class inside the Models folder.
Authors.cs
namespace Angular13ASP.NET6SPA.Models
{
public class Authors
{
public string? AuthorId { get; set; }
public string? Author { get; set; }
public int Count { get; set; }
}
}
We can create Category class.
Category.cs
namespace Angular13ASP.NET6SPA.Models
{
public class Category
{
public string? Name { get; set; }
public int Count { get; set; }
}
}
We can create our DB context class for Entity framework.
MyDbContext.cs
using Microsoft.EntityFrameworkCore;
namespace Angular13ASP.NET6SPA.Models
{
public class MyDbContext : DbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options)
{
}
public DbSet<ArticleMatrix>? ArticleMatrices { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
}
We can create our API controller AnalyticsController and add web scraping code inside it.
AnalyticsController.cs
using Angular13ASP.NET6SPA.Models;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Net;
using System.Xml.Linq;
namespace Angular13ASP.NET6SPA.Controllers
{
[Route("[controller]")]
[ApiController]
public class AnalyticsController : ControllerBase
{
readonly CultureInfo culture = new("en-US");
private readonly MyDbContext _dbContext;
private readonly IConfiguration _configuration;
public AnalyticsController(MyDbContext context, IConfiguration configuration)
{
_dbContext = context;
_configuration = configuration;
}
[HttpPost]
[Route("CreatePosts/{authorId}")]
public async Task<bool> CreatePosts(string authorId)
{
try
{
XDocument doc = XDocument.Load("https://www.c-sharpcorner.com/members/" + authorId + "/rss");
if (doc == null)
{
return false;
}
var entries = from item in doc.Root.Descendants().First(i => i.Name.LocalName == "channel").Elements().Where(i => i.Name.LocalName == "item")
select new Feed
{
Content = item.Elements().First(i => i.Name.LocalName == "description").Value,
Link = (item.Elements().First(i => i.Name.LocalName == "link").Value).StartsWith("/") ? "https://www.c-sharpcorner.com" + item.Elements().First(i => i.Name.LocalName == "link").Value : item.Elements().First(i => i.Name.LocalName == "link").Value,
PubDate = Convert.ToDateTime(item.Elements().First(i => i.Name.LocalName == "pubDate").Value, culture),
Title = item.Elements().First(i => i.Name.LocalName == "title").Value,
FeedType = (item.Elements().First(i => i.Name.LocalName == "link").Value).ToLowerInvariant().Contains("blog") ? "Blog" : (item.Elements().First(i => i.Name.LocalName == "link").Value).ToLowerInvariant().Contains("news") ? "News" : "Article",
Author = item.Elements().First(i => i.Name.LocalName == "author").Value
};
List<Feed> feeds = entries.OrderByDescending(o => o.PubDate).ToList();
string urlAddress = string.Empty;
List<ArticleMatrix> articleMatrices = new();
_ = int.TryParse(_configuration["ParallelTasksCount"], out int parallelTasksCount);
Parallel.ForEach(feeds, new ParallelOptions { MaxDegreeOfParallelism = parallelTasksCount }, feed =>
{
urlAddress = feed.Link;
var httpClient = new HttpClient
{
BaseAddress = new Uri(urlAddress)
};
var result = httpClient.GetAsync("").Result;
string strData = "";
if (result.StatusCode == HttpStatusCode.OK)
{
strData = result.Content.ReadAsStringAsync().Result;
HtmlDocument htmlDocument = new();
htmlDocument.LoadHtml(strData);
ArticleMatrix articleMatrix = new()
{
AuthorId = authorId,
Author = feed.Author,
Type = feed.FeedType,
Link = feed.Link,
Title = feed.Title,
PubDate = feed.PubDate
};
string category = "Uncategorized";
if (htmlDocument.GetElementbyId("ImgCategory") != null)
{
category = htmlDocument.GetElementbyId("ImgCategory").GetAttributeValue("title", "");
}
articleMatrix.Category = category;
var view = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='ViewCounts']");
if (view != null)
{
articleMatrix.Views = view.InnerText;
if (articleMatrix.Views.Contains('m'))
{
articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000000;
}
else if (articleMatrix.Views.Contains('k'))
{
articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000;
}
else
{
_ = decimal.TryParse(articleMatrix.Views, out decimal viewCount);
articleMatrix.ViewsCount = viewCount;
}
}
else
{
articleMatrix.ViewsCount = 0;
}
var like = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='LabelLikeCount']");
if (like != null)
{
_ = int.TryParse(like.InnerText, out int likes);
articleMatrix.Likes = likes;
}
articleMatrices.Add(articleMatrix);
}
});
_dbContext.ArticleMatrices.RemoveRange(_dbContext.ArticleMatrices.Where(x => x.AuthorId == authorId));
foreach (ArticleMatrix articleMatrix in articleMatrices)
{
await _dbContext.ArticleMatrices.AddAsync(articleMatrix);
}
await _dbContext.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
[HttpGet]
[Route("GetAuthors")]
public IQueryable<Authors> GetAuthors()
{
return _dbContext.ArticleMatrices.GroupBy(author => author.AuthorId)
.Select(group =>
new Authors
{
AuthorId = group.FirstOrDefault().AuthorId,
Author = group.FirstOrDefault().Author,
Count = group.Count()
})
.OrderBy(group => group.Author);
}
[HttpGet]
[Route("GetCategory/{authorId}")]
public IQueryable<Category> GetCategory(string authorId)
{
return from x in _dbContext.ArticleMatrices.Where(x => x.AuthorId == authorId).GroupBy(x => x.Category)
select new Category
{
Name = x.FirstOrDefault().Category,
Count = x.Count()
};
}
}
}
Please note that route of the API controller should not carry “api/” prefix. Otherwise, SPA will not work.
We have added a new API controller now. We must add this controller entry in Angular proxy configuration file inside the context property. Otherwise, API call from Angular will fail.
proxy.conf.js
const { env } = require('process');
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:38078';
const PROXY_CONFIG = [
{
context: [
"/weatherforecast",
"/analytics",
],
target: target,
secure: false,
headers: {
Connection: 'Keep-Alive'
}
}
]
module.exports = PROXY_CONFIG;
Also note that the above context entry is case sensitive.
Finally, we can make the changes below inside the Program.cs file.
Program.cs
using Angular13ASP.NET6SPA.Models;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("ConnStr")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
}
app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html"); ;
app.Run();
We are using a local database. You must run the migration commands below in Package Manager Console to create database and table.
PM > add-migration InititalScript
PM> update-database
We have completed the backend code.
Complete the Angular 13 application
We can run the npm command to install the node modules for Angular application.
We can open the ClientApp folder in command prompt and install node modules. (If you have not installed node modules, the system will automatically install while running the application for the first time.)
npm install
We must install the client libraries below in our Angular application.
- chart.js
- ng2-charts
- bootstrap
- font-awesome
We can use below single npm command to install all these libraries.
npm install bootstrap chart.js font-awesome ng2-charts
We can change styles.css file with code changes given below.
styles.css
@import "~bootstrap/dist/css/bootstrap.css";
@import "~font-awesome/css/font-awesome.css";
We can create a new Angular component Analytics using the command below.
ng generate component analytics
We have received an error. The reason is, we have two different modules in Angular app.
We must specify the correct module name.
ng generate component analytics --module=app.module.ts
We can change the component class file with code below.
analytics.component.ts
import { HttpClient } from '@angular/common/http';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ChartData, ChartOptions } from 'chart.js';
@Component({
selector: 'app-analytics',
templateUrl: './analytics.component.html',
styleUrls: ['./analytics.component.css']
})
export class AnalyticsComponent implements OnInit {
private url!: string;
constructor(private http: HttpClient, private fb: FormBuilder, @Inject('BASE_URL') baseUrl: string) {
this.url = baseUrl + 'analytics';
}
chartData: ChartData<'pie'> = {
labels: [],
datasets: [
{
data: [],
}
]
};
chartOptions: ChartOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: '',
},
legend: {
display: false
},
},
};
authors: Author[] = [];
authorForm!: FormGroup;
showLoader!: boolean;
totalPosts!: number;
categories: string[] = [];
counts: number[] = [];
ngOnInit(): void {
this.authorForm = this.fb.group({
authorId: '',
chartType: 'pie',
author: null,
category: '',
showLegend: false
});
this.showAuthors();
}
showAuthors() {
this.showLoader = true;
this.http.get<Author[]>(this.url + '/getauthors')
.subscribe({
next: (result) => {
this.authors = result;
this.showLoader = false;
},
error: (err) => {
console.error(err);
this.showLoader = false;
},
complete: () => console.info('Get authors completed')
});
}
populateData() {
if (!this.authorForm.value.authorId) {
alert('Please give a valid Author Id');
return;
}
this.categories = [];
this.showLoader = true;
this.clearChart();
this.http.post(this.url + '/createposts/' + this.authorForm.value.authorId, null)
.subscribe({
next: (result) => {
this.showAuthors();
this.showLoader = false;
if (result == true) {
alert('Author data successfully populated!');
}
else {
alert('Invalid Author Id');
}
this.authorForm.patchValue({
author: '',
chartType: 'pie',
showLegend: false
});
},
error: (err) => {
console.error(err);
this.authorForm.patchValue({
author: ''
});
},
complete: () => console.info('Populate data completed')
});
}
fillCategory() {
this.counts = [];
this.authorForm.patchValue({
category: ''
});
this.totalPosts = 0;
this.categories = [];
this.counts = [];
this.authorForm.patchValue({
authorId: this.authorForm.value.author.authorId,
});
if (!this.authorForm.value.author.authorId) {
return;
}
this.showLoader = true;
this.http.get<Categroy[]>(this.url + '/getcategory/' + this.authorForm.value.author.authorId)
.subscribe({
next: (result) => {
result.forEach(x => {
this.totalPosts += x.count;
this.categories.push(x.name);
this.counts.push(x.count);
});
if (!result || result.length == 0) return;
this.chartData = {
labels: this.categories,
datasets: [
{
data: this.counts,
}
]
};
this.chartOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: 'C# Corner Article Categories for : ' + this.authorForm.value.author.author,
},
legend: {
display: this.authorForm.value.showLegend
},
},
};
this.showLoader = false;
},
error: (err) => {
console.error(err);
this.showLoader = false;
},
complete: () => { console.info('Fill category completed') }
});
}
changeLegends() {
this.chartOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: 'C# Corner Article Categories for : ' + this.authorForm.value.author.author,
},
legend: {
display: this.authorForm.value.showLegend
},
},
};
}
clearChart() {
this.chartData = {
labels: [],
datasets: [
{
data: [],
}
]
};
this.chartOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: '',
},
legend: {
display: false
},
},
};
}
}
interface Author {
authorId: string;
author: string;
count: number;
}
interface Categroy {
name: string;
count: number;
}
Change the template file with code below.
analytics.component.html
<form novalidate [formGroup]="authorForm">
<div class="card row card-row">
<div class="card-header">
<div class="row">
<div class="col-md-6">
<img src="../assets/c-sharpcorner.png" class="logo"> C# Corner Author Analytics
</div>
<div class="col-md-6 total-author-position">
<label>Total</label> <label class="total-author-color-change">Authors</label> populated so far : {{authors.length}}
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group row mb-4">
<label class="col-md-3 col-form-label" for="authorId">Author Id</label>
<div class="col-md-4">
<input class="form-control" id="authorId" formControlName="authorId" type="text"
placeholder="Eg: sarath-lal7" />
</div>
<div class="col-md-5">
<button class="btn btn-primary mr-3" (click)="populateData()">
Populate Author Data
</button>
</div>
</div>
<div class="form-group row mb-4">
<label class="col-md-3 col-form-label" for="authorId">Author Name</label>
<div class="col-md-4">
<select class="form-select" formControlName="author" (ngModelChange)="fillCategory()" id="authorId">
<option value="" disabled>Select an Author</option>
<option *ngFor="let myauthor of authors" [ngValue]="myauthor">{{myauthor.author}} </option>
</select>
</div>
<label class="col-md-2 col-form-label" for="chartType">Chart Type</label>
<div class="col-md-3">
<select id="chartType" class="form-select" formControlName="chartType">
<option value="pie">Pie</option>
<option value="doughnut">Doughnut</option>
<option value="polarArea">Polar Area</option>
<option value="radar">Radar</option>
<option value="bar">Bar</option>
<option value="line">Line</option>
</select>
</div>
</div>
<div class="form-group row mb-4" *ngIf="categories.length>0">
<b class="col-md-7">
Total Categories : {{ categories.length}} Total Posts :
{{totalPosts}}
</b>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" formControlName="showLegend"
(ngModelChange)="changeLegends()">
<label class="form-check-label" for="flexCheckChecked">
Show Chart Legends
</label>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="chart-container chart-position" *ngIf="categories.length>0">
<canvas baseChart [data]="chartData" [type]="authorForm.value.chartType" [options]="chartOptions">
</canvas>
</div>
<div class="file-loader" *ngIf="showLoader">
<div class="upload-loader">
<div class="loader"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
Change the style sheet with code below.
analytics.component.css
/* Spin Start*/
.file-loader {
background-color: rgba(0, 0, 0, .5);
overflow: hidden;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100000 !important;
}
.upload-loader {
position: absolute;
width: 60px;
height: 60px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.upload-loader .loader {
border: 5px solid #f3f3f3 !important;
border-radius: 50%;
border-top: 5px solid #005eb8 !important;
width: 100% !important;
height: 100% !important;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Spin End*/
.card-row {
margin: 40px;
height: 600px;
}
.card-header {
background-color: azure;
font-weight: bold;
}
.total-author-position {
text-align: right;
}
.total-author-color-change {
color: blue;
}
.logo {
width: 30px;
}
.chart-position {
position: relative;
height: 17vh;
width: 34vw
}
We can change the NavMenu component now.
nav-menu.component.html
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" [routerLink]="['/']">Angular13ASP.NET6SPA</a>
<button class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target=".navbar-collapse"
aria-label="Toggle navigation"
[attr.aria-expanded]="isExpanded"
(click)="toggle()">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-end"
[ngClass]="{ show: isExpanded }">
<ul class="navbar-nav flex-grow">
<li class="nav-item"
[routerLinkActive]="['link-active']"
[routerLinkActiveOptions]="{ exact: true }">
<a class="nav-link text-dark" [routerLink]="['/']">Home</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/analytic-data']">C# Corner Analytics</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<footer>
<nav class="navbar navbar-light bg-
white mt-5 fixed-bottom">
<div class="navbar-expand m-auto navbar-text">
Developed with <i class="fa fa-heart"></i> by <b>
Sarathlal
Saseendran
</b>
</div>
</nav>
</footer>
nav-menu.component.css
.fa-heart {
color: hotpink;
}
.align-center {
text-align: center
}
.title {
color: black;
font-weight: bold;
font-size: large;
}
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
We can change the Home component
home.component.html
<div style="text-align:center">
<img src="../../assets/Dotnet Core with Angular 13.png" width="700" />
<h1>Easily create SPA with .NET 6.0 and Angular 13</h1>
<div>
<a class="nav-link text-dark" [routerLink]="['/analytic-data']">Click here to see <img src="../assets/c-sharpcorner.png" width="75"> C# Corner Author Analytics</a>
</div>
</div>
We can change the AppModule with the code changes below.
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { CounterComponent } from './counter/counter.component';
import { FetchDataComponent } from './fetch-data/fetch-data.component';
import { AnalyticsComponent } from './analytics/analytics.component';
import { NgChartsModule } from 'ng2-charts';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent,
CounterComponent,
FetchDataComponent,
AnalyticsComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
ReactiveFormsModule,
NgChartsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'counter', component: CounterComponent },
{ path: 'fetch-data', component: FetchDataComponent },
{ path: 'analytic-data', component: AnalyticsComponent },
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
We have completed the entire application. We can run the application now.
You can notice that Angular is running in port 44456 and .NET Core is running in port 5172.
First, the application is running in port 5172 and once the Angular application is ready it will be redirected to port 44456.
This will take some time to start the application locally. We must be patient. Also, sometimes you will get two command prompts for Angular application before the application starts fully. You can close the second command prompt.
Click the menu in the navigation bar or click the link in the center of the page to open the C# Corner analytics page.
We can populate the author data by giving proper author id.
We can deploy this application to Azure now.
We must create a Web App in Azure portal first.
We must choose .NET 6 as the runtime stack.
We can choose a valid App Service plan name and size. I have chosen a basic plan. It will be a shared resource with 240 minutes (about 4 hours) of compute per day.
I have not enabled the Application Insights for this Web app. We can review it and create.
Web App will be ready in a few moments.
I have already created an Azure SQL server database in Azure portal. We must change the database connection string to this Azure SQL database in appsettings.
Now we can deploy our SPA to this web app.
You can click the Publish menu from solution (right click) and choose Azure option.
Click the next button and choose App Service Plan (Windows)
We must choose our Azure subscription.
We can see that the already created Web app is available under the resource group.
Choose the Web app and click on the finish button.
Our publishing profile is created now. We can click the Publish button to deploy our application.
Both Angular and .NET applications will be deployed to a single Azure Web app soon.
We can see that the Web app is working as expected.
You can try this Live App and populate your favorite author’s data.
Conclusion
In this post, we have seen how to create an SPA with ASP.NET Core 6.0 and Angular 13. We have created a Web app which populate the C# Corner Author’s data and display the post category as a chart. We have run this application locally and then created an Azure Web app in Azure portal and deployed our SPA to this Web App. I am still working on this Analytics application and will be adding more interesting features in coming days. Please feel free to give your valuable feedback about this application and article.