Running JavaScript inside a .NET app with JavaScriptEngineSwitcher

There is a recurring situation in .NET projects where the library you actually want only exists in JavaScript. Syntax highlighting is the classic example. C# has options like TextMateSharp, but it wraps a native Oniguruma dependency and adds real deployment friction. JavaScript, meanwhile, has mature, well maintained highlighters like highlight.js and Prism.js with simple APIs and no native dependency headaches on the JS side.

Pulling in an entire Node.js toolchain just to run one small library feels like overkill, and it usually is. The less obvious option is to run the JavaScript engine directly inside the .NET process itself, no Node.js installation required on the host at all.

The Engines Available and Their Trade-offs

Before picking a library to wire this up, it helps to know what is actually running underneath, since the engines split into two genuinely different categories with different deployment implications.

Jering.Javascript.NodeJS is the odd one out, it does not embed an engine at all, it shells out to a Node.js installation already present on the machine. That solves nothing if avoiding a Node.js dependency was the whole point, but it is a reasonable choice if Node is already guaranteed to be present in your deployment environment.

ChakraCore, the engine that used to power pre-Chromium Microsoft Edge, and Microsoft.ClearScript, a wrapper around Google’s V8 engine, both genuinely embed a real JavaScript engine inside your app. Both are native dependencies underneath a C# P/Invoke layer, which means real deployment considerations, you need the correct native binary for each target platform and architecture, and that complexity does not disappear just because the C# API looks clean.

Jint and Jurassic take a different approach entirely, both are JavaScript engines implemented purely in managed .NET code, with zero native dependencies to manage. Jint is an interpreter supporting ES5. Jurassic also targets ES5, with partial ES6 support, but instead of interpreting JavaScript line by line, it compiles the script down to IL, which tends to make it noticeably faster for anything beyond trivial scripts, at the cost of a bit more startup overhead compiling that IL in the first place.

None of these is a universally correct choice. If deployment simplicity matters most and your scripts are small, Jint or Jurassic avoid native dependency headaches entirely. If you need broader modern JavaScript support or maximum raw performance and can tolerate managing a native dependency, ClearScript’s V8 wrapper is the more capable engine.

JavaScriptEngineSwitcher: One API, Many Engines

The genuinely useful piece here is not any single engine, it is JavaScriptEngineSwitcher, a wrapper library providing a consistent C# API across all of the engines above, plus a few more. Each engine gets its own NuGet package, alongside a shared Core package defining the common interface.

Even if you have no intention of ever switching engines, defaulting to JavaScriptEngineSwitcher rather than a given engine’s native API is worth doing anyway, since changing your mind later becomes a package swap and a small init tweak instead of a rewrite. This is a more realistic scenario than it sounds, since a script that runs fine in a lightweight interpreter during development can hit real performance limits once real workloads land on it, at which point migrating to a faster engine is exactly the kind of change you do not want to be doing by hand across every call site.

Worked Example: Running Prism.js from a Console App

Here is the syntax highlighting scenario, worked through concretely. Start by adding the Jurassic engine package, since it has no native dependency to worry about for this example.

dotnet add package JavaScriptEngineSwitcher.Jurassic

Next, download the actual prism.js build you want, Prism’s own site lets you pick a theme and language set, then embed the resulting file directly into the assembly as an embedded resource rather than shipping it as a loose file on disk.

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
  <ItemGroup>
    <PackageReference Include="JavaScriptEngineSwitcher.Jurassic" Version="3.17.4" />
  </ItemGroup>
 
  <!-- Make prism.js an embedded resource -->
  <ItemGroup>
    <None Remove="prism.js" />
    <EmbeddedResource Include="prism.js" />
  </ItemGroup>
 
</Project>

Embedding it this way means the JS file ships inside the compiled assembly itself, no separate file to lose track of during deployment or worry about a relative path breaking in a different working directory.

With that in place, spinning up the engine and loading the library takes two lines.

using JavaScriptEngineSwitcher.Jurassic;
 
// Create an instance of the JavaScript engine
using IJsEngine engine = new JurassicJsEngine();
 
// Execute the embedded resource, loading prism.js into the engine's global scope
engine.ExecuteResource("JsInDotnet.prism.js", typeof(Program).Assembly);

From here, the engine behaves like a small sandboxed JavaScript runtime you can push values into and pull results back out of, using SetVariableValue, Execute, and Evaluate.

string code = @"
using System;
 
public class Test : ITest
{
    public int ID { get; set; }
    public string Name { get; set; }
}";
 
// Push the C# variable into the JS engine's global scope
engine.SetVariableValue("input", code);
engine.SetVariableValue("lang", "csharp");
 
// Call Prism's highlight function inside the engine
engine.Execute($"highlighted = Prism.highlight(input, Prism.languages.csharp, lang)");
 
// Pull the resulting string back out into C#
string result = engine.Evaluate<string>("highlighted");
 
Console.WriteLine(result);

Running this prints the highlighted markup straight to the console, generated entirely by a real copy of Prism.js executing inside the .NET process, with no Node.js runtime installed anywhere on the machine.

When This Approach Actually Makes Sense

This pattern earns its keep when you need one small, well-contained JavaScript library and nothing more, syntax highlighting is the textbook case, but templating helpers, small validation libraries, or a specific algorithm implementation that only exists in JS all fit the same shape. The overhead of embedding an engine is worth it precisely because the alternative, standing up a whole Node.js toolchain for one library, is disproportionate to what you actually need.

It stops making sense once your JavaScript workload grows past that. If you find yourself running a lot of JavaScript, coordinating multiple libraries, or leaning on npm’s package ecosystem broadly rather than one specific tool, you are fighting the platform at that point. Go use Node.js properly, either as a genuine part of your stack or as a sidecar process your .NET app talks to over HTTP, rather than continuing to stretch an embedded engine to cover a job it was never meant to do.

If you are choosing between managed engines like Jint or Jurassic versus a native one like ClearScript’s V8 wrapper purely on performance grounds, benchmark with your actual script and workload rather than trusting general reputation. A compiled engine like Jurassic can outperform an interpreter on CPU-heavy repeated calls, but V8 is a different order of engineering entirely and will usually win on anything computationally serious, the trade-off is accepting the native dependency and its deployment overhead in exchange for that ceiling.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading