A JavaScript Rules Engine in .NET using Jint

In the past, when I’ve needed a user-defined rules engine in .NET, I’ve explored writing a custom domain specific language using the Irony language implementation kit. But mostly, I’ve used SpringFramework.NET which includes an awesome expression evaluation engine.  The expression evaluation engine allowed writing string-based rules and even inline functions which allowed building a basic, user-configurable rules engine in .NET without too much fuss.

While the repository still shows commits, the library seems to have fizzled out and the maintainer has handed the reigns over to the community.

What to do if one needs a user-configurable, scriptable rules engine in 2022 with .NET 6?

Enter Jint.

Jint is a Javascript interpreter for .NET which can run on any modern .NET platform as it supports .NET Standard 2.0 and .NET 4.6.1 targets (and up). Because Jint neither generates any .NET bytecode nor uses the DLR it runs relatively small scripts really fast.

Checking the list of supported JavaScript features, it’s actually quite rich with only some more advanced features being excluded.  Let’s see how we can use this as a rules engine to build a system that allows for user-defined rules and scripts to be executed.

Getting Started

The full code for this example is here in GitHub: https://github.com/CharlieDigital/dotnet6-jint-rules-engine

To begin with, we’ll build a simple front-end that allows the user to pass in:

  1. A context or set of input parameters,
  2. A script which can consume that context and manipulate a resultant output response

When the users clicks EXECUTE, we run the user specified Script, passing in the context and then display the result in Result.

For the context, we want to pass in a JSON object which represents our inputs to our rules.  This can represent some current front-end state, some JSON representation of an entity, or other data context.

For example:

We can write a simple script to test this:

A few things to note:

  1. We can declare variables like msg and assign values to them
  2. I’m referencing the input context as ctx as in ctx.firstName and ctx.lastName
  3. I’m assigning the message to a property message on an object res

These object names are arbitrary and we’ll see how we can wire up Jint on the backend to execute this script.

Backend Script Execution

On the server, we want to receive a request which includes the context and the script (of course you can also load this script from a database or setting, for example).

On line 11, the request.Ctx is concatenated to the script to make the ctx variable available to our script.

Then on line 16, an ExpandoObject is passed in with the name res.  This allows our script to assign arbitrary properties to this object during runtime.  Sweet!

Finally, on line 19, we simply serialize the ExpandoObject that we passed into the engine and we get our result JSON.

This super simple code now allows us to execute JavaScript on the server on behalf of our user!  If we run this:


Adding Interop

Of course, this simple case is contrived.  In a real-world use case, we’d probably have the rules configured by an administrator that we’re pulling from a database.

We’d also want to do more complex things with those rules including potentially interacting with other services, making database calls, doing other useful things.  There are many ways that this can be done, of course, including dropping user code into a serverless runtime (e.g. AWS Lambda) dynamically.  But the beauty of using a tool like Jint is that it is more controlled and way simpler to implement and operate than dynamic deployment and orchestration of serverless functions.

I’d also argue that for most cases for user-defined scripts and rules, it’s probably safer to limit the capabilities of the runtime anyways.  (We’ll take a look later at how to limit the runtime).

For now, let’s introduce a mechanism to allow making an HTTP request and retrieving the content length.

To do so, we can create a simple HttpPlugin class:

It will simply make a request to the specified URL and return the content length.

To make this this available to the Jint engine, we simply add another line:

Now if we run our script:

Nice 🎉!  With this approach, we can hand over a pre-configured HTTP client that can do a number of things like setting up the authentication/authorization, limits on the requests, and other bits.  We can pre-build a library of actions that the script author can tap into and run in a controlled manner on the server.

But Wait, There’s More!

We can also include functions in our script and invoke them:

This means that it’s even possible to create a standard set of commands that you inject into your script (via concatenation) and allow your script authors to have access to standard actions.

Obviously, if you are allowing users to enter arbitrary script, you’ll want to be able to control the scope of execution in terms of resources.  The Jint documentation shows examples of some default constraints:

But it is also possible to implement custom constraints as well.

Conclusion

While the expression evaluation in SpringFramework.NET served me well in the past, Jint opens up a whole new set of options for building a JavaScript rules execution engine in .NET.  It’s incredibly easy to incorporate into your .NET solution and is really elegantly designed from a usability perspective.  Using this approach avoids the pitfalls and complexity of executing arbitrary JavaScript on the server (e.g. stuffing it into a Node container) while providing a flexible, controlled runtime for your user defined scripts.

You may also like...

2 Responses

  1. Daniele says:

    This is pretty cool. Do scripts run in a sandbox though? I would not want users trying to hijack into the system somehow…

    • Charles Chen says:

      Daniele,

      Not a sandbox per set, but the code is interpreted and run inside of the .NET application itself so the functionality is limited to what the interpreter allows. In this case, access to other libraries that can access the file system and network are not available so to provide those services to the script would require adding plugins on the .NET side and supplying them to the interpreter.

      It is a very cool tool to have in one’s toolbox 🙂