CrdtSync 1.1.0
See the version list below for details.
dotnet add package CrdtSync --version 1.1.0
NuGet\Install-Package CrdtSync -Version 1.1.0
<PackageReference Include="CrdtSync" Version="1.1.0" />
<PackageVersion Include="CrdtSync" Version="1.1.0" />
<PackageReference Include="CrdtSync" />
paket add CrdtSync --version 1.1.0
#r "nuget: CrdtSync, 1.1.0"
#:package CrdtSync@1.1.0
#addin nuget:?package=CrdtSync&version=1.1.0
#tool nuget:?package=CrdtSync&version=1.1.0
CrdtSync
A lightweight State-based LWW-Map CRDT (Last-Writer-Wins Map) sync engine for .NET. Mark properties with [Synced], extend SyncableEntity, and get automatic per-field merge with last-writer-wins conflict resolution.
Features
- Per-field conflict resolution — each property tracks its own timestamp, so concurrent edits to different fields on the same entity never conflict
- Last-Writer-Wins — when two devices edit the same field, the most recent write wins automatically
- Hybrid Logical Clock — tolerates clock skew between machines (up to seconds), ensuring correct merge even when system clocks differ
- Compare-and-swap safe — designed for transports without native optimistic concurrency (e.g. Google Drive)
- Zero configuration — just inherit
SyncableEntity, add[Synced]to properties, and implementSyncKey - Transport agnostic — works with any serialization/transport layer (JSON files, REST APIs, Google Drive, databases, etc.)
- Reflection-cached — property metadata is built once per type and reused across all instances
- INotifyPropertyChanged — built-in change notification for MVVM/UI binding
- Logging — optional
Microsoft.Extensions.Loggingintegration; silent by default
Installation
dotnet add package CrdtSync
Quick Start
1. Define your entity
using CrdtSync;
public class TodoItem : SyncableEntity
{
private string _title = "";
private bool _isDone;
private int _priority;
// SyncKey matches entities across devices (must be unique)
public override string SyncKey => _title;
[Synced]
public string Title
{
get => _title;
set => SetField(ref _title, value);
}
[Synced]
public bool IsDone
{
get => _isDone;
set => SetField(ref _isDone, value);
}
[Synced(ClearValue = 0)]
public int Priority
{
get => _priority;
set => SetField(ref _priority, value);
}
}
2. Edit entities (timestamps are tracked automatically)
var item = new TodoItem { SuppressTimestamp = false };
item.Title = "Buy milk";
item.Priority = 3;
// FieldTimestamps now contains HLC timestamps for "Title" and "Priority"
3. Merge from a remote copy
// Single entity merge
bool changed = localItem.MergeFrom(remoteItem);
// Bulk merge — matches by SyncKey, returns changed count + new entities
var (mergeCount, newItems) = SyncableEntity.MergeAll(localList, remoteList, "Device1");
localList.AddRange(newItems);
4. Serialize and transport however you like
using System.Text.Json;
// Serialize
var json = JsonSerializer.Serialize(localList);
// Deserialize (SuppressTimestamp defaults to true, so deserialization won't overwrite timestamps)
var remoteList = JsonSerializer.Deserialize<List<TodoItem>>(json);
Hybrid Logical Clock (HLC)
CrdtSync uses a Hybrid Logical Clock to generate timestamps instead of raw DateTime.UtcNow. This solves a common problem with multi-device sync: clock skew.
When two machines have slightly different system clocks (even a few seconds), a plain DateTime.UtcNow comparison can give the wrong merge result — an edit made later in real time can lose to an earlier edit from a machine with a faster clock.
The HLC guarantees:
- Timestamps are monotonically increasing — every new timestamp is strictly greater than the previous one
- When merging remote data, the clock witnesses remote timestamps and advances past them
- A subsequent local edit always gets a timestamp greater than any previously seen value
This means that as long as devices sync periodically, the HLC self-corrects for clock drift automatically. No NTP configuration required.
// The HLC is used automatically by SetField.
// For testing with simulated clock skew:
HybridClock.SetPhysicalClock(() => DateTime.UtcNow.AddSeconds(-5));
HybridClock.Reset(); // Reset state (for test isolation)
API Reference
SyncableEntity (abstract base class)
| Member | Description |
|---|---|
abstract string SyncKey |
Unique key to match entities across devices (e.g. Name, Id) |
Dictionary<string, DateTime> FieldTimestamps |
Per-field timestamps for merge decisions |
bool SuppressTimestamp |
When true, property changes don't update timestamps. Defaults to true — set to false after construction/deserialization |
bool MergeFrom(SyncableEntity other) |
Merge per-field from another entity. Returns true if any value changed |
void Clear() |
Reset all [Synced] properties to their ClearValue defaults |
static (int, List<T>) MergeAll<T>(...) |
Bulk merge remote list into local list by SyncKey |
static Action<T, Random>[] BuildRandomEditActions<T>() |
Generate random edit actions per [Synced] property (useful for testing) |
static IReadOnlyList<SyncedPropertyMeta> GetSyncedProperties(Type) |
Get cached reflection metadata for a type |
protected bool SetField<T>(ref T, T, string?) |
Set a backing field, auto-update timestamp via HLC, and raise PropertyChanged |
HybridClock (static)
| Member | Description |
|---|---|
static DateTime Now() |
Returns a monotonically increasing timestamp (max of physical clock and last known + 1ms) |
static void Witness(DateTime remoteTime) |
Advances the clock past a remote timestamp. Called automatically during MergeFrom |
static void Reset() |
Resets clock state (for testing only) |
static void SetPhysicalClock(Func<DateTime>?) |
Overrides the physical clock source (for testing clock skew) |
[Synced] attribute
| Property | Type | Default | Description |
|---|---|---|---|
ClearValue |
object? |
null |
Value used by Clear(). If null, uses default(T) (empty string for string) |
DbType |
string |
"" |
Optional SQLite column type hint for DB migration scenarios |
Logging
CrdtSync uses Microsoft.Extensions.Logging. By default logging is disabled (NullLogger). To enable:
// Option A: Pass an ILogger directly
SyncableEntity.SetLogger(myLogger);
// Option B: Pass an ILoggerFactory
SyncableEntity.SetLogger(loggerFactory);
// With Serilog (add Serilog.Extensions.Logging package):
using Serilog.Extensions.Logging;
SyncableEntity.SetLogger(new SerilogLoggerFactory(Log.Logger).CreateLogger("CrdtSync"));
How It Works
CrdtSync implements a State-based LWW-Map CRDT with Hybrid Logical Clock:
- Each entity has a
FieldTimestampsdictionary mapping property names to their last-edit time - When
SetFieldis called (andSuppressTimestampisfalse), the field's timestamp is set viaHybridClock.Now()— which returnsmax(physicalClock, lastKnown + 1ms) - During
MergeFrom, each[Synced]property is compared by timestamp — the newer value wins. Remote timestamps are witnessed by the HLC so future local edits always get a greater timestamp MergeAllmatches entities across lists usingSyncKey, merges existing ones, and returns entities that only exist remotely
This means:
- No central server needed — any device can merge with any other device
- Offline-friendly — edits accumulate timestamps locally and resolve on next sync
- No data loss — only individual fields are overwritten, not entire objects
- Clock-skew tolerant — the HLC ensures correct ordering even when system clocks differ by seconds
Supported Property Types
Any type that works with EqualityComparer<T>.Default and can be set via reflection:
string(default clear value:"")int,bool,double,float,decimalDateTime,Guid- Enums
- Any serializable reference type
Requirements
- .NET 8.0 or later
Microsoft.Extensions.Logging.Abstractions(included automatically)
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.