NxLang.Sdk 0.5.0

dotnet add package NxLang.Sdk --version 0.5.0
                    
NuGet\Install-Package NxLang.Sdk -Version 0.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NxLang.Sdk" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NxLang.Sdk" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="NxLang.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NxLang.Sdk --version 0.5.0
                    
#r "nuget: NxLang.Sdk, 0.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NxLang.Sdk@0.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NxLang.Sdk&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=NxLang.Sdk&version=0.5.0
                    
Install as a Cake Tool

NxLang.Sdk - NX .NET SDK

.NET 10 SDK for NX host, compiler, diagnostics, program artifact, IR generation, and evaluation workflows, implemented in C# and backed by the native Rust FFI library.

  • Assembly: NxLang.Sdk.dll
  • Namespace: NxLang.Nx
  • Primary language: C#
  • Support posture: C# usage is tested and documented today. Other .NET languages should work because the binding is a normal managed assembly over a native library, but they are not yet validated in this repository.

Architecture

┌─────────────────┐
│   .NET Code     │
│  (NxRuntime)    │
└────────┬────────┘
         │ managed wrapper
         ↓
┌─────────────────┐
│  nx_ffi (Rust)  │
│  C ABI Layer    │
└────────┬────────┘
         │
         ↓
┌─────────────────┐
│ NX Interpreter  │
│    (Rust)       │
└─────────────────┘

The managed binding validates the native ABI version at startup. Published package consumers get the native SDK library through normal .NET runtime asset restore, build, test, and publish behavior.

Prerequisites

  • .NET SDK: .NET 10.0
  • Rust: the workspace toolchain declared in rust-toolchain.toml when building NX from source
  • OS: Linux, macOS, or Windows

Build

Build the native SDK library first:

cargo build --release -p nx-ffi

Then build the managed solution:

dotnet build bindings/dotnet/NxLang.sln

Run the C# test suite:

dotnet test bindings/dotnet/NxLang.sln

The test project imports bindings/dotnet/build/NxLang.Sdk.targets, which copies the native library from target/release into the test output directory.

To test against a debug native build instead, build nx_ffi without --release and pass the native library configuration explicitly:

cargo build -p nx-ffi
dotnet test bindings/dotnet/NxLang.sln -p:NxSdkNativeLibraryConfiguration=Debug

Supported Integration Workflows

Primary: PackageReference

Applications should reference the published SDK package:

<ItemGroup>
  <PackageReference Include="NxLang.Sdk" Version="0.1.0" />
</ItemGroup>

The package contains NxLang.Sdk.dll and the native nx_ffi runtime assets for supported runtime identifiers under runtimes/<rid>/native/. Package consumers do not need to vendor the NX repository, import bindings/dotnet/build/NxLang.Sdk.targets, or install Rust just to build, test, run, or publish an application that uses NxLang.Sdk.

Publish for a supported runtime identifier when creating a deployable application:

dotnet publish MyApp.csproj -c Release -r linux-x64 --self-contained false

Initial supported RIDs are:

  • linux-x64
  • osx-arm64
  • win-x64

Application-owned .nx source files and domain libraries are not packaged by NX. Embed them, copy them as content, or otherwise stage them from the consuming application.

Package Publishing

Cross-package CI setup is documented in docs/deployment-setup.md, and the recurring release runbook is in docs/deployment.md. The NxLang.Sdk package is published from the complete deployables-Complete artifact after package metadata verification and RID smoke tests pass. Production publishing prefers NuGet.org trusted publishing with NUGET_API_KEY as a gated fallback.

Advanced: Source ProjectReference

Use a direct project reference to the managed binding and import the staging targets file:

<ItemGroup>
  <ProjectReference Include="external/nx/bindings/dotnet/src/NxLang.Sdk/NxLang.Sdk.csproj" />
</ItemGroup>

<Import Project="external/nx/bindings/dotnet/build/NxLang.Sdk.targets" />

Use this flow when contributing to NX or intentionally testing unreleased SDK changes:

  1. Vendor NX source into your repository.
  2. Run cargo build --release -p nx-ffi in the vendored NX checkout.
  3. Build your .NET solution.
  4. Let NxLang.Sdk.targets copy the native library from target/release into your application output.

Optional properties:

  • NxSdkNativeLibraryConfiguration: choose Debug or Release when NxSdkNativeLibraryDir is not set. Defaults to Release.
  • NxSdkNativeLibraryDir: override the directory that contains the built native library.
  • NxSdkStageNativeLibrary: set to false if you want to stage the library yourself.
  • NxSdkFailIfNativeLibraryMissing: set to true to fail the build when the native library is missing.

Advanced: Built Assembly Reference

If you cannot use ProjectReference, reference the built managed assembly directly and copy the native library alongside your application's output:

  • Linux: target/release/libnx_ffi.so
  • macOS: target/release/libnx_ffi.dylib
  • Windows: target/release/nx_ffi.dll

The managed SDK looks for the native library in the application base directory and the managed assembly directory.

Migration From Source Consumption

To migrate an application from a vendored NX checkout to the package:

  1. Remove the ProjectReference to bindings/dotnet/src/NxLang.Sdk/NxLang.Sdk.csproj.
  2. Remove the manual import of bindings/dotnet/build/NxLang.Sdk.targets.
  3. Add PackageReference Include="NxLang.Sdk".
  4. Remove consumer-side cargo build -p nx-ffi steps that only existed to stage the runtime.
  5. Keep application-specific .nx files in the application and stage them with application-owned content rules.

Usage

Basic Evaluation

using MessagePack;
using NxLang.Nx;

int result = NxRuntime.Evaluate<int>("let root() = { 42 }");
string text = NxRuntime.Evaluate<string>("let root() = { \"Hello, NX!\" }");
bool flag = NxRuntime.Evaluate<bool>("let root() = { true }");

Canonical Raw Bytes

using MessagePack;
using NxLang.Nx;

byte[] resultBytes = NxRuntime.EvaluateBytes("let root() = { 42 }");

int value = MessagePackSerializer.Deserialize<int>(resultBytes);

The raw-byte APIs now let you choose the returned wire format per call. MessagePack remains the default:

using System.Text;
using NxLang.Nx;

byte[] jsonBytes = NxRuntime.EvaluateBytes(
    "let root() = { { answer: 42 } }",
    NxOutputFormat.Json);

string json = Encoding.UTF8.GetString(jsonBytes);

Reusable Program Artifacts

using NxLang.Nx;

using NxLibraryRegistry registry = new();
registry.LoadFromDirectory("/app/question-flow");
using NxProgramBuildContext buildContext = registry.CreateBuildContext();

string source = """
    import "../question-flow"
    let root() = { answer() }
    """;

using NxProgramArtifact program = NxProgramArtifact.Build(source, buildContext, "/app/main.nx");
int value = NxRuntime.Evaluate<int>(program);

Build a NxProgramArtifact when you want to reuse the same resolved program across evaluation or component lifecycle calls. If the source imports local NX libraries, preload them in a NxLibraryRegistry and build through a NxProgramBuildContext so program construction uses the selected loaded snapshots instead of reading libraries from disk on demand. The parameterless NxProgramArtifact.Build(source, fileName) convenience still exists, but it now creates a transient empty registry/build-context pair internally before calling the native build API.

Program Module Codegen

Use NxProgramArtifact.GenerateJSProgramModule when a managed host needs cacheable JavaScript source for a resolved NX program rather than immediate interpreter evaluation:

using NxLang.Nx;

using NxProgramArtifact program = NxProgramArtifact.Build(source, "/app/main.nx");
NxGeneratedJSProgramModule generated = program.GenerateJSProgramModule(
    new NxJSProgramModuleOptions
    {
        LogicalModuleName = "app/main",
        RuntimeImportSpecifier = "nx:runtime",
    });

string sourceText = generated.SourceText;
string runtimeAbi = generated.RuntimeAbi;

The generated source is one host-neutral JavaScript ESM module. It imports NX runtime helpers from the configured runtime specifier and does not include host wrappers, nx-runtime.js, or Cloudflare/Rivet packaging. The returned metadata includes the program fingerprint, runtime ABI, function entrypoint exports, and component/schema exports so managed hosts can cache and validate the module without parsing generated source.

In-Memory Workspaces

Use NxWorkspace when source modules come from editor buffers, database rows, or other logical records and should not be written to temporary files:

using NxLang.Nx;

NxWorkspace workspace = new([
    NxWorkspaceModule.FromSourceText(
        "app/main.nx",
        """
        import { answer } from "../shared/value.nx"
        let root(): int = { answer() }
        """),
    NxWorkspaceModule.FromSourceText(
        "shared/value.nx",
        "export let answer(): int = { 42 }"),
]);

using NxLibraryRegistry registry = new();
using NxProgramBuildContext buildContext = registry.CreateBuildContext();

IReadOnlyList<NxDiagnostic> diagnostics = NxRuntime.ValidateWorkspace(workspace, buildContext);
using NxProgramArtifact program =
    NxProgramArtifact.BuildWorkspace(workspace, "app/main.nx", buildContext);

Workspace identities are logical names, not filesystem paths. NX uses / separators, normalizes . and .., rejects identities or imports that escape the workspace root, and resolves imports by exact normalized identity before falling back to libraries already visible in the supplied NxProgramBuildContext. Diagnostics preserve normalized workspace identities and calculate spans from the submitted source bytes. Workspace validation returns NX source diagnostics as data; malformed workspace inputs such as duplicate normalized identities are rejected as interop argument errors. Workspace artifact builds throw NxEvaluationException when static diagnostics or a missing entry identity prevent artifact creation.

Direct JSON Output

using System.Text.Json;
using NxLang.Nx;

JsonElement json = NxRuntime.EvaluateJson("let root() = { { answer: 42 } }");
int answer = json.GetProperty("answer").GetInt32();

Use the JSON convenience APIs when C# needs a parsed JSON view without introducing MessagePack into that call path. Use the raw-byte overloads with NxOutputFormat.Json when you want UTF-8 JSON bytes that can be forwarded directly to another client.

Constant Case Encoding

NX has one declaration form for a union. A case is constant when it declares no fields and its union declares no base, so the case carries nothing beyond its own name. A constant union is one whose cases are all constant — the closed set of named constants that enum used to declare.

A constant case is encoded as the bare authored case string on the wire, both in raw and typed layers, for JSON and MessagePack alike:

"dark"

This holds wherever the case appears. In a constant union such as type ThemeMode = light | dark every value takes this form; in a union that also has payload cases, the constant ones take it while the payload ones take the $type map described below.

Raw APIs (EvaluateBytes, EvaluateJson, EvaluateComponentJson, InitializeComponentJson, DispatchComponentActionsJson) emit the case string directly. When the host feeds a raw value back into the runtime for a slot whose declared NX type is that union, the runtime resolves the string against the union's case list. Unknown cases surface through the standard argument type-mismatch error path.

Typed generated DTOs use the same string contract. A constant union generates a CLR enum plus an explicit wire-format mapping type, and relies on NxEnumJsonConverter<TEnum, TWire> and NxEnumMessagePackFormatter<TEnum, TWire> from NxLang.Sdk to (de)serialize the authored case string. The CLR enum is the generated host shape for a constant union; it is not a separate NX concept.

Use the raw APIs when you need a schema-free value tree. Use typed generated models when you want ergonomic host-side enums.

Discriminated Union Encoding

NX discriminated union cases are encoded as canonical maps with a $type discriminator whose value is the fully scoped case name. Payload fields keep their authored NX wire names:

{
  "$type": "LoadState.failed",
  "message": "Offline"
}

Raw APIs expose the same shape for JSON and MessagePack. A constant case of the same union, such as LoadState.idle, is a bare string rather than a map, because it carries nothing a map would hold:

"idle"

A fieldless case of a union that extends an abstract base is not constant — it carries the base's fields — and so keeps the $type map form.

Generated C# union roots use JsonPolymorphic/JsonDerivedType for JSON and NxPolymorphicMessagePackFormatter<T> for MessagePack, so serializing or deserializing through the generated union root type preserves the same $type map contract:

LoadState state = new LoadStateFailed
{
    Message = "Offline"
};

string json = JsonSerializer.Serialize(state);
byte[] bytes = MessagePackSerializer.Serialize(state);

LoadState fromJson = JsonSerializer.Deserialize<LoadState>(json)!;
LoadState fromMessagePack = MessagePackSerializer.Deserialize<LoadState>(bytes);

A union that mixes the two kinds of case therefore has two wire shapes, and generated readers accept both. NxPolymorphicMessagePackFormatter<T> and the generated JSON converter read a bare string as the union's constant case of that name and a $type map as a payload case; generated C# exposes a constant case as a [NxConstantCase] singleton, for example LoadStateIdle.Instance, which serializes back to the bare string.

Component Evaluation

Use EvaluateComponent when the host owns current component state and only needs the rendered body. Evaluation is pure: it does not create or consume StateSnapshot, dispatch actions, invoke action handlers, or return effects.

using NxLang.Nx;

string source = """
    component <SearchBox placeholder:string = "Find docs" /> = {
      state { query:string }
      <TextInput value={query} placeholder={placeholder} />
    }
    """;

TextInputElement rendered =
    NxRuntime.EvaluateComponent<SearchBoxProps, SearchBoxState, TextInputElement>(
        source,
        "SearchBox",
        new SearchBoxProps { Placeholder = "Find docs" },
        new SearchBoxState { Query = "docs" });

For JSON workflows, the result is the rendered value directly rather than a lifecycle wrapper:

JsonElement renderedJson =
    NxRuntime.EvaluateComponentJson(
        source,
        "SearchBox",
        new SearchBoxProps { Placeholder = "Find docs" },
        new SearchBoxState { Query = "docs" });

string value = renderedJson.GetProperty("value").GetString()!;

Raw-byte overloads let the caller choose MessagePack or JSON output. Props and state inputs are always supplied as MessagePack bytes:

byte[] propsBytes = MessagePackSerializer.Serialize(new SearchBoxProps { Placeholder = "Find docs" });
byte[] stateBytes = MessagePackSerializer.Serialize(new SearchBoxState { Query = "docs" });

byte[] jsonBytes = NxRuntime.EvaluateComponentBytes(
    source,
    "SearchBox",
    NxOutputFormat.Json,
    propsBytes,
    stateBytes);

When evaluating repeatedly or resolving imports from preloaded libraries, build a NxProgramArtifact once and call the artifact overload:

using NxProgramArtifact program = NxProgramArtifact.Build(source);

TextInputElement renderedAgain =
    NxRuntime.EvaluateComponent<SearchBoxProps, SearchBoxState, TextInputElement>(
        program,
        "SearchBox",
        new SearchBoxProps { Placeholder = "Find docs" },
        new SearchBoxState { Query = "docs" });

Component Lifecycle

using NxLang.Nx;
using System.Text.Json;

string source = """
    action SearchSubmitted = { searchString:string }

    component <SearchBox placeholder:string emits { SearchSubmitted } /> = {
      state { query:string = {placeholder} }
      <TextInput value={query} placeholder={placeholder} />
    }
    """;

NxComponentInitResult<TextInputElement> init =
    NxRuntime.InitializeComponent<SearchBoxProps, TextInputElement>(
        source,
        "SearchBox",
        new SearchBoxProps { Placeholder = "Find docs" });

byte[] savedSnapshot = init.StateSnapshot;

NxComponentDispatchResult<TextInputElement, SearchSubmittedAction> dispatch =
    NxRuntime.DispatchComponentActions<SearchSubmittedAction[], TextInputElement, SearchSubmittedAction>(
        source,
        savedSnapshot,
        new[]
        {
            new SearchSubmittedAction
            {
                SearchString = "docs"
            }
        });

A dispatch result carries the body re-rendered against the new state (Rendered), the effects for the host in order (Effects), and the snapshot to pass to the next call (StateSnapshot). A failing batch throws NxEvaluationException and leaves the snapshot you passed in as the current state.

Handlers in Rendered Output

Rendered output from initialization and dispatch represents each bound handler as an ActionHandler record with the action it accepts and a token. Type the property as NxActionHandlerRef to read it, and dispatch a NxHandlerInvocation<TAction> to run the handler. An update record it returns patches the component's state:

string source = """
    external component <Button value:int = 0 emits { Tapped { } } />
    component <Counter /> = {
      state { count:int = 0 }
      <Button value={count} onTapped=<Update count={count + 1} /> />
    }
    """;

[MessagePackObject]
public sealed class ButtonElement
{
    [Key("value")] public int Value { get; set; }
    [Key("onTapped")] public NxActionHandlerRef OnTapped { get; set; } = new();
}

[MessagePackObject]
public sealed class ButtonTapped
{
    [Key("$type")] public string Type { get; set; } = "Button.Tapped";
}

NxComponentInitResult<ButtonElement> init = NxRuntime.InitializeComponent<ButtonElement>(source, "Counter");

NxComponentDispatchResult<ButtonElement, object> tapped =
    NxRuntime.DispatchComponentActions<NxHandlerInvocation<ButtonTapped>[], ButtonElement, object>(
        source,
        init.StateSnapshot,
        new[] { init.Rendered.OnTapped.Invoke(new ButtonTapped()) });

// tapped.Rendered.Value == 1; use tapped.Rendered.OnTapped for the next dispatch.

A token is valid only with the snapshot returned by the same call: every dispatch, even one with an empty batch, returns fresh tokens and retires the previous ones. Pure evaluation output carries no tokens. A batch that mixes emitted actions and handler invocations can be passed as an object[].

Function Values in Rendered Output

A function value in NX names a declaration and captures nothing, so rendered output represents one as a Function record with the declaring module's identity and the function's name — a template bound to a list, most often. Type the property as NxFunctionRef to read which function it was handed:

string source = """
    external component <List ItemTemplate:(<function Item:object Index:int />: string)? />
    let <Row Item:object Index:int />: string = "r"
    let root() = <List ItemTemplate={Row} />
    """;

[MessagePackObject]
public sealed class ListElement
{
    [Key("ItemTemplate")] public NxFunctionRef? ItemTemplate { get; set; }
}

ListElement rendered = NxRuntime.Evaluate<ListElement>(source, "templates.nx");
// rendered.ItemTemplate.Module == "templates.nx"; rendered.ItemTemplate.Name == "Row"

typegen types a function-typed member this way too. A .NET host can read a function value and pass the record along, but not call it: a function value is invoked by the NX program that received it, and a Function record supplied back as a prop, as state, or inside an action is refused.

Update Records and NxOptional<T>

Generated <Name>_update DTOs type every property as NxOptional<T>, which tells an unset property ("leave this field unchanged") apart from one that is cleared. null is the .NET spelling of a cleared field — the NX empty value {} — and only a field the target declares optional (email?:string) can be cleared, so the accessor's value type is nullable only for such a field: User_update.Email is NxOptional<string?> while Name is NxOptional<string>. Unset properties are omitted from both JSON and MessagePack, a cleared one is written as null, and a missing key reads back as unset:

User_update patch = new() { Email = null };   // clears email; Name stays unset
string json = JsonSerializer.Serialize(patch); // {"$type":"User.Update","email":null}

The runtime checks every record a host passes in — as a prop, in explicit state, or inside an action (even one with no bound handler), at any nesting depth — against its NX declaration. A property the NX type does not declare, null for a field that is not optional, or an empty array for a name:T+ field, fails the call with an NxEvaluationException naming the field, so a DTO that has drifted from the NX source cannot turn "unchanged" into "cleared". On the way out, an optional field the NX value leaves empty is an omitted key, so it reads as null, and a host may send null, an empty array, or no key at all for one; the runtime reads all three as the empty value.

If the host wants JSON results instead of typed MessagePack models:

NxComponentInitResult<JsonElement> initJson =
    NxRuntime.InitializeComponentJson(
        source,
        "SearchBox",
        new SearchBoxProps { Placeholder = "Find docs" });

NxComponentDispatchResult<JsonElement, JsonElement> dispatchJson =
    NxRuntime.DispatchComponentActionsJson(
        source,
        initJson.StateSnapshot,
        new[]
        {
            new SearchSubmittedAction
            {
                SearchString = "docs"
            }
        });

MessagePack Polymorphism Migration

Generated C# polymorphic DTOs now use the canonical NX MessagePack map shape with a $type string key instead of MessagePack Union envelopes.

  • Remove any custom host assumptions that polymorphic records/actions are encoded as Union arrays.

  • Keep generated action/record DTO classes free of explicit Type/$type data members.

  • Ensure abstract polymorphic roots keep the generated polymorphism attributes so runtime serialization can resolve concrete descendants from $type.

  • Initialization returns the rendered element plus an opaque StateSnapshot byte array that the host owns.

  • Dispatch consumes that saved snapshot and an ordered action list, then returns effect actions plus the next snapshot.

  • Evaluation accepts explicit props and explicit current state, then returns only the rendered value.

  • Reuse a saved StateSnapshot only with the exact same NxProgramArtifact revision that produced it. Mixing snapshots across program revisions is rejected.

  • Use evaluation for host-owned transparent state. Use initialization and dispatch for NX-owned component lifecycles that need opaque snapshot round-tripping and handler effects.

  • The managed source-based component helpers build transient NxProgramArtifacts internally and then call the native program-artifact component APIs. The public native C ABI itself is artifact-first.

  • State defaults run only during initialization in this change. Declarative state-update actions are still a follow-up, so dispatch currently preserves state values while still producing effect actions from bound handlers.

  • Component runtime inputs remain MessagePack-only in this phase. Typed prop/state/action overloads still serialize those inputs as MessagePack before calling the runtime.

  • JSON component results encode state_snapshot as base64 on the wire, and the managed binding decodes that back to StateSnapshot bytes for later dispatch calls.

Error Handling

using NxLang.Nx;

try
{
    int result = NxRuntime.Evaluate<int>("let x = ");
}
catch (NxEvaluationException ex)
{
    foreach (NxDiagnostic diagnostic in ex.Diagnostics)
    {
        if (diagnostic.Severity == NxSeverity.Error)
        {
            Console.WriteLine(diagnostic.Message);
        }
    }
}

All source-driven APIs run the shared NX static-analysis pipeline before any runtime execution. If parsing, lowering, scope building, or type checking reports errors, the call returns the full diagnostic set and does not execute root, component evaluation, component initialization, or component dispatch.

Generated Types

NX type generation remains C#-first:

# Single NX file to stdout or a chosen file
nxlang typegen Person.nx --language csharp --csharp-namespace MyApp.Models > Person.g.cs

# Full NX library to a generated output directory
nxlang typegen ./models --language csharp --csharp-namespace MyApp.Models --output ./generated

Generation now honors NX export visibility, so only declarations marked export are emitted. Library generation writes one .g.cs file per contributing module under the requested output directory. The generated host shape for a union follows from whether it is constant. A constant union generates a CLR enum using the authored NX case spellings for both JSON and MessagePack, the same bare-string shape raw runtime payloads carry. A union with any payload case generates an abstract root plus sealed case DTOs whose JSON and MessagePack attributes use the canonical $type map shape, with each constant case emitted as a [NxConstantCase] singleton that serializes as its bare string. Generated C# enums and unions rely on shared helpers from NxLang.Sdk under NxLang.Nx.Serialization, so the project that compiles the generated files must reference NxLang.Sdk in addition to the serializer packages it already uses. The generated constant-union output emits the enum itself plus an explicit wire-format mapping type; the JSON converter and MessagePack formatter implementation comes from the shared SDK assembly.

A record that declares NX type parameters — type Range = { T:type start:T end:T } — generates a real C# generic, Range<T>, and an applied type generates the instantiation, so week:<Range T=int/> is a Range<long>. Unlike a component contract, nothing is erased: the host names the concrete instantiation at its own deserialization site.

The record's <Name>_update companion follows it, so a patch is usable at the instantiation the host holds:

Range<long> before = new() { Start = 1, End = 5 };
Range<long> after = new() { Start = 1, End = 9 };

Range_update<long> patch = Range_update<long>.Diff(before, after);
long end = patch.End.Value;              // typed as the record's field, not object
Range<long> applied = patch.Apply(before);

Range_update<T> derives from NxUpdate<Range<T>>, its key table is RangeProperties<T>, and its Start and End are NxOptional<T>. The wire is unaffected by the parameter: a patch serializes as {"$type":"Range.Update","end":9} in either format, with no type argument. A component's state companion still erases the component's type parameters, because the type it patches (<Name>_state) is already concrete.

Because a generic type cannot name its own converter in an attribute — an attribute argument cannot use type parameters (CS0416) — a generic companion names NxUpdateRecordJsonConverterFactory from the SDK for JSON, and for MessagePack a generated <Name>_updateFormatter<T> shim beside it. Both are wired up by generation; nothing is needed at the call site, and a non-generic companion is unchanged.

AOT note. Closed instantiations round-trip with the reflection-based resolvers of both serializers, which is what JsonSerializer and MessagePackSerializer use by default. A source-generated or otherwise AOT-safe resolver has no open generic to generate from, so each instantiation the host actually serializes must be named to it — a [JsonSerializable(typeof(...))] entry per instantiation for System.Text.Json, and a generated formatter per instantiation for MessagePack. This covers the update companions too: Range_update<long> is its own instantiation and needs its own entry, separately from Range<long>. NxUpdateRecordJsonConverterFactory also closes its converter through MakeGenericType and Activator.CreateInstance, which a trimmed or AOT publish cannot see, so a host publishing that way should keep the companion instantiations it uses rooted.

Troubleshooting

Native SDK library could not be found

Package consumers should restore, build, and publish for one of the supported runtime identifiers so the NxLang.Sdk package can stage the matching native SDK asset. Source consumers should build crates/nx-ffi and import bindings/dotnet/build/NxLang.Sdk.targets to automate that copy step.

Native SDK ABI mismatch

Rebuild both the managed and native pieces from the same NX source revision. NxLang.Sdk.dll and nx_ffi must come from the same checkout.

Entry point not found for new component lifecycle methods

Build or rebuild the native nx_ffi library for the same configuration as your managed output. For example:

cargo build -p nx-ffi
dotnet test bindings/dotnet/NxLang.sln -p:NxSdkNativeLibraryConfiguration=Debug

Project Structure

bindings/dotnet/
├── build/
│   └── NxLang.Sdk.targets
├── src/
│   └── NxLang.Sdk/
│       ├── Interop/
│       ├── Serialization/
│       ├── NxRuntime.cs
│       ├── NxDiagnostic.cs
│       ├── NxSeverity.cs
│       └── Properties/
├── tests/
│   └── NxLang.Sdk.Tests/
├── Directory.Packages.props
├── NxLang.sln
└── README.md
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 25 9/29/2026
0.4.0 51 9/25/2026
0.3.0 85 9/20/2026
0.2.0 92 9/18/2026
0.1.0 93 9/15/2026