AphrodyBun GitHub

Runtime docs · Runtime · Interop & Tooling

.NET

Run the .NET SDK inside Bun, install and resolve .NET without scripts, run C# files, and call .NET assemblies from JavaScript with bun:dotnet

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
FlagValuesDefault
--channelA.B, A.B.Cxx, LTS, STS, latestA.B of --version, else LTS
--versionan exact version from the channelthe newest release of the channel
--runtimedotnet, aspnetcore, windowsdesktopthe SDK
--install-dira directory%LOCALAPPDATA%\Microsoft\dotnet on Windows, ~/.dotnet elsewhere
--archx64, x86, arm64the machine's architecture (musl Linux gets the linux-musl-* builds)
--forceoverwrite files that already existexisting files are kept, like dotnet-install
--dry-runprint the archive that would be installed
--jsonprint { 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) and DOTNET_INSTALL_DIR
  • On Windows, the registry key HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\<arch>\InstallLocation (32-bit and 64-bit views), %ProgramFiles%\dotnet and %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 every dotnet on PATH

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:

main.csts
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.

Native.cs
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>
.NETJavaScript
TaskPromise<void>
Task<T>Promise<T>
Func<…>, Action<…>function
int, double, …number
stringstring

.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 }.