Persistence & Transactions¶
Meerkat provides rich APIs for single-document persistence, bulk persistence, physical and soft removal, and ambient multi-document transactions.
Single Document Persistence¶
Save and SaveAsync perform an upsert on the underlying collection: inserting the document if it does not exist, or replacing the entire document if a match on Id is found.
var student = new Student { FirstName = "Ada", LastName = "Lovelace" };
// Asynchronous save
await student.SaveAsync();
// Synchronous save
student.Save();
When saving:
1. CreatedAt (if not already set) and UpdatedAt are populated if TrackTimestamps = true.
2. Property transformations ([Lowercase], [Uppercase]) are executed.
3. PreSave() lifecycle hook is invoked.
4. The document is replaced / inserted in MongoDB using ReplaceOne.
5. PostSave() lifecycle hook is invoked.
Bulk Persistence¶
To persist multiple entities in a single batched operation, use the SaveAll / SaveAllAsync extension methods from the meerkat.Collections namespace:
using meerkat.Collections;
var students = new[]
{
new Student { FirstName = "Ada", LastName = "Lovelace" },
new Student { FirstName = "Charles", LastName = "Babbage" }
};
// Asynchronous bulk save
await students.SaveAllAsync<Student, ObjectId>();
// Synchronous bulk save
students.SaveAll<Student, ObjectId>();
Removal¶
Remove by ID¶
// Async
await Meerkat.RemoveByIdAsync<Student, ObjectId>(id);
// Sync
Meerkat.RemoveById<Student, ObjectId>(id);
Remove First Match¶
// Async
await Meerkat.RemoveOneAsync<Student, ObjectId>(x => x.FirstName == "Ada");
// Sync
Meerkat.RemoveOne<Student, ObjectId>(x => x.FirstName == "Ada");
Remove All Matches¶
// Async
await Meerkat.RemoveAsync<Student, ObjectId>(x => x.LastName == "Lovelace");
// Sync
Meerkat.Remove<Student, ObjectId>(x => x.LastName == "Lovelace");
Model Instance Removal¶
Models inheriting from Schema<TId> can remove themselves directly:
Soft Delete¶
Opt into soft deletion per collection by setting [Collection(SoftDelete = true)].
Meerkat stores DeletedAt (DateTimeOffset?) as a UTC BSON date. The IsDeleted property is computed dynamically (DeletedAt != null) and is not persisted.
[Collection(SoftDelete = true, TrackTimestamps = true)]
public class Student : Schema<ObjectId>
{
public string FirstName { get; set; } = string.Empty;
}
Behaviour When Enabled¶
- Queries & Counts:
Query,Find*,FindById*,Count*,Exists*, increment/decrement, and fluentUpdate*automatically exclude soft-deleted documents. - Deletions:
Remove*,RemoveById*,RemoveOne*, andstudent.DeleteAsync()setDeletedAt(andUpdatedAtif timestamps are tracked) instead of issuing a physical MongoDB delete command. - Index: An index on
DeletedAt(deleted_at_idx) is created automatically.
Including Soft-Deleted Documents¶
Pass includeDeleted: true to bypass the soft delete filter:
var active = await Meerkat.FindByIdAsync<Student, ObjectId>(id);
var includingDeleted = await Meerkat.FindByIdAsync<Student, ObjectId>(id, includeDeleted: true);
var all = await Meerkat.Query<Student, ObjectId>(includeDeleted: true).ToListAsync();
long totalCount = await Meerkat.CountAsync<Student, ObjectId>(includeDeleted: true);
Restoring Soft-Deleted Documents¶
// Restore by ID
await Meerkat.RestoreByIdAsync<Student, ObjectId>(id);
// Restore by predicate
await Meerkat.RestoreAsync<Student, ObjectId>(x => x.LastName == "Lovelace");
// Instance helper
await student.RestoreAsync();
Hard (Permanent) Removal¶
To bypass soft delete and permanently delete documents from MongoDB:
// Hard remove by ID
await Meerkat.HardRemoveByIdAsync<Student, ObjectId>(id);
// Hard remove by predicate
await Meerkat.HardRemoveAsync<Student, ObjectId>(x => x.LastName == "Lovelace");
Note
Soft delete does not cascade to related documents (Meerkat does not maintain an association graph). Standard unique indexes still apply to soft-deleted records. If uniqueness is only required among active records, define a partial unique index.
Transactions¶
Meerkat exposes ambient multi-document transactions. Writes and reads executed within the callback automatically join the transaction without requiring explicit session parameters on Save, Remove*, Update*, Increment*, Count*, or Query/Find* APIs.
Replica Set Requirement
MongoDB multi-document transactions require a replica set or sharded cluster. Standalone MongoDB instances do not support transactions.
await Meerkat.WithTransactionAsync(async _ =>
{
var order = new Order { Number = "1001" };
await order.SaveAsync();
var line = new OrderLine { OrderId = order.Id, Qty = 2 };
await line.SaveAsync();
});
Transaction Semantics¶
- Commit: If the callback completes successfully, the transaction commits automatically.
- Rollback: If the callback throws an exception, the transaction rolls back all modifications.
- Read-Your-Writes: Reads inside the callback see uncommitted writes made earlier in the same transaction.
- Nesting: Nested
WithTransaction/WithTransactionAsynccalls throwInvalidOperationException.
Overloads and Return Values¶
// Synchronous transaction
Meerkat.WithTransaction(() =>
{
var order = new Order { Number = "1002" };
order.Save();
});
// Returning a value from transaction
var orderId = await Meerkat.WithTransactionAsync(async _ =>
{
var order = new Order { Number = "1003" };
await order.SaveAsync();
return order.Id;
});