Skip to content

Change tracking

Daniel Frantík edited this page May 24, 2026 · 1 revision

Change tracking

4.x feature. Change tracking is available from tik4net.entities 4.0.

When you load an entity with any Load* method, tik4net takes an automatic snapshot of the entity's field values. When you subsequently call Save, the snapshot is diffed against the current state of the object and only the changed fields are sent to the router as a /set command. If nothing changed, the API call is skipped entirely.

This eliminates the extra LoadById round-trip that 3.x performed before every update and reduces traffic on heavily modified rule lists.


How it works

LoadById<FirewallFilter>("*1")
    │
    └─ router returns fields ──► entity populated
                                 snapshot taken (all field values stored)

rule.Comment = "WAN in"
rule.Disabled = false

Save(rule)
    │
    └─ diff snapshot ─► only "comment" changed
                        sends: /ip/firewall/filter/set =.id=*1 =comment=WAN in
                        skips "disabled" (unchanged)

Basic usage

// Load — snapshot is taken automatically
var rule = connection.LoadById<FirewallFilter>("*1");

// Modify one field
rule.Comment = "WAN in";

// Save — only =comment= is sent to the router
connection.Save(rule);
// No-op: nothing changed → no API call at all
var addr = connection.LoadById<IpAddress>("*3");
connection.Save(addr); // returns immediately, router untouched

Save modes

Each Save call can specify a TikSaveMode to override the default behavior:

Mode Behavior
TikSaveMode.Default Uses TikDefaults.SaveMode (see below)
TikSaveMode.OnlyChanges Diff against snapshot; no snapshot → send all fields
TikSaveMode.FullUpdate Round-trip LoadById to compute diff, same as 3.x
// Force full update for this one call
connection.Save(rule, saveMode: TikSaveMode.FullUpdate);

// Explicit field filter still works as before (overrides change tracking)
connection.Save(rule, usedFieldsFilter: new[] { "comment", "disabled" });

Global default

To restore 3.x behavior for the whole application:

// In your startup / Program.cs
TikDefaults.SaveMode = TikSaveMode.FullUpdate;

⚠️ Breaking change vs 3.x

In 3.x, Save always performed a LoadById round-trip before the /set call to compute the diff.

In 4.x, Save uses the local snapshot instead. The observable difference:

Scenario 3.x 4.x
Entity loaded via Load*, one field changed round-trip + sends 1 field no round-trip, sends 1 field
Entity loaded via Load*, nothing changed round-trip + sends 0 fields (no-op /set) no round-trip, no API call
Entity created with new and id set manually round-trip + diff sends all writable fields (no snapshot)
Entity created with new, no id insert /add insert /add (unchanged)

Migration: Code that relies on the round-trip refreshing the entity state before save will behave differently. If you need the old behavior, set TikDefaults.SaveMode = TikSaveMode.FullUpdate globally or pass saveMode: TikSaveMode.FullUpdate per call.


Advanced: TikChangeTracker API

The tracker is accessible via the ChangeTracker() extension method on ITikConnection. All methods on the tracker accept the entity instance as key.

var tracker = connection.ChangeTracker();

// Inspect changes before saving
if (tracker.HasChanges(rule, metadata))
{
    var changes = tracker.GetChanges(rule, metadata);
    foreach (var kv in changes)
        Console.WriteLine($"{kv.Key}: {kv.Value.Old}{kv.Value.New}");
}

// Mark entity clean — next Save is a no-op
tracker.ResetSnapshot(rule, metadata);

// Remove entity from tracking — next Save sends all fields
tracker.Forget(rule);

// Manually take a snapshot (e.g. after cloning an entity)
tracker.TakeSnapshot(clonedRule, metadata);

TikEntityMetadata is obtained from TikEntityMetadataCache.GetMetadata<TEntity>().


Edge cases

Scenario Behavior
new MyEntity(); Save() No id → /add insert, snapshot taken after
new MyEntity { Id = "*1" }; Save() No snapshot → sends all writable fields
Cloned entity (CloneEntity) No snapshot → sends all writable fields. Call tracker.TakeSnapshot to opt in.
Save twice without changes Second call is a no-op (snapshot already reset after first save)
Read-only properties changed in code Never included in diff
Entity GC'd Snapshot released automatically (ConditionalWeakTable)
LoadAsync Snapshot taken in the callback, same as synchronous load
LoadListenAsync No snapshot — push events represent current state, not a prior baseline
LoadWithDuration No snapshot — time-windowed results are not intended for save
Singleton entity (IsSingleton = true) Snapshot taken; diff works; .id is not required
Multiple threads TikChangeTracker is thread-safe

Partial .proplist loads (future)

When a load is performed with a restricted .proplist (e.g. via the planned TikLink Select projection), the snapshot tracks only the returned fields. The diff then only covers those fields, leaving unloaded fields untouched on the router.

This mechanism is already in place for forward compatibility — it activates automatically when a partial .proplist is detected on the command.

Clone this wiki locally