ValueLink 0.119.0

dotnet add package ValueLink --version 0.119.0
                    
NuGet\Install-Package ValueLink -Version 0.119.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="ValueLink" Version="0.119.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ValueLink" Version="0.119.0" />
                    
Directory.Packages.props
<PackageReference Include="ValueLink" />
                    
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 ValueLink --version 0.119.0
                    
#r "nuget: ValueLink, 0.119.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 ValueLink@0.119.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=ValueLink&version=0.119.0
                    
Install as a Cake Addin
#tool nuget:?package=ValueLink&version=0.119.0
                    
Install as a Cake Tool

NuGet Build and Test

ValueLink is a C# source generator and collection library for managing objects through multiple indexes and orderings. Each object stores its own links; a generated Goshujin (owner) maintains the corresponding chains. Generated setters keep indexes in sync with value changes.

Features include ordered and hash indexes, lists, queues, stacks, observable collections, bounded sliding windows, Tinyhand serialization, configurable isolation, and incremental synchronization.

Japanese guide

Contents

Requirements and installation

Use the .NET 10 SDK or later, a net10.0 or later target, and C# 14 or later. The NuGet package includes the source generator.

dotnet add package ValueLink

Declare linked models as partial classes or record classes and annotate them with [ValueLinkObject]. Nested models require partial containing types. Generic models are supported. A linked model may inherit ordinary members, but inheriting from another [ValueLinkObject] model is not supported.

Quick start

This complete console example indexes the same people by ID and age. AddValue = true requests a generated setter; it is false by default.

using System;
using ValueLink;

var people = new Person.GoshujinClass();
var ada = new Person(1, "Ada", 30);
people.Add(ada);
people.Add(new Person(2, "Bea", 20));

Console.WriteLine(people.IdChain.FindFirst(1)?.Name); // Ada
ada.AgeValue = 18; // Updates AgeChain as well as the underlying field.

foreach (var person in people.AgeChain)
{
    Console.WriteLine($"{person.Name}: {person.AgeValue}");
}
// Ada: 18
// Bea: 20

people.Remove(ada); // Removes all of Ada's links and clears her owner.
Console.WriteLine(ada.Goshujin is null); // True

/// <summary>Represents a person indexed by ID and age.</summary>
[ValueLinkObject]
public partial class Person
{
    [Link(Type = ChainType.Ordered, Primary = true)]
    public int Id { get; private set; }

    public string Name { get; private set; }

    [Link(Type = ChainType.Ordered, AddValue = true,
        Accessibility = ValueLinkAccessibility.Public)]
    private int age;

    public Person(int id, string name, int age)
    {
        this.Id = id;
        this.Name = name;
        this.age = age;
    }
}

Ownership and generated members

By default, the generator adds a nested GoshujinClass, an object's Goshujin property, and one NameChain/NameLink pair per link. A field named age produces AgeChain, AgeLink, and, when requested, AgeValue.

For None and Serializable isolation, assigning obj.Goshujin = owner or calling owner.Add(obj) transfers ownership and updates automatic links. Assigning null or calling owner.Remove(obj) removes the object from all chains. Each object has one owner at a time.

Direct chain operations change only that chain's membership. Assign the owner first; inserting an object into another owner's chain throws UnmatchedGoshujinException. Link fields contain collection state and should not be copied or edited directly. Likewise, copying a linked record with with does not create an independently linked object; use a RepeatableRead writer for record updates.

Set Primary = true on a chain that contains every owned object. It supplies the owner's Count, enumeration order, and serialization order. ReadCommitted owners expose their contents through operations such as ForEach and GetArray, rather than IEnumerable<T>.

Operation Effect
owner.SomeChain.Clear() Clears that chain; preserves owner references and other chains
owner.ClearChains() Clears all chains; preserves object owner references
owner.ClearAll() Removes objects found in the primary chain from all chains and clears their owner references

ClearAll is generated as a public operation only for None or Serializable owners with a primary chain. It uses a pooled snapshot, invokes removal callbacks, and clears the buffer before returning it to the pool. Objects added by callbacks are outside that snapshot. Other configurations throw NotImplementedException through IGoshujin.ClearAll. Acquire the owner lock when clearing synchronized owners. Materialize a snapshot before mutating a chain during enumeration.

Use generated value properties or partial properties to change indexed values. Writing a backing field or an ordinary property directly bypasses index maintenance and notifications.

Partial properties

A partial property can provide the public API directly, without a separate Value suffix:

using ValueLink;

/// <summary>Represents an item with an automatically maintained ID index.</summary>
[ValueLinkObject]
public partial class IndexedItem
{
    [Link(Type = ChainType.Ordered, Primary = true)]
    public partial int Id { get; set; }
}

Chains

n is the number of linked objects. Costs below refer to direct operations on one chain, without callbacks or owner-wide updates. No chain provides synchronization by itself.

ChainType Collection Main operations and costs
List ListChain<T> Index, membership, and removal: O(1). Add/insert: amortized O(1). Removal fills the gap with the last object; insertion may move the displaced object to the end.
LinkedList LinkedListChain<T> Add, move, remove by object, and link navigation: O(1). Preserves explicit list order.
StackList StackListChain<T> Push, peek, pop, and remove by object: O(1). Enumeration runs from bottom to top.
QueueList QueueListChain<T> Enqueue, peek, dequeue, and remove by object: O(1). Enumeration follows dequeue order.
Ordered OrderedChain<TKey, TObject> Key search, add, remove, and bounds: O(log n). Ascending key order; duplicate keys allowed.
ReverseOrdered OrderedChain<TKey, TObject> The same ordered chain with reversed comparison and traversal.
Unordered UnorderedChain<TKey, TObject> Hash lookup, add, and removal: expected O(1), worst-case O(n). Duplicate keys allowed; enumeration is unsorted.
Observable ObservableChain<T> Index: O(1). Append: amortized O(1); moving an existing object, search, insertion, and removal: O(n). Raises collection-change notifications.
SlidingList SlidingListChain<T> Bounded circular window. Position lookup and append: O(1). Head removal can scan holes; resizing and enumeration scale with window capacity.
None No collection Generates value/notification support without a chain.

Enumeration takes O(n) for ordinary lists and trees. Hash chains may scan unused backing slots; sliding chains may scan empty window positions. Constructing an enumerator is not the same cost as traversing the collection.

Ordered and unordered chains provide FindFirst, ContainsKey, TryGetValue, Keys, Objects, and KeyObjects. Use ordered FindAll(key) or unordered Enumerate(key) to enumerate duplicate keys. A missing FindFirst result is null for class models. Ordered chains also expose GetLowerBound, GetUpperBound, and inclusive GetRange; bounds follow the configured comparison order. Links on linked and ordered chains allow neighbor navigation.

Sliding windows

Sliding chains start with zero capacity and always require manual insertion, regardless of AutoLink:

using System;
using ValueLink;

var owner = new WindowItem.GoshujinClass();
owner.WindowChain.Resize(4);
var item = new WindowItem { Goshujin = owner };
Console.WriteLine(owner.WindowChain.Add(item)); // True
int position = item.WindowLink.Position;
Console.WriteLine(owner.WindowChain.Get(position) == item); // True

/// <summary>Represents an object placed manually in a bounded window.</summary>
[ValueLinkObject]
public partial class WindowItem
{
    [Link(Type = ChainType.SlidingList, Name = "Window")]
    public WindowItem() { }
}

Count counts live objects; Consumed also counts holes left by removal. StartPosition is the logical window start, and EndPosition is exclusive. Positions wrap within the underlying integer range. Add returns false when full or already linked. Set(position, obj) can replace an entry within the capacity window and unlinks the displaced object. Resize preserves logical positions and returns false if the new capacity is smaller than Consumed.

Attribute options

ValueLinkObject

Option Default Purpose
GoshujinClass "GoshujinClass" Generated owner type name
GoshujinInstance "Goshujin" Generated owner property name
ExplicitPropertyChanged "PropertyChanged" Event name used for generated notifications
Isolation None Concurrency model described below
Restricted false Makes the owner property internal and defaults links to private access with AddValue = false; explicit link options can override those defaults
Integrality false Enables hash-based difference synchronization

Empty name options select the defaults above.

Option Default Purpose
Type None Chain implementation
Name Target member name, initial letter capitalized Prefix for chain, link, and generated value names; specify distinct names for multiple indexes on one member
Primary false Selects the chain used for owner enumeration and serialization
Unique false Identifies the key used by isolation, journaling, and synchronization
AddValue false Generates a property that updates links when the value changes
AutoLink true Adds the link when ownership is assigned; ignored for sliding insertion
AutoNotify false Raises PropertyChanged from generated value changes
Accessibility PublicGetter Controls generated value/link access
TargetMember Empty Selects an accessible field or property for a constructor-level link
UnsafeTargetChain Empty Shares another chain instead of creating a new chain

Unique does not turn the underlying chain into a duplicate-rejecting collection. RepeatableRead commits check the selected unique key; callers that manipulate raw chains must preserve their own uniqueness invariant.

PublicGetter gives the generated value a public getter and inherits setter access from the target. Use Public to expose both accessors, or Protected, Private, or Inherit as needed. Getter-only targets do not receive writable value properties. Partial properties retain their declared accessor visibility.

With AutoLink = false, manually choose initial membership after assigning the owner. A generated value change can still add or update that link. Constructor attributes can define unkeyed chains or index a member using TargetMember. UnsafeTargetChain requires compatible key/object types and the correct ref obj.SomeLink argument for each shared entry; see TargetChainTest.

Generator options

Apply [ValueLinkGeneratorOption] to a class to set GenerateToFile or CustomNamespace. GenerateToFile = true writes source into an existing Generated folder beside that file; otherwise the compiler receives the generated source normally. CustomNamespace changes the module initializer namespace, not model namespaces. AttachDebugger is currently a reserved option with no effect.

Notifications and callbacks

Use AutoNotify = true with AddValue = true to generate INotifyPropertyChanged support. If the model already provides the event, the generator uses it. ExplicitPropertyChanged selects a custom event name. EqualityComparer<T>.Default suppresses redundant setter updates and notifications, including repeated floating-point NaN assignments. Generated linked setters cache one immutable PropertyChangedEventArgs per property and closed model type. The general-purpose SetProperty helper accepts dynamic names and creates arguments when it raises an event.

ObservableChain<T> separately implements INotifyCollectionChanged and INotifyPropertyChanged for collection changes. Its notifications run on the calling thread; UI applications must perform mutations on the appropriate thread. It identifies reference-type objects by identity, so distinct equal-valued records can coexist.

For a link named Age, the generator recognizes these optional instance methods:

private bool AgeLinkPredicate() => this.age >= 18;
private void AgeLinkAdded() { /* React to generated insertion. */ }
private void AgeLinkRemoved() { /* React to generated removal. */ }

The predicate controls generated insertion. Added/removed callbacks accompany generated link operations; direct calls to a chain bypass these model callbacks. Keep them short and account for the calling operation's lock scope. See AdditionalMethodTest and NotifyPropertyChangedTest.

Serialization and journaling

Add [TinyhandObject] and [Key] attributes to serialize models and their generated owners with Tinyhand. Choose a primary chain that contains all objects. The generated owner serializer preserves supported chain memberships and ordering and restores object ownership.

using System;
using Tinyhand;
using ValueLink;

var original = new StoredItem.GoshujinClass();
original.Add(new StoredItem { Id = 1, Name = "Ada" });
byte[] bytes = TinyhandSerializer.Serialize(original);
var restored = TinyhandSerializer.Deserialize<StoredItem.GoshujinClass>(bytes)!;
Console.WriteLine(restored.IdChain.FindFirst(1)?.Name); // Ada

/// <summary>Represents a serializable item indexed by ID.</summary>
[TinyhandObject]
[ValueLinkObject]
public partial class StoredItem
{
    [Key(0)]
    [Link(Type = ChainType.Ordered, Primary = true)]
    public int Id { get; set; }

    [Key(1)]
    public string Name { get; set; } = string.Empty;
}

Initialize ordinary properties before adding objects; use generated setters for later indexed changes. Treat sliding-window capacity and membership as runtime configuration to restore explicitly. See SerializationContractTest for round trips, cloning, and independent memberships.

[TinyhandObject(Structural = true)] enables structural/journal integration. Use a primary chain and a unique keyed link, then connect the owner to an IStructuralRoot implementation. Generated operations can record changes, but storage, journal persistence, and replay are supplied by the host application. Serializing an owner alone does not provide durable storage. See JournalTest.

NativeAOT

ValueLink generates static Tinyhand formatter registrations for owners, including closed generic models, private nested models, and unions. It does not construct owner formatters through runtime reflection. The library enables .NET trimming and AOT analyzers with IsAotCompatible.

ValueLink references the published Tinyhand 0.144.1 package, which includes the static registration fixes for unresolved and anonymous types. Restore and build normally; see NativeAOT setup and limitations.

On a host with the NativeAOT prerequisites, publish and run the smoke test:

dotnet publish NativeAotTest/NativeAotTest.csproj -c Release -r win-x64 -o artifacts/native-aot/publish
./artifacts/native-aot/publish/NativeAotTest.exe

Windows x64 has been verified locally. The CI workflow also includes Linux x64; use linux-x64 and run the executable without .exe on Linux. Always test the published executable; dotnet test alone verifies JIT execution.

Closed generic types must be discoverable at compilation time. Explicit [assembly: TinyhandRegister(typeof(MyModel<int>))] roots cover types used only through external generic helpers. Containing types needed to access private generic arguments must be partial. When another generated model stores a collection of owners, declare the owner as a partial class with [TinyhandObject(External = true)] so Tinyhand can resolve its type before ValueLink runs; see NativeContracts.

Isolation

Mode Model
None Caller controls all synchronization
Serializable Generated owner provides LockObject; callers lock compound reads and mutations
ReadCommitted Keyed owner delegates data retrieval and locking to IDataLocker<TData> adapters
RepeatableRead Record classes publish updated copies through exclusive writers

Serializable

Protect collection operations and indexed mutations with the owner's lock. For a model configured with Isolation = IsolationLevel.Serializable:

using (owner.LockObject.EnterScope())
{
    // Read or mutate this owner's chains and objects here.
}

GetArray() takes a snapshot under the lock and copies object references. The Serializable LockObject is a non-reentrant semaphore: call GetArray() outside an already-held owner lock. Storage lifecycle operations also synchronize their work. Coordinate both owners when transferring objects between them.

ReadCommitted

Configure a unique key and implement IDataLocker<TData> on the linked adapter. The generated owner supplies Find, TryGet, TryLock, TryDelete, ForEach, and GetArray. Timeouts, cancellation tokens, and optional factories are forwarded to the adapter, which implements data storage and locking.

TryLock returns ValueTask<DataScope<TData>>. Inspect Result for Retrieved, Created, or a failure, and dispose the scope to release its lock. IsValid requires a live lock and non-null data, including for value-type data; it becomes false after release. Result remains available after disposal. Repeated disposal of the same variable is safe, but do not dispose multiple copies of this mutable struct. UnlockAndDelete releases the lock and requests deletion through the associated storage object. Both release operations invalidate the scope before calling adapters, including when an adapter throws.

Protected objects delay deletion unless forced. Store/release and delete operations propagate through Tinyhand structural objects. See ReadCommittedContractTest for an in-memory adapter and lifecycle examples.

RepeatableRead

Use a partial record class with a unique keyed link. Acquire a writer, change its properties, call Commit, and dispose it. Commit returns the published record, or null if it cannot publish, such as on a unique-key conflict. Disposing without a commit discards unpublished edits.

Writer reads and unchanged assignments for fields and auto-properties avoid copying the record. Custom accessors still run on the editable copy, preserving their side effects. The first changed assignment or explicit access to Instance creates the editable copy. Rollback discards pending edits while retaining the writer lock. A commit updates every keyed index attached to a changed member; reverting to the original unique key does not conflict with the record itself. Repeated Dispose calls are safe; other writer operations throw ObjectDisposedException after disposal. A writer is for one caller and must not be used concurrently.

using System;
using ValueLink;

var accounts = new Account.GoshujinClass();
using (var writer = await accounts.TryLockAsync(1, AcquisitionMode.GetOrCreate))
{
    if (writer is null)
    {
        throw new InvalidOperationException("The account could not be acquired.");
    }

    writer.Balance = 100;
    Account published = writer.Commit()
        ?? throw new InvalidOperationException("The account could not be committed.");
    Console.WriteLine(published.Balance); // 100
}

/// <summary>Represents an account updated through a repeatable-read writer.</summary>
[ValueLinkObject(Isolation = IsolationLevel.RepeatableRead)]
public partial record Account
{
    [Link(Type = ChainType.Unordered, Primary = true, Unique = true)]
    public int Id { get; private set; }

    public int Balance { get; private set; }
}

Readers retain the record version they acquired. Record copies are shallow: clone a mutable list, array, or nested object before changing it through the writer. Do not mutate a published record or a shared reference directly.

TryGet retrieves a record. TryLock waits synchronously for a writer; TryLockAsync supports a timeout and cancellation token. A timeout returns null; cancellation while waiting propagates as OperationCanceledException. Overloads without a timeout use ValueLinkGlobal.LockTimeout. Direct chain reads still require the owner lock or a snapshot.

Both keyed isolation modes use AcquisitionMode.GetOnly, GetOrCreate, and CreateOnly. GetOnlyIgnoreState requests adapter-specific state handling; it does not bypass an invalid owner or create missing data.

Incremental synchronization

Integrality compares cached hashes, requests differing objects, and integrates responses. Enable [ValueLinkObject(Integrality = true)] together with [TinyhandObject], a unique unmanaged struct key such as int, and None or Serializable isolation. Structs containing managed references are rejected because keys are copied as raw bytes.

This local broker demonstrates the request/response contract; replace it with your transport as needed:

using System;
using System.Threading.Tasks;
using Tinyhand;
using ValueLink;
using ValueLink.Integrality;

var source = new SyncItem.GoshujinClass();
var target = new SyncItem.GoshujinClass();
source.Add(new SyncItem { Id = 1, Name = "Ada" });
var engine = new Integrality<SyncItem.GoshujinClass, SyncItem>
{
    MaxItems = 1_000,
    RemoveIfItemNotFound = true,
};

var result = await engine.Integrate(target, (request, cancellationToken) =>
{
    cancellationToken.ThrowIfCancellationRequested();
    return Task.FromResult(engine.Differentiate(source, request));
});
Console.WriteLine(result.Result); // Success

/// <summary>Represents an item synchronized by a unique ID.</summary>
[TinyhandObject]
[ValueLinkObject(Integrality = true)]
public partial class SyncItem
{
    [Key(0)]
    [Link(Type = ChainType.Unordered, Primary = true, Unique = true)]
    public int Id { get; set; }

    [Key(1)]
    public string Name { get; set; } = string.Empty;
}

MaxItems limits reported keys and new items; RemoveIfItemNotFound removes local entries absent from the remote key list. MaxMemoryLength limits object-response packets, not probe responses. MaxIntegrationCount limits object-request iterations after probing. Override Validate to accept/reject incoming objects and Trim for application-specific removal; the default Trim removes nothing.

The broker transfers ownership of its returned BytePool.RentMemory to the engine. When calling Differentiate outside a broker, return that buffer after use. Request bytes are valid only until the broker task completes.

Integration may make partial changes before returning an incomplete/error result. Error responses retain the iteration and integration counts completed so far. Broker exceptions and cancellation propagate. Serialize concurrent runs targeting the same owner. Reported counts cover integration and trimming, but exclude removals during key comparison; IsModified is not a complete change log. Generated link setters, including Tinyhand partial-property update hooks, invalidate both object and owner hashes. After directly changing serialized fields, ordinary properties, or nested mutable content, call ((IIntegralityObject)item).ClearIntegralityHash() yourself.

Performance

Choose chains for the operations you need and add only the indexes you use. Embedded links avoid searching for an object's node when removing it. Each additional chain consumes memory and adds work to ownership changes or indexed updates. Record writers allocate copies, and callbacks or serialization can dominate collection costs.

Measure against representative workloads using the benchmarks in this repository:

dotnet run --project Benchmark/Benchmark.csproj -c Release -- --filter "*ChainMaintenanceBenchmark*"
dotnet run --project Benchmark/Benchmark.csproj -c Release -- --filter "*AllocationBenchmark*"

Use Release builds and compare both timings and allocations. Results depend on runtime, hardware, collection size, key distribution, and enabled features.

Ordered, unordered, and sliding ICollection.CopyTo operations use struct enumerators without boxing. Concrete chain enumeration also avoids boxing where the chain exposes a struct enumerator; enumerating through an interface may allocate. Pooled ClearAll snapshots and notification caches remove repeated temporary allocations after warm-up. Pool misses, collection growth, ordered/linked nodes, observable collection events, and record edits can still allocate. These optimizations do not make every operation allocation-free. See the measured comparison for the benchmark conditions and results.

Build and tests

Run from the repository root:

dotnet restore ValueLink.slnx
dotnet build ValueLink.slnx -c Release --no-restore
dotnet test --project xUnitTest/xUnitTest.csproj -c Release --timeout 60s
dotnet test --solution ValueLink.slnx -c Debug --timeout 60s

The suite uses xUnit v3 and Microsoft.Testing.Platform, configured in global.json; use the --project or --solution switch. It exercises chains, ownership, serialization, isolation, synchronization, and source generation. See the test coverage map for contracts and limitations.

Project Purpose
ValueLink Public attributes, chains, isolation, and synchronization APIs
ValueLinkGenerator Roslyn source generator
xUnitTest Contract, integration, and regression tests
NativeAotTest Native executable sharing behavioral checks with xUnit
NativeAotModels Cross-assembly generic fixtures for native and xUnit checks
QuickStart Runnable usage examples
Playground Development experiments and storage adapter examples
Benchmark BenchmarkDotNet performance experiments

ValueLink is distributed under the MIT license.

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 (4)

Showing the top 4 NuGet packages that depend on ValueLink:

Package Downloads
Netsphere

Netsphere is a network protocol for Lp.

BigMachines

A source-generated state-machine library for .NET.

CrystalData

CrystalData is a simple and versatile storage engine for C#.

Lp

Lp is an experimental program that create value.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.119.0 38 9/6/2026
0.118.4 46 9/5/2026
0.118.3 67 9/5/2026
0.118.2 85 9/3/2026
0.118.1 40 9/3/2026
0.118.0 35 9/3/2026
0.117.2 85 9/1/2026
0.117.1 95 8/31/2026
0.117.0 232 8/20/2026
0.116.1 142 7/14/2026
0.116.0 165 5/27/2026
0.115.4 302 5/11/2026
0.115.3 210 5/11/2026
0.115.2 235 5/7/2026
0.115.1 258 4/18/2026
0.115.0 207 4/6/2026
0.114.2 142 4/6/2026
0.114.1 185 4/5/2026
0.114.0 125 4/5/2026
0.113.0 134 4/5/2026
Loading failed