From MongoDB 6.0 to 9.0 and C# Driver 2.x to 3.x: Release Model, What's New and a Migration Checklist
You cannot go from MongoDB 6.0 to 9.0 directly; the major versions are installed in turn and the feature compatibility version is raised at every step. When moving to C# driver 3.x, the real risk is not compile errors but the decimals that 2.x wrote as strings and the DateTimeOffsets it wrote as arrays. With real output: 9.0 refusing to open 6.0 data, the 7.0-8.0-9.0 steps, the same LINQ query giving a wrong result on 2.30 and zero results on 3.14, and the migration that moves the data.
Version note: The examples in this post were run on the MongoDB 6.0.30, 7.0.48, 8.0.38 and 9.0.8 Community Server images, .NET 10 and MongoDB.Driver 2.30.0 and 3.14.0. The driver example connects to 9.0.8; the server version does not change the driver's behavior.
Guess First
An old service of Bookstore, the online bookstore used as the shared example across this site's series, has been running with MongoDB.Driver 2.30 for years and has written books with a decimal price and a DateTimeOffset date. The repo's samples/DriverUpgrade example can build the same code with both drivers: 2.30.0 by default, 3.14 with -p:MongoDriverVersion=3.14.0. Three books are written (390.50, 530.90 and 1450.00) and the same LINQ query runs with both drivers:
IQueryable<PricedBook> cheap = books.AsQueryable().Where(b => b.ListPrice < 500m);
dotnet run --project samples/DriverUpgrade -- seed
dotnet run --project samples/DriverUpgrade -p:MongoDriverVersion=3.14.0
There is only one book under 500 TRY, Dune. How many books does 2.30 return, and how many 3.14?
driver 2.30.0: bookstore.driver_upgrade.Aggregate([{ "$match" : { "ListPrice" : { "$lt" : "500" } } }])
driver 2.30.0: books under 500: Dune 390.50 (listed 2024-06-01 09:00 +03:00), The Art of Computer Programming 1450.00 (listed 2024-06-01 09:00 +03:00)
driver 2.30.0: stored types: ListPrice String, ListedAt Array
driver 3.14.0: bookstore.driver_upgrade.Aggregate([{ "$match" : { "ListPrice" : { "$lt" : { "$numberDecimal" : "500" } } } }])
driver 3.14.0: books under 500:
driver 3.14.0: stored types: ListPrice String, ListedAt Array
2.30 returned two books, and one is wrong: 2.x wrote decimal as a string by default, and the query compared with the string "500"; in string order "1450.00" is less than "500". This bug may have been in production for years. 3.14 returned zero books: 500m now goes as Decimal128, and BSON does not compare a string with a number. Upgrading the driver "fixed" the query, but because the data is still in the old format, the screen shows no books at all. In this last post we cover two upgrades together: MongoDB's release model and the upgrade path from 6.0 to 9.0, then the move from C# driver 2.x to 3.x and a checklist.
The Release Model: Major, Minor and Feature Update
The version number is X.Y.Z: X is the major version (it can bring backward-incompatible changes and data formats that earlier versions cannot read), Y the minor, Z the patch. Major versions come once a year: 7.0 in August 2023, 8.0 in October 2024, 9.0 in September 2026. The story of minor versions has changed. With 8.2, minor versions became available to self-managed Community and Enterprise as well; starting with 9.0, minor versions only reach Atlas clusters with Auto Upgrades enabled. For self-managed deployments there is a yearly Feature Update release numbered x.5, carrying the same support period as the major version. So a self-managed Bookstore will move 9.0 → 9.5 → 10.0. The support calendar follows: support for 6.0 ended on July 31, 2025; 7.0's ends on August 31, 2027, 8.0's on October 31, 2029, and 9.0's on October 31, 2031. A system still on 6.0 receives no security patches today.
What Changed from 6.0 to 9.0?
| Version | Changes covered in this series |
|---|---|
| 7.0 | Queryable Encryption equality queries GA, compound wildcard indexes |
| 8.0 | Up to 36% better read throughput according to the release notes, client-level bulkWrite (post 10), Queryable Encryption range queries |
| 8.2 | Minor versions opened to self-managed, Queryable Encryption prefix/suffix/substring public preview |
| 9.0 | The constraint validation level (post 5), the per-operation memory limit (post 8), the open transaction limit (post 12), $queryStats on by default (post 14), server-side JavaScript no longer deprecated (post 8), queryKnobs, prefix/suffix/substring GA and mongocryptd deprecated (post 14) |
9.0 has one more change that does not fit in the table: time series collections are now stored as a single namespace made of compressed buckets; they are no longer writable views. If you are thinking of moving the view counters from post 5 to a time series collection, start with 9.0. I have been tracking SERVER-89953 from 9.0's known issues list since post 6 (at feature compatibility version 9.0, listIndexes reports collation: { locale: "simple" } for indexes with simple collation, and some ODMs therefore rebuild the indexes on every start); on 9.0.8 I could not see this field on an index upgraded from 6.0 either, after moving to feature compatibility version 9.0 (title_1 in the output below). The release notes still list it as an open issue; if you use an ODM, check the log after the upgrade for index rebuilds.
The Upgrade Path: In Order and with the FCV
You can only move from one major version to the next; 9.0's upgrade page requires 8.0 or 8.3, and from anything older you first climb to 8.0 step by step. Replacing the binary and enabling the new version's data formats are separate steps: the second is the FCV. While the FCV stays at the old version, going back to the previous binary is possible. In the lab I created a volume with 6.0, loaded Bookstore's books and an index, then tried to open the same volume directly with 9.0:
docker volume create bookstore-upgrade
docker run -d --name bookstore-upgrade -v bookstore-upgrade:/data/db -v "${PWD}\data:/lab/data:ro" -v "${PWD}\scripts:/lab/scripts:ro" mongodb/mongodb-community-server:6.0-ubuntu2204
docker exec bookstore-upgrade mongoimport --db bookstore --collection books --file /lab/data/books.json --quiet
docker exec bookstore-upgrade mongosh --quiet --eval "db.getSiblingDB('bookstore').books.createIndex({ title: 1 })"
docker exec bookstore-upgrade mongosh --quiet /lab/scripts/15-check.js
docker stop bookstore-upgrade
docker rm bookstore-upgrade
version 6.0.30 | FCV 6.0 | books 77 | title_1: {"v":2,"key":{"title":1},"name":"title_1"}
{"t":{"$date":"2026-11-14T15:00:59.731+00:00"},"s":"F", "c":"CONTROL", "id":20573, "ctx":"initandlisten","msg":"Wrong mongod version","attr":{"error":"UPGRADE PROBLEM: Found an invalid featureCompatibilityVersion document (ERROR: Location4926900: Invalid featureCompatibilityVersion document in admin.system.version: { _id: \"featureCompatibilityVersion\", version: \"6.0\" }. See https://docs.mongodb.com/master/release-notes/8.0-compatibility/#feature-compatibility. :: caused by :: Invalid feature compatibility version value '6.0'. Expected one of the following versions: ''9.0', '8.3', '8.0'. See https://docs.mongodb.com/master/release-notes/8.0-compatibility/#feature-compatibility.). If the current featureCompatibilityVersion is below 8.0, see the documentation on upgrading at https://docs.mongodb.com/master/release-notes/8.0/#upgrade-procedures."}}
9.0 did not start: the FCV document in the data directory says 6.0, and 9.0 accepts only 9.0, 8.3 or 8.0. The data was not damaged; the container stopped before starting. The right way is three steps on the same volume; at each step the new binary first starts with the old FCV and is checked, then the FCV is raised:
docker run -d --name bookstore-upgrade -v bookstore-upgrade:/data/db -v "${PWD}\data:/lab/data:ro" -v "${PWD}\scripts:/lab/scripts:ro" mongodb/mongodb-community-server:7.0-ubuntu2204
docker exec bookstore-upgrade mongosh --quiet /lab/scripts/15-check.js
docker exec bookstore-upgrade mongosh --quiet --eval "db.adminCommand({ setFeatureCompatibilityVersion: '7.0', confirm: true }).ok"
docker exec bookstore-upgrade mongosh --quiet /lab/scripts/15-check.js
version 7.0.48 | FCV 6.0 | books 77 | title_1: {"v":2,"key":{"title":1},"name":"title_1"}
1
version 7.0.48 | FCV 7.0 | books 77 | title_1: {"v":2,"key":{"title":1},"name":"title_1"}
version 8.0.38 | FCV 7.0 | books 77 | title_1: {"v":2,"key":{"title":1},"name":"title_1"}
1
version 8.0.38 | FCV 8.0 | books 77 | title_1: {"v":2,"key":{"title":1},"name":"title_1"}
version 9.0.8 | FCV 8.0 | books 77 | title_1: {"v":2,"key":{"title":1},"name":"title_1"}
1
version 9.0.8 | FCV 9.0 | books 77 | title_1: {"v":2,"key":{"title":1},"name":"title_1"}
(The 8.0 and 9.0 steps are the same commands; the image tag and the FCV value change.) The first line of each step shows the new binary running with the old FCV; running your application in this state for a while and raising the FCV if nothing goes wrong is the right order. Since 7.0, setFeatureCompatibilityVersion requires confirm: true, so that it is not run carelessly. According to the documentation, since 8.3 going back to the immediately previous minor or major version is supported, that is, from 9.0 to 8.0; but going back two steps, to 7.0, is not. On a replica set you do the same without downtime by restarting the members one by one (secondaries first, the primary last after a stepDown; post 13).
C# Driver 2.x to 3.x: the Compiler First
The example's 2.x-specific settings are in an #if DRIVER2 block. 2.30 already gives CS0618 obsolete warnings on these lines; building the same lines against 3.14 turns the warnings into errors:
#if DRIVER2
// Driver 2.x setup, as many projects have it
BsonDefaults.GuidRepresentationMode = GuidRepresentationMode.V3;
MongoClientSettings settings = MongoClientSettings.FromConnectionString(connectionString);
settings.LinqProvider = LinqProvider.V3;
#else
// Driver 3.x: V3 Guid mode and LINQ3 are the only options, the settings are gone
MongoClientSettings settings = MongoClientSettings.FromConnectionString(connectionString);
#endif
dotnet build samples/DriverUpgrade -p:MongoDriverVersion=3.14.0 -p:DefineConstants=DRIVER2
samples/DriverUpgrade/Program.cs(14,14): error CS0117: 'BsonDefaults' does not contain a definition for 'GuidRepresentationMode'
samples/DriverUpgrade/Program.cs(14,39): error CS0103: The name 'GuidRepresentationMode' does not exist in the current context
samples/DriverUpgrade/Program.cs(16,10): error CS1061: 'MongoClientSettings' does not contain a definition for 'LinqProvider' and no accessible extension method 'LinqProvider' accepting a first argument of type 'MongoClientSettings' could be found (are you missing a using directive or an assembly reference?)
samples/DriverUpgrade/Program.cs(16,25): error CS0103: The name 'LinqProvider' does not exist in the current context
samples/DriverUpgrade/Program.cs(39,1): error CS0246: The type or namespace name 'IMongoQueryable<>' could not be found (are you missing a using directive or an assembly reference?)
All of these errors are in the upgrade guide: LINQ2 was removed and LINQ3 is the only option, GuidRepresentationMode.V2 was removed and you must register a GuidSerializer explicitly as in post 9, and plain IQueryable<T> replaced IMongoQueryable<T>. Other items in the guide: .NET Framework 4.7.2 or .NET Core 3.x minimum, AWS authentication and in-use encryption moved to separate packages (MongoDB.Driver.Authentication.AWS, MongoDB.Driver.Encryption), and TLS 1.0 and 1.1 support ended. Within 3.x the server baseline keeps rising too: 3.5 dropped MongoDB 4.0 and earlier, 3.10 dropped 4.2 and earlier. The 2.30 build output gave one more reason: NuGet reported known vulnerability warnings (NU1902, NU1903) for the transitive dependencies SharpCompress 0.30.1 and Snappier 1.0.0.
Then the Data: decimal and DateTimeOffset
Compiler errors are visible; the real risk is the silent kind, as in "Guess First". 3.0 changed two serialization defaults: decimal is now Decimal128, and DateTimeOffset is now a document with DateTime, Ticks and Offset fields (the form we saw in post 9). For reading old data there are two ways: put [BsonRepresentation(BsonType.String)] and [BsonRepresentation(BsonType.Array)] on the properties and keep the old format, or move the data to the new format once. Because we want numeric queries and sorting on the price, the second:
const prices = db.driver_upgrade.updateMany(
{ ListPrice: { $type: "string" } },
[{ $set: { ListPrice: { $toDecimal: "$ListPrice" } } }]
);
// 2.x array: [local ticks, offset in minutes]; 3.x document: { DateTime (UTC), Ticks, Offset }
// $set with a document on an array field would apply it to every element, so the array is moved aside first
const dates = db.driver_upgrade.updateMany(
{ ListedAt: { $type: "array" } },
[
{ $set: { legacy: "$ListedAt" } },
{ $unset: "ListedAt" },
{ $set: { ListedAt: {
DateTime: { $toDate: { $subtract: [
{ $divide: [{ $subtract: [ticks, NumberLong("621355968000000000")] }, 10000] },
{ $multiply: [offset, 60000] }] } },
Ticks: ticks,
Offset: offset
} } },
{ $unset: "legacy" }
]
);
ListPrice converted: 3 | ListedAt converted: 3
driver 3.14.0: bookstore.driver_upgrade.Aggregate([{ "$match" : { "ListPrice" : { "$lt" : { "$numberDecimal" : "500" } } } }])
driver 3.14.0: books under 500: Dune 390.50 (listed 2024-06-01 09:00 +03:00)
driver 3.14.0: stored types: ListPrice Decimal128, ListedAt Document
After the migration 3.14 gave the right answer: only Dune, with the date and offset preserved. The first version of the script made an instructive mistake: when I assigned a document with $set to a field that is an array, MongoDB applied the document to every element of the array and ListedAt became an array of two documents; 3.14 could not read it and threw a FormatException. That is why the old array is first moved to a legacy field and removed. On a large collection, run the same migration in small batches with saved progress, as in post 5; if you need code that can read both the old and the new format during the transition, writing a custom serializer that deserializes both is an option.
The Migration Checklist
- Server: which major are you on, and when does its support end? If you are below 8.0, plan the intermediate steps to 9.0.
- At each step, the binary first, then watch the application at the old FCV, last
setFeatureCompatibilityVersionwithconfirm: true. - Read the compatibility and known issues pages of the target version; for 9.0 the per-operation memory limit, the open transaction limit and
SERVER-89953. - Driver: first move to 2.30 and clear all
CS0618warnings, then move to 3.x. - Look for string
decimal, arrayDateTimeOffsetand subtype 3Guid(post 9) in the data; count them with$typequeries. - Rehearse the upgrade on a restored copy of production data (the restore test from post 14) and compare query results on both versions.
- If you use the EF Core provider, remember that breaking changes can arrive in minor versions too (post 11).
In Production
Do not put the driver and server upgrades in the same deploy. First upgrade the server with the old FCV and run the application with the same driver; then upgrade the driver in a separate deploy. Only this way can you tell which change broke something.
Measure the same queries before and after the upgrade. The SBE difference in post 7 and the memory limit in post 8 showed that the same pipeline can produce a different plan and a different error when the version changes. Keep the explain output of your most frequent queries and compare it after the upgrade.
Common Pitfalls
Raising the FCV right away. If you raise the FCV right after the binary change, you close the door to going back; if a problem shows up a few days later, you cannot return to the old version. Raise the FCV after the application has run on the new binary without problems for a while.
Treating the upgrade as done because it compiles. The query in "Guess First" compiled with both drivers and threw no error; one returned a wrong result, the other an empty one. The test of an upgrade is not the compiler but integration tests running against real data (post 10).
A Small Experiment
★☆☆ Add [BsonRepresentation(BsonType.String)] to PricedBook.ListPrice and run the query on 3.14 with the unmigrated data. How many books come back, and why does the wrong result from 2.30 return?
★★☆ Stop the upgrade steps at 9.0 and, while the FCV is 8.0, open the volume with the 8.0 image again. Then set the FCV to 9.0 and repeat the experiment; what does 8.0 say this time?
★★★ Write a report script that counts the values in 2.x format ($type: "string" prices, array dates, subtype 3 Guids) in all of Bookstore's collections, and run it on the restored copy from post 14.
The series ends here. Bookstore's catalog, orders and reviews now live in MongoDB: the document model and schema patterns, indexes and aggregation, the .NET driver and the EF Core provider, consistency, replica sets, sharding, security and version migration. From here, the Database Performance and Scaling series covers the general scaling journey, and the Distributed Systems Fundamentals series goes into the theory we applied in these posts.