Micro Frontends in .NET

2021-May-13

Introduction

Micro Frontends is the latest architecture that's promising to make large applications more maintainable. This article describes my example using .NET technologies.

The application is deployed here: http://mfepoc.rgbco.uk

The GitHub repository is here: https://github.com/.../MfePoc

The Goal

There might be several reasons to use Micro Frontends:

  • Migrating to a new technology incrementally;
  • Splitting the codebase so multiple teams can work independently;
  • Decoupling a large codebase.
Whatever the reason, one of the key points is that each module should be independently deployable, and have minimal coupling to the rest of the application.

The Domain

The example needs a domain that is complicated enough to demonstrate multiple technologies while being simple enough to put together an application in a reasonable timeframe.

This application has the following modules:

  • Colour Generation; Allows the user to generate the primary colours Red, Green, and Blue.
  • Colour Mixing; Allows the user to mix the generated colours into secondary colours.
  • Sales; Facilitates selling the secondary colours separately, or as a 'white' package that combines them, where prices fluctuate randomly against the colour sale market.
  • Reporting; Reports on the current total of sales, and the value and description of each sale.
  • Dashboard. Combines all of the above modules into a single dashboard to see an overview of everything.

Technology

To split up the application and minimise coupling between modules, each module will run in its own process. This allows each module to be completely responsible for its own technology stack, and run different versions of the .NET runtime.

Each module is hosted in an IIS sub-folder, and has its own IIS Application Domain. Modules communicate with each other by publishing messages on a bus. For this example, I've implemented a simple file-system bus.

The file-system bus is not intended to be production worthy - it's just to demonstrate how inter-process communication might work. In a production application you might want to use a real messaging technology (e.g., RabbitMQ), and probably have a library abstracting the transport details (e.g., MassTransit, NServiceBus, Rebus).

The application is deployed on IIS for simplicity. In a grander solution, the current vogue might be to move each module to its own pod in a Kubernetes cluster, with an Ingress controller to route HTTP requests to the appropriate pod.

The Generation and Sales modules are implemented using Blazor Server. The Mixing and Reporting modules are implemented using Blazor WebAssembly. The Dashboard and Home page are implemented using traditional server-side MVC.

Build + Deployment

The build is scripted using MSBuild, and every commit automatically triggers a build+deploy on AppVeyor: Project Badge

Each module has its own build script that generates a .zip file that is uploaded to the server. For a .NET project, this is just a dotnet publish ..., and then an MSBuild <ZipDirectory .../> task.

There is a single setup script that creates/updates the virtual folder structure and application pools in IIS. If a new module is required, then this script is manually run to create the IIS folder, but thereafter, the module can update itself.

The zip file is uploaded and automatically unzipped using ZipDeploy, again for simplicity of this demo.

This demo has a single build script; the script runs each module's build separately, and each zipped package is uploaded and deployed separately. This provides the opportunity to separate these onto different build machines to parallelise the process. In addition, each build could potentially detect that no changes have been made (e.g., using Bazel) to avoid unnecessary build and testing when only updating a single module. Each module could potentially have its own Git repository.

Local Development Environment

In order to prevent any JavaScript security issues, we want all the modules to be hosted on the same port. Kestrel cannot host more than one application on the same port. IIS Express might be able to, but it appeared to be more trouble than it was worth. The easiest option is to use a real IIS server locally.

Each module is configured in the project settings to point to a sub-folder on IIS:

Micro Frontend Project Settings

Micro Frontend Project Settings

In order to run the whole application together, each invidual module's project must be set to startup:

Solution Startup Settings

Shared Layout

As described above, each module has its own build and is free to create its (zip) package however it chooses. However, we still want the application to look consistent between modules.

There is a shared Razor Class library that contains the layout for the screen, and the menu. Each module also has a configuration file (that is deployed, and read at runtime) to define its menu and a URL for warmup. Below is the Generation module's configuration file:

    
    <xml>
        <Warmup Name="Generation" Url="/Generation" />
        <Menu Position="0" Text="Generation">
            <MenuItem Text="Index" Path="/Generation/" />
            <MenuItem Text="Red" Path="/Generation/GenerateRed" />
            <MenuItem Text="Green" Path="/Generation/GenerateGreen" />
            <MenuItem Text="Blue" Path="/Generation/GenerateBlue" />
        </Menu>
    </xml>
    

This allows the home page to show a check/warmup for each module:

Module Status/Warmup

This also allows the (shared) menu code to display for all modules, but without modules being coupled together.

Generation Module's Menu

Ideally the user would move seamlessly around the application, and would not notice the difference between technologies in different modules.

For MVC applications, it is typical to see page refreshes while navigating between pages. Loading pages without refreshing in an MVC application is typically implemented using ajax/pjax.

Blazor does not require page refreshes between pages within a module, but between modules is challenging. Blazor Server can only connect to a single circuit for a single process, so you can't have multiple Blazor Server processes running on a single page. It might be possible to combine multiple Blazor WebAssembly packages into a single download (and have each talk to their own server-side host), but this would probably require some build-time packaging magic (which might couple these modules back together).

Overall, it seems overkill to try and prevent page refreshing between modules, and it is up to the individual module to decide whether that is needed as a priority. If your modules are potentially split along a similar structure to your organisation, then there might be little need for a single user to navigate heavily between modules.

Combining Components

WebComponents can be incorporated into any web application, but we are not able to package Blazor components as WebComponents. Instead, the Dashboard demonstrates putting controls from multiple modules (mixing Blazor Server and WebAssembly) into a single page using iframes.

Since the modules are hosted as sub-applications there are no cross-site scripting restrictions, which allows components inside an iframe to communicate with its hosted page. As an example, if you try to mix a secondary colour in either the mixing module's page, or inside the Dashboard, you get an error notification. If the component is hosted inside an iframe, it detects this, and raises the event up to the host page to display the notification: mfeNotify.js

    
        var inIframe = window.self !== window.top;

        if (inIframe) {
            window.top.$(window.top).trigger('notify', {
                message: message
            });
        } else {
            internalNotify(message);
        }
    

... and in the host page:

    
        $(window).on('notify', function (e, d) {
            internalNotify(d.message);
        });

        function internalNotify(message) {
            // display notification ...
    

Using iframes for components solves some problems, but when using WebAssembly (especially on older devices) this can be quite slow. As an example, the mixing controls took too long to startup (it was fine on a modern device, but some older devices struggled). The application demonstrates a couple of ways of mitigating this: dual-run as Blazor Server, or lazy-render when required.

Dual Run

Blazor components are implemented separately to the binding (Server or WebAssembly). As such, it is possible to code components in a way that they could be run both server and client-side. The mixing controls were proving to be slow to startup (on slower devices), so for the main mixing screen, the components run in WebAssembly, while on the Dashboard, the same code is running using Blazor Server.

Lazy Render

The reporting module is also implemented using Blazor WebAssembly, and again, on older/slower devices the startup time was prohibitive on the Dashboard (along with all the other module's controls). Instead of displaying the report automatically, there is a button that allows the user to choose to display the report (or not), and this only creates the iframe when the user clicks the button. It is now for the user to decide for themselves if they want to take that hit.

Other Considerations

Micro Frontends seems (to me) like a more maintainable way of structuring large applications. On .NET, Blazor promises to be the way forward for web development on Microsoft technologies, and there's a lot to like about it. The key decision might be between Blazor Server and Blazor WebAssembly. Some considerations on this:

Page Load Time

Blazor WebAssembly is not transpiling C# to JavaScript. It is running your real .NET assemblies on a pre-wasm'd .NET runtime in the browser. Given how much this is doing, it is perhaps unsurprising that this is slow on some devices. (Although it can be quite fast on more modern ones.) This might be a make/break reason to go with WebAssembly.

Programming Model

I find when using Blazor Server, since everything can be in a single project (.csproj), and because all the code is running on the server and can talk directly to the domain logic, the programming model can be considerably simpler and more productive to deliver functionality in. The only downside being latency on slower connections, which again might be a make/break reason to go with Blazor Server.

Continuous Deployment

If you have a CI/CD pipeline that continually pushes new builds to production, then the Blazor Server cannot seamlessly survive a restart. All the circuits will be lost when the process recycles. If people are in the middle of work, that would be an unbearable break in the useability of the application. This might require a convoluted solution of having multiple web servers, and moving sessions from one to the other when they are idle, and only recycling a server once there's no circuits running on it. Perhaps in future, Blazor Server might have some kind of persistent circuit storage (like the ASP.NET State Service used to provide), but for now, continuous deployment would be a challenge for Blazor Server I suspect.