Executing JavaScript inside of .NET Core using JavaScriptServices

Posted on Wednesday, 28 Sep 2016

Recently, we were lucky enough to have Steve Sanderson speak at .NET South West, a Bristol based .NET meet up I help organise. His talk was titled SPAs (Single Page Applications) on ASP.NET Core and featured a whole host of impressive tools and APIs he's been developing at Microsoft, all aimed at aiding developers building single page applications (including Angular, Knockout and React) on the ASP.NET Core platform.

As Steve was demonstrating all of these amazing APIs (including  server side rendering of Angular 2/Reacts applications, Angular 2 validation that was integrated with .NET Core MVC's validation) the question that was at the end of everyone's tongue was "How's he doing this?!".

When the opportunity finally arose, Steve demonstrated what I think it one of the coolest parts of the talk - the JavaScriptServices middleware - the topic of this blog post.

Before continuing, if you develop single page apps in either Angular, React or Knockout then I'd highly recommend you check out the talk, which can also be found here.

What is JavaScriptServices?

JavaScriptServices is a .NET Core middleware library that plugs into the .NET Core pipeline which uses Node to execute JavaScript (naturally this also includes Node modules) at runtime. This means that in order to use JavaScriptServices you have to have Node installed the host machine.

How does it work and what application does it have? Let's dive in and take a look!

Setting up JavaScriptServices

Before we continue, it's worth mentioning that it looks like the package is currently going through a rename (from NodeServices to JavaScriptServices) - so you'll notice the API and NuGet package is referenced NodeServices, yet I'm referring to JavaScriptServices throughout. Now that that's out of the way, let's continue!

First of all, as mentioned above, JavaScriptServices relies on Node being installed on the host machine, so if you don't have Node installed then head over to NodeJs.org to download and install it. If you've already got Node installed then you're good to continue.

As I alluded to earlier, setting up the JavaScriptServices middleware is as easy as setting up any other piece of middleware in the in the new .NET Core framework. Simply include the JavaScriptServices NuGet package in your solution:

Install-Package Microsoft.AspNetCore.NodeServices -Pre

Then reference it in your Startup.cs file's ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddNodeServices();
}

Now we have the following interface at our disposal for calling Node modules:

public interface INodeServices : IDisposable
{
    Task<T> InvokeAsync<T>(string moduleName, params object[] args);
    Task<T> InvokeAsync<T>(CancellationToken cancellationToken, string moduleName, params object[] args);

    Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args);
    Task<T> InvokeExportAsync<T>(CancellationToken cancellationToken, string moduleName, string exportedFunctionName, params object[] args);
}

Basic Usage

Now we've got JavaScriptServices setup, let's look at getting started with a simple use case and run through how we can execute some trivial JavaScript in our application and capture the output.

First we'll begin by creating a simple JavaScript file that contains the a Node module that returns a greeting message:

// greeter.js
module.exports = function (callback, firstName, surname) {

    var greet = function (firstName, surname) {
        return "Hello " + firstName + " " + surname;
    }

    callback(null, greet(firstName, surname));
}

Next, we inject an instance of INodeServices into our controller and invoke our Node module by calling InvokeAsync<T> where T is our module's return type (a string in this instance).

public DemoController {

    private readonly INodeServices _nodeServices;

    public DemoController(INodeServices nodeServices)
    {
        _nodeServices = nodeServices;
    }

    public async Task<IActionResult> Index()
    {
        string greetingMessage = await _nodeServices.InvokeAsync<string>("./scripts/js/greeter", "Joseph", "Woodward");

        ...
    }

}

Whilst this is a simple example, hopefully it's demonstrated how easy it and given you an idea as to how powerful this can potentiall be. Now let's go one further.

Taking it one step further - transpiling ES6/ES2015 to ES5, including source mapping files

Whilst front end task runners such as Grunt and Gulp have their place, what if we were writing ES6 code and didn't want to have to go through the hassle of setting up a task runner just to transpile our ES2015 JavaScript?

What if we could transpile our Javascript at runtime in our ASP.NET Core application? Wouldn't that be cool? Well, we can do just this with JavaScriptServices!

First we need to include a few Babel packages to transpile our ES6 code down to ES5. So let's go ahead and create a packages.json in the root of our solution and install the listed packages by executing _npm install _at the same level as our newly created packages.json file.

{
    "name": "nodeservicesexamples",
    "version": "0.0.0",
    "dependencies": {
        "babel-core": "^6.7.4",
        "babel-preset-es2015": "^6.6.0"
    }
}

Now all we need to register the NodeServices service in the ConfigureServices method of our Startup.cs class:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddNodeServices();
    ...
}

After this we want to create our Node module that will invoke the Babel transpiler - this will also include the source mapping files.

// /node/transpilation.js

var fs = require('fs');
var babelCore = require('babel-core');

module.exports = function(cb, physicalPath, requestPath) {
    var originalContents = fs.readFileSync(physicalPath);
    var result = babelCore.transform(originalContents, {
        presets: ['es2015'],
        sourceMaps: 'inline',
        sourceFileName: '/sourcemapped' + requestPath
    });

    cb(null, result.code);
}

Now comes the interesting part. On every request we want to check to see if the HTTP request being made is for a .js extension. If it is, then we want to pass its contents to our JavaScriptServices instance to transpile it to ES6/2015 JavaScript, then finish off by writing the output to the output response.

At this point I think it's only fair to say that if you were doing this in production then you'd probably want some form of caching of output. This would prevent the same files being transpiled on every request - but hopefully the follow example is enough to give you an idea as to what it would look like:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, INodeServices nodeServices)
{
    ...

    // Dynamically transpile any .js files under the '/js/' directory
    app.Use(next => async context => {
        var requestPath = context.Request.Path.Value;
        if (requestPath.StartsWith("/js/") && requestPath.EndsWith(".js")) {
            var fileInfo = env.WebRootFileProvider.GetFileInfo(requestPath);
            if (fileInfo.Exists) {
                var transpiled = await nodeServices.InvokeAsync<string>("./node/transpilation.js", fileInfo.PhysicalPath, requestPath);
                await context.Response.WriteAsync(transpiled);
                return;
            }
        }

        await next.Invoke(context);
    });

    ...
}

Here, all we're doing is checking the ending path of every request to see if the file exists within the /js/ folder and ends with .js. Any matches are then checked to see if the file exists on disk, then is passed to the transpilation.js module we created earlier. The transpilation module will then run the contents of the file through Babel and return the output to JavaScriptServices, which then proceeds to write to our application's response object before invoking the next handler in our HTTP pipeline.

Now that's all set up, let's go ahead and give it a whirl. If we create a simple ES2016 Javascript class in a wwwroot/js/ folder and reference it within our view in a script tag.

// wwwroot/js/example.js

class Greeter {
    getMessage(name){
        return "Hello " + name + "!";
    }
}

var greeter = new Greeter();
console.log(greeter.getMessage("World"));

Now, when we load our application and navigate to our example.js file via your browser's devtools you should see it's been transpiled to ES2015!

Conclusion

Hopefully this post has giving you enough of an understanding of the JavaScriptServices package to demonstrate how powerful the library really is. With the abundance of Node modules available there's all sorts of functionality you can build into your application, or application's build process. Have fun!

Back