Skip to content

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's lib/<tfm>/*.dll into a per-worker collectible AssemblyLoadContext. Requires the WM process to have a CLR runtime (which it does, via the DotNetDll.Bridge — a separate managed DLL the AOT-published WM embeds via libhostfxr).
  • NativeDllEngine — loads the nupkg's runtimes/<rid>/native/lib*.so into the host process via NativeLibrary.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-dllDotNetDllEngine, application/x-native-dllNativeDllEngine. 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 — wraps Virtufin.Api.Client.GatewayClient.InvokeAsync (managed gRPC). The base class ApiCommandWorker<T> returns this from its Gateway property.
  • AotGatewayAdapter — wraps WorkerContext.InvokeGateway (host-installed C-ABI function pointer). The AOT shim's Gateway property 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