Upgrading to v26
v26 changes how IDs are stored and generated. Use this guide before deploying v26 against an existing database.
Upgrade checklist
- Back up your database and test the upgrade on a restored copy.
- Inspect your stored IDs and references. If string properties were stored as BSON ObjectIds, choose one of the storage options below.
- Convert affected library-owned string references even if you keep ObjectId storage for entity IDs. Leave file-chunk IDs unchanged.
- Replace custom ID-generation overrides, register any required serializers, and review direct relationship API usage.
- Verify reads, saves, relationships, deletes, and file downloads against your upgraded test database before deploying.
Data migration for existing databases
Databases written by previous versions store ObjectId-parseable string IDs as BSON ObjectId in entity _id fields, join record ParentID/ChildID values, One<T>.ID/ModifiedBy.UserID references, embedded Entity IDs, and [BINARY_CHUNKS].FileID. After this upgrade, public plain string ID/reference properties can no longer read those values and new writes store plain strings. [BINARY_CHUNKS].FileID is the compatibility exception described below. Choose one of:
Keep ObjectId storage for entity
_ids (recommended when you have data): you cannot put an attribute on the inheritedEntity.IDproperty, and you also cannot remap that inherited member on each concrete type (BsonClassMap.RegisterClassMap<Book>(...).MapIdProperty(b => b.ID)throws becauseIDis declared onEntity, notBook). Use one of:- Class map on the
Entitybase for ObjectId-formatted IDs only (before first use of anyEntitysubclass / beforeDB.InitAsync). This affects every type that inheritsEntity, including nested/embedded entities that use the same base. The strict serializer below cannot write arbitrary custom-format string IDs. It is not a drop-in replacement for the former[AsObjectId]serializer, which accepted both BSON strings and ObjectIds. For mixed/custom ID formats, use the per-entity option below or migrate to string storage:// usings: MongoDB.Bson, MongoDB.Bson.Serialization, // MongoDB.Bson.Serialization.IdGenerators, MongoDB.Bson.Serialization.Serializers BsonClassMap.RegisterClassMap<Entity>(cm => { cm.AutoMap(); cm.SetIgnoreExtraElements(true); cm.MapIdProperty(e => e.ID) .SetSerializer(new StringSerializer(BsonType.ObjectId)) .SetIdGenerator(StringObjectIdGenerator.Instance); }); - Per-entity ObjectId storage requires owning the ID member yourself via
IEntity(do not try to re-map inheritedEntity.IDper concrete type):public class Book : IEntity { [BsonId, AsObjectId] // or [BsonRepresentation(BsonType.ObjectId)] for ObjectId-only strings public string ID { get; set; } = null!; }
Join records follow each entity's stored
_idrepresentation automatically.- Class map on the
Convert stored ObjectIds to strings (offline, with backups). Treat this as a controlled, idempotent migration: only rewrite documents whose field is still BSON ObjectId (
$type: "objectId"), and ensure destination string_ids cannot collide (unique index on_idis automatic; for reference fields, re-check uniqueness if you rely on it). Sketch for a single collection:// BACK UP FIRST. Run offline / during a write freeze. // Idempotent: only documents whose _id is still ObjectId are rewritten. db.MyCollection.find({ _id: { $type: "objectId" } }).forEach(d => { const oldId = d._id; const newId = String(oldId); // Stop for manual reconciliation rather than silently leaving two versions. if (db.MyCollection.findOne({ _id: newId })) { throw new Error(`destination string id already exists: ${newId}`); } const copy = Object.assign({}, d, { _id: newId }); db.MyCollection.insertOne(copy); db.MyCollection.deleteOne({ _id: oldId }); });This is a migration sketch, not a complete migration tool. An interruption between insert and delete can leave both versions; reconcile them before rerunning. Secondary unique indexes can also prevent inserting a copy while the original exists. Plan how to preserve and validate those constraints, or migrate through a separate collection, before using this approach.
Apply the same ObjectId-to-string rewrite to every affected field:
- entity
_idfor each collection that uses plain string IDs - join collections (
[Parent~Child(Prop)]/[(Prop)Parent~Child(Prop)])ParentIDandChildID One<T>.IDreference fields (andOne<T,TIdentity>when the identity is string)ModifiedBy.UserID- nested/embedded documents that stored an
Entity-style string ID as ObjectId
Do not convert
[BINARY_CHUNKS].FileID(see below). Nested arrays/subdocuments need the same$type: "objectId"filter on the nested path, not only top-level_id.- entity
Always convert (or accept string storage for) library-owned string reference fields: One<T>.ID and ModifiedBy.UserID no longer use [AsObjectId] and always store/read plain strings. Leaving those as BSON ObjectId will fail deserialization after upgrade even if entity _ids are kept as ObjectId via the class map.
File chunks: [BINARY_CHUNKS].FileID still uses [AsObjectId], so retain existing BSON ObjectId values for ObjectId-format file IDs. This remains true even if file entity metadata _id values are converted to BSON strings: the chunk serializer converts an ObjectId-format parent ID string to BSON ObjectId when querying, so existing chunks continue to match. Do not convert those FileID values to BSON strings; current library filters would not match them. Non-ObjectId-format file IDs are stored as strings automatically.
API and serialization changes
The following details cover source compatibility and initialization order. They apply independently of the data migration option you choose.
- Many relationships now work with any entity ID type/representation (dynamic IDs).
[AsBsonId]has been removed and join records (JoinRecord.ParentID/ChildID) are nowBsonValueproperties that hold the exact stored representation of the related entities'_idvalues (string, ObjectId, long, Guid, custom-represented IDs, etc.).JoinRecordnow derives fromObjectIdEntityinstead ofEntity.One<T>.IDremainsstring; useOne<TEntity, TIdentity>for non-string one-to-one references. [AsObjectId]is no longer applied by the library to public entity/reference IDs. The attribute still exists for opt-in use on your own properties, but it has been removed fromEntity.ID,One<T>.IDandModifiedBy.UserID. String IDs on those members are now stored as plain strings unless you map/decorate them yourself (see migration notes: inheritedEntity.IDcannot be re-attributed; use a class map or implementIEntity). Internal[BINARY_CHUNKS].FileIDstill uses[AsObjectId]so existing file data keeps matching ObjectId-format parent IDs.Many<TChild,TParent>.ParentsQueryable(...)now acceptsobject/IEnumerable<object?>child IDs instead ofstring/IEnumerable<string>, plusIReadOnlyList<TId> where TId : structfor value-type ID arrays.- Guid IDs: entities with
GuidID properties require a Guid serializer to be registered beforeDB.InitAsync, e.g.BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); HasDefaultID()instance methods removed fromEntity,ObjectIdEntityand internal types. Unsaved-entity detection is via the (now public)HasDefaultID()extension method, which uses the resolvedIIdGenerator.IsEmptywhen a generator exists (sostring.Emptyand custom empty sentinels regenerate) and otherwise compares the ID to its CLR default. Remove your own implementations/overrides; prior instance overrides were already ignored by the library internally.GenerateNewID()removed fromIEntityand all entity base classes.IEntityis now a marker interface. ID generation is driven by the mongodb driver'sIIdGenerator, resolved per entity type in this order: a generator registered with the newDB.RegisterIdGenerator<TEntity>(...)(works for any entity, includingEntitysubclasses), the generator on the entity'sBsonClassMap, one registered for the ID's CLR type viaBsonSerializer.RegisterIdGenerator(...), then library defaults forstring(ObjectId-formatted strings, same as before),ObjectIdandGuidIDs. AGenerateNewID()extension method replaces the instance method. If you overrodeGenerateNewID()for a custom ID format, register anIIdGeneratorviaDB.RegisterIdGenerator<TEntity>()instead. Otherwise string IDs will silently receive ObjectId-format values, and IDs of other types without a resolvable generator will throw on save.- The library's global serializers and conventions now register via a module initializer (previously the
DBstatic constructor).Date/FuzzyString/decimaldefaults are supplied through anIBsonSerializationProvider(not eagerRegisterSerializer/TryRegisterSerializer), so a user-registered serializer for those types wins when registered before the first lookup of the type. Note:TryRegisterSerializerthrows if a different serializer is already registered; it does not let the second caller win.
Batch ID overloads
Batch raw-ID APIs (DeleteAsync, Many.AddAsync/RemoveAsync, ParentsQueryable/ParentsFluent) accept value-type ID arrays through IReadOnlyList<TId> where TId : struct overloads. Use these for Guid[], long[], and ObjectId[]; existing boxed/reference-ID overloads remain available.