Bun hosts .NET in its own process through hostfxr, the library behind the dotnet command. bun dotnet runs the .NET SDK, bun run app.cs runs a C# file, and the bun:dotnet module calls .NET code from JavaScript. Bun finds every .NET install on the machine and applies the same global.json and roll-forward rules as dotnet.
bun dotnet setup # .NET SDK, LTS channel
bun dotnet --version # the .NET SDK, running inside Bun
bun run app.cs # a .NET 10 file-based app
Install .NET
bun dotnet setup downloads .NET from the official release metadata (releases-index.json). It checks the archive's SHA-512 and extracts it. It runs no installer script, so PowerShell and bash are not needed.
bun dotnet setup # latest SDK of the LTS channel
bun dotnet setup --channel 10.0 # latest 10.0 SDK
bun dotnet setup --channel 10.0.1xx # latest SDK of the 10.0.1xx feature band
bun dotnet setup --version 10.0.100 # exact SDK version
bun dotnet setup --runtime aspnetcore --channel STS
bun dotnet setup --install-dir ./.dotnet --arch arm64 --dry-run --json
| Flag | Values | Default |
|---|---|---|
--channel | A.B, A.B.Cxx, LTS, STS, latest | A.B of --version, else LTS |
--version | an exact version from the channel | the newest release of the channel |
--runtime | dotnet, aspnetcore, windowsdesktop | the SDK |
--install-dir | a directory | %LOCALAPPDATA%\Microsoft\dotnet on Windows, ~/.dotnet elsewhere |
--arch | x64, x86, arm64 | the machine's architecture (musl Linux gets the linux-musl-* builds) |
--force | overwrite files that already exist | existing files are kept, like dotnet-install |
--dry-run | print the archive that would be installed | |
--json | print { product, version, rid, name, url, sha512, installDir, status } |
If the version is already installed, bun dotnet setup prints already installed and downloads nothing. Set BUN_DOTNET_FEED (or pass --feed) to read the release metadata from a mirror.
Find and resolve installs
Bun looks for .NET in all of these places, for every architecture:
DOTNET_ROOT_<ARCH>,DOTNET_ROOT,DOTNET_ROOT(x86)andDOTNET_INSTALL_DIR- On Windows, the registry key
HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\<arch>\InstallLocation(32-bit and 64-bit views),%ProgramFiles%\dotnetand%ProgramFiles(x86)%\dotnet - On Linux and macOS,
/etc/dotnet/install_location[_<arch>],/usr/share/dotnet,/usr/lib/dotnet,/usr/local/share/dotnet, Homebrew, snap and Nix locations ~/.dotnet, the user install directory, and everydotnetonPATH
Bun reads the install layout on disk (host/fxr, sdk, shared, metadata/workloads). It does not use the Windows Installer product list.
bun dotnet info replaces dotnet --info. It starts no process and no SDK. It lists the SDK the current directory selects, the host, workloads, SDKs and runtimes, every other install it found, the .NET Framework versions on Windows, and the DOTNET_* variables. --json prints the same data as an object.
bun dotnet resolve prints the SDK that dotnet would use in the current directory. It follows global.json (version, rollForward, allowPrerelease, paths, errorMessage) with the semantics of the .NET host. Pass a .runtimeconfig.json to see which shared framework versions an app binds to. That lookup follows rollForward, applyPatches, DOTNET_ROLL_FORWARD and DOTNET_ROLL_FORWARD_TO_PRERELEASE.
bun dotnet resolve
# 10.0.401
bun dotnet resolve bin/App.runtimeconfig.json
# Microsoft.NETCore.App 10.0.0 -> 10.0.12
Shell environment
bun dotnet env prints the variables that point a shell at the selected install: DOTNET_ROOT, DOTNET_HOST_PATH, and PATH with the install first.
eval "$(bun dotnet env --shell sh)" # bash, zsh
bun dotnet env --shell ps1 | Invoke-Expression # PowerShell
bun dotnet env --shell cmd > dotnet-env.cmd # cmd.exe
bun dotnet env --shell json
Bun caches the result per global.json in %LOCALAPPDATA%\bun\dotnet (or ~/.cache/bun/dotnet, or BUN_DOTNET_CACHE_DIR). It recomputes the result when an install directory, global.json or a DOTNET_* variable changes. bun dotnet sync recomputes it now. --refresh skips the cache for one call.
The .NET SDK inside Bun
Every other bun dotnet command goes to the .NET muxer, which runs in the Bun process: bun dotnet build, bun dotnet new console, bun dotnet test, and so on. A Bun executable named dotnet (or dotnet.exe) behaves like bun dotnet. If no install is found, bun dotnet says so and suggests bun dotnet setup.
bun run app.cs runs a .NET 10 file-based app, with #:package, #:sdk and #:property directives. It is the same as bun dotnet run --file app.cs -- <args>.
Files ending in .csjs, .csts and .cstsx are JavaScript, TypeScript and TSX that use .NET. Bun runs them like .js, .ts and .tsx, and csjs:dotnet is an alias of bun:dotnet there:
import dotnet from "csjs:dotnet";
console.log(dotnet.resolve().sdk?.version);
bun:dotnet
Installs
info(cwd?), env(cwd?, refresh?) and resolve(runtimeConfig?, cwd?) return the data of bun dotnet info --json, bun dotnet env --shell json and bun dotnet resolve --json. locate(dotnetRoot?) returns the install that bun:dotnet loads: { dotnetRoot, hostfxr, source, muxer, sdks, runtimes }.
import { info, resolve } from "bun:dotnet";
for (const install of info().installs) console.log(install.root, install.sdks);
resolve("bin/App.runtimeconfig.json").frameworks; // [{ name, requested, resolved }]
Native entry points
functionPointer returns the address of a static .NET method. unmanaged wraps it as a bun:ffi function. Methods marked [UnmanagedCallersOnly] need no delegateType. For other methods, pass the delegate type that matches their signature.
public static class Native
{
[UnmanagedCallersOnly]
public static int Add(int a, int b) => a + b;
}
import dotnet from "bun:dotnet";
const add = dotnet.unmanaged(
{ assembly: "./bin/Fixture.dll", type: "Fixture.Native, Fixture", method: "Add" },
{ args: ["i32", "i32"], returns: "i32" },
);
add(20, 22); // 42
The first call starts the CLR with the newest installed Microsoft.NETCore.App. Call initialize(runtimeConfig) first to start it from a .runtimeconfig.json. One CLR runs per process. initialize returns 0 when it started the CLR, and 1 or 2 when the CLR was already running. loadAssembly(path) loads an assembly into the default load context. Failures throw an Error with code: "ERR_BUN_DOTNET" and, for .NET errors, the hresult.
Object model
load(assembly) exposes .NET types as JavaScript objects through Node API for .NET. It needs the packages that @aphrody/bun-dotnet ships. Install that package, or set BUN_DOTNET_NODE_API to its dotnet/out/pkg directory.
import dotnet from "bun:dotnet";
const { Fixture } = dotnet.load("./bin/Fixture.dll");
Fixture.Calc.Add(20, 22); // 42, static method
const counter = new Fixture.Counter(5); // constructor
counter.Increment(); // instance method
counter.Value; // 6, property
await Fixture.Calc.EchoAsync("bun"); // "bun!": a Task<string> is a Promise
Fixture.Calc.Apply(x => x * 3, 14); // 42: a JS function passed as Func<int, int>
| .NET | JavaScript |
|---|---|
Task | Promise<void> |
Task<T> | Promise<T> |
Func<…>, Action<…> | function |
int, double, … | number |
string | string |
.NET events are not projected as JavaScript events. A .NET method can subscribe a delegate that calls a JavaScript function:
public void OnChanged(Action<int> listener) => Changed += (_, value) => listener(value);
generateTypes({ assembly, output }) writes TypeScript declarations for an assembly. Pass module: "esm" for ES module declarations and references for dependent assemblies.
dotnet(args), build, run, newProject and test run bun dotnet in a child process and resolve to { stdout, stderr, exitCode }.