Hey there! Ever hit that wall where rewriting a rock-solid .NET library just to stay "pure JS" feels like insanity? Or maybe you're staring at legacy cryptographic code that's been battle-tested for years, and the thought of porting it makes you want to cry into your coffee. Yeah, we've all been there – sometimes you just need TypeScript's flexibility and .NET's raw power without losing your mind.
Maybe you're dealing with legacy .NET libraries that you can't (or don't want to) rewrite, or you need cryptographic signing that's already battle-tested in C#. Or perhaps you're building a Node.js app that needs to tap into Windows-specific APIs without dragging the whole app into .NET-land. Sound familiar?
Here's the thing: this pattern isn't some wild experiment – it's proven in production. VS Code itself uses TypeScript for the editor while spawning .NET processes for features like OmniSharp (C# IntelliSense) and debugging adapters. GitHub's Copilot extension? Same deal – TypeScript orchestration with heavyweight AI inference running in separate processes. Electron apps do this all the time when they need OS-level integrations. The sidecar pattern lets each technology do what it does best, without forcing everything into a single runtime.
So here's what I thought: why not create a minimal, no-BS template that shows exactly how to wire this up? This PoC strips away the domain-specific stuff (originally built for electronic prescriptions in healthcare, but that's beside the point) and gives you the bare essentials: TypeScript spawning a .NET sidecar, managing its lifecycle, and communicating cleanly across Linux and Windows. Clone it, adapt it, solve your own interop headaches. If this saves you a day of Stack Overflow rabbit holes, I'll call it a win. Let's dive in!
Here's the backstory: I originally built this for Japan's electronic prescription system (yeah, healthcare tech – fun times). The catch? There was this digital signature library that started life as a Java implementation, then got ported to .NET, and I absolutely had to use it. No alternatives, no shortcuts – it was the only game in town for compliance reasons.
But here's where it got interesting. I wanted to run this whole thing on Linux (because vendor lock-in is the worst), but I also needed a UI layer that didn't feel like a 2005 Windows Forms throwback. So I thought: why not split it? Use TypeScript for the flexible, cross-platform UI stuff, and let .NET handle the heavyweight cryptographic signing under the hood. Spoiler alert: it worked beautifully.
Once I proved the pattern worked, I realized this setup isn't just useful for prescription signatures – it's a universal solution for any TS + .NET interop scenario. So I stripped out all the healthcare-specific bits (JAHIS standards, HPKI cards, XML schemas – you don't need to know any of that) and distilled it down to the bare essentials: how to spawn a .NET sidecar from TypeScript, keep them in sync, and handle cross-platform quirks gracefully.
What you're looking at:
- TypeScript Side (ts-app): Acts as the "launcher" or main app. It spawns the .NET executable as a subprocess, manages its lifecycle (including graceful shutdown when the parent exits), and handles platform-specific paths (Linux vs. Windows).
- .NET Side (dotnet-sidecar): A minimal console app that simulates a long-running service. It monitors stdin for parent disconnection (a key sidecar survival pattern), logs its activity, and shuts down cleanly when told to.
No fancy IPC protocols (no gRPC, no message queues) – just stdin/stdout and process signals. Because boring, obvious solutions are often the ones that actually survive production.
Here's what happens when you run this PoC:
sequenceDiagram
participant TS as TypeScript<br/>(launcher.ts)
participant OS as OS Process Manager
participant NET as .NET Sidecar<br/>(Program.cs)
TS->>OS: spawn("dotnet-sidecar") with stdin/stdout pipes
OS->>NET: Start new process
activate NET
NET->>NET: Task.Run: Start stdin monitor
NET-->>TS: stdout: "Sidecar started. PID: X"
NET-->>TS: stdout: "gRPC server listening..."
NET->>NET: Main thread: while(true) await Task.Delay(1000)
Note over TS,NET: Both processes running independently
TS->>TS: Demo timer (5s) expires
TS->>TS: process.exit(0)
TS->>OS: Terminate and close pipes
Note over NET: stdin monitoring task detects closure
NET->>NET: Console.In.Peek() returns -1
NET-->>TS: stdout: "Parent disconnected. Shutting down."
NET->>NET: Environment.Exit(0)
deactivate NET
OS->>OS: Clean up process resources
Key mechanism: The .NET sidecar runs two things simultaneously:
- Main thread: An infinite loop (
while(true) await Task.Delay(1000)) simulating long-running work - Background task: Continuously monitors
Console.In.Peek()for stdin closure
When the TypeScript parent exits, the OS closes the stdin pipe. The background task detects this (Peek() returns -1) and calls Environment.Exit(0), terminating the entire process. No heartbeats, no polling – just OS-level pipe behavior doing the heavy lifting.
- Cross-OS Compatibility: Tested on Ubuntu (Linux) and Windows – the .NET binary runs without a hitch on both.
- Subprocess Magic: Uses Node.js's child_process to spawn and communicate with the .NET app via stdin/stdout. No gRPC or fancy IPC yet – keeping it simple for verification.
- Build Artifacts: The ts-app/bin folder contains platform-specific executables (e.g., for Linux and Windows), making it easy to distribute or containerize.
In short: If TS can ping .NET and get a pong back, we're golden for expanding to real JAHIS data handling and XAdES signing down the line.
Here's the file tree for quick navigation:
.
├── dotnet-sidecar
│ ├── Program.cs # The .NET console app entry point
│ ├── bin # Build outputs (Debug/Release)
│ ├── dotnet-sidecar.csproj # .NET project file
│ └── obj # Intermediate build files
└── ts-app
├── bin # Compiled TS binaries (Linux/Windows executables)
├── launcher.ts # Main TS script to launch and communicate with .NET
├── node_modules # NPM dependencies
├── package-lock.json # Locked dependencies
├── package.json # TS project config and scripts
└── tsconfig.json # TypeScript compiler optionsWant to run this yourself? It's straightforward – no external deps beyond Node.js and .NET SDK.
- Node.js (v18+ recommended)
- Download from the official Node.js website
- For Linux users: You can also use your package manager (e.g.,
apt install nodejs npmon Ubuntu) or refer to NodeSource distributions for the latest versions - For Windows users: Download the installer from nodejs.org or use a package manager like Chocolatey (
choco install nodejs)
- .NET SDK 8.0+
- For Linux users: Refer to Microsoft's official guide for installing .NET on Linux
- For example, if you're on Ubuntu 22.04, see this specific installation guide
- For Linux: Basic build tools (e.g., apt install build-essential on Ubuntu)
-
Clone the Repo:
git clone https://github.com/sakai-sktech/open-rx.git cd open-rx -
Build the .NET Sidecar:
-
Navigate to dotnet-sidecar:
cd dotnet-sidecar -
Build for Linux (if on Linux or for cross-platform deployment):
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o ../ts-app/bin/linux -
Build for Windows (if on Windows or for cross-platform deployment):
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ../ts-app/bin/win -
This creates a self-contained, single-file executable in
ts-app/bin/linux/orts-app/bin/win/(e.g.,dotnet-sidecarfor Linux,dotnet-sidecar.exefor Windows). -
The
--self-containedflag bundles the .NET runtime, so end users don't need .NET installed. -
On Linux, you may need to grant execute permissions:
chmod +x ../ts-app/bin/linux/dotnet-sidecar
-
-
Build the TS App:
-
Navigate to ts-app:
cd ../ts-app npm install tsc # Or use `npm run build` if scripted
-
This compiles launcher.ts to JS, and you can package it with pkg or similar for binaries in bin/ (as in your setup).
-
-
Run the PoC:
-
From the project root directory, execute:
npx ts-node ts-app/launcher.ts
-
Expected output:
[TS] Launching sidecar: /home/sakai/dev/ukhpkimock/ts-app/bin/linux/dotnet-sidecar [TS] Application running. Will exit in 5 seconds... [.NET]: [.NET] Sidecar started. PID: 91939 [.NET]: [.NET] OS: Unix 6.14.0.36 [.NET]: [.NET] gRPC server listening... (simulated) [TS] Demo complete. Exiting. [TS] Shutting down. Terminating sidecar...
-
Note: You may see a deprecation warning
(node:xxxxx) [DEP0180] DeprecationWarning- this is harmless and can be ignored. -
The demo automatically exits after 5 seconds, gracefully terminating the .NET sidecar.
-
Test on both Linux and Windows to confirm cross-platform compatibility.
-
If things go sideways (e.g., path issues), check console logs – we've kept error handling verbose for debugging.
Common Issues:
- "dotnet-sidecar not found": Ensure you've built the .NET sidecar in step 2
- "Permission denied": On Linux, the binary may need execute permissions:
chmod +x ts-app/bin/linux/dotnet-sidecar - TypeScript errors: Run
npm installin the ts-app directory to ensure all dependencies are installed
This isn't just about spawning processes – it's about freedom of choice. Want to leverage battle-tested .NET libraries without abandoning your TypeScript ecosystem? Need to escape vendor lock-in by running on Linux? This pattern gives you both, without compromise.
The sidecar approach means you're not rewriting working code just to fit a technology mandate. You're composing solutions from the best tools available, regardless of runtime. That's what sustainable software looks like.
This PoC is intentionally minimal – it's a template, not a complete solution. The goal here is to show you the plumbing works, so you can use it as a starting point for your own projects.
Want to see this pattern in production? Check out Real-World Examples – OmniSharp, Prisma, GitLens, and more use this exact architecture.
Got a use case where TypeScript's flexibility meets .NET's power? Maybe you need to call a legacy .NET library from a modern Node.js app, or vice versa. This setup gives you a proven pattern for:
- Spawning platform-specific binaries from TS
- Managing parent-child process lifecycles
- Handling cross-platform paths and signals
- Building self-contained executables for easy distribution
Take this, tweak it, build something cool. That's what open source is all about, right? If you end up using this in a project (especially in healthcare or other regulated industries), I'd love to hear about it – drop a note or open a discussion!
And hey, if you spot ways to improve this PoC, PRs are always welcome. Let's make inter-process communication less painful for everyone.
Feel free to fork, PR, or open issues! We're all about open-source vibes in healthcare tech.
This project is licensed under the MIT License - see the LICENSE file for details.
Thanks for checking this out! If this PoC sparks ideas or saves you time, star the repo or drop a note. Let’s make cross-runtime interop boring, reliable, and lock-in-free.