Switching up your Spotify experience with microfrontends and Blazor
Dante De Ruwe
Posted on May 7, 2021
In my previous article, I talked about creating a Netflix clone using Piral: an open-source framework for creating modular applications. I highly recommend giving that article a quick read first, if you are not yet familiar with microfrontends and/or Piral.
Since then, I've been making contributions on GitHub and working closely together with the maintainers of Piral, to provide a helping hand to improve their framework in any way I could.
The main bulk of the improvements was made in their converter for Blazor.
In this article, I will share my experiences creating a microfrontend web app with Blazor and Piral. I'll also give a little behind-the-scenes look at how this was made possible, highlight some of the quirks of using Blazor in a microfrontend solution, and explain how the combination of Piral and Blazor has improved.
First of all, let's discuss the demo application that I created to showcase the use of Blazor with Piral: Spiralfy. A clever – or some would say cheesy – play on words between Spotify and Piral, of course. But what does it do?
Log in with your Spotify premium account, and access a way to switch up your Spotify experience!
Overview
For a long time now, I wanted a way to shuffle play my playlists. I'm not talking about shuffling the songs within one playlist, that's something you can obviously already do. The feature I wanted could be described as "swiping through playlists": Spiralfy picks one playlist at random, shuffle playing its songs, and whenever you feel like you want a different vibe, you let Spiralfy pick a new playlist to listen to.
(I got inspired in part by lofi.cafe, where you can switch through curated lofi playlists like they were radio stations. But I wanted the user to be able to use their own Spotify playlists instead.)
The code
"Talk is cheap. Show me the code" ~ Linus Thorvalds
Spiralfy is a modular distributed web application, also known as a microfrontend application. In my previous microfrontend project I decided to make separate GitHub repositories for each and every module; to really demonstrate that these are highly decoupled and autonomous. This time, I chose to bundle all parts into one repository (a monorepo), just because it would be easier to discover and browse all the code at once. You can find the code on git.io/spiralfy
First of all, let's discuss the demo application that I created to showcase the use of Blazor with Piral: Spiralfy. A clever – or some would say cheesy – play on words between Spotify and Piral, of course. But what does it do?
Log in with your Spotify premium account, and access a way to switch up your Spotify experience!
For a long time now, I wanted a way to shuffle play my playlists. I'm not talking about shuffling the songs within one playlist, that's something you can obviously already do. The feature I wanted could be described as "swiping through playlists"…
What are these words?
In case you did not read my previous article: a quick summary here. In the Piral framework, pilets are the individual feature modules, also known as microfrontends. Pilets are usually published to a feed service. The Piral instance (aka app shell) will pull all registered pilets from the feed service, and put them where they need to go as defined by the pilets themselves.
The Spiralfy appshell
For the Spiralfy app shell, I decided to go with piral-core instead of the full piral framework.
I actually started with the full version of Piral, but after realizing that I will not be using any dashboards, translation, notifications... and other fancy features (bundled in a collection called piral-ext) I migrated to piral-core.
Quite often the scenario is that somebody starts with piral but then realized that one or the other plugin should not be included. (...) In any of these cases a migration from piral to piral-core makes sense.
There were only 2 plugins that I actually wanted: piral-menu, in case I would want to add items to the navigation menu in an easy way; and piral-blazor, for reasons explained below.
So, the index.tsx file looks a little bit like this:
import*asReactfrom'react';import{render}from'react-dom';import{createBlazorApi}from'piral-blazor';import{createInstance,Piral,SetErrors,SetLayout}from'piral-core';import{createMenuApi}from'piral-menu';import{errors,layout}from'./layout';constfeedUrl='https://feed.piral.cloud/api/v1/pilet/spiralfy';constinstance=createInstance({plugins:[createBlazorApi(),//this is where the magic is included ✨createMenuApi()],requestPilets:()=>fetch(feedUrl).then(res=>res.json()).then(res=>res.items)});constpiral=(<Piralinstance={instance}><SetLayoutlayout={layout}/><SetErrorserrors={errors}/></Piral>);render(piral,document.querySelector('#app'));
Things to notice about this setup:
the plugins each come from their respective packages, not from piral.
in the full Piral framework, we would use renderInstance. Piral-core however, does not come with react bundled. It means we should use the standard render method from react-dom to render our Piral instance. (read more here)
Although I called piral-blazor a plugin, it is actually considered a converter: a package that brings support to use a different UI framework. Piral supports around 15 different UI frameworks, other than React.
If you would like more information on how piral-blazor works, I would suggest you read the second part of this article, too!
I included the Blazor converter in the app shell, because I already knew I was going to add a Blazor pilet. It's also possible to load piral-blazor from a pilet. This is for example useful if the app shell already existed and you don't want to change it. This is beyond the scope of this article.
The player: a simple pilet
The player pilet is rather barebones. It uses the following npm package, which is neat wrapper around the Spotify web playback SDK.
This allows us to have Spotify playback from the browser. If we only used the Spotify API, we could control the playback, but we would have to have an active device it is playing on. This eliminates that.
Since I wanted to make my own layout for a player in Blazor, I'm setting this one to display:none. This way it's loaded, but it's also hidden from view. Yes I am aware that this is hacky, stop bullying me! :/
While the pilet is simple, this is however demonstrating a niche problem that is pretty well solved by the fact that Piral (and microfrontends in general) can be very technology-independent. If you want to write an app in Blazor, but a certain feature has a pretty nice Javascript library already: You can most of the time just use it from a different microfrontend. This way, there's way less need in having to struggle with any JS interop.
The controls: a Blazor pilet!
Now for the interesting bit: the Blazor pilet!
To create the Blazor pilet, I followed the documentation for Piral.Blazor, from their README on GitHub. Piral.Blazor is a set of NuGet packages that will make Piral work from the .NET side.
All .NET things to make Blazor work seamlessly in microfrontends using Piral. 🧩
Here is also a quick video on the process, if you don't like reading:
The process boils down to installing a template. If you want to transform an existing Blazor app however, all you have to do is defining the app shell name, installing the Piral.Blazor.Tools package that will create the right files for your pilet, and installing Piral.Blazor.Utils to be able to use custom Piral attributes in your code.
To make the interaction with the Spotify API a lot easier I used the following great NuGet package, which provides fully typed responses and requests.
🔉 A Client for the Spotify Web API, written in C#/.NET
Alright, after this round of NuGet installing, we can talk about the components.
A standard page in Blazor, using the @page directive, will work as expected, and will be automatically registered on the pilet API. This is what I used to register the player on the homepage.
For an extension, like a login button that should end up in the app shell header, we can use the PiralExtension attribute, specifying the name of the extension slot you want to render into.
@attribute [PiralExtension("header-items")]
@if(_username is null){
<a href="@_authUri">Login via Spotify</a>
}
else
{
<p>Welcome @_username</p>
}
@code {
//...
}
And... I would say... That's almost the entire story. Because Piral.Blazor does some pretty neat stuff under the hood, the developer experience of creating a Blazor pilet is really similar to creating a regular Blazor WASM application!
Let's run it!
Because we are using Piral, running the Blazor pilet does include an extra step. We need to use the Piral CLI to do its magic! Again, the Piral.Blazor docs to the rescue!
From your Blazor project folder, run your pilet via the Piral CLI 🚀
cd ../piral~/<project-name>
npm start
(You could also add a --feed argument, as outlined here)
In addition to this, if you want to debug your Blazor pilet using for example Visual Studio, you can just run the pilet with IISExpress. (if you want to use Blazor 3.2, there are extra things to consider. You can read about them here. The cool kids use Blazor 5 anyway ⌐■_■ )
This way, you can really use the entire Blazor debugging experience: hitting breakpoints, stepping through your code, and so on.
Dev story: making Blazor work with microfrontends
Achievement unlocked: you have reached part 2 of the story. In this part, I wanted to take the opportunity to tell you about how the combination of Piral and Blazor has matured over the period that I was actively contributing to its codebase; working closely together with the maintainers of Piral.
Looking back
Now, how was Piral with Blazor organized some 2 or 3 months ago?
This does not look too complicated. Let's break it down.
For the Piral instance, we rely of course on the Piral framework (although, as said before, this could also be piral-core). Next to that, we also need piral-blazor as a converter.
Under the hood, piral-blazor would actually download the Piral.Blazor.Core NuGet package, which would contain
Blazor framework files: dlls, boot files, metadata, etc.
custom code that would
expose some methods that can be invoked from the JS side to register, load and unload components
make sure activated components get rendered in a <div> with a specific id
provide a way to register dependencies from the pilets in the DI container
...
piral-blazor would include these files in the Piral instance, and could call the exposed methodes using JS interop. This then would allow exposing some functions in the pilet API to allow pilets to activate components from their setup function.
The pilets would be created using the official template. The template created files that can be divided into 2 categories:
the Blazor files
other files, mainly TS, codegen and JSON files, where the registration with the pilet API was handled (setup function etc...)
The Blazor files can be considered to be the heart of the pilet, while the other files were just there to be the proverbial "glue" that would make everything integrate into the Piral framework.
Still confused? Below I give an example on how this all worked.
Example
Let's say you know how to create the most beautiful counter component in Blazor. You want to use this counter as a microfrontend in your React application, and give it a dedicated page. Maybe you would also want to render it as an extension on another part of your web app.
Of course, you are already using Piral. You would add the piral-blazor converter to the plugins of your Piral instance.
Then, you would then set up a pilet using Piral.Blazor.Template, and add your counter component. To expose this component to be able to get picked up by Piral you would add an ExposePilet attribute. This would look like this:
These last lines would register the counter component as a page and an extension via the pilet API. piral-blazor would then via Piral.Blazor.Core lookup the component in the defined references, activate it using JS invokable methods, and integrate it somewhere in the webpage.
What needed improvement, and why?
There are several aspects where the aforementioned ecosystem could improve. Below I outline some of the goals that were set to make this better:
The user should be able to select the version of Blazor they want, independently of the piral-blazor version (because the latter is tied to the Piral version).
There should be a way to transform an existing Blazor application into a Blazor pilet with minimal effort.
Registering Blazor dependencies is cumbersome and error-prone: they need to be manually entered, and if they are not in the right order they will not load correctly. Ideally, we would have an automatic solution.
Registering pages and extensions onto the pilet API should be possible purely from Blazor, without having to do any TypeScript configuration (in the setup function). Additionally, to define pages we should not use a custom attribute, but use the built-in @page directive from Blazor.
Debugging a Blazor pilet from for example Visual Studio should be possible: triggering breakpoints, stepping through the code, ...
Various improvements to single-page navigation, static files, scoped razor styles, ...
What has changed and improved?
Let's dive right in by providing an updated diagram of the Piral Blazor ecosystem:
Let's go over the improvement goals and see how this new architecture fulfills them:
1. The user should be able to select the version of Blazor they want, independently of the piral-blazor version (because the latter is tied to the Piral version).
This first goal was made possible by extracting the responsibility of dealing with Blazor files into a new npm package called blazor, and including it as a peer dependency of piral-blazor. This way, the user can install a piral-blazor version that corresponds to their Piral version, and choose any version of the blazor package to include any version of Blazor (e.g. blazor@3.2.x will resolve to the .NET Blazor 3.2 release train).
2. There should be a way to transform an existing Blazor application into a Blazor pilet with minimal effort.
For this goal, we created Piral.Blazor.Tools. The workflow for transforming a Blazor project into a pilet now looks something like this:
Add a PiralInstance property to your .csproj file with the name of your Piral instance.
Install the Piral.Blazor.Tools package
Build the project. The first time you do this, this can take some time as it will fully scaffold the pilet.
(stuff omitted for brevity. Read the complete description here)
This is the reason the template is now grayed out on the diagram: you don't need it anymore (but it's still nice to get a quick start)!
Template or not, the tools package will do all the heavy lifting and create all files needed for integration with the final framework. But this time, they will not be mixed in with the Blazor files: the user should not see these at all! We decided to put them all in a piral~ folder. This final tilde makes it so that this folder will by default not be checked in, thanks to the default .NET .gitignore file.
3. Registering Blazor dependencies is cumbersome and error-prone: they need to be manually entered, and if they are not in the right order they will not load correctly. Ideally, we would have an automatic solution.
The tools package allows us to scaffold the pilet, and also copy over any files that we like to the user's pilet. We used this to fix this problem, by letting the tools copy over a blazor.codegen file.
If you are unfamiliar with codegen files, no worries, I was too! They are basically ways to generate code at build-time. Here you can learn more about one of the loaders that Piral can use (this is one for Parcel):
Parcel plugin for bundle-time code generation. Simple, powerful, and flexible. 📦
blazor.codegen looks for a JSON file called project.assets.json to build up a dependency graph for the Blazor project, and then traverses/flattens this graph in the right order: dlls without dependencies (=leaves in the graph) should be loaded first, then their parents, and so on. (For you algorithm nerds: this is depth-first post-order traversal 🤓 ).
It will then use this ordered list of dependencies to generate the pilet API integration code for us at build-time! (How this works will become clearer together with the next goal)
4. Registering pages and extensions onto the pilet API should be possible purely from Blazor, without having to do any TypeScript configuration (in the setup function). Additionally, to define pages we should not use a custom attribute, but use the built-in @page directive from Blazor.
This was an interesting challenge. What has to happen for this to work is that we should have a list of all components that are defined as pages or extensions. And as an additional challenge: we need this list before the components are even loaded, so we cannot simply get them at runtime.
Luckily, when Blazor gets compiled, this @page directive just gets converted into a [RouteAttribute], and we also have the [PiralExtension] attribute, so it's all attributes and we can treat these almost in the same way!
What we came up with, is the Piral.Blazor.Analyzer: a command line tool that will use reflection on the Blazor project dll to extract all components that have certain attributes. The codegen then calls dotnet Piral.Blazor.Analyzer <args>; the output of which gives us something like this:
The codegen then has all the required information to create code for functions that the setup function needs. The index.tsx file then will use these generated functions from the codegen, and becomes:
5. Debugging a Blazor pilet from for example Visual Studio should be possible: triggering breakpoints, stepping through the code, ...
This was an interesting challenge. The first step for me was learning how the debugger actually works. I stumbled upon an excellent blog post by Safia Abdalla. She's a software engineer at Microsoft on the .NET team.
After some experiments and proof-of-concept work, we found out that in Blazor 3.2, we could not load the dlls and pdbs (symbol files) dynamically. They had to be included in the blazor.boot.json file for them to get picked up by the debugger.
We solved this by configuring the Piral CLI using a kras script injector. I won't go into too much detail here, so I'm oversimplifying massively: kras is a proxy/mock server for your frontend.
Efficient server proxying and mocking in Node.js. 💪
While it is most commonly used to mock a backend, we can use it as a proxy too! You can configure kras with js files included in a mocks folder. Let's take a look at a code snippet:
This last part is what we mentioned before: when we debug we want to modify the blazor.boot.json file to include our pilet dlls and pdbs, so they get loaded when the debug starts.
As we see on line 5, a request should be proxied if the url points to one of our unique dlls or pdbs. This means the shared dlls from the appshell will just be loaded as normal, but the dlls the Pilet brings will be proxied. On line 9 we can see where to: the requests for the pilet dlls will be proxied to the url where IISExpress is running (we can get this from the launchSettings.json file by the way).
Because of this hack, we can now trigger breakpoints in Visual Studio, step through the code, inspect variables, etc.!
Oh, by the way, .NET 5 made stuff a lot easier with the inclusion of lazy loading! I won't give a full explanation, but it boils down to: we can now just load the pilet dlls and pdbs lazily, and everything just works! Proxying and tweaking the boot file isn't necessary!
6. Various improvements to single-page navigation, static files, scoped razor styles, ...
I won't go into details about these. There are always things to improve about code, and these were some major ones :)
Example
We take the same scenario as before: the counter. With the new setup, if we want a page, there is nothing at all to do that is special. We just need a regular Blazor page. If we also want to expose an extension, we can use the new [PiralExtension] attribute from Piral.Blazor.Utils
The tooling will do all the rest! No more screwing around in TypeScript for a setup function to manually create and keep updated! Isn't that convenient?
(For more advanced uses you can still define an optional setup.tsx file. Refer to the README to see how that works.)
Final thoughts
My first thought after writing this article would be "damn, I wrote quite a long article again", immediately followed by "are people actually going to read this one?". My last post was received very well, and I thank each and everyone that reacted to it! Feel free to let me know in the comments if this article was in any way interesting, helpful or cool!
I enjoyed contributing to Piral a lot. I've always wanted to dive into the open-source community, and because of the guidance given by the Piral maintainers, I feel like I could make a difference in this project (Piral is also pretty damn cool if you ask me).
Looking back on it, in my opinion, using Piral and Blazor has become better in both functionality and developer experience, and I'm really proud of that ("hey, a Belgian guy that says he is proud of something, that's quite rare!"). If you want to see my contributions first-hand; or just criticize my code, here and here are lists of the PRs I made.
Then I want to address Blazor. While I can definitely see the appeal of it, and it's a cool technology: it was pretty hard to get a grasp on the technical side of it. Lots of the stuff that's going on is quite magical at first. I'm glad the entire thing is open-source, because finding solutions often meant peeking behind the curtain and reading the source files on the dotnet/aspnetcore repo.
Because of this however, I've learned an awful lot about how Blazor WebAssembly works; what the limitations and weird quirks are, etc. It also just broadened my knowledge of the .NET ecosystem as a whole.