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

To download the source code for this article, you can visit our GitHub repository.

Let’s talk a bit about ObjectId.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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 ObjectIds 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 DateTimes.

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!