Power FX in JavaScript
It’s been a while since Microsoft announced and open-sourced its low-code engine called Power FX. The implementation, however, is only available in .NET, which doesn’t allow it to run on client without backend out of box. Let’s take a look at how to achieve that!
Along with the language repo itself, Microsoft also has a repo with samples, where one of those samples is an interactive playground powered by ASP.NET Core. The experience is also hosted and available directly from your browser, so you don’t have to run it.
Over the past few years, Microsoft implemented support for WebAssembly in ASP.NET and it is still continuously getting a lot of attention. Thanks to this, I decided to try to port the Power FX sample to run directly in the browser, without any need of server-side.
The main point of this was to be able to evaluate Power FX directly from JavaScript (an existing React application in my case). Blazor support two-way interop with JavaScript - calling JavaScript from .NET, and calling .NET from JavaScript. We will use the last option.
Once Blazor loads, you can execute DotNet.invokeMethodAsync
and DotNet.invokeMethod
from your JavaScript, passing the the required parameters - assembly name, method ID and arguments.
Everything which I implemented was done in Program.cs
, simply methods annotated with JSInvokable
attribute. The methods are almost the same as the ones provided in the host sample.
Once compiled, the application outputs wwwroot
folder with _framework
folder in it, which contains all the necessary code. Because I was testing things locally, and didn’t want to complicate the build process, I simply launched a http-server
from the wwwroot
folder on http://localhost:7080
. Next, I had to customize the React part of the sample. Luckily, the sample is very simple - it is a basic create-react-app scaffold with custom page, which displays the interactive formula bar and some debug information.
In the original sample, the communication is done via HTTP calls executed via async fetch
method. Replacing it was really simple. Instead of the original:
const result = await sendDataAsync('eval', JSON.stringify({ context, expression }));
The code now calls the following:
const result = await DotNet.invokeMethodAsync<string>("PowerFxWasm", "EvaluateAsync", context, expression);
But before I could call this method, I had to load the Blazor code into the existing page (this could have been done nicer, and will probably change as I move forward with this). Because I was hosting the script locally on a different host (React app runs on port 3000), the scripts and required resources weren’t loading due to CORS errors. The first fix was to run the http-server
with --cors
parameter. Because it was running technically on a different host, I had to also modify the boot of the Blazor app, to provide it with correct hostnames. Thanks to Github issue which was concerning something similar, I managed to get it up and running quite fast:
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = `${process.env.REACT_APP_PFX_WASM_HOST}/_framework/blazor.webassembly.js`;
script.setAttribute("autostart", "false");
script.crossOrigin = "anonymous";
script.onload = async () => {
await Blazor.start({
loadBootResource: function (type, name, defaultUri, integrity) {
console.log(`Loading: '${type}', '${name}', '${defaultUri}', '${integrity}'`);
switch (type) {
case 'dotnetjs':
return `${process.env.REACT_APP_PFX_WASM_HOST}/_framework/${name}`;
default:
return fetch(`${process.env.REACT_APP_PFX_WASM_HOST}/_framework/${name}`, {
credentials: 'omit'
});
}
}
});
ReactDOM.render(
<BrowserRouter basename={baseUrl}>
<PowerFxDemoPage />
</BrowserRouter>,
rootElement);
};
document.body.appendChild(script);
I created an environment variable called REACT_APP_PFX_WASM_HOST
which holds the current hostname, which is then used to load the resources correctly. To avoid CORS issues, we are omitting the credentials from the requests. Same goes for initial script append. If you are on the same host, you don’t need to do this.
The most important thing however is to await the Blazor.start
call, so that we continue only once the application is running (more on that here). The only bad thing is that the types are not fully available at the time of writing, so you may need to go with @ts-ignore
in your code. If you don’t await the start, you will likely end up with No .NET call dispatcher has been set
exception.
Once I made all the changes, I was able to run the code, which is now available on Github. And since it runs in the browser without any backend, it is available on GitHub Pages right from your browser (feel free to check with F12 that no API requests are made 😉).
Futures
So what next? Right now, this is just a simple Proof of Concept to see if it runs and to compare the performance. There are a few things which need to be configured - like tree shaking, since the total size of downloaded resources is 20MB, which is really excessive. Once that is handled, I would like to publish this to a CDN as a library, so that anyone can reference it from their code and use Power FX right away. Eventually with some wrappers to simplify the loading process.
Leave a Comment
Your email address will not be published. Required fields are marked *