More than two years ago I wrote the following in my debut Medium article:
“My desire with [dotNetify] is to get to a state that can support full-stack micro-service on .NET platform: the ability to have a complex web app composable of independent front+back-end modules that can be worked on and deployed by completely autonomous teams with very minimal downtime”
I’m delighted to say that with the release of dotNetify version 3.5, this desire has finally been realized!
Little did I know then, I was describing something that only a few months before has made the ThoughtWorks Technology Radar of things to assess. They call it “micro-frontend”, and for better or worse , that term has entered our (software) industry’s pantheon of buzzwords.
ThoughtWorks elevated the status to “adopt” earlier this year, which I’m glad they did; not just because it makes me feel a bit vindicated, but because we indeed may have found an effective solution to the problem of sprawling front-end monoliths in enterprise applications.
But before I continue with this topic, let me quickly cover my obligatory release announcement.
DotNetify is a free, open-source software that helps you build reactive and real-time web application on .NET Core server. It’s unique MVVM approach puts the view models on the back-end and written in C#, and communicate with the UI layer, either written in React, Vue, or Knockout, on web socket through the SignalR technology (but as you will read next, this has become optional in this latest version.)
Multi-Hubs
The most significant update in v3.5 is allowing an SPA to communicate with multiple hub servers. Instead of restricting the client-side views to a single, same-origin SignalR hub server, one can create cross-origin hub proxies and use a new lifecycle hook to switch the connection at runtime.
The above is accomplished by abstracting the part that communicates with the SignalR hub proxy, which incidentally enables high extensibility. The views are no longer coupled to SignalR, and can use different library or protocols to send / receive data.
Web API Mode
If you don’t really need real-time push, you can now opt for HTTP Web API by including MVC services to
Startup.cs
, and setting a flag in the connect options:dotnetify.react.connect("MyViewModel", this, { webApi: true } });
What the flag does is making your client-side component connect through a hub proxy implementation that communicates to the server as normal HTTP requests to the dotNetify’s built-in Web API endpoint. This endpoint will instantiate the requested view model, set property values if the client sends them, return the serialized JSON in the response body, and dispose the instance.
Switching to the Web API mode won’t require too much change to the way you write the C# view models. You just can’t use real-time functions like
PushUpdates()
or multicasting, and you need to keep it stateless; either let the client script manages the state, or always persist the state to a backing store. Any middleware you write will continue to work, except that the hub context argument that’s used to be populated by SignalR hub context will instead get its values from HTTP context.Local Mode
One other new hub proxy implementation in this release is the local mode. This mode is primarily meant for unit testing, with which you can set up a mock view model to global
window
as illustrated here:window.HelloWorld = {
onConnect() {
return { Greetings: 'Hello World' };
}
};
class MyApp extends React.Component {
constructor(props) {
super(props);
this.vm = dotnetify.react.connect('HelloWorld', this);
this.state = { Greetings: '' };
}
/*...*/
}
Setting an object with the same name as the view model ID to window will allow you to circumvent the server-side connection and make the component gets the state from that object instead.
The rest of new version features is detailed in the v3.5 release notes. I will now go back to discussing the micro-frontend pattern that this version intends to facilitate.
Micro-frontend is an architectural pattern that decomposes an otherwise monolithic web application into decoupled, independently deployable smaller applications. Each one provides a subset of the UI, usually along a bounded context, which will then be stitched together by a light orchestrator to present a single, cohesive front-end to its users.
If you think that it sounds more trouble that it’s worth, then most of the time you will be right! Just with microservices, it’s not for everyone. This technique increases both development and deployment complexities, and introduce a whole new set of challenges. So why is this even a ‘thing’?
The answer lies on its ability to effectively address the problems with growing, monolithic front-ends in larger organizations. It’s the same set of reasons that compel them to break their back-ends into microservices:
There are many different techniques people have used to implement micro-frontend. The one recommended with dotNetify is closely aligned to the microservices approach, where aspects of functionality is encapsulated within independent web services (or micro-apps), each with its own build process and deployment pipeline. The domain-driven design approach can be employed on deciding how to slice the problem domain into distinct apps.
The diagram below depicts the proposed architecture:
The entry to the application is provided by what is referred to as a portal service. This service is responsible for performing front-end orchestration, which involves:
The portal service is also responsible for authentication/authorization. It communicates with an identity provider to generate security tokens, and intercept outgoing service requests to inject the tokens into the request header.
In production environment, these services should be put behind an API gateway to secure them and have a single entry point into the system.
A simple sample implementation covering the application layer of said architecture is provided in the
DotNetify.React.Template
nuget:dotnet new -i dotnetify.react.template
dotnet new mfe -o MyApp
// To run the app, follow the instructions in README.md
Executing the command lines above will create a solution consisting of multiple .NET Core projects, representing apps services and a portal service. The UI are mostly developed with React, with one Vue project added into the mix to illustrate the polyglot nature of this solution. Nginx is used for API gateway, and IdentityServer4 for the identity provider.
App Services
Each app service is an ASP.NET Core web application, configured to build the UI scripts using Webpack when the application is compiled (dev build on dotnet run, prod build on dotnet publish).
The main UI script that serves as the Webpack’s entry point is made to export a function that will return the root UI component as a native Web Component. This will allow the portal service that will load the app’s UI script bundle to create and mount the app’s root component in a framework-agnostic manner.
The following is a sample implementation of the main script. The
createWebComponent
function is provided by dotNetify-Elements to wrap the React component inside an HTML custom element:import { createWebComponent } from 'dotnetify-elements/web-components/core';
import MyApp from './components/MyApp';
const elementName = 'my-app-element';
createWebComponent(MyApp, elementName);
export default () => document.createElement(elementName);
Portal Service
The portal service is also an ASP.NET Core web application combined with Webpack. The UI script contains a function called loader which will be executed as soon as the portal starts. This function is given a list containing information on all the app services, including their addresses. For demo purpose, this list is hardcoded.
loader(
[
{
id: 'app1',
label: 'App 1',
routePath: 'app1',
baseUrl: '//localhost:8080/app1',
moduleUrl: '/dist/app.js'
},
...
],
// External dependencies required by the apps.
[ 'dotnetify', 'dotNetifyElements' ]
);
The loader function will ping each app service addresses, and on getting a response, use SystemJS module loader to load the app, and pass the module function that creates the app's root component to an object that manages and displays these components.
function importApp(app) {
const appUrl = app.baseUrl + app.moduleUrl;
return SystemJS.import(appUrl)
.then(module => updatePortal({
...app,
rootComponent: module.default
}));
}
When the app’s root component is mounted, it will communicate to its own hub server. Since the address is relative, the portal needs to intercept every connect request and redirect it to the correct address. The portal can do this by implementing
dotnetify.connectHandler
and use the special identifier that each app will need to provide when its component makes the connect call to forward the request appropriately:const apps = /* registered apps */;
dotnetify.connectHandler = vmConnectArgs => {
const appId = vmConnectArgs.options.appId;
const app = apps.find(x => x.id === appId);
if (app) {
app.hub = app.hub || dotnetify.createHub(app.baseUrl);
return {
...vmConnectArgs,
hub: app.hub,
options: {
...vmConnectArgs.options,
headers: { Authorization: 'Bearer ' + getAccessToken()}
}
};
}
};
Along with the hub change, it too injects the access token to the connect request. The service’s back-end is set up to get the JWT signing keys from IdentityServer4 to authenticate the token.
Finally, the portal uses the NavMenu component from dotNetify-Elements to dynamically build and run the client-side routing. Any app that use dotNetify routing system will be able to integrate and nest their routes within the portal's root path.
Shared UI Library
With each micro-frontend app being developed in isolation and deployed independently, naturally there are concerns over visual inconsistencies, and ballooning size due to duplicated dependencies. This can be mitigated by agreeing to use a shared set of core UI components as basic building blocks for the app’s more domain-specific components.
Some organizations have sufficient resources to build their own UI design system, which usually includes a set of themed stylesheets and a common UI component library. These modules are always referenced by the apps, but can be declared external when bundling. Only the portal loads them during its initialization so it can be made available to all apps.
The reference implementation uses dotNetify-Elements for this common UI library. This is a set of specialized UI components designed to work with dotNetify view models and fully support reactive programming model. It's built with React, but it exposes the same components as HTML custom elements, making them reusable in other non-React frameworks.
DotNetify v3.5 brings notable enhancements to support micro-frontend pattern to web applications developed with .NET Core back-end, either with SignalR or with standard HTTP Web API.
Detailed documentation can be found on dotNetify website. Please direct further questions or feedback to the github issues forum. Show support for this project with your stars!