Recently I blogged hosting a Blazor App on Azure Storage. In the same way, any SPA application can be hosted on Azure Storage. Here, I’m showing hosting an Angular App. With Azure Storage, static website hosting can be used to host SPA applications. Hosting websites with Azure Storage is a cheap way for static website hosting. In the back-end, Azure Functions can be used – which offers a consumption based cost model.
Prepare Azure storage
First, let’s create a new Azure Storage account. After logging into the Microsoft Azure portal, you can create an Azure Storage account. To use static website hosting, the account needs to be a StorageV2 account.
After creating this Azure Storage account, you can configure it to enable static websites. You can find this configuration in the Settings tab – select Static website, and click the button Enabled, and add an entry point for the application, e.g Index.html:
With the feature Static website enabled, you can see the the primary endpoint that can be used to access the website as soon as the needed files are deployed. A container named $web is created – you need to upload the files needed by the website into this container.
Create a Angular project
I’m using the command line to create an Angular project. You can install the Angular CLI using
npm install -g @angular/cli
In case you do not have npm installed, get npm.
A new Angular project (without any ASP.NET Core backend) can be created with
ng new angularsample
This command creates a project structure and downloads a bunch of files from the NPM server.
After changing to the newly created directory
cd angularsample
you can start the test server coming with the packages with
ng serve --open
The --open
argument starts the default browser, so the Angular app is opened. The serve
command is specified in the file project.json
to start the test-server locally.
Deployment of the Angular Project
To create the files for deployment, the build
command can be used. An optimized build for production is created with the --prod
flag. The flag --build-optimizer
reduces the bundle sizes for the JavaScript and CSS files.
ng build --prod --build-optimizer
This build creates the dist
folder that can be used to deploy the containing files.
To upload files to Azure Storage, multiple options exist, but with most options one by one file needs to be uploaded to the storage account. To upload multiple files at once, you can use Visual Studio Code. The extension Azure Storage allows to manage your Azure Storage Accounts, and to upload files to deploy the files for a static website. Be sure to select a subfolder in the dist directory containing the Index.html file as well as the JavaScript and CSS files.
After installing the extension Azure Storage vor Visual Studio Code, click on the Azure icon on the left, and then the upload button to upload the files. Because in the portal Azure Storage has been configured to enable static websites, the container named $web is created in the Azure Storage account. This is the container where the upload goes to.
The extension Azure Storage for Visual Studio Code s currently in preview. I’m using version 0.4. To enable the multi-file upload to static websites, you need to define the user setting “azureStorage.preview.staticWebsites”: true.
After deploying completes, you can access the Angular app using the link from the Azure Portal.
Using Azure Functions
The Angular app is running. However, we don’t want to stop at this point. SPA applications without some functionality in the back-end are not that useful.
Using services hosted in Azure App Services is not really useful when saving money by hosting the Angular app with Azure Storage. In case you already use Azure App Services, you can also host the Angular website with the same hosting plan. Saving money using Azure Storage, you can also save money with the back-end services by not to reserving CPU and memory, but using a consumption plan instead, e.g. with Azure Functions.
With Function Apps you can choose between a Consumption Plan and an App Service Plan. Depending on the number of requests and the data you download, one or the other plan is the better option to use. If you don’t have continuous load on your service, the Consumption Plan can be the cheaper one. In case you already run an App Service Plan that is not fully busy, you can use it host your Functions there as well. With Azure Functions, you don’t need to change your API if the load changes, you can easily switch the Hosting Plan.
Using Visual Studio 2017, you can use a project template to create Azure Functions. With Azure Functions v2, a .NET Standard Library is created.
Azure Functions offer different ways to activate them. Calling it from the Angular app, an HTTP Trigger can be used to activate the Function App. With HTTP triggers, a Storage Account is not needed. I’m selecting access rights anonymous to allow anonymous users to access to the list of books returned from the function app.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Book | |
{ | |
public Book() { } | |
public Book(int bookId, string title, string publisher) | |
=> (BookId, Title, Publisher) = (bookId, title, publisher); | |
public int BookId { get; set; } | |
public string Title { get; set; } | |
public string Publisher { get; set; } | |
public override string ToString() => Title; | |
} |
The implementation of the Azure function returns a list of Book
objects:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class BooksFunction | |
{ | |
[FunctionName("BooksFunction")] | |
public static IActionResult Run( | |
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]HttpRequest req, TraceWriter log) | |
{ | |
log.Info("C# HTTP trigger function processed a request."); | |
return new OkObjectResult(GetBooks()); | |
} | |
public static IEnumerable<Book> GetBooks() | |
=> new List<Book> | |
{ | |
new Book(1, "Enterprise Services with the .NET Framework", "Addison Wesley"), | |
new Book(2, "Professional C# 6 and .NET Core 1.0", "Wrox Press"), | |
new Book(3, "Professional C# 7 and .NET Core 2.0", "Wrox Press") | |
}; | |
} |
Configuring CORS with Azure Functions
What needs to be done with the Azure Function to enable it to be invoked from Angular is to configure CORS. You can configure CORS from the Azure Portal selecting the name of the Function App, and selecting the tab Platform features. From there, the API configuration CORS is available. You need to enter the URL used by the Angular app to allow the Angular app accessing the Azure Function.
Call Azure Functions from Angular
To invoke the Azure Function from Angular, the Angular app is extended, and a component created to invoke the API service. You can create Angular components with the Angular CLI:
ng generate component books
With the BooksComponent
component, the HttpClient
component is injected in the constructor of the BooksComponent
class. The BASE_URL
is retrieved via dependency injection as well. When the result is received, it is written to the public Book[]
named books
. Using the TypeScript definition for an interface, the Book
type is defined with members with the same property names as the JSON format is created from the Azure Function.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, Inject } from '@angular/core'; | |
import { HttpClient } from '@angular/common/http'; | |
@Component({ | |
selector: 'app-books', | |
templateUrl: './books.component.html', | |
styleUrls: ['./books.component.css'] | |
}) | |
export class BooksComponent { | |
public books: Book[]; | |
constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) { | |
http.get<Book[]>(baseUrl + 'api/BooksFunction').subscribe(result => { | |
this.books = result; | |
console.log("retrieved" + this.books); | |
console.log("title: " + this.books[0].title); | |
}, error => console.error(error)); | |
} | |
} | |
interface Book { | |
bookId: number; | |
title: string; | |
publisher: string; | |
} |
The user interface accesses the public member books
, and shows its members in an HTML table using Angular data binding.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h1>Books</h1> | |
<p *ngIf="!books"><em>Loading…</em></p> | |
<table class='table' *ngIf="books"> | |
<thead> | |
<tr> | |
<th>Id</th> | |
<th>Title</th> | |
<th>Publisher</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr *ngFor="let book of books"> | |
<td>{{ book.bookId }}</td> | |
<td>{{ book.title }}</td> | |
<td>{{ book.publisher }}</td> | |
</tr> | |
</tbody> | |
</table> |
Publishing and running the application, you can see the books returned from the Azure Function with the Angular app hosted in Azure Storage.
Summary
Why using Azure Storage instead of Azure App Services with static web content? It’s just a lot cheaper using Azure Storage instead of an App Service, and it scales big. With the backend, Azure Functions can be used – also offering a cheap variant compared to App Services, and this variant offers big scaling.
Static website hosting with Azure Storage is currently in preview – but it looks very promising.
More Information
Read chapter 32, Web API of my new book Professional C# 7 and .NET Core 2.0 on how to share code between an ASP.NET Core Web API project and an Azure Function app.
In another blog post I’ve shown the same Azure features with ASP.NET Core Blazor in the frontend: Hosting Blazor Apps on Azure Storage
Static website hosting in Azure Storage
Get the complete sample in the Azure folder from More Samples!
Enjoy programming and learning,
Christian
Greate sample ! There is one error – in CORS screenshot delete „/“ after url of function.
LikeLike
Have you attempted to extend your example to secure the Azure Functions using AAD authentication? I.e., do you know how to adapt the configuration for an Angular app hosted on Azure Storage as a static website interacting with AAD-secured AZ Functions? Supposedly, the MSAL-Angular library can accommodate this scenario. https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-angular/README.md.
LikeLike
One issue that still exists is that you cannot add a secure custom domain, which makes this service less useful. You get a cert mismatch error when trying to do so. MS does document this at https://docs.microsoft.com/en-us/azure/storage/blobs/storage-custom-domain-name and suggests using their CDN service. Which comes at additional cost and is unnecessary for some applications.
LikeLike