Background
As a software developer, one is bound to come across the weird requirement from a client, "I want a mobile app". Yeah, sure no problem, let me get my dev environment setup correctly. You are bound to reach for something like Flutter or React Native or Ionic for a single codebase cross-platform approach because why not right? Then your client says, "But I am not willing to deal with the stores, can't we get around that?" Of course, we can, why not build a PWA. Users can save it onto their phone from the web and we can have extra native features like push notifications etc. "Great! Let's go for it"!
Getting into it
Well building a PWA is not too far off from building any other web app like a SPA am I right? Pick and choose from React, Angular, Vue and the like...
Well issue one is that I am a .NET developer. Oh boy!
Easy solution: .NET's crown jewel framework, BLAZOR.
Time to build
With building the app there were a few things to consider:
- Will the app need a backend? (yes)
- Is authentication required? (yes, I want to use Microsoft)
- What are the core functionalities of the app? (more web-like features)
- Feasibility of building a PWA over Native App? (cheaper to deploy)
With all those considered the building began. Well with the app requiring a backend and authentication, I reached for the Blazor WebAssembly template with the Co-Hosted & PWA options enabled. Authentication from there was an easy choice, "Individual Accounts" or "Microsoft Identity Platform". Well with an existing Microsoft Active Directory it makes perfect sense to go for Microsoft Identity Platform. So right at the end this what the options looked like...
Easy enough going from there was simply configuring all the relevant Microsoft Entra ID applications on Azure for the Client app and the Server app and letting Visual Studio do all the heavy lifting of configuring everything for me. The end result looks very similar to how any Blazor WASM app with ASP.NET Core Hosted solution looks like. Only a few minor differences due to the "PWA" option that was selected which means the client project had a few more files specifically in the wwwroot
folder:
- service-worker.js
- service-worker.published.js
- manifest.json
- icon-512.png
What next?
I finally have a running project which I can actually install on my device, I mean Edge tells me I can.
That's the easy part done. Fast forward all the design implementations and coding out all the features, comes the time to finally deploy the app to a test environment and test it on an actual mobile device. Easy enough I deploy the server project to an IIS server like I would a normal AspNet Core app and it runs smoothly. I then update the URLs in Azure Entra ID for the authentication to work and Bob was my uncle, or so I thought. Enter classic tale of a developer; "It worked on my local", I now ran into a few issues that I had to resolve, and I learned something while doing so.
PWA Learnings
The tale of building a PWA is that it's meant to be cross platform and not have any issues but oh it did.
DateTime compatibility
Turned out Android & iOS do not format the date and time the same way and the iOS format was not compatible with C#'s DateTime type.
Solution: build a helper method that will parse the datetime input correctly.
public static DateTime GetCorrectDateTime(string inputDate)
{
DateTime _newDate;
string format = "dd/MM/yyyy HH:mm:ss";
try
{
// Try to parse using the default format and culture
_newDate = DateTime.Parse(inputDate);
}
catch (FormatException)
{
// If the default format fails, try to parse using the specified format and culture
_newDate = DateTime.ParseExact(inputDate, format, CultureInfo.InvariantCulture);
}
return _newDate;
}
Install prompt
When a web app is a PWA, the browser picks up on that and is able to prompt the user to install the app onto their device, turns out not all browsers support that. Ooops!
Solution: there is none, 👀 just ensure on iOS the user is using Safari and on Android they use Chrome. Read more here...
Authentication state
When the user hadn't been using the app for some time and the Azure token expires and they open up a page that makes an API call, it would fail. This is because the internal API calls have a HttpMessageHandler
which is BaseAddressAuthorizationMessageHandler
which extends the AuthorizationMessageHandler
class which tries to add the authentication token to the request header and when that fails an exception is thrown. If not handled the app breaks. 🤯💥
Solution: use the TryGetToken
method on the AccessTokenResult
to get the user's authentication token and if that fails redirect to login before making any calls using the HTTP Client.
@code {
private const string Prisma = "Prisma";
public bool Loading { get; set; } = true;
protected override async Task OnInitializedAsync()
{
var accessTokenResult = await AccessTokenProvider.RequestAccessToken();
if (!accessTokenResult.TryGetToken(out var token))
{
accessTokenResult.InteractionOptions.TryAddAdditionalParameter("prompt", "login");
NavigationManager.NavigateToLogin(accessTokenResult.InteractiveRequestUrl,
accessTokenResult.InteractionOptions);
}
Loading = false;
}
}
Hopefully in future releases there will be a much more graceful way of handling this issue.
Deploying new updates
Well since this is a PWA I should just be able to deploy the changes and not have to worry about the user having to update the app manually like they would via the store. Yes & No. Huh? Yes you can deploy the changes but they will not be able to see the changes until they close ALL active instances of the app on their device. This means the actual installed version and any open tabs in their browser need to be closed before updates can happen.
Solution: Why not just force the app to use the updated version...enter this line of code in the service-worker.published.js
. Blazor actually copies all the contents in the service-worker.published.js
into service-worker.js
when building for production hence we make the change in there.
// Activate the new service worker as soon as the old one is retired.
self.skipWaiting();
The line above is added like so:
async function onInstall(event) {
console.info('Service worker: Install');
// Activate the new service worker as soon as the old one is retired.
self.skipWaiting();
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
Outro
The question that is always asked around Blazor is, "Will it ever replace Angular or React or Vue?" Honest truth? Nope it will not but I will say that it is a really great alternative to using those frameworks especially if you are a dotnet developer and want to continue using C# even on the frontend. I would definitely use Blazor again. 🎉
Useful Links
- Web Dev: https://web.dev/learn/pwa/
- MDN: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
- PWA Install Criteria: https://web.dev/articles/install-criteria- Lighthouse Report: https://developer.chrome.com/docs/lighthouse/pwa/load-fast-enough-for-pwa
- Microsoft Learn: https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app?view=aspnetcore-7.0&tabs=visual-studio
- Auth with Entra ID: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/hosted-with-microsoft-entra-id?view=aspnetcore-7.0
- Pushing updates: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers
Top comments (1)
nice article, can you shere more from your experience with Blazor?
in terms of not having breaking changes in each version and haveing an easy way to upgrade due to end of life and lack of updated browser competabilities and security issues, on which client side framework would you recommend to use instead of AngularJS and Angular 7 having the backend running on .Net Framework 4.8 and .Net 8?
Some comments have been hidden by the post's author - find out more