Easily Create SPA With .NET 6.0 and Angular 13 and Deploy To Azure
Easily Create SPA With .NET 6.0 and Angular 13 and Deploy To Azure
0 And
Angular 13 And Deploy To Azure
Easily Create SPA With .NET 6.0 And
Angular 13 And Deploy To Azure
Sarathlal Saseendran
28.7k
10
11
facebook
o
o
o
o
Expand
Angular13ASP.NET6SPA.zip
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.
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.
If you open the project file (.csproj) you will see the details below.
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.
HtmlAgilityPack
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
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
}
C#
Copy
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;
}
}
}
C#
Copy
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; }
}
}
C#
Copy
Authors.cs
namespace Angular13ASP.NET6SPA.Models
{
public class Authors
{
public string? AuthorId { get; set; }
public string? Author { get; set; }
public int Count { get; set; }
}
}
C#
Copy
Copy
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; }
Copy
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
};
if (result.StatusCode == HttpStatusCode.OK)
{
strData = result.Content.ReadAsStringAsync().Result;
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));
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()
};
}
}
}
C#
Copy
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 PROXY_CONFIG = [
{
context: [
"/weatherforecast",
"/analytics",
],
target: target,
secure: false,
headers: {
Connection: 'Keep-Alive'
}
}
]
module.exports = PROXY_CONFIG;
JavaScript
Copy
Finally, we can make the changes below inside the Program.cs file.
Program.cs
using Angular13ASP.NET6SPA.Models;
using Microsoft.EntityFrameworkCore;
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("ConnStr")));
app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
app.MapFallbackToFile("index.html"); ;
app.Run();
C#
Copy
We are using a local database. You must run the migration commands below in Package
Manager Console to create database and table.
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
chart.js
ng2-charts
bootstrap
font-awesome
We can use below single npm command to install all these libraries.
styles.css
@import "~bootstrap/dist/css/bootstrap.css";
@import "~font-awesome/css/font-awesome.css";
CSS
Copy
We can create a new Angular component Analytics using the command below.
We have received an error. The reason is, we have two different modules in Angular app.
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']
})
chartData: ChartData<'pie'> = {
labels: [],
datasets: [
{
data: [],
}
]
};
chartOptions: ChartOptions = {
responsive: true,
plugins: {
title: {
display: true,
text: '',
},
legend: {
display: false
},
},
};
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;
}
JavaScript
Copy
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>
Copy
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
}
CSS
Copy
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>
Markup
Copy
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;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
CSS
Copy
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>
Markup
Copy
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';
@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 { }
JavaScript
Copy
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 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.
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.
You can click the Publish menu from solution (right click) and choose Azure option.
Click the next button and choose App Service Plan (Windows)
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.