x86cc.KVBind.Core 0.1.0

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

KVBind

Typed C# bindings for canonical key-value document data.

KVBind is a typed C# runtime for editing large schema-driven object graphs as canonical key-value data. It gives each aggregate a Git-like editing model: committed snapshots, user draft overlays, replayable commits, patch operations, validation, and change reactions.

It is intended for large forms and document-like aggregates where users may spend time drafting changes before committing them and APIs need precise patch operations.

KVBind gives you:

  • draft editing over committed object state
  • easy change detection and semantic diffs
  • patch-based editing for API workflows
  • replayable commits that can reconstruct an aggregate over time
  • definition-driven validation and dependency behavior
  • generated C# property accessors over a shared runtime

https://github.com/user-attachments/assets/dbbd4264-4d86-4db5-8c68-da4fd882f18d

Mental Model

Think of each KVBind aggregate as a small Git-like repository for an object graph.

A root object has committed state, draft state, and a history of changes. You can edit through typed C# properties or patch operations, inspect what changed, commit the draft, discard parts of it, and replay commits to reconstruct state over time.

KVBind is also close to event sourcing for UI edits. It records user-intended changes over large forms or object graphs, while still maintaining an effective current projection for normal typed reads.

Quick Example

Define your model as ordinary C# node types. Fields can be generated from partial [KVBind] properties, while collections and groups are regular runtime nodes.

public partial class Agreement : KVRootNode
{
    [KVBind("Title")]
    public partial string? Title { get; set; }

    [KVBind("General")]
    public AgreementGeneral General { get; } = new();

    [KVBind("LineItems")]
    public KVCollectionNode<AgreementLineItem> LineItems { get; } = new();

    [KVBind("Party")]
    public partial AgreementParty? Party { get; private set; }

    [KVBind("Summary")]
    public partial string? Summary { get; set; }

    public void RecalculateSummary(KVChangeContext<Agreement> context)
    {
        Summary = $"Changed: {context.ChangedPath}";
    }
}

public partial class AgreementGeneral : KVFieldGroupNode
{
    [KVBind("Code")]
    public partial string? Code { get; set; }
}

public partial class AgreementLineItem : KVCollectionItemNode
{
    [KVBind("Description")]
    public partial string? Description { get; set; }

    [KVBind("Amount")]
    public partial decimal Amount { get; set; }
}

public abstract partial class AgreementParty : KVNestedNode;

public partial class CompanyParty : AgreementParty
{
    [KVBind("CompanyName")]
    public partial string? CompanyName { get; set; }
}

public partial class PersonParty : AgreementParty
{
    [KVBind("FullName")]
    public partial string? FullName { get; set; }
}

Then define the schema and runtime behavior with the DSL:

var builder = new KVBindBuilder<Agreement>();

builder.Field(x => x.Title, options =>
{
    options.Validation(profiles => profiles
        .For<FullValidationProfile>(rules => rules.Required().MaxLength(100)));
});

builder.Field(x => x.Summary);

builder.FieldGroup(x => x.General, group =>
{
    group.Field(x => x.Code);
});

builder.Collection(x => x.LineItems, collection =>
{
    collection.Item<AgreementLineItem>(item =>
    {
        item.Field(x => x.Description);
        item.Field(x => x.Amount);
    });

    collection.MinCount(1);
    collection.Validation(profiles => profiles
        .For<FullValidationProfile>(rules =>
            rules.AggregateSum<AgreementLineItem, decimal>(x => x.Amount)
                .LessThanOrEqual(10_000m)));
});

builder.NestedNode(x => x.Party, nested =>
{
    nested.Bind<CompanyParty>("COMPANY", company =>
    {
        company.Field(x => x.CompanyName);
    });

    nested.Bind<PersonParty>("PERSON", person =>
    {
        person.Field(x => x.FullName);
    });
});

builder.OnChange(
    path => path.Collection(x => x.LineItems).Field(x => x.Amount),
    x => x.RecalculateSummary);

var definition = builder.Build();

Typed Runtime Access

Once a root is bound to a runtime model, you work with normal typed properties and collection APIs.

var snapshot = new KVSnapshot();
var overlay = KVOverlay.Create(snapshot, user: "alice");
var model = KVModelRoot.Create(overlay, definition);
var agreement = KVRootNode.Create<Agreement>(model, definition);

agreement.Title = "Services Agreement";
agreement.General.Code = "MSA-2026";

var itemId = Guid.NewGuid();
var item = agreement.LineItems.Create(itemId);
item.Description = "Implementation services";
item.Amount = 120m;

var changes = agreement.GetAllChanges();

Direct property edits, patch operations, validation, change tracking, and commits all flow through the same overlay-backed runtime.

Core Runtime Model

KVBind separates committed data, draft edits, replayable changes, and typed runtime access.

The runtime model maps to the Git-like workflow:

  • KVSnapshot is the committed projection of canonical path/value data.
  • KVOverlay is a user-owned draft over that projection, similar to uncommitted changes.
  • KVCommit is a replayable immutable changeset produced from an overlay.
  • KVModelRoot binds snapshot and overlay data to a runtime definition.
  • KVRootNode is the typed aggregate root API.

A typical edit/commit flow:

var snapshot = new KVSnapshot();
var overlay = KVOverlay.Create(snapshot, user: "alice");
var model = KVModelRoot.Create(overlay, definition);
var agreement = KVRootNode.Create<Agreement>(model, definition);

agreement.Title = "Updated agreement";

var draftChanges = agreement.GetAllChanges();
var commit = agreement.CreateCommit(DateTimeOffset.UtcNow);

snapshot.Apply(commit);

Applications own persistence of snapshots, overlays, and commits. KVBind provides the runtime behavior and data structures.

Canonical Storage, Flexible Layout

KVBind stores values by canonical paths instead of serializing the current C# object graph shape. Typed nodes, field groups, sections, and UI layouts can evolve without automatically forcing persisted data migrations.

Definition DSL

The definition DSL describes schema, validation, patch behavior, collection item types, nested node variants, and change reactions.

Fields

builder.Field(x => x.Title);

Field Groups

builder.FieldGroup(x => x.General, group =>
{
    group.Field(x => x.Code);
});

Collections

builder.Collection(x => x.LineItems, collection =>
{
    collection.Item<AgreementLineItem>(item =>
    {
        item.Field(x => x.Description);
        item.Field(x => x.Amount);
    });
});

Collection rows use immutable GUID row identity.

var item = agreement.LineItems.Create(Guid.NewGuid());

Nested Nodes

Nested nodes model a nullable polymorphic slot: one active subtype at a path.

builder.NestedNode(x => x.Party, nested =>
{
    nested.Bind<CompanyParty>("COMPANY", company => company.Field(x => x.CompanyName));
    nested.Bind<PersonParty>("PERSON", person => person.Field(x => x.FullName));
});

Patch operations can initialize or drop the active subtype:

agreement.Patch(
    KVPatchOperation.Init("/Party", "COMPANY"),
    KVPatchOperation.Set("/Party/CompanyName", "Contoso Ltd."));

Change Reactions

Change reactions let definitions react to direct typed setters and patch mutations.

builder.OnChange(
    path => path.Collection(x => x.LineItems).Field(x => x.Amount),
    x => x.RecalculateSummary);

The reaction method receives a context:

public void RecalculateSummary(KVChangeContext<Agreement> context)
{
    Summary = $"Changed: {context.ChangedPath}";
}

Reactions bubble through field groups, collections, collection items, and nested nodes. Reaction execution state is scoped to the root aggregate, so separate edits start cleanly. KVBind detects active reaction cycles such as A -> B -> A and keeps a maximum chain-length guard for runaway non-repeating chains.

Validation

Validation rules are attached to fields, groups, collections, and profiles.

builder.Field(x => x.Title, options =>
{
    options.Validation(profiles => profiles
        .For<FullValidationProfile>(rules => rules.Required().MaxLength(100)));
});

Collection rules can validate count and aggregate values:

builder.Collection(x => x.LineItems, collection =>
{
    collection.Item<AgreementLineItem>(item => item.Field(x => x.Amount));
    collection.MinCount(1);
    collection.Validation(profiles => profiles
        .For<FullValidationProfile>(rules =>
            rules.AggregateSum<AgreementLineItem, decimal>(x => x.Amount)
                .LessThanOrEqual(10_000m)));
});

Validation profiles are marker objects selected by the root:

public sealed record QuickValidationProfile : KVValidationProfile
{
    public static QuickValidationProfile Instance { get; } = new();
    private QuickValidationProfile() { }
}

public sealed record FullValidationProfile : KVValidationProfile
{
    public static FullValidationProfile Instance { get; } = new();
    private FullValidationProfile() { }
}

protected override KVValidationProfile GetValidationProfile()
{
    return IsReadyForFullReview
        ? FullValidationProfile.Instance
        : QuickValidationProfile.Instance;
}

Run validation explicitly or through patch results:

var validation = agreement.Validate();

var patch = agreement.Patch(KVPatchOperation.Set("/Title", ""));
var patchValidation = patch.Validate();

Patching

Patch operations address canonical paths and run sequentially in the order supplied. This allows one request to create a row and then set fields on that row.

var itemId = Guid.NewGuid();

var result = agreement.Patch(
    KVPatchOperation.Add("/LineItems", new KVAddPatchPayload(itemId)),
    KVPatchOperation.Set($"/LineItems/{itemId:D}/Description", "Implementation services"),
    KVPatchOperation.Set($"/LineItems/{itemId:D}/Amount", 120m));

var validation = result.Validate();

Built-in operations:

  • SET updates a field value.
  • UNSET removes a field value from the draft.
  • ADD creates a collection item with a client-provided GUID.
  • REMOVE removes a collection item.
  • MOVE reorders a collection item.
  • INIT selects a nested node subtype.
  • DROP clears a nested node slot.
  • DISCARD discards draft changes at a path.

Custom collection operations can be registered on collection definitions:

builder.Collection(x => x.LineItems, collection =>
{
    collection.Operation<GroupLineItems>("GROUP", x => x.GroupLineItems);
    collection.Item<AgreementLineItem>(item => item.Field(x => x.Amount));
});

Built-in operation names are reserved and cannot be overridden.

Source Generation

KVBind uses partial [KVBind] properties for typed accessor generation.

public partial class Agreement : KVRootNode
{
    [KVBind("Title")]
    public partial string? Title { get; set; }
}

The generated property implementation reads and writes through the bound KVBind runtime. The DSL defines schema and behavior; the generator keeps the model surface ergonomic.

Current Status

KVBind is actively evolving toward a standalone package. The current runtime focuses on the core model: canonical field data, draft overlays, typed wrappers, patching, validation, nested nodes, collections, and change reactions.

APIs may change while the runtime is hardened.

Build And Test

From a standalone KVBind repository:

dotnet build
dotnet test

When working directly with the KVBind projects:

dotnet build x86cc.KVBind.Core/x86cc.KVBind.Core.csproj
dotnet test x86cc.KVBind.UnitTests/x86cc.KVBind.UnitTests.csproj
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.
  • net10.0

    • No dependencies.

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.1.0 92 6/14/2026