NxLang.Sdk
0.5.0
dotnet add package NxLang.Sdk --version 0.5.0
NuGet\Install-Package NxLang.Sdk -Version 0.5.0
<PackageReference Include="NxLang.Sdk" Version="0.5.0" />
<PackageVersion Include="NxLang.Sdk" Version="0.5.0" />
<PackageReference Include="NxLang.Sdk" />
paket add NxLang.Sdk --version 0.5.0
#r "nuget: NxLang.Sdk, 0.5.0"
#:package NxLang.Sdk@0.5.0
#addin nuget:?package=NxLang.Sdk&version=0.5.0
#tool nuget:?package=NxLang.Sdk&version=0.5.0
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.tomlwhen 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-x64osx-arm64win-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:
- Vendor NX source into your repository.
- Run
cargo build --release -p nx-ffiin the vendored NX checkout. - Build your .NET solution.
- Let
NxLang.Sdk.targetscopy the native library fromtarget/releaseinto your application output.
Optional properties:
NxSdkNativeLibraryConfiguration: chooseDebugorReleasewhenNxSdkNativeLibraryDiris not set. Defaults toRelease.NxSdkNativeLibraryDir: override the directory that contains the built native library.NxSdkStageNativeLibrary: set tofalseif you want to stage the library yourself.NxSdkFailIfNativeLibraryMissing: set totrueto 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:
- Remove the
ProjectReferencetobindings/dotnet/src/NxLang.Sdk/NxLang.Sdk.csproj. - Remove the manual import of
bindings/dotnet/build/NxLang.Sdk.targets. - Add
PackageReference Include="NxLang.Sdk". - Remove consumer-side
cargo build -p nx-ffisteps that only existed to stage the runtime. - Keep application-specific
.nxfiles 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
Unionarrays.Keep generated action/record DTO classes free of explicit
Type/$typedata 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
StateSnapshotbyte 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
StateSnapshotonly with the exact sameNxProgramArtifactrevision 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_snapshotas base64 on the wire, and the managed binding decodes that back toStateSnapshotbytes 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 | Versions 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. |
-
net10.0
- MessagePack (>= 3.1.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.