Introduction
A Virtufin worker is a self-contained .NET 10 class library packaged as a NuGet nupkg. The WorkManager loads the worker into its process via one of two engines:
DotNetDllEngine(default) — loads the nupkg'slib/<tfm>/*.dllinto a per-worker collectibleAssemblyLoadContext. Requires the WM process to have a CLR runtime (which it does, via theDotNetDll.Bridge— a separate managed DLL the AOT-published WM embeds vialibhostfxr).NativeDllEngine— loads the nupkg'sruntimes/<rid>/native/lib*.sointo the host process viaNativeLibrary.Load. Does not require a CLR; the worker exports C-ABI functions directly (Process,FreeResult).
The MIME type sent at CreateWorker time selects the engine: application/x-dotnet-dll → DotNetDllEngine, application/x-native-dll → NativeDllEngine. The same nupkg can be registered under both MIME types if it ships both layouts (the --format both packaging mode).
The IGatewayClient abstraction
The worker's handler logic does not care which engine loaded it. Handler code calls backend services through IGatewayClient:
var response = await gateway.InvokeAsync(
"websocketmanager", // service
"Connect", // method
new JsonObject { ["url"] = url, ... }); // request
Two adapters implement IGatewayClient:
ManagedGatewayAdapter— wrapsVirtufin.Api.Client.GatewayClient.InvokeAsync(managed gRPC). The base classApiCommandWorker<T>returns this from itsGatewayproperty.AotGatewayAdapter— wrapsWorkerContext.InvokeGateway(host-installed C-ABI function pointer). The AOT shim'sGatewayproperty returns this.
A worker author writes one IGatewayClient-using block of code; it works in both targets.
The shared-handler pattern
The recommended worker shape is three files per worker:
src/
├── Virtufin.Worker.MyWorker.Managed/ # managed csproj
│ ├── MyWorker.csproj
│ ├── MyWorker.cs # ~30 LOC managed shim
│ └── MyWorkerLogic.cs # ~700 LOC shared logic
└── Virtufin.Worker.MyWorker.Native/ # AOT csproj
├── MyWorker.Native.csproj
└── MyWorkerNative.cs # ~50 LOC AOT shim
The shared MyWorkerLogic.cs is compiled into both csproj files (the AOT csproj links to it via <Compile Include="..\...MyWorker.Managed\MyWorkerLogic.cs" Link="Logic.cs"/>). The handler body is byte-identical regardless of target. The two shims are 30 and 50 lines respectively, differing only in:
- the base class (ApiCommandWorker<T> for managed, CommandWorker<T> for AOT)
- the Gateway property (managed default; AOT overrides to AotGatewayAdapter(AotNative<...>.CurrentContext))
- the entry-point ABI (IWorker.ProcessAsync interface for managed; [UnmanagedCallersOnly] Process + FreeResult for AOT)
In-process crash isolation (NativeAOT)
A NativeAOT worker runs inside the WorkManager process via NativeLibrary.Load. A segfault, stack overflow, or P/Invoke that corrupts the heap terminates the WorkManager and all of its other in-process workers — unlike Python workers, there is no process sandbox. Native DLLs must therefore come from a fully trusted source (the registered NuGet feed with content-addressed versioning) and must be treated as root-equivalent in the trust model.
See also
- Getting Started — build, publish, run the binance example
- Worker Author Guide — author a new worker in the recommended shape
- Engines — the DotNetDll vs NativeDll contrast in detail