Universal Dashboard is a web framework for PowerShell. We take advantage of ASP.NET Core with a React SPA to allow users to create websites using PowerShell. In this post we will go over how we use dynamically loaded React components at runtime to provide a plug-in architecture. Components are not included in the main React bundle but served as separate JavaScript files from ASP.NET Core when they are requested from the client.

Architecture of Universal Dashboard

Universal Dashboard is a PowerShell module that runs within the PowerShell.exe or Pwsh.exe process. The module starts an ASP.NET Core web server that is configured using PowerShell cmdlets. Within PowerShell, ASP.NET Core endpoints are registered so that call backs from the React client can call PowerShell scriptblocks.

When the client loads the website from the browser, the ASP.NET Core web server sends down the core React package. This package is responsible for the framework that will then load plugins to provide different components for the developer to include within their website.

Most components are not included in this main bundle. Instead, they are separate PowerShell modules that are loaded at runtime. These modules register assets, such as JavaScript and map files, with the ASP.NET Core web server to provide these components are runtime. This also means that additional modules can be installed that provide components that are not part of the core set of components.

ASP.NET Core Configuration

ASP.NET Core is configured from within a PowerShell runspace. This takes place using various PowerShell cmdlets and classes that are defined within the Universal Dashboard ecosystem. Whenever a plugin (or module in PowerShell lingo) is imported that provides a custom component, it registers the component with the Universal Dashboard Asset Service. The asset service is responsible for providing the location of JavaScript files that could be requested by the client browser. The asset’s full path is provided to the asset service so the web server knows where to find it later. An asset ID is then provided to be referenced as the component is loaded. Additional assets, such as images or map files, can also be registered so they are available to the browser to request them.

Whenever a component is loaded in JavaScript that hasn’t been loaded from the server previously, the JavaScriptController is called to load the asset that was registered in PowerShell. This endpoint provides the JavaScript bundle that will then register the additional components within the React app.

This works fine for resources we know we will be loading but JavaScript bundles that use code splitting or bundles that include additional assets, such as images, require that we can load any URL that can be requested. To handle this case, we use the UseStatusCodePages extension method to check requests coming in. If we have an asset in the Asset Service that matches the URL, then we return that resource rather than forwarding to one of our controllers or showing a 404 page.

Once this configuration has been done, the asset is now registered with the service and ready to be requested by the client.

Using a Component From PowerShell

Universal Dashboard serializes PowerShell hashtables into JSON that is then sent to the client SPA. A service within the React app then determines the correct component to render based on the props of the hashtable. Within PowerShell component, such as New-UDHelmet, you’ll see that the function is just returning a hashtable.

You’ll also notice that we are setting the asset ID and a type name on the hashtable. This allows the React component to load the correct JavaScript file and load the correct component from the JavaScript file.

This hashtable will serialize to the following JSON.

Rending the Component in React

To render the component in React, the app uses a Render Service to cache a list of loaded and registered components. When a JavaScript file is loaded, it registers its components with Universal Dashboard and when those components are requested to be rendered, they will be available for any calls to React.createElement.

All components rendered in Universal Dashboard pass through the renderComponent function. This function evaluates whether or not a component is registered with the system. If it is, it will call React.createElement to create a new React element based on the registered component and the props that was provided via the PowerShell hashtable.

If a component isn’t registered, and has an asset ID, it then uses the LazyElement component to attempt to load the JavaScript file dynamically and render the newly loaded component.

In the componentWillMount function within LazyElement, the assetId is used to call the JavaScript controller within ASP.NET Core. This looks up the correct JavaScript asset and then adds a script tag to the head to load the new plugin’s bundle.

Within the plugin, a call is made to registerComponent. As the JavaScript file loads it will call this function to register any components with Universal Dashboard. This makes the components available to the renderComponent function. The type name is included here so that when components are requested of the said type, they will be looked up and the React component function will be returned.

Once the script is loaded, the loading state is set to false and the component will pass through renderComponent again. Since the component was registered as the script was loaded, it will be available to be rendered.

Conclusion

In this blog post we went over the steps necessary to create a plug-in architecture with ASP.NET Core and React. You can dynamically register components via a service in ASP.NET Core and then have those components loaded into the React app without having to actually include them in your core bundle.

For a full example of how this works, visit the Universal Dashboard GitHub repository.