Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ditto.live/llms.txt

Use this file to discover all available pages before exploring further.

Overview

This guide covers the essential changes needed to migrate your Ditto C# app from v4 to v5. The main architectural shift is moving from identity-based initialization to a four-phase configuration model with async/await APIs. Also required: v5 uses DQL (Ditto Query Language) for all data operations. See the DQL Migration Guide for query migration steps.

AI Agent Prompt

Use this prompt when working with an AI coding assistant to migrate your Ditto C# app from v4 to v5.
I need help migrating a Ditto C#/.NET application from v4 to v5. Focus on these critical changes:

INITIALIZATION:
Replace `new Ditto(DittoIdentity, string)` with `Ditto.Open(DittoConfig)` or `await Ditto.OpenAsync(DittoConfig)`.
- Create a `DittoConfig` with the constructor: `new DittoConfig(databaseId, connect, persistenceDirectory?)`
- For server/cloud: `new DittoConfig(databaseId: "id", connect: new DittoConfigConnect.Server(new Uri("https://...")))`
- For offline/P2P: `new DittoConfig(databaseId: "id", connect: new DittoConfigConnect.SmallPeersOnly(privateKey: null))`
- For shared key: `new DittoConfig(databaseId: "id", connect: new DittoConfigConnect.SmallPeersOnly(privateKey: "your-shared-key"))`
- The old identity classes (`DittoOfflinePlayground`, `DittoOnlinePlayground`, `DittoOnlineWithAuthentication`, `DittoSharedKey`, `DittoManual`) are all removed
- `DittoIdentityType` enum is removed entirely
- Remove `UpdateTransportConfig` workaround patterns for adding websocket URLs β€” `DittoConfig` handles the server URL directly. Note: `UpdateTransportConfig()` still exists but signature changed: v4 takes `Action<DittoTransportConfig>`, v5 takes `Action<DittoTransportConfigBuilder>` (builder pattern)
- Remove `DisableSyncWithV3()` calls β€” v3 protocol is fully dropped in v5
- Remove `await ditto.Store.ExecuteAsync("ALTER SYSTEM SET DQL_STRICT_MODE = false")` β€” v5 defaults to `false`
- IMPORTANT: If your v4 app used the default (strict=true), set `DQL_STRICT_MODE=true` BEFORE starting sync to maintain v4 behavior. Objects that were replaced whole in v4 will silently merge at field level in v5 without this.
- Use `using var ditto = Ditto.Open(config);` for IDisposable cleanup
- By default, the Transport Config no longer needed to set websocketUrl for Big Peer as this is automatically set by the SDK from the auth server URL.  

NAMESPACE CHANGES:
v5 reorganizes types into sub-namespaces. You will get compile errors until these are updated.
- `DittoSDK.Auth` β€” `DittoAuthenticator`, `DittoAuthenticationProvider`, `DittoAuthenticationExpirationHandler`
- `DittoSDK.Exceptions` β€” `DittoException`
- `DittoSDK.Store` β€” `DittoStoreObserver`, `DittoQueryResult`, `DittoQueryResultItem`
- `DittoSDK.Sync` β€” `DittoSyncSubscription`
- `DittoSDK.Transport` β€” `DittoPeer`, `DittoPresenceGraph`, `DittoPresence`, `DittoConnection`, `DittoConnectionType`
- `DittoSDK.Logging` β€” `DittoLogLevel`, `DittoLogger`
- `DittoSDK.DiskUsage` β€” `DittoDiskUsageItem`

SYNC:
The sync API moved from top-level Ditto methods to the `Ditto.Sync` sub-object.
- `ditto.StartSync()` β†’ `ditto.Sync.Start()`
- `ditto.StopSync()` β†’ `ditto.Sync.Stop()`
- `ditto.GetTransportDiagnostics()` β†’ removed in v5 (no replacement)
- `ditto.IsSyncActive` β†’ `ditto.Sync.IsActive`

AUTHENTICATION:
The `IDittoAuthenticationDelegate` interface is removed. Replace with `DittoAuthenticationExpirationHandler` async delegate.
- Set `ditto.Auth.ExpirationHandler = async (ditto, timeUntilExpiration) => { ... }` after Ditto.Open()
- The delegate signature is: `Task DittoAuthenticationExpirationHandler(Ditto ditto, TimeSpan timeUntilExpiration)`
- First param is the `Ditto` instance. Access auth via `ditto.Auth.LoginAsync()`
- Second param is `TimeSpan` (not seconds as a number). Check `timeUntilExpiration == TimeSpan.Zero` for initial auth.
- Use `DittoAuthenticationProvider.Development` for playground/dev tokens
- The handler is called when `timeUntilExpiration == TimeSpan.Zero` (initial auth or expired) and when `timeUntilExpiration > TimeSpan.Zero` (token expiring soon). Handle both cases.
- `authenticator.LoginWithToken(token, provider)` β†’ removed, use `ditto.Auth.LoginAsync(token, provider)` instead
- `authenticator.Login(token, provider)` β†’ removed (sync overload gone), use `ditto.Auth.LoginAsync(token, provider)`
- `authenticator.LoginWithCredentials(username, password, provider)` β†’ removed
- `IDittoAuthenticationDelegate.AuthenticationStatusDidChange()` β†’ removed, use `DittoAuthenticator.ObserveStatus()` for cancellable observation
- New: `ditto.Auth.Logout()` β€” explicit logout support
- New: `DittoAuthenticator.ObserveStatus()` β€” returns `IDisposable` observer for auth status changes

OBSERVERS β€” `DittoLiveQuery` β†’ `DittoStoreObserver`:
The legacy `DittoLiveQuery` from collection `.ObserveLocal()` is replaced by `DittoStoreObserver` from `Store.RegisterObserver()`.
- `DittoLiveQuery` type β†’ `DittoStoreObserver`
- `collection.Find("...").ObserveLocal((docs, event) => { ... })` β†’ `store.RegisterObserver("SELECT ...", args, (DittoQueryResult result) => { ... })`
- The callback receives `DittoQueryResult` with `.Items` property (list of `DittoQueryResultItem`)
- `observer.Cancel()` for cleanup (replaces `liveQuery.Stop()`)

BACK PRESSURE (RegisterObserver with signalNext):
Use the `Action<DittoQueryResult, Action>` overload for manual back-pressure control.
- Standard observer: `store.RegisterObserver(query, result => { ... })` β€” auto-signals after handler returns
- Async observer: `store.RegisterObserver(query, async result => { ... })` β€” auto-signals after Task completes
- Back-pressure: `store.RegisterObserver(query, args, async (result, signalNext) => { await ...; signalNext(); })` β€” must call `signalNext()` when ready for next update
- Changes during processing are coalesced β€” you get the latest state, not every intermediate change

COLLECTION API β†’ DQL:
The entire Collection/Document API is removed. Replace all collection operations with DQL via `Store.ExecuteAsync()`.
- `store.Collection("cars")` β†’ removed. Use `store.ExecuteAsync("SELECT * FROM cars")`
- `collection.FindAll().Exec()` β†’ `await store.ExecuteAsync("SELECT * FROM cars")`
- `collection.Find("color == 'blue'").Exec()` β†’ `await store.ExecuteAsync("SELECT * FROM cars WHERE color = :color", new Dictionary<string, object> { ["color"] = "blue" })`
- `collection.FindById(id).Exec()` β†’ `await store.ExecuteAsync("SELECT * FROM cars WHERE _id = :id", new Dictionary<string, object> { ["id"] = "car-1" })`
- `collection.Upsert(value)` β†’ `await store.ExecuteAsync("INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE", new Dictionary<string, object> { ["doc"] = value })`
- `collection.FindById(id).Update(doc => { doc["color"].Set("green"); })` β†’ `await store.ExecuteAsync("UPDATE cars SET color = :color WHERE _id = :id", new Dictionary<string, object> { ["color"] = "green", ["id"] = "car-1" })`
- `collection.FindById(id).Remove()` β†’ `await store.ExecuteAsync("DELETE FROM cars WHERE _id = :id", new Dictionary<string, object> { ["id"] = "car-1" })`
- `collection.FindById(id).Evict()` β†’ `await store.ExecuteAsync("EVICT FROM cars WHERE _id = :id", new Dictionary<string, object> { ["id"] = "car-1" })`
- `collection.FindAll().Sort("color", DittoSortDirection.Ascending).Limit(20).Offset(40).Exec()` β†’ `await store.ExecuteAsync("SELECT * FROM cars ORDER BY color ASC LIMIT 20 OFFSET 40")`
- `DittoSortDirection` enum is removed β€” use `ASC`/`DESC` in DQL
- IMPORTANT: `ExecuteAsync` takes `Dictionary<string, object>` for arguments β€” NOT anonymous objects. `RegisterObserver` and `RegisterSubscription` DO accept anonymous `object` arguments.
- Always use parameterized queries with `:paramName` β€” NEVER use string interpolation

SUBSCRIPTIONS β€” `DittoSubscription` β†’ `DittoSyncSubscription`:
- `collection.Find("color == 'red'").Subscribe()` β†’ `ditto.Sync.RegisterSubscription("SELECT * FROM cars WHERE color = :color", new { color = "red" })`
- `DittoSubscription` type β†’ `DittoSyncSubscription`
- `sub.Cancel()` for cleanup
- Note: `RegisterSubscription` accepts anonymous `object` arguments

TRANSACTIONS β€” `Write()` β†’ `TransactionAsync()`:
- `ditto.Store.Write(tx => { tx.Scoped("cars").Upsert(value); })` β†’ `await ditto.Store.TransactionAsync(async tx => { await tx.ExecuteAsync("INSERT INTO ...", args); })`
- The callback now receives `DittoTransaction` (not `DittoWriteTransaction`) and uses async DQL
- `DittoWriteStrategy.UpdateDifferentValues` is removed β€” use `ON ID CONFLICT DO MERGE` in DQL

PRESENCE:
- `peer.PeerKeyString` β†’ `peer.PeerKey`
- `peer.Os` (string) β†’ `peer.Os` (DittoPeerOS? β€” type changed from string to nullable enum)
- `peer.IsDittoCloudConnected` β†’ `peer.IsConnectedToDittoServer`
- `connection.PeerKeyString1` / `PeerKeyString2` β†’ `connection.PeerKey1` / `PeerKey2`
- `connection.ApproximateDistanceInMeters` β†’ **removed, no replacement**
- `DittoPresenceGraph`, `DittoPeer`, `DittoConnection`, `DittoConnectionType` moved to `DittoSDK.Transport` namespace

DITTO CLASS:
- `new Ditto(identity, path)` β†’ `Ditto.Open(config)` or `await Ditto.OpenAsync(config)`
- `ditto.SiteId` β†’ removed (use peer identity from presence graph)
- `ditto.SDKVersion` β†’ use static `Ditto.Version` property
- `ditto.DisableSyncWithV3()` β†’ removed
- `ditto.StartSync()` / `StopSync()` β†’ `ditto.Sync.Start()` / `Stop()`
- `ditto.GetTransportDiagnostics()` β†’ removed

DISK USAGE:
- `ditto.DiskUsage.Exec()` β†’ `ditto.DiskUsage.Item` (property, not method)
- `DittoDiskUsageChild` β†’ `DittoDiskUsageItem` (type renamed)
- Types moved to `DittoSDK.DiskUsage` namespace

LOGGING:
- `DittoLogger.SetLoggingEnabled(true)` β†’ `DittoLogger.IsEnabled = true`

ERRORS:
- `DittoException` moved to `DittoSDK.Exceptions` namespace
- Add `using DittoSDK.Exceptions;` where exceptions are caught

REMOVED APIS (no replacement):
- `DittoLiveQueryEvent.Hash()` β€” document change hashing removed
- `DittoLiveQueryEvent.HashMnemonic()` β€” human-readable hash removed
- `DittoWriteStrategy.UpdateDifferentValues` β€” use `ON ID CONFLICT DO MERGE` in DQL
- `IDittoAuthenticationDelegate.AuthenticationStatusDidChange()` β€” use `DittoAuthenticator.ObserveStatus()`
- `DittoUpdateResult` (and `Set`, `Removed`, `Incremented` subclasses) β€” DQL returns row counts only
- `DittoPendingCursorOperation.ObserveLocalWithNextSignal()` β€” use `RegisterObserver` with `signalNext` overload
- `DittoSmallPeerInfoSyncScope` enum β€” use `ALTER SYSTEM SET smallPeerInfoSyncScope = '...'`
- `DittoSortDirection` enum β€” use `ASC`/`DESC` in DQL
- `DittoStore.FetchAttachment(DittoAttachmentFetchDelegate)` β€” use async `Func`/Task overload
- `DittoIdentityType` enum β€” replaced by `DittoConfig` constructor with `DittoConfigConnect` types
- `Ditto.DisableSyncWithV3()` β€” v3 sync dropped
- `Ditto.GetTransportDiagnostics()` β€” transport diagnostics removed
- `Ditto.SiteId` β€” use peer identity from presence graph
- `connection.ApproximateDistanceInMeters` β€” peer distance estimation removed

NEW APIS:
- `Ditto.Open(DittoConfig)` / `Ditto.OpenAsync(DittoConfig)` β€” factory methods replacing constructor
- `DittoConfig(databaseId, connect, persistenceDirectory?)` β€” constructor replaces identity classes
- `DittoConfigConnect.Server` / `DittoConfigConnect.SmallPeersOnly` β€” connect mode types
- `Ditto.Sync.Start()` / `Stop()` β€” replaces `StartSync()` / `StopSync()`
- `Ditto.Version` (static property) β€” replaces `Ditto.SDKVersion`
- `Ditto.DeviceName` β€” custom peer device name (get/set)
- `DittoAuthenticationExpirationHandler` β€” async delegate replacing `IDittoAuthenticationDelegate`
- `DittoAuthenticator.LoginAsync()` β€” replaces `LoginWithToken()` / `Login()`
- `DittoAuthenticator.ObserveStatus()` β€” returns `IDisposable` auth status observer
- `DittoAuthenticator.Logout()` β€” explicit logout
- `DittoStore.TransactionAsync()` β€” async DQL transaction replacing `Write()`; uses `DittoTransaction`
- `DittoStoreObserver` β€” replaces `DittoLiveQuery`
- `DittoQueryResultItem` β€” single DQL result row with `.Value` property
- `DittoSyncSubscription` β€” replaces `DittoSubscription`, exposes `QueryString`, `QueryArguments`, `Cancel()`
- `WifiAwareConfig` / `WifiAwareConfigBuilder` β€” Android Wi-Fi Aware transport config (MAUI)
- `DittoSyncPermissions.RequestPermissionsAsync()` β€” Bluetooth/Wi-Fi permission requests (MAUI)
- `DittoDiskUsageItem` β€” replaces `DittoDiskUsageChild`
- `DittoDiskUsage.Item` (property) β€” replaces `Exec()` method
- `DittoSDK.Auth`, `DittoSDK.Exceptions`, `DittoSDK.Store`, `DittoSDK.Sync`, `DittoSDK.Transport`, `DittoSDK.Logging`, `DittoSDK.DiskUsage` β€” new sub-namespaces

BEFORE (v4):
```csharp
using DittoSDK;

var ditto = new Ditto(
    identity: DittoIdentity.OnlinePlayground(
        appId: "your-app-id",
        token: "your-token",
        enableDittoCloudSync: false,
        customAuthUrl: "https://your-server.ditto.live"
    )
);

ditto.UpdateTransportConfig(config =>
{
    config.Connect.WebSocketUrls.Add("wss://...");
});
await ditto.Store.ExecuteAsync("ALTER SYSTEM SET DQL_STRICT_MODE = false");
ditto.DisableSyncWithV3();
ditto.StartSync();

// Collection-based CRUD
var collection = ditto.Store.Collection("cars");
collection.Upsert(new Dictionary<string, object>
{
    { "_id", "car-1" }, { "color", "red" }, { "make", "Toyota" }
});
var docs = collection.FindAll().Exec();
collection.FindById(new DittoDocumentId("car-1")).Update(doc =>
{
    doc["color"].Set("blue");
});
collection.FindById(new DittoDocumentId("car-1")).Remove();

// Live query
DittoLiveQuery liveQuery = collection.FindAll()
    .ObserveLocal((docs, dittoCollectionEvent) =>
    {
        UpdateUI(docs);
    });

// Subscription
DittoSubscription sub = collection.Find("color == 'red'").Subscribe();

// Write transaction
ditto.Store.Write(tx =>
{
    tx.Scoped("cars").Upsert(new Dictionary<string, object>
    {
        { "_id", "car-2" }, { "color", "green" }
    });
});

// Cleanup
liveQuery.Stop();
sub.Cancel();
ditto.StopSync();
ditto.Dispose();
```

AFTER (v5):
```csharp
using DittoSDK;
using DittoSDK.Auth;
using DittoSDK.Store;
using DittoSDK.Exceptions;

var config = new DittoConfig(
    databaseId: "your-database-id",
    connect: new DittoConfigConnect.Server(new Uri("https://your-server.ditto.live"))
);

using var ditto = await Ditto.OpenAsync(config);

ditto.Auth.ExpirationHandler = async (ditto, timeUntilExpiration) =>
{
    try
    {
        await ditto.Auth.LoginAsync(
            "your-token",
            DittoAuthenticationProvider.Development);
    }
    catch (DittoException error)
    {
        Console.WriteLine($"Auth failed: {error}, time until expiration: {timeUntilExpiration}");
    }
};

// If maintaining v4 behavior, set strict mode BEFORE starting sync
// await ditto.Store.ExecuteAsync("ALTER SYSTEM SET DQL_STRICT_MODE = true");

ditto.Sync.Start();

// DQL-based CRUD
await ditto.Store.ExecuteAsync(
    "INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE",
    new Dictionary<string, object> { ["doc"] = new Dictionary<string, object> { ["_id"] = "car-1", ["color"] = "red", ["make"] = "Toyota" } });

var result = await ditto.Store.ExecuteAsync("SELECT * FROM cars");

await ditto.Store.ExecuteAsync(
    "UPDATE cars SET color = :color WHERE _id = :id",
    new Dictionary<string, object> { ["color"] = "blue", ["id"] = "car-1" });

await ditto.Store.ExecuteAsync(
    "DELETE FROM cars WHERE _id = :id",
    new Dictionary<string, object> { ["id"] = "car-1" });

// Store observer (replaces live query)
DittoStoreObserver observer = ditto.Store.RegisterObserver(
    "SELECT * FROM cars",
    (DittoQueryResult result) =>
    {
        UpdateUI(result.Items);
    });

// Sync subscription (replaces collection subscription)
DittoSyncSubscription sub = ditto.Sync.RegisterSubscription(
    "SELECT * FROM cars WHERE color = :color",
    new { color = "red" });

// DQL transaction (replaces write transaction)
await ditto.Store.TransactionAsync(async tx =>
{
    await tx.ExecuteAsync(
        "INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE",
        new Dictionary<string, object> { ["doc"] = new Dictionary<string, object> { ["_id"] = "car-2", ["color"] = "green" } });
});

// Cleanup
observer.Cancel();
sub.Cancel();
ditto.Sync.Stop();
// ditto.Dispose() handled by 'using' statement
```

CHECKLIST:

Step 1 β€” Update Package:
- Update `DittoSDK` NuGet package to v5.x

Step 2 β€” Fix Namespace Imports:
- Add `using DittoSDK.Auth;` where auth types are used
- Add `using DittoSDK.Exceptions;` where `DittoException` is caught
- Add `using DittoSDK.Store;` for `DittoStoreObserver`, `DittoQueryResult`, `DittoQueryResultItem`
- Add `using DittoSDK.Sync;` for `DittoSyncSubscription`
- Add `using DittoSDK.Transport;` for `DittoPeer`, `DittoConnection`, `DittoPresenceGraph`
- Add `using DittoSDK.Logging;` for `DittoLogger`, `DittoLogLevel`
- Add `using DittoSDK.DiskUsage;` for `DittoDiskUsageItem`

Step 3 β€” Replace Initialization:
- Replace `new Ditto(identity, path)` with `Ditto.Open(config)` or `await Ditto.OpenAsync(config)`
- Replace identity classes with `new DittoConfig(databaseId, connect)` constructor
- Remove `DittoIdentityType` enum references
- Add `using var` for IDisposable cleanup

Step 4 β€” Update Sync API:
- Replace `ditto.StartSync()` β†’ `ditto.Sync.Start()`
- Replace `ditto.StopSync()` β†’ `ditto.Sync.Stop()`
- Remove `GetTransportDiagnostics()` calls (removed in v5)
- Remove `ditto.DisableSyncWithV3()` calls
- Set `DQL_STRICT_MODE=true` BEFORE `Sync.Start()` if maintaining v4 strict behavior

Step 5 β€” Update Authentication:
- Replace `IDittoAuthenticationDelegate` implementations with `DittoAuthenticationExpirationHandler` async delegate
- Handler signature: `Task DittoAuthenticationExpirationHandler(Ditto ditto, TimeSpan timeUntilExpiration)`
- Replace `LoginWithToken()` / `Login()` / `LoginWithCredentials()` β†’ `LoginAsync()`
- Use `DittoAuthenticationProvider.Development` for playground tokens
- Handle `timeUntilExpiration == TimeSpan.Zero` (initial) vs `> TimeSpan.Zero` (refresh)
- Update status observation to `DittoAuthenticator.ObserveStatus()` (returns `IDisposable`)

Step 6 β€” Replace Collection API with DQL:
- Search for `ditto.Store.Collection(` and replace all with DQL `ExecuteAsync()`
- Replace `.FindAll()`, `.Find(`, `.FindById(` with DQL `SELECT`
- Replace `.Upsert(` with `INSERT INTO ... DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE`
- Replace `.Update(` closures with `UPDATE SET`
- Replace `.Remove()` with `DELETE FROM`
- Replace `.Evict()` with `EVICT FROM`
- Replace `.Sort("field", DittoSortDirection.Ascending).Limit(n).Offset(n)` with `ORDER BY field ASC LIMIT n OFFSET n` in DQL
- Use `Dictionary<string, object>` for `ExecuteAsync` arguments β€” NOT anonymous objects
- Use parameterized queries β€” never string interpolation

Step 7 β€” Replace Live Queries and Subscriptions:
- Replace `DittoLiveQuery` type β†’ `DittoStoreObserver`
- Replace `.ObserveLocal()` β†’ `ditto.Store.RegisterObserver()` with `DittoQueryResult` callback
- Replace `DittoSubscription` β†’ `DittoSyncSubscription`
- Replace `.Subscribe()` β†’ `ditto.Sync.RegisterSubscription()`

Step 8 β€” Replace Write Transactions:
- Replace `ditto.Store.Write(tx => { })` β†’ `await ditto.Store.TransactionAsync(async tx => { })`
- Replace `tx.Scoped("collection").Upsert(...)` β†’ `await tx.ExecuteAsync("INSERT ...")`
- Note: Callback now receives `DittoTransaction` (not `DittoWriteTransaction`)

Step 9 β€” Update Renamed Properties and Types:
- `Ditto.SiteId` β†’ removed (use peer identity from presence graph)
- `Ditto.SDKVersion` β†’ use static `Ditto.Version` property
- `DittoDiskUsageChild` β†’ `DittoDiskUsageItem`
- `DittoDiskUsage.Exec()` β†’ `DittoDiskUsage.Item` (property)
- `peer.PeerKeyString` β†’ `peer.PeerKey`
- `peer.IsDittoCloudConnected` β†’ `peer.IsConnectedToDittoServer`
- `connection.PeerKeyString1` / `PeerKeyString2` β†’ `connection.PeerKey1` / `PeerKey2`
- Remove `connection.ApproximateDistanceInMeters` usage (no replacement)
- `peer.Os` type changed from string to `DittoPeerOS?` (nullable enum)

Step 10 β€” Verify and Test:
- Build compiles with zero errors
- No deprecated API warnings
- DQL strict mode set correctly for your data model
- Auth handler fires before first sync
- Observers receive initial data
- Subscriptions sync data across peers
- Transactions are atomic
- No memory leaks on navigation

Work through the codebase systematically. Show me each file's changes.

Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control:
// 1. Configure
var config = new DittoConfig(
    databaseId: "your-database-id",
    connect: new DittoConfigConnect.Server(new Uri("https://your-server.ditto.live"))
);

// 2. Initialize (async)
var ditto = await Ditto.OpenAsync(config);

// 3. Authenticate
ditto.Auth.ExpirationHandler = async (ditto, timeUntilExpiration) =>
{
    try
    {
        await ditto.Auth.LoginAsync("your-token", DittoAuthenticationProvider.Development);
    }
    catch (Exception error)
    {
        Console.WriteLine($"Ditto auth failed: {error}, time until expiration: {timeUntilExpiration}");
    }
};

// 4. Start sync
ditto.Sync.Start();
These changes provide:
  • Clear separation of connection, initialization, and authentication concerns
  • Async initialization prevents blocking the main thread
  • No workarounds needed (transport config, DQL strict mode, v3 sync disable)
  • Type-safe configuration with compile-time validation
Initialization Migration Steps
1

Replace Initialization

Replace new Ditto() constructor with await Ditto.OpenAsync(config).
var config = new DittoConfig(
    databaseId: "your-database-id",
    connect: new DittoConfigConnect.Server(new Uri("https://your-server.ditto.live"))
);

var ditto = await Ditto.OpenAsync(config);
ditto.Sync.Start();
Key changes:
  • Use DittoConfig instead of DittoIdentity
  • appId β†’ databaseId
  • .OnlinePlayground β†’ new DittoConfigConnect.Server(new Uri(...))
  • .OfflinePlayground β†’ new DittoConfigConnect.SmallPeersOnly(privateKey: null)
  • Add await for async initialization
  • Remove UpdateTransportConfig, DisableSyncWithV3(), and DQL strict mode queries
2

Update Authentication

Set authentication handler separately after initialization.
// Set handler after Ditto.OpenAsync()
// Handler signature: Task DittoAuthenticationExpirationHandler(Ditto ditto, TimeSpan timeUntilExpiration)
ditto.Auth.ExpirationHandler = async (ditto, timeUntilExpiration) =>
{
    // Initial auth or token refresh
    try
    {
        await ditto.Auth.LoginAsync(
        "your-token",
        DittoAuthenticationProvider.Development);
    }
    catch (Exception error)
    {
        Console.WriteLine($"Ditto auth failed: {error}, time until expiration: {timeUntilExpiration}");
    }
};
Key changes:
  • Authentication now uses DittoAuthenticationExpirationHandler async delegate
  • Delegate signature: Task DittoAuthenticationExpirationHandler(Ditto ditto, TimeSpan timeUntilExpiration)
  • First parameter is the Ditto instance β€” access auth via ditto.Auth.LoginAsync()
  • Second parameter is TimeSpan β€” check timeUntilExpiration == TimeSpan.Zero for initial auth
  • Handler called when auth required (TimeSpan.Zero) or token expiring (> TimeSpan.Zero)
  • Use DittoAuthenticationProvider.Development for playground tokens
  • Custom auth: new DittoAuthenticationProvider("your-provider")

Additional Changes

DQL Strict Mode Behavior Change

Breaking Change: v5 defaults to DQL_STRICT_MODE=false, which fundamentally changes how DQL queries behave.
  • v4 default: Objects treated as REGISTER (whole-object replacement)
  • v5 default: Objects treated as MAP (field-level merging)
This affects the behavior of all DQL SELECT, INSERT, and UPDATE operations.
Choose the appropriate migration path based on your current v4 configuration:
If you’re currently using the v4 default (DQL_STRICT_MODE=true), you must explicitly set strict mode to true in v5 before starting sync or executing any queries.
Failing to set strict mode will cause objects to merge at the field level instead of replacing entirely, which can result in unexpected data behavior and perceived data loss.
To migrate to DQL_STRICT_MODE=false (the new v5 default), contact Ditto Customer Support for guidance.
var config = new DittoConfig(
    databaseId: "your-database-id",
    connect: new DittoConfigConnect.Server(new Uri("https://your-server.ditto.live"))
);
var ditto = await Ditto.OpenAsync(config);

// IMPORTANT: Set strict mode BEFORE starting sync or running queries
await ditto.Store.ExecuteAsync("ALTER SYSTEM SET DQL_STRICT_MODE = true");

// Now safe to start sync
ditto.Sync.Start();
If you explicitly set DQL_STRICT_MODE=false in v4, no changes are required.v5 uses DQL_STRICT_MODE=false as the default, so your existing DQL queries will behave identically. You can upgrade freely.Optional: You can remove the explicit ALTER SYSTEM SET DQL_STRICT_MODE = false statement in v5 since this is now the default behavior.
var config = new DittoConfig(
    databaseId: "your-database-id",
    connect: new DittoConfigConnect.Server(new Uri("https://your-server.ditto.live"))
);
var ditto = await Ditto.OpenAsync(config);

// No need to set DQL_STRICT_MODE - false is now the default
ditto.Sync.Start();
The Legacy Query Builder has been removed in v5. All queries must be converted to DQL before upgrading.Good news: Legacy Query Builder functionality has 1:1 support with DQL_STRICT_MODE=false (the v5 default), making migrations straightforward.Migration Steps:
  1. In v4: Set DQL_STRICT_MODE=false after initialization:
    var identity = DittoIdentity.OnlinePlayground(
        appId,
        playgroundToken,
        false,
        authUrl);
    
    ditto = new Ditto(identity, tempDir);
    
    // Set DQL_STRICT_MODE to false for Query Builder compatibility
    await ditto.Store.ExecuteAsync("ALTER SYSTEM SET DQL_STRICT_MODE = false");
    
    ditto.StartSync();
    
  2. Convert all queries from Legacy Query Builder to DQL See the C# Legacy→DQL Migration Guide for detailed conversion examples.
  3. Upgrade to v5 No DQL configuration changes requiredβ€”v5 defaults to DQL_STRICT_MODE=false.
For additional guidance or questions, contact Ditto Customer Support.

Default Persistence Directory

v5 includes the database ID in the default directory name: ditto-{databaseId} instead of ditto To maintain v4 compatibility:
var config = new DittoConfig(
    databaseId: "your-database-id",
    connect: new DittoConfigConnect.Server(new Uri("https://your-server.ditto.live")),
    persistenceDirectory: "/app/data/ditto"  // Old v4 path
);

WifiAwareConfig (new)

// Configure Wi-Fi Aware (Android)
var transportConfig = ditto.TransportConfig;
transportConfig.PeerToPeer.WifiAware.Enabled = true;
ditto.TransportConfig = transportConfig;

DittoSyncPermissions (MAUI only)

// Request Bluetooth and Wi-Fi permissions on Android/iOS
await DittoSyncPermissions.RequestPermissionsAsync();

Back Pressure (RegisterObserver with signalNext)

Use the Action<DittoQueryResult, Action> overload of RegisterObserver for manual back-pressure control. The signalNext Action must be called when your handler is ready for the next update. Coalescing: changes that occur while your handler executes are coalesced β€” you get the latest state.
// Standard (auto-signal after handler returns)
ditto.Store.RegisterObserver(query, result => UpdateUI(result.Items));

// Async (auto-signal after Task completes)
ditto.Store.RegisterObserver(query, async result => {
    await ExpensiveOperationAsync(result.Items);
});

// Back-pressure (manual signal)
ditto.Store.RegisterObserver(
    query,
    new Dictionary<string, object> { ["color"] = "blue" },
    async (result, signalNext) => {
        await ExpensiveOperationAsync(result.Items);
        signalNext();  // Call when ready for the next update
    }
);
The signalNext parameter is a plain Action (no named type).

Disk Usage

// v4
var execResult = ditto.DiskUsage.Exec();
// DittoDiskUsageChild β†’ DittoDiskUsageItem

// v5
var item = ditto.DiskUsage.Item;  // Property, not method
// Type: DittoDiskUsageItem (renamed from DittoDiskUsageChild)

Ditto Logger Changes

In v5, you now set logging enabled or disabled using the DittoLogger.IsEnable property.
//new property to set logging enabled or disabled
DittoLogger.IsEnabled = true;

Namespace Changes

Types have moved to new namespaces in v5:
TypeOld NamespaceNew Namespace
DittoLogLevel, DittoLoggerDittoSDKDittoSDK.Logging
DittoStoreObserver, DittoQueryResult, DittoQueryResultItemDittoSDKDittoSDK.Store
DittoSyncSubscriptionDittoSDKDittoSDK.Sync
DittoPeer, DittoPresenceGraph, DittoPresence, DittoConnection, DittoConnectionTypeDittoSDKDittoSDK.Transport
DittoAuthenticator, DittoAuthenticationProvider, DittoAuthenticationExpirationHandlerDittoSDKDittoSDK.Auth
DittoExceptionDittoSDKDittoSDK.Exceptions
DittoDiskUsageItemDittoSDKDittoSDK.DiskUsage

Property changes

TypeV4 PropertyV5 Property
DittoPeerPeerKeyStringPeerKey
DittoPeerOs (string)Os (DittoPeerOS?) β€” type changed
DittoPeerIsDittoCloudConnectedIsConnectedToDittoServer
DittoConnectionPeerKeyString1PeerKey1
DittoConnectionPeerKeyString2PeerKey2
DittoConnectionApproximateDistanceInMetersREMOVED β€” no replacement

API Renames

v4.14v5.0
ditto.IsSyncActiveditto.Sync.IsActive
new Ditto(DittoIdentity, string)Ditto.Open(DittoConfig) or Ditto.OpenAsync(DittoConfig)
Ditto.StartSync()Ditto.Sync.Start()
Ditto.StopSync()Ditto.Sync.Stop()
Ditto.SiteIdRemoved β€” use peer identity from presence graph
Ditto.SDKVersionDitto.Version (static property)
Ditto.Identity propertyRemoved β€” use DittoConfig.DatabaseId
IDittoAuthenticationDelegate interfaceDittoAuthenticationExpirationHandler async delegate
DittoAuthenticator.AuthenticationDelegateDittoAuthenticator.ExpirationHandler
DittoAuthenticator.LoginWithToken() / Login() / LoginWithCredentials()DittoAuthenticator.LoginAsync()
DittoStore.Write(Action<DittoWriteTransaction>)DittoStore.TransactionAsync(Func<DittoTransaction, Task>)
DittoLiveQueryDittoStoreObserver
DittoSubscriptionDittoSyncSubscription
DittoDiskUsage.Exec()DittoDiskUsage.Item (property)
DittoDiskUsageChildDittoDiskUsageItem
DittoError classException types in DittoSDK.Exceptions namespace
DittoException (in DittoSDK)DittoException (in DittoSDK.Exceptions)
PeerKeyStringPeerKey
IsDittoCloudConnectedIsConnectedToDittoServer

APIs Removed

Removed APINotes
Ditto.DisableSyncWithV3()v3 peer-to-peer sync interoperability dropped entirely
Ditto.GetTransportDiagnostics()Transport diagnostics removed
Ditto.SiteIdRemoved β€” use peer identity from presence graph
Ditto.SDKVersion (instance)Use static Ditto.Version instead
DittoLiveQueryEvent.Hash()Document change hashing removed; no v5 equivalent
DittoLiveQueryEvent.HashMnemonic()Human-readable hash mnemonic removed
DittoWriteStrategy.UpdateDifferentValuesNo ON ID CONFLICT DO UPDATE_DIFFERENT_VALUES in DQL
IDittoAuthenticationDelegate.AuthenticationStatusDidChange()Status observation now via DittoAuthenticator.ObserveStatus()
DittoUpdateResult (and subclasses)DQL mutations return affected-row counts only
DittoPendingCursorOperation.ObserveLocalWithNextSignal()Use RegisterObserver with signalNext overload instead
DittoSmallPeerInfoSyncScope enumSync scope is now a DQL ALTER SYSTEM string setting
DittoSortDirection enumUse ASC/DESC in DQL ORDER BY; no enum type in v5
DittoStore.FetchAttachment(DittoAttachmentFetchDelegate)Use async Func/Task overload
DittoIdentityType enumReplaced by DittoConfig factory methods

New APIs in v5

  • Ditto.Sync.Start() / Ditto.Sync.Stop() β€” replaces Ditto.StartSync() / StopSync()
  • Ditto.Version (static) β€” replaces Ditto.SDKVersion
  • Ditto.DeviceName β€” custom peer device name (get/set)
  • DittoAuthenticationExpirationHandler β€” async delegate replacing IDittoAuthenticationDelegate
  • DittoAuthenticator.ObserveStatus() β€” returns IDisposable observer for auth status changes
  • DittoAuthenticator.Logout() β€” explicit logout support
  • DittoSDK.Auth namespace β€” all authentication types moved here
  • DittoStore.TransactionAsync() β€” async DQL transaction using DittoTransaction
  • DittoStoreObserver β€” replaces DittoLiveQuery
  • DittoSyncSubscription β€” replaces DittoSubscription; exposes QueryString, QueryArguments, Cancel()
  • DittoQueryResultItem β€” new type for single DQL result row with .Value property
  • WifiAwareConfig / WifiAwareConfigBuilder β€” new transport config for Android Wi-Fi Aware (MAUI)
  • DittoSyncPermissions (MAUI) β€” RequestPermissionsAsync() for Bluetooth/Wi-Fi permissions
  • DittoDiskUsageItem β€” replaces DittoDiskUsageChild; uses System.Text.Json
  • DittoDiskUsage.Item (property) β€” replaces Exec() method
  • DittoSDK.DiskUsage namespace β€” disk usage types moved here
  • DittoSDK.Exceptions namespace β€” exception types moved here

Migration Checklist

Update NuGet Package

  • Update DittoSDK NuGet package to v5.x

Fix Namespace Imports

  • Add using DittoSDK.Auth; where auth types are used
  • Add using DittoSDK.Exceptions; where DittoException is caught
  • Add using DittoSDK.Logging; where DittoLogger or DittoLogLevel are used
  • Update any other namespace references per compiler errors

Initialization

  • Replace new Ditto() with await Ditto.OpenAsync(config)
  • Create DittoConfig with databaseId and connect mode
  • Add await for async initialization
  • Update .OnlinePlayground β†’ new DittoConfigConnect.Server(new Uri(...))
  • Update .OfflinePlayground β†’ new DittoConfigConnect.SmallPeersOnly(privateKey: null)
  • Remove UpdateTransportConfig calls
  • Remove ALTER SYSTEM SET DQL_STRICT_MODE = false queries (this is now the v5 default); if your app used the v4 default (strict mode = true), add ALTER SYSTEM SET DQL_STRICT_MODE = true BEFORE Sync.Start() to maintain v4 behavior

Update Sync API

  • Replace ditto.StartSync() β†’ ditto.Sync.Start()
  • Replace ditto.StopSync() β†’ ditto.Sync.Stop()
  • Remove DisableSyncWithV3() calls

Update Authentication

  • Replace IDittoAuthenticationDelegate implementations with DittoAuthenticationExpirationHandler async delegate
  • Replace LoginWithToken() / Login() / LoginWithCredentials() β†’ LoginAsync()
  • Set ExpirationHandler delegate after initialization
  • Use DittoAuthenticationProvider.Development for playground tokens
  • Remove authentication from identity configuration

Observers

  • Replace DittoLiveQuery / .ObserveLocal() with DittoStoreObserver / Store.RegisterObserver()
  • Observer callbacks receive DittoQueryResult with .Items property
  • Use observer.Cancel() for cleanup (replaces liveQuery.Stop())
  • If processing is expensive, use Action<DittoQueryResult, Action> signalNext overload for back-pressure control

Breaking Changes

  • Set PersistenceDirectory if maintaining v4 directory structure
  • Update PeerKeyString β†’ PeerKey
  • Update IsDittoCloudConnected β†’ IsConnectedToDittoServer
  • Replace DittoStore.Write() β†’ DittoStore.TransactionAsync() (callback uses DittoTransaction, not DittoWriteTransaction)
  • Add using DittoSDK.Auth;, using DittoSDK.Exceptions; as needed
  • UpdateTransportConfig() callback parameter changed from Action<DittoTransportConfig> to Action<DittoTransportConfigBuilder> (builder pattern)

Update Renamed Types

  • Ditto.SiteId β†’ Removed (use peer identity from presence graph)
  • Ditto.SDKVersion β†’ Ditto.Version (static)
  • DittoDiskUsageChild β†’ DittoDiskUsageItem
  • DittoDiskUsage.Exec() β†’ DittoDiskUsage.Item (property)
  • DittoSubscription β†’ DittoSyncSubscription
  • DittoLiveQuery β†’ DittoStoreObserver

Verification

  • Build compiles with zero errors
  • No deprecated API warnings
  • DQL strict mode set correctly for your data model
  • Auth handler fires before first sync
  • Observers update UI immediately
  • No memory leaks on navigation

Common Pitfalls

  1. DQL_STRICT_MODE silent change: Default flipped. Objects merge field-by-field instead of replacing whole. Set DQL_STRICT_MODE=true BEFORE Sync.Start() if migrating existing data.
  2. Missing await on ExecuteAsync: ExecuteAsync returns Task<DittoQueryResult>. Forgetting await causes silent no-op.
  3. DittoSortDirection removed: Sort("field", DittoSortDirection.Ascending) won’t compile. Use ORDER BY field ASC in DQL.
  4. DittoIdentityType enum: Removed entirely. Code checking identity type won’t compile.
  5. LoginWithToken() removed: Must use LoginAsync(). The sync overloads are gone.
  6. Write() vs TransactionAsync(): The callback signature changed completely β€” the callback now receives DittoTransaction (not DittoWriteTransaction) and uses async DQL, not collection-based mutations.
  7. ExecuteAsync argument type: ExecuteAsync takes Dictionary<string, object> β€” anonymous objects like new { color = "blue" } are NOT accepted. Use new Dictionary<string, object> { ["color"] = "blue" }. Note: RegisterObserver and RegisterSubscription DO have overloads accepting anonymous object arguments.
  8. ObserveLocalWithNextSignal() removed from collection API: Back-pressure IS supported in v5 via the RegisterObserver overload that takes Action<DittoQueryResult, Action> where the second Action is the signalNext callback.