Incendia.MongoTracker
3.0.0
dotnet add package Incendia.MongoTracker --version 3.0.0
NuGet\Install-Package Incendia.MongoTracker -Version 3.0.0
<PackageReference Include="Incendia.MongoTracker" Version="3.0.0" />
<PackageVersion Include="Incendia.MongoTracker" Version="3.0.0" />
<PackageReference Include="Incendia.MongoTracker" />
paket add Incendia.MongoTracker --version 3.0.0
#r "nuget: Incendia.MongoTracker, 3.0.0"
#:package Incendia.MongoTracker@3.0.0
#addin nuget:?package=Incendia.MongoTracker&version=3.0.0
#tool nuget:?package=Incendia.MongoTracker&version=3.0.0
Incendia.MongoTracker
MongoTracker is a lightweight change tracker for MongoDB Client. It does not replace MongoDB.Driver; instead, it extends it by automatically tracking entity changes, generating atomic update operations, and supporting bulk writes. MongoTracker works seamlessly with nested objects, collections, and versioned fields.
Features
- Change Tracking: Automatically detects changes in fields, nested objects, and collections.
- Atomic Operations: Generates minimal
$set,$pushand$pulloperations. - Collections & Nested Objects: Supports
IsChild(),IsSet(),IsTrackedSet(),IsCollection(). - Optimistic Concurrency: Versioned fields (
IsVersion()) and concurrency tokens (IsConcurrencyToken()), withMongoConcurrencyExceptionon conflicts. - Compatibility: Works on top of standard
IMongoCollection<T>.
Installation
dotnet add package Incendia.MongoTracker
Targets netstandard2.1 and requires MongoDB.Driver 3.9.0 or later.
Usage
1. Configure your model
var config = new ModelBuilder();
config.Entity<Book>(e =>
{
e.Property(b => b.Id).IsIdentifier();
e.Property(b => b.Audiobook).IsChild();
e.Property(b => b.Authors).IsSet();
e.Property(b => b.Chapters).IsTrackedSet();
e.Property(b => b.LastUpdate).IsVersion();
});
Property modes
| Mode | Use for | What is written |
|---|---|---|
| (none) | Scalars, strings, enums, immutable values | A changed value is written with $set as a whole |
IsIdentifier() |
The document key, the key of tracked set elements | Not included in updates; used in the update filter and to match set elements |
IsChild() |
A nested object | Changed fields with $set on Parent.Field; a new object or null as a whole |
IsSet() |
An unordered collection of values (tags, ids) | Only added: $push with $each; only removed: $pullAll; both: $set of the whole array |
IsCollection() |
An ordered list of values | Any difference in content or order: $set of the whole array |
IsTrackedSet() |
A collection of nested objects | Only added: $push; only removed: $pull by identifier ($pullAll by value without one); only modified: $set inside the elements; mixed: $set of the whole array |
IsVersion() |
An int, long or DateTime version |
Set to the next value on every update; the original value is checked in the filter |
IsConcurrencyToken() |
A value you manage yourself | The original value is checked in the filter; written like a plain property when changed |
IsIgnored() |
Data that must not be tracked | Never compared and never included in updates (inserted documents are written as they are) |
- Properties do not have to be configured: everything not listed is tracked as a plain property, and so is
Property(x => x.Name)without a mode. - A property without a mode is compared with
Equalsto the value captured byTrack. Changes made in place to a list or a nested object (the same instance) are therefore not detected; only assigning another value is. Configure such properties withIsChild(),IsSet(),IsCollection()orIsTrackedSet(). - Properties ignored by BSON serialization (
[BsonIgnore]or not mapped in a registered class map) are skipped. - Update paths use BSON element names and member serializers (
[BsonElement],[BsonRepresentation], conventions). - Concurrency tokens of nested objects (
IsChild()) are checked in the filter too, by their full path (Child.Token). - The version belongs to the root entity. In the rare case when the stored document is a wrapper around the actual
entity, the version can instead be declared on one nested object (
IsChild()).
2. Initialize the tracker
var tracker = new MongoTracker<Book>(config);
3. Start tracking existing entities or add new
var book = await context.Books.AsQueryable().FirstAsync(b => b.Title == "A Game of Thrones");
book = tracker.Track(book);
var book2 = new Book() { Title = "The Shining" };
tracker.Add(book2);
4. Modify tracked entities
// Update a field in a nested object
book.Audiobook.Duration = TimeSpan.FromHours(30).TotalMinutes;
// Update a nested collection inside a tracked object
book.Chapters.First().Footnotes.Add("Important note");
// Add a new element to a collection
book.Chapters.Add(new BookChapter { Name = "Bran", StartPage = 31, EndPage = 50 });
Tracked set elements with identifiers
Configure an identifier on the element type of a tracked set, so its elements are addressed by it instead of by their position in the array. For example, a room whose viewers are changed by several users at once:
public class Room
{
public Guid Id { get; set; }
public List<Viewer> Viewers { get; set; } = [];
}
public class Viewer
{
public Guid Id { get; set; }
public bool OnPause { get; set; }
public List<string> Tags { get; set; } = [];
}
config.Entity<Room>(e =>
{
e.Property(r => r.Id).IsIdentifier();
e.Property(r => r.Viewers).IsTrackedSet();
});
config.Entity<Viewer>(e =>
{
e.Property(v => v.Id).IsIdentifier();
e.Property(v => v.Tags).IsSet();
});
- modified elements are updated with
$setonViewers.$[i0].OnPauseand the array filter{ "i0._id": <id> }; - nested collections are addressed the same way:
$pushintoViewers.$[i0].Tags; - removed elements are pulled by identifier:
$pull: { Viewers: { _id: { $in: [...] } } }; - an element replaced by another instance with the same identifier is tracked as modified;
- several elements with the same identifier in one set are not allowed.
Without an identifier, modified elements are addressed by their position (Viewers.2.OnPause) and removed elements
are pulled by their whole value. That breaks when the array is changed concurrently: another write that removes an
element shifts the positions, so the update lands on a different element. Use identifiers for arrays that can be
modified in parallel.
When a single save combines additions, removals or modifications in the same set, the whole array is replaced with $set,
because MongoDB does not allow these operations on one field in a single update. Protect such saves with a version
field.
5. Mark the entity for deletion
tracker.Delete(book);
6. Commit changes to MongoDB
var result = await tracker.SaveChangesAsync(context.Books);
7. Handle concurrency conflicts
A version field can be int, long or DateTime. Integer versions are incremented, DateTime versions receive the
current UTC time (millisecond precision). The new value is written back into the entity after a successful save.
Prefer integer versions: they cannot collide.
If an update or delete matches fewer documents than expected (the document was changed or removed since it was loaded),
SaveChanges throws MongoConcurrencyException and the tracked changes are not accepted. Operations that did
succeed are already persisted, so pass a session with a transaction when the whole save must be atomic:
try
{
await tracker.SaveChangesAsync(context.Books, session);
}
catch (MongoConcurrencyException)
{
await session.AbortTransactionAsync();
// Reload the data and retry the operation
}
Sample Project
The repository includes a sample project, Incendia.MongoTracker.Sample, demonstrating the library's core features.
For a real-world usage example, see Incendia.Identity.Mongo or Overoom.
License
MongoTracker is distributed under the MIT License.
Feedback
If you have questions, suggestions, or encounter issues, please create an issue in the project repository.
MongoTracker is a simple yet powerful tool for MongoDB that helps you efficiently manage data in your application. Try it in your project and experience its convenience and performance!
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- MongoDB.Driver (>= 3.9.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Incendia.MongoTracker:
| Package | Downloads |
|---|---|
|
Incendia.Identity.Mongo
An ASP.NET Core Identity MongoDB store powered by Incendia.MongoTracker. Uses atomic, partial updates instead of overwriting documents. Works with standard IdentityUser and IdentityRole without requiring model changes. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0 | 45 | 10/1/2026 |
| 2.1.0 | 180 | 1/30/2026 |
| 2.0.6 | 136 | 1/24/2026 |
| 2.0.5 | 134 | 1/24/2026 |
| 2.0.4 | 129 | 1/24/2026 |
| 2.0.3 | 170 | 1/21/2026 |
| 2.0.2 | 130 | 1/17/2026 |
| 2.0.1 | 210 | 11/26/2025 |
| 2.0.0 | 203 | 11/26/2025 |
| 1.0.7 | 169 | 10/24/2025 |
| 1.0.6 | 209 | 9/30/2025 |
| 1.0.5 | 240 | 8/30/2025 |
| 1.0.4 | 235 | 8/30/2025 |
| 1.0.3 | 214 | 6/16/2025 |
| 1.0.2 | 213 | 6/14/2025 |
| 1.0.1 | 220 | 6/14/2025 |
| 1.0.0 | 354 | 6/11/2025 |