Array operators

Overview

There are many times when you want to match documents based on array field values. Luckily, MongoDB and the C# driver provide the following 3 array query operators that help you build array based queries.

Operator

Description

Size

Match documents based on the array's size

ElemMatch

Match documents when array's elements match specified conditions

All

Match documents when all specified values are contained in the array

These operators may seem simple at first πŸ˜‡ , but when combined with other MongoDB features such as projection or unwind, you will find that you can build quite complex queries! πŸ’ͺ

Other than the operators themselves, you can create array field base queries using lambda expressions and the methods provided by Enumerable, such as Enumerable.Any

Size operator - $size

The $size operator is applied on array fields and matches documents when an array has a specific number of elements. MongoDB C# driver, doesn't contain a dedicated method for the $size operator but can resolve it from the Countproperty or Count() method of IEnumerabletypes.

The following sample finds:

  • Traveler documents having VisitedCountries array with exact 5 elements

  • Traveler documents having VisitedCountries array with more than 10 elements

ArrayOperators.cs
var collection = database
    .GetCollection<Traveler>(Constants.TravelersCollection);

var fiveVisitedCountriesFilter = await collection
    .Find(t => t.VisitedCountries.Count == 5).ToListAsync();
    
var moreThan10VisitedCountries = await collection
    .Find(t => t.VisitedCountries.Count > 10).ToListAsync();

Notice that using the BsonDocument approach, you need to write the exact query you would write in the shell.

db.travelers
	.find({ "visitedCountries.10" : { "$exists" : true } })

πŸ§™β€β™‚οΈ This uses the <array>.<index> notation to check if the array contains an element at 11th position, which would also mean that has more than 10 documents

ElemMatch operator - $elemMatch

The $elemMatch operator is used to match elements inside array fields based on one or more criteria.

Builders<T>.Filter
    .ElemMatch(doc => doc.<array-field>,<expressions>[])

The sample filters Traveler documents that their VisitedCountries array field contains a VisitedCountry element with name Greece and TimesVisited = 3.

ArrayOperators.cs
var collection = database
    .GetCollection<Traveler>(Constants.TravelersCollection);

var visitedGreeceExactly3Times = Builders<Traveler>.Filter
    .ElemMatch(t => t.VisitedCountries,
    country => country.Name == "Greece" 
        && country.TimesVisited == 3);

var visitedGreeceExactly3TimesTravelers = await collection
    .Find(visitedGreeceExactly3Times).ToListAsync();

You might be temped to match array elements using the $and operator as follow:

ArrayOperators.cs
var greeceVisitedFilter = Builders<Traveler>.Filter
    .AnyEq("visitedCountries.name", "Greece");

var visitedTimesFilter = Builders<Traveler>.Filter
    .AnyEq("visitedCountries.timesVisited", 3);

// shell
db.travelers.find({$and: [ 
    {"visitedCountries.name" : "Greece"}, 
    {"visitedCountries.timesVisited":3}])

This is wrong because it doesn't apply the criteria on each array element at a time but at all elements. This means that it might match documents that indeed contain a visited country with name "Greece" which hasn't TimesVisited = 3, but a document matched because it also contains another visited country, e.g. Italy with TimesVisited = 3.

The following sample filters Traveler documents that their VisitedCountries array field contains a VisitedCountry element TimesVisited = 3 but this time, the country's name can be either Greece or Italy.

ArrayOperators.cs
var collection = database
    .GetCollection<Traveler>(Constants.TravelersCollection);

// filter on country name
var countryNameFilter = Builders<VisitedCountry>.Filter
    .In(c => c.Name, new[] {"Greece", "Italy"});

// filter on times visited  
var countryTimesVisitedFilter = Builders<VisitedCountry>.Filter
    .Eq(c => c.TimesVisited, 3);

var visitedGreeceOrItalyExactly3Times = Builders<Traveler>.Filter
    .ElemMatch(t => t.VisitedCountries,
            Builders<VisitedCountry>.Filter
            .And(countryNameFilter, countryTimesVisitedFilter));

Enumerable.Any - AnyEq

To check if an array field contains a specified value you can use the Enumerable.Any or the FilterDefinitionBuilder<T>.AnyEq methods.

The sample finds the Traveler documents where "Greece" is contained in the VisitedCountries array field.

ArrayOperators.cs
var collection = database
    .GetCollection<Traveler>(Constants.TravelersCollection);

var greeceTravelers = await collection
    .Find(t => t.VisitedCountries
        .Any(c => c.Name == "Greece")).ToListAsync();

You can go further, and add an || operator in the Any method. This will combine $elemMatch and $in operators to build the query.

var collection = database
  .GetCollection<Traveler>(Constants.TravelersCollection);

var greeceItalyTravelers = await collection
  .Find(t => t.VisitedCountries
      .Any(c => c.Name == "Greece" || c.Name == "Italy")).ToListAsync();

All operator - $all

The $all operator is applied on array fields and matches documents when the array field contains all the items specified. You use the All operator when you want to ensure that an array contains (or doesn't) a list of values.

Builders<T>.Filter
    .All(doc => doc.<array-field>,<values>[])

The sample finds all Traveler documents having "Backpacking" and "Climbing" values on their Activities list. Activities is an array of string values.

ArrayOperators.cs
var collection = database
    .GetCollection<Traveler>(Constants.TravelersCollection);

var climbingAndBackpackingFilter = Builders<Traveler>.Filter
    .All(t => t.Activities, 
        new List<string> { "Backpacking", "Climbing" });

var climbingAndBackpackingTravelers = await collection
    .Find(climbingAndBackpackingFilter).ToListAsync();

The order of the array values passed in the All method doesn't matter, in the same way it doesn't matter when writing the query in the shell with the $all operator

Last updated