DEV Community

loading...
Cover image for Switching up your Spotify experience with microfrontends and Blazor

Switching up your Spotify experience with microfrontends and Blazor

Dante De Ruwe
Full-stack developer fascinated by software architecture. Enthusiastic, eager to learn, always looking for a challenge. International intern in Munich. Writing a thesis about microfrontends.
Updated on ・17 min read

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.

Contents

Spiralfy: a modular web application

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

GitHub logo DanteDeRuwe / spiralfy

A different way of using Spotify! Built using a microfrontend approach with Piral, Blazor and React. Read more on https://bit.ly/spiralfy-article

switching up your Spotify experience with microfrontends and Blazor


Netlify Status

Made with React , Blazor and Piral





Article

In this DEVCommunity article, I share my experiences creating a microfrontend web app with Blazor and Piral. I also give a little behind-the-scenes look at how this was made possible

About

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"…

For now, Spiralfy exists in 3 parts:

  • the Piral instance: spiralfy-appshell
  • the player pilet
  • the controls pilet

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.

The Piral docs page on piral-core actually describes this scenario pretty well:

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 * as React from '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';

const feedUrl = 'https://feed.piral.cloud/api/v1/pilet/spiralfy';

const instance = createInstance({
  plugins: [
    createBlazorApi(), //this is where the magic is included ✨
    createMenuApi()
  ],
  requestPilets: () => fetch(feedUrl).then(res => res.json()).then(res => res.items)
});

const piral = (
  <Piral instance={instance}>
    <SetLayout layout={layout} />
    <SetErrors errors={errors} />
  </Piral>
);

render(piral, document.querySelector('#app'));
Enter fullscreen mode Exit fullscreen mode

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)

piral-blazor

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.

GitHub logo gilbarbara / react-spotify-web-playback

A simple player for Spotify's web playback

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.

GitHub logo smapiot / Piral.Blazor

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.

GitHub logo JohnnyCrazy / SpotifyAPI-NET

🔉 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 {
   //...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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?

The Piral Blazor ecosystem in March, 2021

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:

@attribute [ExposePilet("my-awesome-counter")]     //!

<div>
    <p>Current count: @count</p>
    <button @onclick="Increment">Increment</button>
</div>

@code {
    int count = 0;

    void Increment()
    {
        count++;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then you would edit the created index.tsx file to something like:

import { PiletApi } from '<name-of-piral-instance>';

export function setup(app: PiletApi) {
  app.defineBlazorReferences([
    require.resolve('./My.Dependency.dll'),
    require.resolve('./My.Components.dll'),
  ])
  app.registerPage('/counter', app.fromBlazor('my-awesome-counter')); //page
  app.registerExtension('counter-slot', app.fromBlazor('my-awesome-counter')); //extension
}
Enter fullscreen mode Exit fullscreen mode

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:

  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).
  2. There should be a way to transform an existing Blazor application into a Blazor pilet with minimal effort.
  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.
  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.
  5. Debugging a Blazor pilet from for example Visual Studio should be possible: triggering breakpoints, stepping through the code, ...
  6. 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:

The Piral Blazor ecosystem in May, 2021

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:

  1. Add a PiralInstance property to your .csproj file with the name of your Piral instance.
  2. Install the Piral.Blazor.Tools package
  3. 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):

GitHub logo FlorianRappl / parcel-plugin-codegen

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:

{
  "routes": ["/counter"],
  "extensions": {
    "My.Components.Counter": ["my-awesome-counter"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

import { PiletApi } from '<name-of-piral-instance>';
import * as Blazor from './blazor.codegen';

export function setup(app: PiletApi) {
  Blazor.registerDependencies(app);
  Blazor.registerPages(app);
  Blazor.registerExtensions(app);
  //...
}
Enter fullscreen mode Exit fullscreen mode

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.

captainsafia image

The blogpost can be found on her personal blog: Under the hood with debugging in Blazor WebAssembly. There, she explains the concept of the debugging proxy.

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.

GitHub logo FlorianRappl / kras

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:

/* mocks/debug.js */

//...
const toProxy = [...uniqueAssemblyNames, ...uniquePdbNames];
const shouldBeProxied = ({ url }) => toProxy.filter(x => url.endsWith(x))?.length && url.startsWith('/_framework/_bin');

module.exports = (ctx, req, res) => {
    if (shouldBeProxied(req)) {
        return proxy(req, iisUrl);
    } else if (req.url.endsWith('blazor.boot.json')) {
        return returnWithTweakedBBJson(res);
    }
};
Enter fullscreen mode Exit fullscreen mode

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

@page "/counter"
@attribute [PiralExtension("my-awesome-counter")] 

<div>
    <p>Current count: @count</p>
    <button @onclick="Increment">Increment</button>
</div>

@code {
    int count = 0;

    void Increment()
    {
        count++;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (6)

Collapse
softwareprogrammer profile image
Matt Parker

Thanks for such a detailed article, however I seem to hit issues and still don't get the full picture.

On attempting to build my blazor pilet in Visual Studio, it always seems to fail for me `The command "npx pilet new aero-shell-ui --base ..\piral~ --target WF1.Authentication" exited with code 1.'

Do pilets have to sit on a certain path relative to the piral instance?

I also can't seem to find much documentation on what a "private" feed would look like for my own pilets being served up from my own API. Does the build of a pilet produce an NPM that is referenced in the feed, or is the index.js referenced in the feed such as I can see in this sample feed - feed.piral.cloud/api/v1/pilet/blaz...

If you can offer any guidance it is much appreciated

Collapse
dantederuwe profile image
Dante De Ruwe Author

Thanks for your comment!

First off, let me mention that questions like these are best asked in the Piral Community Gitter chat. There are a lot of great people there that can answer your questions way better than I can.

I'll at least try to answer your question: the Piral instance and the pilets are completely separate in terms of folder structure. Piral.Blazor.Tools uses the Piral instance that is published to an npm registry as an emulator. So if your Piral Instance is not published, Piral.Blazor.Tools will fail. (You COULD provide the path to your Piral instance tarball in your csproj, but publishing the emulator package would be more reliable)

As for your second question, maybe this can be of use:
github.com/smapiot/sample-pilet-se...

Collapse
softwareprogrammer profile image
Matt Parker

Ah - I hadn't realised that the Piral instance needs to be published. Also, many thanks for the link to Glitter as I was unaware of that too.

Appreciate your help :)

Thread Thread
dantederuwe profile image
Dante De Ruwe Author

You're very welcome! :)

Collapse
iotcloudarchitect profile image
iotcloudarchitect

I like to read your well-structured and top-down oriented articles!
Keep on going!

Collapse
dantederuwe profile image
Dante De Ruwe Author

Thanks a lot! I enjoy writing these :)