Skip to content

About

A unified change model for .NET. Source-generated types for partial state, editing, and change tracking in C#. NativeAOT friendly.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

SparseTypes

NuGet Version GitHub Actions Workflow Status DeepWiki

A unified change model for .NET.

SparseTypes generates change-tracking, diffing, patching, and merging companion types from C# models.

Motivation

Consider a case where you directly edit settings.Theme = "dark".
In that situation, information about the previous value and exactly where it changed is lost.
This makes it inconvenient to track edit history or update the UI based on the changed parts.

Additionally, consider a scenario where multiple people are editing the same settings.
Unrelated changes should be merged automatically, and conflicts should be detected and reported clearly.

Implementing these features required a lot of redundant code. It's tedious, isn't it?

SparseTypes solves this by generating companion types for your C# models at compile time to track changes, create diffs, apply patches, and merge updates.

It's simple to use: just add the [SparseModel] attribute to your model.
That's it. All the necessary companion types are generated automatically.

[SparseModel]                 // add this attribute
public partial class Settings // mark the class as partial
{
    public string? Theme { get; set; }
    public DatabaseSettings? Database { get; set; }
}

// Reachable nested models are also marked as partial.
public partial class DatabaseSettings
{
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 5432;
}

Behind the scenes, many classes are generated, but users don't need to be aware of it.

Let's take a look at how it can be used.

Quick Start

Simple example

With the .NET 10 SDK or later, you can run a complete single-file program.
Save the file as quickstart.cs and run it: dotnet run quickstart.cs.

#:package SparseTypes@*

using SparseTypes;

var settings = new Settings
{
    Label = "default",
    Database = new() { Host = "db.local", Port = 5432 },
};

// 1. Start an edit session (captures the initial baseline)
var session = settings.CreateEditSession();

// 2. Edit through the observable proxy (writes through to settings)
session.Observable.Label = "production";
session.Observable.Database!.Port = 7432;

// 3. Inspect dirty state and typed transitions
var changes = session.CreateChangeSet();

Console.WriteLine(settings.Label); // production
Console.WriteLine(session.HasChanges); // True
Console.WriteLine(changes.Label.IsChanged); // True
Console.WriteLine(changes.Database.Port.Before.Value); // 5432
Console.WriteLine(changes.Database.Port.After.Value); // 7432

// 4. Revert edits back to the session baseline
session.RevertChanges();
Console.WriteLine(session.HasChanges); // False
Console.WriteLine(settings.Label); // default

// ---------
[SparseModel]
public partial class Settings
{
    public string? Label { get; set; }
    public DatabaseSettings? Database { get; set; }
}
public partial class DatabaseSettings
{
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 5432;
}

What happened?

Let's walk through it step by step.

  1. Start an edit session with CreateEditSession.
    • The starting values are captured internally at this point.
  2. Update values through session.Observable.
    • Editing through this object raises INotifyPropertyChanged notifications.
    • This makes it work as-is with UI frameworks such as WPF.
  3. Collect the differences with CreateChangeSet.
    • For each value you can check whether it changed (changes.X.IsChanged) and, if so, get the before/after values (changes.X.Before|After.Value).
    • You can also collect all changed locations as an array (changes.EnumerateChanges).
  4. Revert changes with RevertChanges.
    • This rolls back to the values at the start (step 1).
    • You can also accept changes (AcceptChanges).

Of course, there is much more that didn't fit here! For example...

Wire transport and concurrent rebase

When changes cross process boundaries, serialize the ChangeSet to a ChangePayload and reconcile it against the latest server state:

// 1. Serialize the typed change set for wire transport
var json = JsonSerializer.Serialize(changes.ToPayload());
// 2. Deserialize on the server
var incoming = JsonSerializer.Deserialize<Settings.ChangePayload>(json)!.ToChangeSet();
// 3. Rebase incoming changes onto latest server state
var rebased = incoming.RebaseOnto(updated);

RebaseOnto works like git rebase: it reports conflicts where they occur and merges everything else automatically. With ChangePayload and Rebase, partial changes fit naturally even in environments where the client and server are separated.

Install

Install the SparseTypes package from NuGet:

dotnet add package SparseTypes

We also provide several other libraries. For details, see here.

Learn more

See the Documentation Index for the full map.

License

Licensed under the Apache-2.0 License.

About

A unified change model for .NET. Source-generated types for partial state, editing, and change tracking in C#. NativeAOT friendly.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages