Table of Contents

Change Tracking

MongoObject provides automatic change tracking for efficient MongoDB updates. Only the fields that have changed are sent to the database.


How Change Tracking Works

MongoObject uses the INotifyPropertyChanged pattern combined with C# 14 partial properties to detect changes:

// When you retrieve a document, tracking is automatically enabled
var user = await monitor.Get(userId);

// Any property change is detected and recorded
user.Name = "New Name";      // Tracked: Name changed
user.Email = "new@test.com"; // Tracked: Email changed

// Only changed fields are sent to MongoDB
await monitor.SaveChanges(user);
// Generates: { $set: { "Document.Name": "New Name", "Document.Email": "new@test.com" } }

The TrackingObservableObject Base Class

All generated document classes inherit from TrackingObservableObject, which provides:

Core Features

Feature Description
INotifyPropertyChanged Standard .NET property change notification
Change Dictionary Records which properties have changed
Nested Tracking Tracks changes in nested objects
Pipeline Generation Creates MongoDB update operations

Key Methods

public abstract class TrackingObservableObject
{
    // Enable change tracking
    public void TrackChanges();
    
    // Clear recorded changes
    public void ClearChanges();
    
    // Generate MongoDB update pipeline
    public bool TryGetPendingUpdatesPipeline<T>(out UpdateDefinition<MongoDocument<T>>? update);
}

What Gets Tracked

Simple Property Changes

user.Name = "Updated";  // Tracked
user.Age = 30;          // Tracked

Setting to Null

user.MiddleName = null;  // Tracked as $unset operation

Collection Changes

user.Tags.Add("new-tag");        // Tracked (entire collection replaced)
user.Roles.Remove("admin");      // Tracked (entire collection replaced)

Dictionary Changes

user.Preferences["theme"] = "dark";  // Tracked (entire dictionary replaced)

Nested Object Changes

user.Address.Street = "123 Main St";  // Tracked if Address is also a TrackingObservableObject

Generated Update Operations

When you call SaveChanges(), MongoObject generates an efficient update pipeline:

$set Operations

For properties that have been assigned new values:

user.Name = "New Name";
user.Age = 25;

// Generates:
// { $set: { "Document.Name": "New Name", "Document.Age": 25 } }

$unset Operations

For properties set to null:

user.MiddleName = null;
user.Birthday = null;

// Generates:
// { $unset: ["Document.MiddleName", "Document.Birthday"] }

Automatic Metadata Updates

Every update automatically includes:

{
  $set: {
    "Metadata.LastModifiedAt": "$$NOW",
    "Metadata.Version": { $add: [{ $ifNull: ["$Metadata.Version", 0] }, 1] }
  }
}

Tracking Lifecycle

---
config:
  htmlLabels: false
---
flowchart TD
    A1[Document Retrieved Get]
    A2[Document deserialized from MongoDB]
    A3[TrackChanges called automatically]
    A4[PropertyChanged event handler attached]
    B1[Properties Modified]
    B2[SetField called for each property]
    B3[Changes recorded in _changes dictionary]
    B4[Nested objects tracked recursively]
    C1[SaveChanges Called]
    C2[ProcessPossibleChanges evaluates complex types]
    C3[TryGetPendingUpdatesPipeline builds update]
    C4[Update sent to MongoDB]
    C5[ClearChanges resets tracking]
    subgraph b1 [ ]
      subgraph a1 [Document Retreived]
        direction LR
        A1 --> A2 --> A3 --> A4
      end
    end
    subgraph b2 [ ]
      subgraph a2 [Properties Modified]
        direction LR
        B1 --> B2 --> B3 --> B4
      end
    end
    subgraph b3 [ ]
      subgraph a3 [SaveChanges]
        direction LR
        C1 --> C2 --> C3 --> C4 --> C5
      end
    end
    b1 --> b2 --> b3

Subscribing to Changes

You can subscribe to change notifications:

// Subscribe to changes on a specific document
using var subscription = monitor.OnChange(user, () =>
{
    Console.WriteLine("Document changed!");
});

// Make changes
user.Name = "New Name";  // Triggers the callback

Efficient Updates Example

public async Task UpdateUserEmail(string userId, string newEmail)
{
    // 1. Get the document (tracking enabled)
    var user = await monitor.Get(userId);
    
    // 2. Make the change
    user.Email = newEmail;
    
    // 3. Save only the changed field
    await monitor.SaveChanges(user);
    
    // MongoDB receives:
    // { $set: { "Document.Email": "new@email.com", "Metadata.LastModifiedAt": "$$NOW", ... } }
    // NOT the entire document!
}

Best Practices

1. Retrieve, Modify, Save

// ✓ Recommended pattern
var user = await monitor.Get(id);
user.Name = "New Name";
await monitor.SaveChanges(user);

2. Batch Multiple Changes

// ✓ Make all changes before saving
var user = await monitor.Get(id);
user.Name = "New Name";
user.Email = "new@test.com";
user.Age = 30;
await monitor.SaveChanges(user);  // Single update with all changes

3. Use Distributed Locks for Concurrent Access

// ✓ Lock before modifying
await using var lockScope = await monitor.LockDocument(user);
user.Balance += 100;
await monitor.SaveChanges(user, lockScope);

4. Avoid Long-Running Tracking

// ✗ Don't keep documents tracked for long periods
var user = await monitor.Get(id);
// ... hours later ...
user.Name = "New Name";  // May have stale data

// ✓ Retrieve fresh data when needed
var user = await monitor.Get(id);  // Fresh data
user.Name = "New Name";
await monitor.SaveChanges(user);

Next Steps