G9SyncData.Client 1.1.0

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

G9SyncData

Offline-first data sync for .NET: PostgreSQL on the server, SQLite on devices, SignalR in between — exact, resumable, observable and NativeAOT-compatible from the first line.

 PostgreSQL ──capture──▶ G9SyncData.Server ──SignalR (G9SignalRSuperNetCore)──▶ G9SyncData.Client ──▶ SQLite
 (triggers, snapshot                engine + hub                                 engine + queue          (triggers,
  cursor, notify)          ◀── pushes (idempotent, per-row results) ◀──                                 staging)

Status: 1.1.0. The engines, PostgreSQL and SQLite providers, SignalR transports, source generator, server-side change feed and OpenTelemetry instrumentation are implemented; 141 unit and 170 end-to-end tests pass against real PostgreSQL, SQLite and SignalR (the end-to-end suite in both capture modes), the client and 200k/500k-row sites pass on the Android emulator, and the NativeAOT client publishes without warnings. It is the sync engine in production use by a fleet field application, which replaced a Dotmim.Sync fork with it outright. Benchmarks are in AiGuides/19-Performance.md. Read AiGuides/17-Known-Limitations.md before adopting.

Every package in this family releases together and must not be mixed. The protocol frame layout is negotiated across Client/Server/Protocol, so a client from one version against a server from another is exactly the mismatch the version pins.

Packages

Pick by role. Each pulls what it needs, so a device app normally names only the two client packages and a server only the server ones.

Package Take it when
G9SyncData.Client.Sqlite a device/client app storing into SQLite
G9SyncData.Client.SignalR that client talks over SignalR (the supported transport)
G9SyncData.Server.PostgreSql the server whose PostgreSQL is the source of truth
G9SyncData.Server.SignalR that server hosts the sync hub
G9SyncData.Server.EfCore derive the sync model from an existing EF Core IModel
G9SyncData.Testing test helpers for building suites against the engine
G9SyncData.Client / .Server / .Abstractions / .Protocol pulled in for you; name directly only when writing your own store or transport
G9SyncData.SourceGenerators normally not referenced — see below
dotnet add package G9SyncData.Client.Sqlite
dotnet add package G9SyncData.Client.SignalR

The client model source generator ships inside G9SyncData.Client (analyzers/dotnet/cs), so a client gets it with no extra reference. Do not also reference G9SyncData.SourceGenerators: the generator would load twice and every generated partial would collide. The standalone package exists for tooling that wants the analyzer alone.

⛔ All packages in the family carry one version and must be kept on it together.

Why

G9SyncData replaces a Dotmim.Sync fork. Its guarantees target the failures that fork produced in the field:

Guarantee How
A change is never skipped The cursor is a PostgreSQL transaction snapshot, not a timestamp: a transaction still open during a pull is delivered by the next one (06).
One bad row never blocks a device Pushes apply set-based; on failure they bisect with savepoints and reject only the offending rows with their real SQLSTATE (06).
Retries are safe Every push carries a batch id; the server replays the stored result instead of re-applying (04).
Interrupted downloads resume Pulls are planned with exact counts, staged chunk by chunk on the device and applied atomically (07).
Lost access removes exactly the lost rows Reconciliation moves out only the difference; a policy decides per row — delete (default), quarantine the user's unsynced work (default for edited rows), mark, retain until a condition, or redact (07).
Permission and filter changes self-heal Hash-bucket reconciliation moves only the difference (07).
Visibility is the application's own An include can be an EF Core query: the DbContext's filters decide what a device holds, translated into the store's own statements, never run separately (05).
Conflicts resolve per scenario Ordered rules per table — conditions over which columns each side changed, who edited last, whether the row still exists, or anything a rule looks up — each with its own resolution (05).
Device writes run the server's own code Uploaded rows can be written through the application's DbContext (its SaveChanges logic and interceptors run), and rule-bearing flows can be commands: the server's handler runs once, in order, in the push transaction, and the device converges to the result (05).
Reconciliation is nearly free when little changed An optional digest cache proves a view unchanged against the database and answers from memory: 4 ms instead of 0.36–0.6 s for a 144 k-row farm (19).
Large scopes stay linear A scope's keys are materialised once per download and paged by primary key: 144 k rows in ~0.8 s, a 560-row farm in ~8 ms, on a 1.28 M-row table; every page is bounded on every table it reads (19).
Big first downloads are fast on the device Rows arrive in key order and apply with trigger execution off for the sync's own connection, plain indexes rebuilt once after an empty table is filled, bound straight from the frame without allocations: a 500,000-row site syncs in ~10 s on the Android emulator, applying in 3–4 s (08, 19).
Few bytes on slow networks MessagePack hub protocol (no base64) and column-grouped frames: 500,000 rows download as 20.6 MB instead of 30.7 MB (11, 04).
Leaving Dotmim.Sync is one call RetireDotmimCaptureAsync imports the rows Dotmim had not uploaded and removes only Dotmim's triggers and tables (16).
A restored database cannot fool devices A copied or restored database rotates its epoch, and a cursor newer than the database is refused: devices reconcile once instead of silently diverging (06).
Progress users can trust Monotonic, planned progress with learned phase weights (09).
Errors you can switch on Stable codes end to end, including through SignalR (15).
AOT and iOS without interpreter Source-generated JSON, MessagePack shapes, logging and client model; hand-written binary codec (14).
Measured where it runs An opt-in Android suite runs the client and 200k/500k-row sites on a device or emulator and records bytes, CPU, memory and UI-thread gaps (12).

Packages

Package Use it in Contains
G9SyncData.Abstractions both enums, error codes, keys, digests, parameters, entity attributes
G9SyncData.Protocol both wire DTOs, source-generated JSON context, G9RowBatch codec, hub contract
G9SyncData.Server server model builder, engine, auth context, upload pipeline, hosting
G9SyncData.Server.PostgreSql server PostgreSQL capture (triggers or WAL), reads, visibility key sets, push apply, reconciliation
G9SyncData.Server.EfCore server sync model from an EF Core IModel; subscriptions whose visibility is an EF Core query (not AOT-compatible, server only)
G9SyncData.Server.SignalR server G9SyncHub (MessagePack and JSON), pokes, MapG9SyncHub()
G9SyncData.Client device engine, run queue, progress, reconciliation
G9SyncData.Client.Sqlite device SQLite store (bring your own native SQLite bundle)
G9SyncData.Client.SignalR device SignalR transport, MessagePack by default (no ASP.NET Core server assemblies)
G9SyncData.SourceGenerators device client model from [G9AttrSyncTable] entities
G9SyncData.Testing tests in-memory transport with fault injection

Infrastructure requirements

Server — PostgreSQL. Two change-capture backends, chosen by configuration:

Backend PostgreSQL requirement When to use it
WAL / logical replication (default in v2) wal_level=logical, a replication slot, a role with REPLICATION, and max_slot_wal_keep_size set the normal choice: no write-path cost, sees every writer, ordered by commit LSN
Triggers (v1, still supported) none beyond CREATE rights in the g9sync schema managed databases where wal_level cannot be changed

Turning on WAL capture is an infrastructure change with production prerequisites — a restart, a slot that retains WAL when its consumer stalls, a role, pg_hba.conf, and monitoring that must exist BEFORE it ships. All of it, including a checklist written to be handed to an Ops team as-is and a runbook for slot loss and failover, is in AiGuides/20-Infrastructure-PostgreSQL.md. Read that chapter before enabling it in any environment.

Selecting WAL capture:

builder.Services.AddG9SyncDataServer(model)
    .UsePostgreSql(dataSource, postgres =>
    {
        postgres.CaptureMode = G9EPostgresCaptureMode.WriteAheadLog;
        postgres.Wal.SlotName = "g9sync";                 // one slot per deployment, unique in the cluster
        postgres.Wal.PublicationName = "g9sync";
        postgres.Wal.ReplicationConnectionString = replicationConnectionString;   // a role with REPLICATION
    });

On first start the provider creates the publication and the slot, then seeds each synced table once. Pushes never wait for the consumer; the consumer only affects how quickly other writers' changes reach devices.

Client — SQLite. The engine brings no native SQLite of its own: the application owns that choice, so there is exactly one SQLite provider in the process (an application that ships two has, in practice, taken a SIGSEGV for it). The store talks to SQLitePCLRaw.core directly — no Microsoft.Data.Sqlite — and runs on the 2.x or 3.x generation, whichever the app pins. A command can be recorded on the app's own connection (sqlite-net's Handle), so it commits with the app's writes.

Development environment on this machine. PostgreSQL 18.1 in Docker at D:\Postgress (docker compose up -d), published on localhost:5432, holding a ~3.9 GB copy of production as agriwise_production for realistic testing. wal_level=logical was enabled there on 2026-09-16; the compose file carries the settings and the reasoning inline, and the original was preserved beside it.

Quick start

Server (ASP.NET Core)

var dataSource = NpgsqlDataSource.Create(builder.Configuration.GetConnectionString("Sync"));

builder.Services.AddG9SyncDataServer(model => model
        .Table("Operation", "Pot", t => t
            .Key("Id")
            .Column("SiteId", G9ESyncColumnType.Guid, nullable: false)
            .Column("Code", G9ESyncColumnType.String, nullable: false)
            .Column("IsDeleted", G9ESyncColumnType.Boolean, nullable: false)
            .Conflicts(G9EConflictPolicy.ColumnMerge))
        .Subscription("site-pots", s => s
            .Parameter("SiteId", G9ESyncColumnType.Guid)
            .Include("Operation.Pot", "{row}.\"SiteId\" = @SiteId", "NOT {row}.\"IsDeleted\"")),
        options => options.TokenSigningKey = signingKeyBytes)   // required with more than one instance
    .UsePostgreSql(dataSource)
    .UseSignalR();

var app = builder.Build();
app.UseWebSockets();
app.MapG9SyncHub().RequireAuthorization();

The store installs its capture objects in the g9sync schema at startup (or generates a reviewable script: GenerateInstallScriptAsync). Domain tables only gain three statement-level triggers each.

Visibility can also be an EF Core query over the application's own DbContext (package G9SyncData.Server.EfCore):

.Subscription("farm-pots", s => s
    .Parameter("FarmId", G9ESyncColumnType.Guid)
    .ServerParameter("PermissionStamp", G9ESyncColumnType.String)   // a changed stamp makes devices reconcile
    .IncludeQuery<AppDbContext, Pot>("Operation.Pot", (db, scope) =>
    {
        var farm = scope.Get<Guid>("FarmId");
        return db.Pots.Where(p => p.Ridge.Block.Field.FarmId == farm);
    }, whenSql: "NOT {row}.\"IsDeleted\""))

The query is translated, never executed by the library; every such query is probed at startup, and a wrong one stops the server instead of failing devices (05, ADR-0029).

Device (MAUI, console, NativeAOT)

[G9AttrSyncTable("Operation.Pot", LocalName = "Pot")]
public sealed class Pot
{
    public Guid Id { get; set; }
    public Guid SiteId { get; set; }
    [G9AttrSyncColumn(Nullability = G9ESyncNullability.NotNull)] public string Code { get; set; } = "";
    public bool IsDeleted { get; set; }
}

SQLitePCL.Batteries_V2.Init();   // native SQLite from the bundle your app references

services.AddG9SyncDataClient(G9SyncGeneratedModel.Model, o => o.AppVersion = "1.0.0")
    .UseSqlite(o => o.DatabasePath = databasePath)
    .UseSignalR((sp, o) =>
    {
        o.Url = "https://sync.example.com/g9sync";
        o.AccessTokenProvider = () => sp.GetRequiredService<ITokenSource>().GetAccessTokenAsync();
    });

var engine = provider.GetRequiredService<G9CSyncEngine>();
await engine.InitializeAsync();
await engine.SubscribeAsync("site-pots", new G9CSyncParameters().Set("SiteId", siteId));

var run = engine.Run(new G9CSyncRequest { Priority = G9ESyncPriority.UserInteractive });
await foreach (var p in run.ReadProgressAsync()) overlay.Report(p.Overall, p.Phase);
var result = await run.Completion;

The app keeps writing its tables as it always did; capture triggers record what to upload.

Samples

# a disposable PostgreSQL 13+ is expected at localhost:15433 (or set G9SYNC_SAMPLE_PG)
dotnet run --project samples/G9SyncData.Samples.Server
dotnet run --project samples/G9SyncData.Samples.AotClient -- http://localhost:5210/g9sync --watch
Invoke-RestMethod -Method Post "http://localhost:5210/sample/pots?count=100"   # the client is poked and pulls

dotnet publish samples/G9SyncData.Samples.AotClient -c Release -r win-x64       # NativeAOT proof

Build and test

dotnet build G9SyncData.slnx
dotnet test tests/G9SyncData.Tests
# Point the tests at a disposable PostgreSQL. On this machine that is the Docker instance in
# D:\Postgress (password there too - it is deliberately not repeated in this repository):
$env:G9SYNC_TEST_PG = "Host=localhost;Port=5432;Username=iman;Password=<see D:\Postgress\docker-compose.yml>;Database=postgres"
dotnet test tests/G9SyncData.IntegrationTests
# Android device or emulator (opt-in): the client and large sites on a real Android runtime
$env:G9SYNC_ANDROID = "auto"; dotnet test tests/G9SyncData.IntegrationTests --filter "FullyQualifiedName~Android"

Details: AiGuides/03-Build-Test-Release.md and AiGuides/12-Testing.md.

Documentation

Start at AiGuides/00-AiGuide.md. The research and design plan this implementation follows is G9SyncData.md in the agriculture repository root (not part of this repository).

Repository

https://dev.azure.com/G9TM/G9SyncData/_git/G9SyncData (private). CI lives in azure-pipelines.yml and has not been run yet — create the pipeline in Azure DevOps and fix whatever the hosted agent disagrees with.

License

MIT

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

Showing the top 3 NuGet packages that depend on G9SyncData.Client:

Package Downloads
G9SyncData.Client.SignalR

G9SyncData SignalR client transport on G9SignalRSuperNetCore: the resilient G9 connection with its jittered reconnect policy, a hand-written AOT-safe hub proxy over MessagePack (default) or the source-generated wire JSON, stable error codes and server pokes. References no ASP.NET Core server assembly (safe for MAUI).

G9SyncData.Client.Sqlite

G9SyncData SQLite client store: model-driven DDL with declared indexes, trigger capture with a monotonic local sequence and dirty-column masks, subscription membership, resumable staged pulls applied atomically, reconciliation digests and the local change feed. NativeAOT-compatible.

G9SyncData.Testing

G9SyncData test kit: an in-memory transport that drives a real G9CSyncServerEngine in-process, with wire-faithful JSON round trips and fault injection (fail before or after the server, break streams mid-way, latency). For app and library tests.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 0 9/21/2026
1.0.0 43 9/20/2026