The C# Dev Kit team at Microsoft recently made a smart move: instead of using traditional C++ native addons built with node-gyp for platform-specific tasks like reading the Windows Registry, they now use C# with .NET Native AOT. This shift reduces developer friction, simplifies CI pipelines, and leverages the team's existing .NET expertise. Below, we explore how and why this works through a series of questions and answers.
Why did the C# Dev Kit team move from C++ to C# Native AOT for Node.js addons?
The traditional approach required node-gyp to compile C++ addons during installation. This meant every developer needed an old version of Python, a tool they'd rarely touch directly. For a .NET-focused team, this added unnecessary complexity and friction. New contributors had to set up Python, and CI pipelines had to provision and maintain it, slowing builds and adding extra dependencies. Since the team already had the .NET SDK, using C# and Native AOT streamlined everything. Now, addons are built with familiar .NET tools, eliminating the Python requirement and reducing maintenance overhead. The result is a more efficient engineering system with fewer moving parts.

How do Node.js native addons actually work?
A Node.js native addon is essentially a shared library (e.g., .dll on Windows, .so on Linux, .dylib on macOS) that exports a specific entry point. When Node.js loads this library, it calls the function napi_register_module_v1. The addon then registers any functions it provides, and from that point on, JavaScript treats it like any other module. The interface enabling this is N-API (Node-API), a stable, ABI-compatible C API for building addons. N-API doesn't care what language produced the shared library—only that it exports the right symbols and calls the right functions. This flexibility is key to using .NET Native AOT, which can produce shared libraries with arbitrary native entry points.
What makes N-API suitable for use with .NET Native AOT?
N-API requires only that the shared library exports napi_register_module_v1 and uses the N-API functions for registration and operations. It is a C API with a stable ABI, so the calling convention and data structures are fixed. .NET Native AOT compiles C# code into a native shared library that can expose functions via [UnmanagedCallersOnly]. Because N-API does not mandate the implementation language, you can write the addon entirely in C# and still meet all the requirements. This opens the door for .NET developers to create Node.js addons without learning C++ or dealing with node-gyp's quirks. The AOT compilation ensures the output is a native binary that Node.js can load directly.
What does the project file for a .NET Native AOT addon look like?
The project file is minimal and focuses on three key settings. First, you set the target framework to a recent .NET version like net10.0. Second, you enable PublishAot to instruct the SDK to produce a native shared library instead of a managed assembly. Third, you allow unsafe code with AllowUnsafeBlocks because N-API interop involves function pointers and fixed buffers. Here's a concise example:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
After building and publishing with dotnet publish, you get a shared library (e.g., addon.dll) that Node.js can load.

How do you define the module entry point for Node.js in C#?
Node.js expects the shared library to export napi_register_module_v1. In C#, you use the [UnmanagedCallersOnly] attribute on a static method to mark it as the entry point. You specify the entry point name and the calling convention (typically CallConvCdecl). The method receives two parameters: env (a pointer to the N-API environment) and exports (a pointer to the exports object). Inside, you initialize any needed state and then register your functions using N-API calls. For example:
[UnmanagedCallersOnly(EntryPoint = "napi_register_module_v1", CallConvs = [typeof(CallConvCdecl)])]
public static nint Init(nint env, nint exports)
{
// Initialize, then register functions
RegisterFunction(env, exports, "readStringValue"u8, ...);
return exports;
}
This setup lets C# code act as a fully compliant Node.js addon.
Can you show a simple example of reading the Windows Registry with this approach?
Yes, the full addon would include a function like readStringValue that uses .NET's Microsoft.Win32.Registry or direct P/Invoke to query the registry. For brevity, the core steps are: inside the Init method, define a native function pointer from a C# lambda or method, then call napi_create_function and napi_set_named_property to expose it to JavaScript. The function itself would accept a key path and value name as parameters, and return the string value. On the Node.js side, you load the addon with require('./build/addon.node') and call addon.readStringValue(...). The same pattern works for other platform-specific tasks, making it easy to reuse.
What are the overall benefits of using .NET Native AOT for Node.js addons?
Switching to C# and Native AOT eliminates the need for node-gyp and an old Python installation. Developers on the .NET team no longer need to manage a separate toolchain, and CI pipelines become simpler and faster. Additionally, C# offers strong typing, modern language features, and better tooling within the Visual Studio ecosystem. The approach also maintains compatibility: the addon still uses the stable N-API, so it works with any Node.js version that supports N-API. Finally, because .NET Native AOT produces a true native binary, performance remains on par with C++ addons. The team at Microsoft has proven this is a viable, streamlined alternative for .NET–focused projects.