-
Notifications
You must be signed in to change notification settings - Fork 98
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.
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)
// 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 untouchedEach 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" });To restore 3.x behavior for the whole application:
// In your startup / Program.cs
TikDefaults.SaveMode = TikSaveMode.FullUpdate;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.
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>().
| 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 |
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.