Windows only, Electron-like desktop apps on .NET Native AOT + WebView2. One executable, no runtime to install, no Chromium to ship, Windows already has both. x86, x64 and ARM64.
A window is a real HWND, the UI is a web page, and the two talk over a typed bridge. That's the whole idea. The Fluent UI gallery below is an AOTrino app, and it weighs 4 MB.
dotnet new install AOTrino.Templates
dotnet new aotrino -o MyApp && cd MyApp
dotnet run
AOTrino is a stack of optional layers. Each one is useful without the ones above it, and the bottom layer
alone is a first-class way to use it, most of the samples here are a hand-written index.html and no npm at
all.
graph BT
A["<b>AOTrino</b>, the NuGet<br/><i>C#: the window, the WebView, the bridge, the exe</i>"]
B["<b>@aotrino/client</b><br/><i>TypeScript types over the injected runtime</i>"]
C["<b>@aotrino/react</b><br/><i>hooks and the caption gesture. Behaviour, no CSS</i>"]
D["<b>@aotrino/fluent</b><br/><i>a Fluent UI look. A choice, not a mandate</i>"]
A --> B --> C --> D
style A fill:#0f6cbd,color:#fff,stroke:#0f6cbd
style B fill:#2899f5,color:#fff,stroke:#2899f5
style C fill:#77b7f7,color:#000,stroke:#77b7f7
style D fill:#b4d6fa,color:#000,stroke:#b4d6fa
| Level | You write | You get | Start with |
|---|---|---|---|
| Plain | index.html |
A window, and .NET at the other end of chrome.webview.hostObjects |
dotnet new aotrino |
| + client | TypeScript | The bridge, typed: host<MyApi>(), appWindow, shared buffers |
dotnet new aotrino + npm i @aotrino/client |
| + react | React | useHostCall / useHostValue, a drag region that maximizes on double-click |
dotnet new aotrino-react |
| + fluent | Fluent UI | A window that follows the Windows theme, caption included | dotnet new aotrino-fluent |
Nothing above the first level is required by the first level. And @aotrino/client adds no runtime: the C#
side already injected it, so the package is types and a thin wrapper over what's on the page, which is what
keeps the two from drifting apart.
graph LR
subgraph EXE["MyApp.exe, one file, ~11 MB, or 4 MB packed"]
NET["<b>.NET AOT</b><br/>AOTrinoWindow<br/>your host objects"]
WR["<b>WebRoot</b><br/><i>embedded resources,<br/>extracted at startup</i>"]
end
WV["<b>WebView2</b><br/><i>the Edge already on the machine</i>"]
NET -- "host objects, shared buffers" --> WV
WV -- "window.__aotrino, postMessage" --> NET
WR -- "index.html" --> WV
style NET fill:#0f6cbd,color:#fff,stroke:#0f6cbd
style WR fill:#2899f5,color:#fff,stroke:#2899f5
style WV fill:#5c5c5c,color:#fff,stroke:#5c5c5c
To build and run, the .NET 10 SDK is all you need. The WebView2 runtime is already on any up-to-date Windows, and an AOTrino app offers a download link if it isn't.
To publish the single exe, add the MSVC linker. The SDK compiles to native code but doesn't ship a linker,
so dotnet publish ends in link.exe and stops with "Platform linker not found" without it. No IDE required,
the standalone build tools are enough:
winget install Microsoft.VisualStudio.BuildTools --override "--passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
Add --add Microsoft.VisualStudio.Component.VC.Tools.ARM64 for ARM64. If you already have Visual Studio, the
same thing lives in the installer as Desktop development with C++. Full list:
aka.ms/nativeaot-prerequisites.
dotnet new aotrino -o MyApp
cd MyApp && dotnet run
Three source files and a page. WebRoot\dist\index.html is embedded in the exe: edit it and rebuild. To call
.NET, register a host object:
protected override void RegisterHostObjects() => AddHostObject("app", new MyApi(this));const api = chrome.webview.hostObjects.app; // every member is async, even a property read
document.title = await api.getTitle();dotnet new aotrino-react -o MyApp
cd MyApp && dotnet run
dotnet build runs npm install and Vite for you. The caption is yours to style, the behaviour comes from
the package:
import { TitleBar, useHostCall } from "@aotrino/react";
<TitleBar showMinimize showMaximize /> // drag, double-click to maximize, window buttons
const ping = useHostCall(() => api.ping()); // pending/result/error, no useEffectdotnet new aotrino-fluent -o MyApp
cd MyApp && dotnet run
<AOTrinoProvider> {/* follows the Windows app theme, live */}
<TitleBar /> {/* caption, window buttons and a theme picker, in Fluent's own tokens */}
<MyContent />
</AOTrinoProvider>dotnet publish -r win-x64 -c Release
One exe, about 11 MB, nothing to install alongside it. UPX takes x86/x64 to roughly
4 MB (it can't pack ARM64 yet). In this repo, publish.bat -upx does every sample for all three architectures
in one go.
Note: for AOT, ensure you have all the required prerequisites, documented at https://aka.ms/nativeaot-prerequisites, in particular the Desktop Development for C++ workload in Visual Studio.
Fifteen of them, in Samples, each one is a single idea. The level each is written at is noted: most need no npm at all.
Plain HTML. The smallest AOTrino app: a window, a page, a version string. If you read one sample, read this one, it's a complete desktop application in about 40 lines.
Plain HTML. What crosses the bridge: properties, methods, Task<T> as a real promise, exceptions as
rejections, arrays, JSON. The live reference for docs/BRIDGE.md.
Plain HTML. .NET draws with Direct2D into a shared buffer and the page shows it in a <canvas> with no
copy, the path for pixels, which the bridge is the wrong tool for.
Plain HTML. The two hosting models side by side in one process: the WebView as a Direct Composition visual (the default) and as a classic child HWND. Same page, different windows.
Plain HTML. A Windows 11 system backdrop, Mica, Acrylic, Tabbed, showing through a page that leaves its edges transparent. Only possible because the WebView is a Direct Composition layer and not an opaque child window.
Plain HTML. The left half is the page, the right half is a live Windows.Graphics.Capture of the screen,
drawn with Direct2D and composited beside the page by .NET, which is not something a web page gets to do.
Plain HTML + an injected script. NavigationMode.Web: the one sample that browses the real internet, its
chrome injected into every document. It registers no host objects, docs/SECURITY.md
explains why that's worth being deliberate about. Takes --url.
Plain HTML. A local file browser over a host object, with a preview pane: what "a desktop app, not a web
page" actually buys. Drag files out to Explorer and drop them back in, which a page can do in neither
direction, and dropping copies into the folder on screen with real paths rather than File objects. Also where
SystemInfo is demonstrated.
Plain HTML. A file manager over the whole Windows shell namespace, not the file system: the same code browses
This PC, an iPhone or Android over MTP (Media Transfer Protocol, no drive letter), and a .zip or .7z opened
as a folder, because the backend is
IShellItem and not a path. Real shell thumbnails, previews that pair WebView2 with WIC for the widest reach
(a .heic the browser can't read is decoded by an installed codec), a real ConPTY console, the real right-click
shell menu, and live change notifications with toasts. The listing streams and virtualizes, so a folder of any size
opens at once. The shell-namespace counterpart to File Explorer above.
React + @aotrino/client + @aotrino/react. Hello World one level up: a typed host object, hooks, and a caption whose drag and double-click come from the package rather than from this app.
React + @aotrino/react. Live .NET process state, working set, GC, threads, with auto-refresh. No
useEffect, no Promise.all and no polling timer in the app: the hooks own all of it.
Fluent UI, the whole pyramid. One window that reads the host, calls .NET, and re-themes itself from the caption's sun/moon button. Almost no CSS of its own: Fluent's tokens do it.
Fluent UI, the flagship. A WinUI-Gallery-shaped tour where every card is a live demo with the code that produced it. Includes a table of 500,000 rows that lives in .NET and crosses the bridge 200 at a time.
Plain HTML. Every string in the window, the page and the caption alike, comes from a .resx compiled into the
executable, and a combo switches language with nothing restarting. It also shows the negotiation
a page cannot do for itself: the ordered list of languages the user chose in Windows, and the best match the app
can actually serve from it.
Blazor WebAssembly. C# on both sides of the bridge, wasm in the page and Native AOT in the host, with the
DTOs and the source-generated JSON in one shared source project, so the two cannot disagree about the wire
format. It scans a drive by walking it, or by reading the NTFS master file table in seconds when it is
elevated, and draws the result as a full depth treemap, every directory on the drive, tens of thousands of
tiles redrawn while the scan is still growing the tree. That map is Direct2D into a shared buffer rather than
DOM, and the reason is worth reading in Treemap.cs: the drawing is not the hard part, carrying that many
rectangles across the bridge every frame is.
- Architecture and maintenance, what the repo is made of, where every version lives, what to do weekly/monthly/every six months, and the traps.
- Security model, what the defaults are (local-first,
NavigationMode), what they aren't, and where the decisions stay yours. - The bridge, how JS calls .NET and back: host objects, what crosses, async results, exceptions, and the escape hatch.
- Front end, hand-written pages need nothing, the optional
@aotrino/clienttypes the bridge for React/TypeScript apps, without a registry. - Localization, one place where a string is written, whatever the front end is made of, and why the catalog crosses the bridge instead of each string.
- Theming, light/dark that follows Windows, picked from the caption and remembered, what the
FluentUIsamples get for two lines, and how to change it.
git clone https://github.com/aelyo-softworks/AOTrino
dotnet build AOTrino.slnx
publish.bat -upx rem every sample, AOT, x86 + x64 + ARM64, compressed
That should be all of it: the interop assemblies come from the published
DirectNAot,
DirectNAot.Extensions and
WebView2Aot packages, the samples that use it also take
WicNetCore and
ShellN.Extensions the same way, and the build takes care of npm.
Nothing else to install, nothing to run first. Override a version with -p:DirectNAotVersion=... or
-p:WebView2AotVersion=.... See docs/MAINTENANCE.md.
MIT. See LICENSE.
















