콘텐츠로 이동

C# application contract

이 콘텐츠는 아직 번역되지 않았습니다.

NetWiv applications use @netwiv/api for frontend core and plugin calls. Pinned upstream implementations remain private inside the facade; direct Tauri frontend imports are no longer supported. The C# namespace is NetWiv; NetWiv.Platform.Desktop supplies the native desktop runtime. This document describes the implemented integration surface, not a claim that every TAURI® operation has passed conformance testing.

Tauri concept C# surface Contract
Builder Builder Registers commands, state, plugins, capabilities and windows before building an application.
App App : IDisposable Owns the runtime and application lifetime; call Run on the UI thread.
AppHandle AppHandle Provides application operations to commands and callbacks. Native UI work is dispatched to its owning event loop.
State<T> State<T> Retrieves application-managed state; application code owns synchronization of mutable state.
Window, Webview, WebviewWindow Same names Window and WebView labels occupy separate namespaces. A bare window does not imply a WebView.
#[command] [Command] and NetWiv.Generators Generates direct static or DI-resolved instance invocation and serialization roots for NativeAOT.
generate_handler! InvokeHandler Selects generated command names explicitly.
Rust future Task / ValueTask An asynchronous C# operation does not move the UI event loop off its owning thread.
Channel<T> / resource ID Channel, Resource subclasses Callbacks are bound to a document generation; resource handles have explicit close/dispose lifetimes.
Tauri plugin IPlugin, [Plugin] NuGet references and generated static factories root plugins without runtime assembly discovery.

PascalCase applies to C# names. It does not rename JavaScript command strings, argument properties, event names, resource IDs or serialized enum tags. Rust traits, lifetimes, macros and ABI are not binary-compatible with C#; their corresponding registration and ownership rules are explicit in the SDK.

App and AppHandle return nullable values from GetWindow, GetWebview and GetWebviewWindow, corresponding to Rust Option<T>. Test the result before using a label that may have been closed. A window is a WebviewWindow only while it hosts exactly its same-label WebView; a differently named child makes it a multi-WebView window. An unrelated same-label WebView in another window does not qualify. Webview.Window follows native reparenting for retained SDK handles. Windows, Webviews and WebviewWindows return collection snapshots; document/window closure is reflected in subsequent lookups.

App and AppHandle expose Manage<T>, State<T>, TryState<T> and Unmanage<T>(out T). Runtime registration returns false for a duplicate type; Builder.Manage rejects duplicates when building. Registry access is synchronized, but mutable state values still need application synchronization. TryState returns a nullable State<T>; Unmanage uses the C# Try/out convention to retain an unambiguous absent result for both value and reference types.

Successfully managed IDisposable objects are disposed when the app is disposed, including cleanup after failed construction. Removing a value transfers cleanup responsibility to the caller; a rejected duplicate stays caller-owned. One object registered under multiple type keys is disposed once. C# cannot enforce Rust borrow lifetimes or consume arguments: retained State<T> references remain CLR references, and callers must not use disposed state after application shutdown. Use IDisposable for deterministic cleanup; garbage collection is not a Rust Drop replacement. Cleanup attempts all values and aggregates disposal failures.

Guest core:resources:close calls must match the owning WebView and document generation of managed resources registered through InvokeContext.AddResource. The ownership check and removal are atomic. Trusted backend calls retain access, and app-scoped resources created through Resources.Add remain shareable.

Guest calls to registered C# commands (including generated and managed plugin commands) execute on the .NET thread pool, including synchronous work before the first await. Backend InvokeAsync calls retain caller-thread execution. Native window operations keep their event-loop dispatch. This NetWiv scheduling contract applies on Windows, Linux and macOS; it is not a claim of identical Tauri/Rust command scheduling. It avoids freezing the WebView during synchronous handlers without requiring every app to wrap every command in Task.Run.

Commands may run concurrently. Protect shared state with locks or a semaphore; never depend on invocation arrival order. Migrate handlers that use thread-affine native APIs to AppHandle.Dispatch, or use the SDK’s asynchronous window APIs. Await I/O with the supplied cancellation token. Backend callers doing CPU-heavy work must explicitly schedule it away from the UI thread. Avoid .Result and .Wait() on native UI operations.

The CancellationToken supplied to commands combines caller cancellation with application shutdown and, for guest calls, the originating WebView document’s lifetime. Reloading or closing that document cancels its pending commands. Cancellation is cooperative: application code must pass the token to asynchronous work or observe it explicitly. Already accepted native UI mutations retain their existing completion contract.

Use InvokeContext.AddResource(value) for resources created on behalf of a guest. The handle is closed when its document is replaced or closed; a late registration from an obsolete document is rejected and disposes an IDisposable value. Trusted backend registrations remain application-owned. AppHandle.Resources.Add keeps its explicit application lifetime for callers that intentionally need it.

AppHandle.Restart() requests normal exit. After the event loop returns, App.Run disposes the old app and plugins before launching the current executable with its arguments and working directory. Repeated pending requests coalesce; cleanup failure prevents relaunch. Custom runtime hosts that do not use App.Run must not treat Restart as an immediate process launch.

UseEmbeddedConfig respects create: false and the default configured title NetWiv App. An explicit app.windows list replaces previously added windows, including an empty list. An omitted list preserves explicit C# Window(...) calls, but loading configuration without either source of windows creates no implicit UI. Windows added after configuration remain explicit additions. The no-configuration Builder convenience default remains separate from this configuration behavior. Windowless apps can run the native event loop, query windows/WebViews and exit explicitly.

Invalid backend event names use Tauri’s exact error text and Rust Debug escaping in ArgumentException.Message. Guest event command deserialization retains its distinct upstream argument-error message.

App, AppHandle, Window, Webview and WebviewWindow expose Listen, Once, ListenAny, OnceAny, Emit and EmitTo. Listen/Once select that manager’s target; ListenAny/OnceAny receive every target for the named event. Emit broadcasts regardless of which manager sends it. EmitTo accepts either a RuntimeEventTarget or a label, which matches that label across target kinds. These synchronous backend operations share the same event bus as guest delivery. Existing EmitAsync methods on window/WebView wrappers remain available.

Dispose the returned listener subscription to unsubscribe, or pass AppEvent.Id to any manager’s Unlisten. IDs identify backend listeners across all managers; unknown IDs are ignored. A once listener is removed before its callback, so a reentrant emission cannot invoke it again. The underlying AppHandle.Events bus retains its existing global-listener default; App.Listen selects App. The Rust manager emit/emit_to tests preserve all four original emitter types and exact recipient/payload assertions.

Reference NetWiv.Core, NetWiv.Platform.Desktop and NetWiv.Generators, all at the same version. See the package quickstart for feed setup. Keep [STAThread] on the Windows entry point. On macOS, keep a synchronous Main and call app.Run() on the process’s original main thread; Task.Run or an asynchronous Main continuation cannot own AppKit’s event loop. The example below meets both requirements:

using NetWiv;
using NetWiv.Platform.Desktop;
using System.Text.Json.Serialization;
[assembly: CommandJsonContext(typeof(AppJsonContext))]
internal static class Program
{
[STAThread]
private static void Main()
{
using var app = new Builder()
.UseRuntime(new DesktopRuntime())
.Metadata(new("Greeting", "0.1.0", "dev.example.greeting"))
.Window("main", "Greeting", "index.html")
.UseEmbeddedAssets()
.InvokeHandler("greet")
.Build();
app.Run();
}
}
public static class Commands
{
[Command("greet")]
public static string Greet(string name) => $"Hello, {name}";
}
[JsonSerializable(typeof(string))]
public partial class AppJsonContext : JsonSerializerContext;

The matching frontend remains:

import { invoke } from '@netwiv/api/core'
const greeting = await invoke('greet', { name: 'NetWiv' })

Embed frontend files under the netwiv.assets. resource prefix, or use the NetWiv.Build MSBuild targets with NetWivFrontendDist. UseEmbeddedConfig consumes the expanded netwiv.config.json resource. Typed commands must declare the source-generated JsonSerializerContext through [CommandJsonContext]; see the generator tests and conformance application for an executable example.

To use dependency injection, configuration, options, ILogger and hosted services, build the app with NetWiv.Hosting instead; see Generic Host integration. Builder.UseServiceProvider and Builder.UseLoggerFactory connect an existing provider without the host.

Without an app command manifest, registered application commands are allowed from bundled local documents and denied from remote documents. Core and plugin commands still require capability permissions. With an app command manifest, every custom command requires a resolved capability permission. Denies take precedence. Permission allow/deny lists contain exact, case-sensitive command keys; * has no wildcard meaning there. Window and WebView capability target patterns retain their upstream glob behavior. netwiv build reads application permissions/**/*.json and permissions/**/*.toml and embeds their declarations in the effective configuration consumed by UseEmbeddedConfig. Permission JSON5 files are ignored, matching the pinned loader. A nonempty permission/set/default manifest enables application ACL even when no C# AppCommandManifest call is present; an empty permissions directory preserves manifest-absent defaults. Sets retain each leaf command’s own scopes. During netwiv dev, changes to permission/capability files and configuration overlays regenerate that embedded ACL and restart the backend. Invalid TOML/JSON configuration stops the old backend, reports the error, and waits for a corrected file. The frontend server and initial development hook remain session-scoped; restart netwiv dev when changing their configuration. Calling private window.__TAURI_INTERNALS__ directly is outside the supported public API surface.

App.AssetProtocolScope() and AppHandle.AssetProtocolScope() return the live FileScope when the file asset protocol is enabled. AllowFile, AllowDirectory, ForbidFile and ForbidDirectory extend it at runtime. Their paths are literal: a filename containing *, ? or brackets does not become a broad glob grant. IsForbidden and IsAllowed preserve deny precedence. Pattern properties return snapshots. Listen/Once return an ID for Unlisten; scope callbacks may change the scope again without holding a native/UI lock. A scope change immediately affects subsequent asset: protocol requests.

Plugin Initialize registers commands and permissions while building. Plugin settings are available as PluginContext.Config; ReadConfig(JsonTypeInfo<T>) reads a typed value with generated NativeAOT metadata. Missing settings are JSON null. See plugin configuration for setup and capability registration. Plugin Setup and application Setup execute once the native event loop and initial windows are ready. Native callbacks never unwind a managed exception through the C ABI. A callback failure stops the runtime and is rethrown after native cleanup.

Reload, navigation and WebView closure invalidate old command replies, channels and guest event listeners. A late reply is discarded instead of being delivered to a new document that happens to use the same label. Accepted native mutations complete normally even when caller cancellation arrives later, so newly-created resource handles are not lost.

OnMenuEvent and OnTrayIconEvent observe native events; emitting a similarly named guest event does not invoke these native-only hooks. Menu IDs retain the application-supplied identifier. Tray positions retain fractional native coordinates.

Window.OnCloseRequested accepts synchronous or asynchronous handlers. Native closing is deferred while subscribed; the handler may call PreventDefault. After a successful handler completes, the SDK destroys the window unless prevention was requested. Disposing the subscription prevents future calls and does not cancel a handler already running. Handler and subsequent destroy errors are reported through AppHandle.EventHandlerFailed; without a subscriber they are written to standard error. A failed handler does not trigger destroy.

Window event conveniences include resize, move, focus/blur, scale, theme and drag/drop. The returned IDisposable removes all underlying listeners. Once removes its listener before invocation, including concurrent emissions.

Windows, Webviews and WebviewWindows are local lifecycle snapshots. GetAllWindowsAsync, GetAllWebviewsAsync and GetAllWebviewWindowsAsync query the native runtime. Their singular async lookups return null when absent; GetFocusedWindowAsync checks current native focus. The pinned guest’s combined window listing wraps all window labels, including bare windows, whereas the local WebviewWindows snapshot contains only known combined objects.

Monitor and cursor queries are available on a Window so the native caller has an explicit window context. Monitor work areas retain physical coordinates; cursor coordinates use PhysicalPosition<double> to retain fractional values. Window icons and overlays accept an Image resource, an encoded byte buffer or a path. See pinned upstream behavior for the explicit C# backend differences around colors and WebView parent labels. Explicit Resource.CloseAsync preserves invalid-ID errors on repeated close. DisposeAsync is idempotent after a successful close, following .NET cleanup conventions without masking errors from an explicit API call.

RuntimeInvokeResponse.Deserialize<T>(JsonTypeInfo<T>) corresponds to Tauri’s InvokeResponseBody::deserialize. Pass metadata from a generated JSON context; runtime reflection is not required. JSON values use that context’s serializer contract. Raw responses deserialize as numeric sequences, and byte[] uses the Tauri byte-vector array format (including for JSON responses), not STJ’s default Base64 string format. Decoding raw bytes into byte[] returns an independent array. Invalid target shapes throw JsonException.

UseEmbeddedConfig() reads app.security.assetProtocol for the wry runtime. It is disabled by default. For example, a scope of {"enable":true,"scope":{"allow":["$APPDATA/images/**"],"deny":["$APPDATA/images/private/**"]}} allows files below the application’s image directory while giving deny patterns precedence. The unchanged guest convertFileSrc(path) produces the corresponding asset URL. A configured CSP must separately permit that URL for its intended use.

File scope matching uses literal path separators, canonical filesystem paths, and the pinned platform default for leading dots (Unix requires a literal dot; Windows does not). These filesystem scopes are separate from command capabilities. Responses use the native WebView’s initial document origin for CORS. The legacy direct-binding backends reject enabled asset configuration explicitly. Dynamic scope mutation is exposed through AppHandle.AssetProtocolScope() and FileScope allow/forbid methods, snapshots and change events.

Application restore, managed development and managed/NativeAOT publishing use the native assets already in NuGet. They do not execute Cargo or download a runtime library at startup. Deployment currently uses the NativeAOT application plus the platform DLL/SO and its documented OS dependencies. Framework maintainers build the native adapter separately using the pinned Rust toolchain.

See native architecture for the supported backend and validation boundaries for outstanding coverage. Isolation has an automatic differential suite; full desktop acceptance remains unfinished. Consult the relevant conformance results for a specific behavior.


TAURI® is a trademark of The Tauri Programme within the Commons Conservancy. WRY is a trademark of The Tauri Programme within the Commons Conservancy.

NetWiv is an independent project. Its framework, plugins and templates are not affiliated with, endorsed by, or officially approved by The Tauri Programme within the Commons Conservancy.