Create Progressive Web Apps with .NET using Blazor
Blazor has build-in support for PWAs now, see https://docs.microsoft.com/aspnet/core/blazor/progressive-web-app?view=aspnetcore-6.0&tabs=visual-studio. You can create a new Blazor application using the command dotnet new blazorwasm -o MyBlazorPwa — pwa.
TLDR: You can create PWAs using the .NET Blazor framework. A sample is here and the code is on GitHub.
Progressive Web Apps (PWAs) are one of the current trends in the web world. They make it possible to install web apps on the device which does not only make your websites work offline but also provides features like push notifications. Sites like Twitter are already jumping on the PWA train and with Google, Microsoft, Apple and Firefox all major browser and operating system vendors are actively working on supporting the standard. Microsoft even adds PWAs to their Microsoft Store with Twitter being one of the first available PWA there, replacing the old UWP version.
At the same time, Microsoft announced Blazor, a client side .NET Framework that runs in your browser on top of WebAssembly. Unlike Silverlight for example, WebAssembly is supported in most modern browsers (you can guess which one does not support it) so you don’t need to download some weird plugin or extension. It also builds on web standards like HTML and CSS and provides all features you would expect from a web framework like routing, a component-model, UI layout and dependency injection. Visit the Blazor GitHub page to get a full overview of the features.
The only logical next step is to combine these two technologies. By doing so, you can build rich web applications using C# and your favorite libraries and don’t need to build a separate mobile or desktop app. In the following guide, I will show you how this is done. The guide is suited for complete ASP.NET and Blazor beginners but contains summaries at some points for the experienced developer.
This post will guide you trough the manual steps to setup a project and configure it to serve a PWA. After I initially published this post a project appeared which adds the PWA functionally automatically trough MSBuild, you should definitely check it out on GitHub!
Prerequisite
To get started we need a few things. First of all, a browser that fully supports PWAs to test what we are doing. The mobile versions of Chrome, Firefox and Safari already support PWAs out of the box. On the desktop side of things, Chrome and Edge support installable PWAs, Firefox supports Service-Workers but not installing PWAs.
The second thing we need is an environment to build Blazor websites which is already included when installing Visual Studio 2019, 16.4 with the ASP.NET and web development workload, Visual Studio for Mac, or the .NET Core SDK starting with 3.1. Additionally the latest blazor template needs to be installed using the dotnet CLI: dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.2.0-preview1.20073.1
For more information on the blazor setup and basics, Microsoft Docs provides a comprehensive starting guide.
All set? Lets start!
Create the project
If you are familiar with Blazor or ASP.NET Core in general, you can just create an Blazor App| Blazor WebAssembly App project for .NET Core 3.1 and head to the next part. If not, you get detailed instructions below.
Creating the project is quite streight forward. Open Visual Studio and hit File | New | Project and select Blazor App. In the following dialog, you will be prompted to select a template. Set the Version to ASP.NET Core 3.1 and select Blazor WebAssembly App, this will generate you a standalone Blazor website project. Selecting Blazor (ASP.NET Core hosted) would generate you a backend project to your website and Blazor (Server-side in ASP-NET Core) will create a project where the client logic is executed on the server. You can use the command line to create the project as well dotnet new blazorwasm
Visual Studio will now generate the project for you and will present you with something like this.
Let’s see what we got here:
- wwwroot will contain the root index.html, css and meta files. Bootstrap is already added for your convenience :)
- Pages contains all, you might have guessed it, pages of your website. These have the .cshtml extension you may already know from Razor.
- Shared contains base .cshtml files that host our pages.
- _Imports.razor sets default dependencies. Yes, .NET dependencies in a website.
- App.razor can be ignored since it will be removed in the future
- Program.cs is the entry point for the program, it tells the framework to use the Startup.cs
- Startup.cs contains logic that will be executed when the app starts, you can register components or depdencys into the dependencies container here for example
You can now start the project by clicking on the start button or pressing F5. Before starting the project for the first time you should change the runner from IIS Express to the Standalone execution like shown below. The command line alternative would be dotnet run
Now a browser window should open that shows the website. If you inspect the network in the developer console, you can see all the .dll’s that are getting loaded. Great, if this would be a tutorial on how to create a Blazor website, we would be finished now but we are only getting started!
Creating the PWA
PWAs need a few things, the most important are:
- A web manifest that contains meta information about the app
- A service worker to cache your app to work offline
- Icons for the installed app
- The website needs to be served over HTTPS, but don’t worry: Serving from localhost does not require HTTPS and I will show you how to easily serve the finished app over HTTPS later on
On top of that, Google provides a checklist with additional requirements your app should meet for a solid user experience.
The first thing is the web manifest. Add a file called manifest.json to the root of your page. You can do so in Visual Studio by right clicking on the wwwroot folder and selecting Add | New Item and search for json. I had the problem that Visual Studio created the file in the wrong encoding. To make sure the encoding is correct, select the file and click File | Save manifest.json As | Click the dropdown beside the Save button and select Unicode (UTF-8 without signature).
Now paste the content below into the manifest.json. You can get an complete overview of the available parameters here.
{
"name": "Blazor PWA Sample",
"short_name": "Blazor PWA",
"icons": [
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone",
"background_color": "#3a0647",
"theme_color": "#052767"
}You need to add the pictures to your wwwroot folder. You can take the icons from the sample repo to begin with.
The last thing in the wwwroot folder is the JavaScript file that will contain our service worker. A service worker is a script that runs in the background and will handle the caching for the PWA. I once again redirect you to a google guide for more detailed information about service workers. Drop a JavaScript file in the wwwroot folder like you did with the json file and copy the following content in there.
console.log("This is service worker talking!");
var cacheName = 'blazor-pwa-sample';
var filesToCache = [
'./',
//Html and css files
'./index.html',
'./css/site.css',
'./css/bootstrap/bootstrap.min.css',
'./css/open-iconic/font/css/open-iconic-bootstrap.min.css',
'./css/open-iconic/font/fonts/open-iconic.woff',
//Blazor framework
'./_framework/blazor.webassembly.js',
'./_framework/blazor.boot.json',
//Our additional files
'./manifest.json',
'./serviceworker.js',
'./icons/icon-192x192.png',
'./icons/icon-512x512.png',
//The web assembly/.net dll's
'./_framework/wasm/dotnet.js',
'./_framework/wasm/dotnet.wasm',
'./_framework/_bin/WebAssembly.Net.Http.dll',
'./_framework/_bin/Microsoft.AspNetCore.Blazor.HttpClient.dll',
'./_framework/_bin/Microsoft.AspNetCore.Blazor.dll',
'./_framework/_bin/Microsoft.AspNetCore.Components.dll',
'./_framework/_bin/Microsoft.AspNetCore.Components.Web.dll',
'./_framework/_bin/Microsoft.Extensions.DependencyInjection.Abstractions.dll',
'./_framework/_bin/Microsoft.Extensions.DependencyInjection.dll',
'./_framework/_bin/Microsoft.JSInterop.dll',
'./_framework/_bin/mscorlib.dll',
'./_framework/_bin/System.Net.Http.dll',
'./_framework/_bin/Mono.WebAssembly.Interop.dll',
'./_framework/_bin/System.dll',
'./_framework/_bin/System.Core.dll',
'./_framework/_bin/Microsoft.Bcl.AsyncInterfaces.dll',
'./_framework/_bin/Microsoft.Extensions.Configuration.Abstractions.dll',
'./_framework/_bin/Microsoft.Extensions.Logging.Abstractions.dll',
'./_framework/_bin/Microsoft.Extensions.Primitives.dll',
'./_framework/_bin/Microsoft.Extensions.Configuration.dll',
'./_framework/_bin/System.Text.Encodings.Web.dll',
'./_framework/_bin/System.Text.Json.dll',
'./_framework/_bin/WebAssembly.Bindings.dll',
'./_framework/_bin/System.Runtime.CompilerServices.Unsafe.dll',
//The compiled project .dll's
'./_framework/_bin/DotnetPwaSample.dll'
];self.addEventListener('install', function (e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function (cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
});self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request, { ignoreSearch: true }).then(response => {
return response || fetch(event.request);
})
);
});
The most important part is the filesToCache property. It contains all files that are needed to be cached in order for the pwa to work. On top of all the framework dll’s, css and icons you need to add the .dll of your project like I did with /_framework/_bin/DotnetPwaSample.dlland do the same for all additional dependencies you use. The logic to cache these files is in the event listener for install. The event listener for fetch contains the corresponding logic to return the cached files. This will not only make the PWA work offline but also decrease load times since all files are cached now.
Now we need to make sure the manifest and service worker are used. In order to do that we add them to the head of the index.html. We also add a theme color meta entry there. The script to register the service worker is added to the bottom like shown below.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width">
<title>Blazor PWA Sample</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
<link rel="manifest" href="manifest.json">
<link rel="script" href="serviceworker.js">
<meta name="theme-color" content="#052767" />
<link rel="apple-touch-icon" href="icons/icon-192x192.png">
</head>
<body>
<app>Loading...</app>
<script src="_framework/blazor.webassembly.js"></script>
<script>
if ('serviceWorker' in navigator) {
console.log('Registering service worker now');
navigator.serviceWorker.register('/serviceworker.js')
.then(function () {
console.log('Service Worker Registered');
});
}
</script>
</body>
</html>Finally, we have to stop the project from generating a .pdb file since that will mess with our cache. Right click your project and select Properties | Build | Advanced … (you might have to scroll down) and set Debugging informations to Embedded.
That’s it! Start your project again, when opening the developer tools and looking into the console, you should see no errors and the following messages on the first start.
You can find the cached files under the Application tab in the Cache | Cache Storage menu entry. When changing to the network tab, selecting offline in the top right corner and reloading the page, you will see that the service worker fetched all files from the cache (the favicon.ico is not working) and that the PWA is working offline.
If you have any trouble, an audit in the chrome developer options under the Audits tab can diagnose your PWA. At this point, only the HTTPS audit should fail.
You can now install the app! Here is how to do it in different browsers:
- Chrome (Desktop) offers the Install YourAppName in the menu, in the top right corner. The app will appear on your desktop and under the chrome apps where you can uninstall it as well (chrome://apps/).
- Microsoft Edge (Desktop) a “plus” icon will appear in the adress bar which will install the PWA.
- Chrome (Android) provides an Add to start screen option which will “install” the PWA on your device.
- Firefox for Android provides an add button in the address bar which will “install” the PWA on your device.
- Safari (iOS) is using the default Share | Add to home screen button to add a PWA. iOS does not support the icon we set, refer to this guide to optimize the PWA for iOS.
Deploy your pwa
You can deploy the PWA as a static site like any other websites by publishing the project. You can publish the project in Visual Studio in the context menu of the project | Start | Folder | Publish or by using the command dotnet publish -c release. This will create a folder under bin\release\netstandard2.1\publish\{ProjectName}\dist where you can find all files you need to serve.
Please note that you might have to modify the start_url value of your manifest.json to the correct URL when deploying your PWA, all other paths are relative and should work.
I hosted my sample on GitHub using the GitHub Pages feature. To do the so you need a public repository and create a folder called “docs” at the root of your repository where you add the content of the dist folder. You also want to create a .nojekyll file in the docs folder to make sure that all files are served properly.
You now just have to go to the Settings | Options | GitHub Pages menu of your GitHub repository and set the Source to master branch /docs folder. Please refer to my sample if you have any trouble.
You can now find your PWA under https://{Username}.github.io/{RepositoryName}.
Acknowledgements
I used the guide Learn how to build a PWA in 5 minutes by Meggin Kearney to get started myself. It includes additional information on how to host the PWA on firebase and add push notifications. I also want to thank Adam Pedley for proofreading this post.
