In this article, we will look at how to retrieve MongoDB documents using ObjectId in C#.
We will only cover how to query a MongoDB database using ObjectId in C#. Check out our article Getting Started with ASP.NET Core and MongoDB for more information on how to set up an ASP.NET Core Web API with MongoDB
Let’s talk a bit about ObjectId.
What is ObjectId in C#?
The ObjectId
is a struct located within the MongoDB.Bson
namespace, and it serves as the unique identifier for each document in a MongoDB database:
ObjectId uniqueId = ObjectId.GenerateNewId();
Thanks to its implementation of the IComparable
and IEquatable
interfaces, we can easily compare ObjectId
instances to determine their relative order and equality:
var firstId = ObjectId.GenerateNewId(); var secondId = ObjectId.GenerateNewId(); int comparisonResult = firstId.CompareTo(secondId); bool areEqual = firstId.Equals(secondId);
A typical ObjectId
is a 12-byte identifier made up of a 4-byte timestamp depicting the creation period, a 5-byte random unique value, and a 3-byte increment counter, initialized to a random value:
5f4b76848753ed99fb1f18ac //Timestamp: 5f4b7684 //Random value: 8753ed99fb //Incrementing counter: 1f18ac
The combination of these components makes ObjectId
a suitable unique identifier.
Creating MongoDB ObjectId in C#
We can create an ObjectId
by initializing it via the constructor. The ObjectId
constructor has two contemporary overloads.
We can pass in a byte array as the constructor argument:
var byteArray = new byte[] { 0x5F, 0x4B, 0x76, 0x84, 0x87, 0x53, 0xED, 0x99, 0xFB, 0x1F, 0x18, 0xAC }; var objectId = new ObjectId(byteArray);
Alternatively, we can also pass in a 24-digit hex string as the constructor argument:
var hexString = "5F4B76848753ED99FB1F18AC"; var objectIdFromHexString = new ObjectId(hexString);
Another approach we can take to create an ObjectId
instance is to call the static ObjectId.GenerateNewId()
method:
var dateTime = new DateTime(2023, 10, 10); var intTimeStamp = 20; var objectId = ObjectId.GenerateNewId(); var objectIdTwo = ObjectId.GenerateNewId(dateTime); var objectIdThree = ObjectId.GenerateNewId(intTimeStamp);
In the preceding code sample, we declare three instances of ObjectId
using the three different overloads of ObjectId.GenerateNewId()
.
In the first declaration, we used the parameterless overload. For the second declaration, we used the overload that takes in a specific DateTime
, and in the third declaration, we used the overload that collects an int
as a parameter.
We can also create an ObjectId
instance by parsing a 24-digit hex string:
var objectIdFromParsedString = ObjectId.Parse("5F4B76848753ED99FB1F18AC");
It is safer to use TryParse()
because it first checks if the string argument can be parsed before parsing it:
bool canParse = ObjectId.TryParse("5F4B76848753ED99FB1F18AC", out ObjectId objectIdFromTryParse);
Let’s look at the various ways we can use ObjectId
to query our database.
Query Items Using MongoDB ObjectId in C#
In the previous article, we defined StudentService.GetById()
which uses a literal string to retrieve a student from the database using IMongoCollection<T>.Find()
.
IMongoCollection<T>.Find()
uses a query filter to retrieve items from our database:
public async Task<Student?> GetById(string id) { return await _studentCollection.Find(s => s.Id == id).FirstOrDefaultAsync(); }
We use the query filter to compare the Id
values of the items in our database with the literal string passed in. Then we use FirstOrDefaultAsync()
to retrieve a single student.
A more flexible approach is to create query filters using Builder<T>
.
Creating queries using Builder<T>
Builder<T>
is a static class that gives us access to different kinds of query builders for a BsonDocument
.
BsonDocument
is a class found in the MongoDB driver, specifically representing a BSON
document. BSON
stands for Binary JavaScript Object Notation. It is an extension of JSON
, offering additional features and support for DateTime and binary data.
Builder<T>.Filter
gives us access to a fluent API we can use to construct filter queries for the T
document.
For our school management system example, Builder<Student>.Filter
exposes an API for querying the Student
documents:
public Task<Student> GetByIdUsingBuilder(string id) { var queryFilter = Builders<Student>.Filter.Eq(s => s.Id, id); return _studentCollection.Find(queryFilter).FirstOrDefaultAsync(); }
In the code snippet, StudentService.GetByIdUsingBuilder()
encapsulates Builder<T>.Filter.Eq()
, which we employed to construct a filter passed to IMongoCollection.Find()
. The Builder<T>.Filter.Eq()
method requires two parameters. The field to filter and the corresponding value to filter for.
The GetByIdUsingBuilder()
method will give us the same result as when we used literal values to query for a student.
Next, let’s see how we can employ the Builder<T>
class to fetch multiple Items.
Get Multiple Items Using MongoDB ObjectId in C#
We can retrieve multiple items from our database using a literal value or the builder object just the same way we did in our previous examples.
Using literal values to retrieve multiple items from the database works better when we are querying a field that is consistent across various documents. For example, when we want to retrieve all the students with the first name “John”:
var students = _studentCollection.Find(s => s.FirstName == "John").ToEnumerable();
Since our primary focus is on querying the database using ObjectId
, which predominantly corresponds to the unique Id
field, using a literal value to retrieve multiple items from the database isn’t applicable in our scenario.
This is because the Id
field always represents a unique identifier in our database, ensuring that each query returns only one item.
Let’s look at some Builders<T>
methods.
We can retrieve items within a range using logical operators like Builders<T>.Filter.Or
and Builders<T>.Filter.And
.
Builders<T>.Filter.And
returns documents that satisfy all of our specified conditions:
public IEnumerable<Student> GetRangeOfStudentsUsingAnd(string minimumId, string maximumId) { var andFilter = Builders<Student>.Filter .And( Builders<Student>.Filter.Gt("_id", ObjectId.Parse(minimumId)), Builders<Student>.Filter.Lte("_id", ObjectId.Parse(maximumId))); return _studentCollection.Find(andFilter).ToEnumerable(); }
In the code, StudentService.GetRangeOfStudentsUsingAnd()
encapsulates a query designed to filter the Id
field in our database, fetching documents with an Id
greater than the provided minimumId
parameter and less than the specified maximumId
parameter.
Notice how we utilized Builders<T>.Filter.Gt()
to create a greater-than filter, which we passed as the first argument to Builders<T>.Filter.In()
. Subsequently, we employed Builders<T>.Filter.Lt()
to generate a less-than filter, serving as the second parameter to Builders<T>.Filter.In()
.
Builders<T>.Filter.Or()
returns documents that satisfy at least one of the specified conditions:
public IEnumerable<Student> GetRangeOfStudentsUsingOr(string minimumId, string maximumId) { var orFilter = Builders<Student>.Filter .Or( Builders<Student>.Filter.Gt("_id", ObjectId.Parse(minimumId)), Builders<Student>.Filter.Lt("_id", ObjectId.Parse(maximumId))); return _studentCollection.Find(orFilter).ToEnumerable(); }
Here, we have StudentService.GetRangeOfStudentsUsingOr()
that defines a query for filtering the Id
field in our database. This time it fetches documents with an Id
greater than the provided minimumId
parameter or less than the specified maximumId
parameter.
Builders<T>.Filter.In()
gives us all the documents where a field matches any value from a given list:
public IEnumerable<Student> GetAllStudentsInTheList(string ids) { var objectIds = ids.Split(',').Select(x => new ObjectId(x.Trim())); var filter = Builders<Student>.Filter.In("Id", objectIds); return _studentCollection.Find(filter).ToEnumerable(); }
In the code, StudentService.GetAllStudentsInTheList()
takes a comma-separated string of ObjectId
s as input and then splits the string into ObjectId
values.
The array of ObjectId
values is used to create a filter using Builders<T>.Filter.In()
. The filter is passed to the Find()
method to retrieve all documents where the Id
property matches any value in the array.
We can also use an empty filter to retrieve all documents from the database:
var students= _studentCollection.Find(Builders<Student>.Filter.Empty).ToEnumerable();
Testing Querying MongoDB With ObjectId
Let’s create controller methods to test the new methods we declared within our StudentService
:
[HttpGet("usingbuilder/{id:length(24)}")] public async Task<ActionResult<IEnumerable<Student>>> GetAStudentUsingBuilder(string id) => Ok(await _studentService.GetByIdUsingBuilder(id)); [HttpGet("andrange")] public ActionResult<IEnumerable<Student>> GetRangeUsingAnd(string minimumId, string maximumId) => Ok(_studentService.GetRangeOfStudentsUsingAnd(minimumId, maximumId)); [HttpGet("orrange")] public ActionResult<IEnumerable<Student>> GetRangeUsingOr(string minimumId, string maximumId) => Ok(_studentService.GetRangeOfStudentsUsingOr(minimumId, maximumId)); [HttpGet("inlist")] public ActionResult<IEnumerable<Student>> GetInList(string objectIds) => Ok(_studentService.GetAllStudentsInTheList(objectIds));
We can use Postman to verify that our API endpoints are working:
Method: POST URL: https://localhost:44370/api/Students Body: { "firstName" : "Little", "lastName": "Missy", "major": "AI", "courses": [ "6144f8a7e4b0a4e8e8a3e8b1" ] }
We see both responses contain the created students:
{ "id": "65646939e03abc36a4907173", "firstName": "Little", "lastName": "Missy", "major": "AI", "courses": [ "655da9e52138075ad7328434" ] }
Now let’s fetch the created student using the GetAStudentUsingBuilder()
:
Method: GET Url: https://localhost:7233/api/Students/usingbuilder/65646939e03abc36a4907173
Once we send the request, we will see the student as a result:
{ "id": "65646939e03abc36a4907173", "firstName": "Little", "lastName": "Missy", "major": "AI", "courses": [ "655da9e52138075ad7328434" ] }
Additional Tests Using Documents With Ids
To facilitate effective testing of our other methods, we need documents with Ids created using different DateTime
s.
Our current implementation of StudentService.Create
ensures that a new Id
is generated at the exact time a student is being created.
For testing purposes, we will first comment out the line of code that generates and assigns the Id
of the created Student
document:
public async Task<Student?> Create(Student student) { // Comment out the line that generates and assigns the ID //student.Id = ObjectId.GenerateNewId().ToString(); await _studentCollection.InsertOneAsync(student); return student; }
Then we override the default MongoDB document validation by including an optional parameter called InsertOneOptions
. This parameter provides configuration options for customizing the insert operation:
public async Task<Student?> Create(Student student) { // Comment out the line that generates and assigns the ID //student.Id = ObjectId.GenerateNewId().ToString(); await _studentCollection.InsertOneAsync(student, new InsertOneOptions() { BypassDocumentValidation = true}); return student; }
When set to true, the BypassDocumentValidation
property of the InsertOneOptions
helps us bypass document validation.
Now when we create a Student
and pass in the ObjectId
we want, the created document will have that specified Id
.
Next, we simulate ObjectId
created from various timestamps:
var objectId = ObjectId.GenerateNewId(DateTime.Parse("2020-10-10")); //5f80eb706d35e17a696994d5 var objectId2 = ObjectId.GenerateNewId(DateTime.Parse("2020-11-10")); //5fa9c9f06d35e17a696994d6 var objectId3 = ObjectId.GenerateNewId(DateTime.Parse("2020-12-10")); //5fd156f06d35e17a696994d7 var objectId4 = ObjectId.GenerateNewId(DateTime.Parse("2020-10-12")); //5f838e706d35e17a696994d8 var objectId5 = ObjectId.GenerateNewId(DateTime.Parse("2020-10-14")); //5f8631706d35e17a696994d9
After that, let’s create a Student
for each of the created ObjectId
:
Method: POST URL: https: //localhost:44370/api/Students Body: { "Id": "5f80eb706d35e17a696994d5", "firstName": "Perry", "lastName": "Jones", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] } Method: POST URL: https: //localhost:44370/api/Students Body: { "Id": "5fa9c9f06d35e17a696994d6", "firstName": "Lilly", "lastName": "Sarah", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] } Method: POST URL: https: //localhost:44370/api/Students Body: { "Id": "5fd156f06d35e17a696994d7", "firstName": "Gabriel", "lastName": "White", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] } Method: POST URL: https: //localhost:44370/api/Students Body: { "Id": "5f838e706d35e17a696994d8", "firstName": "Alex", "lastName": "Snow", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] } Method: POST URL: https: //localhost:44370/api/Students Body: { "Id": "5f8631706d35e17a696994d9", "firstName": "Jack", "lastName": "Frost", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] }
Great! We’ve created our students.
Let’s get students created between the 10th of October 2020 and the 14th of October 2020:
Method: GET URL: https://localhost:44370/api/Students/andrange?minimumId=5f80eb7024399a354bb8b85c&maximumId=5f86317024399a354bb8b860
This query results in:
[ { "id": "5f80eb706d35e17a696994d5", "firstName": "Perry", "lastName": "Jones", "major": "Gravity", "courses": [ "5f5b320a97b51ea2f470a4e2" ] }, { "id": "5f838e706d35e17a696994d8", "firstName": "Alex", "lastName": "Snow", "major": "Gravity", "courses": [ "5f5b320a97b51ea2f470a4e2" ] } ]
Finally, let’s get a list of students whose Ids
appear within a list of concatenated strings:
Method: GET URL: https://localhost:44370/api/Students/inlist?objectIds=5fa9c9f06d35e17a696994d6%2C5f80eb706d35e17a696994d5%2C5fd156f06d35e17a696994d7
We get all the Students whose Ids
appear in our list:
[ { "id": "5f80eb706d35e17a696994d5", "firstName": "Perry", "lastName": "Jones", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] }, { "id": "5fa9c9f06d35e17a696994d6", "firstName": "Lilly", "lastName": "Sarah", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] }, { "id": "5fd156f06d35e17a696994d7", "firstName": "Gabriel", "lastName": "White", "major": "Gravity", "courses": [ "655da9e52138075ad7328434" ] } ]
Conclusion
In this article, we took a deep dive into the various ways in which we can query a MongoDB database using the ObjectId struct.
We looked at how to use literal values and also how to build query filters using the MongoDB Driver Builders<T> class.