MongoDB Geospatial Queries in C#
Rate this tutorial
If you've ever glanced at a map to find the closest lunch spots to you, you've most likely used a geospatial query under the hood! Using GeoJSON objects to store geospatial data in MongoDB Atlas, you can create your own geospatial queries for your application. In this tutorial, we'll see how to work with geospatial queries in the MongoDB C# driver.
Geospatial queries allow you to work with geospatial data. Whether that's on a 2d space (like a flat map) or 3d space (when viewing a spherical representation of the world), geospatial data allows you to find areas and places in reference to a point or set of points.
These might sound complicated, but you've probably encountered these use cases in everyday life: searching for points of interest in a new city you're exploring, discovering which coffee shops are closest to you, or finding every bakery within a three-mile radius of your current position (for science!).
These kinds of queries can easily be done with special geospatial query operators in MongoDB. And luckily for us, these operators are also implemented in most of MongoDB's drivers, including the C# driver we'll be using in this tutorial.
One important aspect of working with geospatial data is something called the GeoJSON format. It's an open standard for representing simple geographical features and makes it easier to work with geospatial data. Here's what some of the GeoJSON object types look like:
1 // Point GeoJSON type 2 { 3 "type" : "Point", 4 "coordinates" : [-115.20146200000001, 36.114704000000003] 5 } 6 7 // Polygon GeoJSON type 8 { 9 "type": "Polygon", 10 "coordinates": [ 11 [ 12 [100.0, 0.0], 13 [101.0, 0.0], 14 [101.0, 1.0], 15 [100.0, 1.0], 16 [100.0, 0.0] 17 ] 18 ] 19 }
While MongoDB supports storing your geospatial data as legacy coordinate pairs, it's preferred to work with the GeoJSON format as it makes complicated queries possible and much simpler.
💡 Whether working with coordinates in the GeoJSON format or as legacy coordinate pairs, queries require the longitude to be passed first, followed by latitude. This might seem "backwards" compared to what you may be used to, but be assured that this format actually follows the
(X, Y)
order of math! Keep this in mind as MongoDB geospatial queries will also require coordinates to be passed in [longitude, latitude]
format where applicable.Alright, let's get started with the tutorial!
- Visual Studio Community (2019 or higher)
To make this tutorial easier to follow along, we'll work with the
restaurants
and neighborhoods
datasets, both publicly available in our documentation. They are both JSON
files that contain a sizable amount of New York restaurant and neighborhood data already in GeoJSON format!💡 These files differ from the the
sample_restaurants
dataset that can be loaded in Atlas! While the collection names are the same, the JSON files I'm asking you to download already have data in GeoJSON format, which will be required for this tutorial.💡 When you reach Step 5 of importing your data into your cluster (Run mongoimport), be sure to keep track of the
database
and collection
names you pass into the command. We'll need them later! If you want to use the same names as in this tutorial, my database is called sample-geo
and my collections are called restaurants
and neighborhoods
.Lastly, to work with geospatial data, a 2dsphere index needs to be created for each collection. You can do this in the MongoDB Atlas portal.
First navigate to your cluster and click on "Browse Collections":
You'll be brought to your list of collections. Find your restaurant data (if following along, it will be a collection called
restaurants
within the sample-geo
database).With the collection selected, click on the "Indexes" tab:
Click on the "CREATE INDEX" button to open the index creation wizard. In the "Fields" section, you'll specify which field to create an index on, as well as what type of index. For our tutorial, clear the input, and copy and paste the following:
1 { "location": "2dsphere" }
Click "Review". You'll be asked to confirm creating an index on
sample-geo.restaurants
on the field { "location": "2dsphere" }
(remember, if you aren't using the same database and collection names, confirm your index is being created on yourDatabaseName.yourCollectionName
). Click "Confirm."Likewise, find your neighborhood data (
sample-geo.neighborhoods
unless you used different names). Select your neighborhoods
collection and do the same thing, this time creating this index:1 { "geometry": "2dsphere" }
Almost instantly, the indexes will be created. You'll know the index has been successfully created once you see it listed under the Indexes tab for your selected collection.
Now, you're ready to work with your restaurant and neighborhood data!
To show these samples, we'll be working within the context of a simple console program. We'll implement each geospatial query operator as its own method and log the corresponding MongoDB Query it executes.
After creating a new console project, add the MongoDB Driver to your project using the Package Manager or the .NET CLI:
Package Manager
1 Install-Package MongoDB.Driver
.NET CLI
1 dotnet add package MongoDB.Driver
Next, add the following dependencies to your
Program.cs
file:1 using MongoDB.Bson; 2 using MongoDB.Bson.IO; 3 using MongoDB.Bson.Serialization; 4 using MongoDB.Driver; 5 using MongoDB.Driver.GeoJsonObjectModel; 6 using System;
For all our examples, we'll be using the following
Restaurant
and Neighborhood
classes as our models:1 public class Restaurant 2 { 3 public ObjectId Id { get; set; } 4 public GeoJsonPoint<GeoJson2DCoordinates> Location { get; set; } 5 public string Name { get; set; } 6 }
1 public class Neighborhood 2 { 3 public ObjectId Id { get; set; } 4 public GeoJsonPoint<GeoJson2DCoordinates> Geometry { get; set; } 5 public string Name { get; set; } 6 }
Add both to your application. For simplicity, I've added them as additional classes in my
Program.cs
file.Next, we need to connect to our cluster. Place the following code within the
Main
method of your program:1 // Be sure to update yourUsername, yourPassword, yourClusterName, and yourProjectId to your own! 2 // Similarly, also update "sample-geo", "restaurants", and "neighborhoods" to whatever you've named your database and collections. 3 var client = new MongoClient("mongodb+srv://yourUsername:yourPassword@yourClusterName.yourProjectId.mongodb.net/sample-geo?retryWrites=true&w=majority"); 4 var database = client.GetDatabase("sample-geo"); 5 var restaurantCollection = database.GetCollection<Restaurant>("restaurants"); 6 var neighborhoodCollection = database.GetCollection<Neighborhood>("neighborhoods");
Finally, we'll add a helper method called
Log()
within our Program
class. This will take the geospatial queries we write in C# and log the corresponding MongoDB Query to the console. This gives us an easy way to copy it and use elsewhere.1 private static void Log(string exampleName, FilterDefinition<Restaurant> filter) 2 { 3 var serializerRegistry = BsonSerializer.SerializerRegistry; 4 var documentSerializer = serializerRegistry.GetSerializer<Restaurant>(); 5 var rendered = filter.Render(documentSerializer, serializerRegistry); 6 Console.WriteLine($"{exampleName} example:"); 7 Console.WriteLine(rendered.ToJson(new JsonWriterSettings { Indent = true })); 8 Console.WriteLine(); 9 }
We now have our structure in place. Now we can create the geospatial query methods!
Since MongoDB has dedicated operators for geospatial queries, we can take advantage of the C# driver's filter definition builder to build type-safe queries. Using the filter definition builder also provides both compile-time safety and refactoring support in Visual Studio, making it a great way to work with geospatial queries.
The
.Near
filter implements the $near geospatial query operator. Use this when you want to return geospatial objects that are in proximity to a center point, with results sorted from nearest to farthest.In our program, let's create a
NearExample()
method that does that. Let's search for restaurants that are at most 10,000 meters away and at least 2,000 meters away from a Magnolia Bakery (on Bleecker Street) in New York:1 private static void NearExample(IMongoCollection<Restaurant> collection) 2 { 3 // Instantiate builder 4 var builder = Builders<Restaurant>.Filter; 5 6 // Set center point to Magnolia Bakery on Bleecker Street 7 var point = GeoJson.Point(GeoJson.Position(-74.005, 40.7358879)); 8 9 // Create geospatial query that searches for restaurants at most 10,000 meters away, 10 // and at least 2,000 meters away from Magnolia Bakery (AKA, our center point) 11 var filter = builder.Near(x => x.Location, point, maxDistance: 10000, minDistance: 2000); 12 13 // Log filter we've built to the console using our helper method 14 Log("$near", filter); 15 }
That's it! Whenever we call this method, a
$near
query will be generated that you can copy and paste from the console. Feel free to paste that query into the data explorer in Atlas to see which restaurants match the filter (don't forget to change "Location"
to a lowercase "location"
when working in Atlas). In a future post, we'll delve into how to visualize these results on a map!For now, you can call this method (and all other following methods) from the
Main
method like so:1 static void Main(string[] args) 2 { 3 var client = new MongoClient("mongodb+srv://yourUsername:yourPassword@yourClusterName.yourProjectId.mongodb.net/sample-geo?retryWrites=true&w=majority"); 4 var database = client.GetDatabase("sample-geo"); 5 var restaurantCollection = database.GetCollection<Restaurant>("restaurants"); 6 var neighborhoodCollection = database.GetCollection<Neighborhood>("neighborhoods"); 7 8 NearExample(restaurantCollection); 9 // Add other methods here as you create them 10 }
⚡ Feel free to modify this code! Change your center point by changing the coordinates or let the method accept variables for the
point
, maxDistance
, and minDistance
parameters instead of hard-coding it.In most use cases,
.Near
will do the trick. It measures distances against a flat, 2d plane (Euclidean plane) that will be accurate for most applications. However, if you need queries to run against spherical, 3d geometry when measuring distances, use the .NearSphere
filter (which implements the $nearSphere
operator). It accepts the same parameters as .Near
, but will calculate distances using spherical geometry.The
.GeoWithin
filter implements the $geoWithin geospatial query operator. Use this when you want to return geospatial objects that exist entirely within a specified shape, either a GeoJSON Polygon
, MultiPolygon
, or shape defined by legacy coordinate pairs. As you'll see in a later example, that shape can be a circle and can be generated using the $center
operator.To implement this in our program, let's create a
GeoWithinExample()
method that searches for restaurants within an area—specifically, this area:In code, we describe this area as a polygon and work with it as a list of points:
1 private static void GeoWithinExample(IMongoCollection<Restaurant> collection) 2 { 3 var builder = Builders<Restaurant>.Filter; 4 5 // Build polygon area to search within. 6 // This must always begin and end with the same coordinate 7 // to "close" the polygon and fully surround the area. 8 var coordinates = new GeoJson2DCoordinates[] 9 { 10 GeoJson.Position(-74.0011869, 40.752482), 11 GeoJson.Position(-74.007384, 40.743641), 12 GeoJson.Position(-74.001856, 40.725631), 13 GeoJson.Position(-73.978511, 40.726793), 14 GeoJson.Position(-73.974408, 40.755243), 15 GeoJson.Position(-73.981669, 40.766716), 16 GeoJson.Position(-73.998423, 40.763535), 17 GeoJson.Position(-74.0011869, 40.752482), 18 }; 19 var polygon = GeoJson.Polygon(coordinates); 20 21 // Create geospatial query that searches for restaurants that fully fall within the polygon. 22 var filter = builder.GeoWithin(x => x.Location, polygon); 23 24 // Log the filter we've built to the console using our helper method. 25 Log("$geoWithin", filter); 26 }
The
.GeoIntersects
filter implements the $geoIntersects geospatial query operator. Use this when you want to return geospatial objects that span the same area as a specified object, usually a point.For our program, let's create a
GeoIntersectsExample()
method that checks if a specified point falls within one of the neighborhoods stored in our neighborhoods collection:1 private static void GeoIntersectsExample(IMongoCollection<Neighborhood> collection) 2 { 3 var builder = Builders<Neighborhood>.Filter; 4 5 // Set specified point. For example, the location of a user (with granted permission) 6 var point = GeoJson.Point(GeoJson.Position(-73.996284, 40.720083)); 7 8 // Create geospatial query that searches for neighborhoods that intersect with specified point. 9 // In other words, return results where the intersection of a neighborhood and the specified point is non-empty. 10 var filter = builder.GeoIntersects(x => x.Geometry, point); 11 12 // Log the filter we've built to the console using our helper method. 13 Log("$geoIntersects", filter); 14 }
💡 For this method, an overloaded
Log()
method that accepts a FilterDefinition
of type Neighborhood
needs to be created.As we've seen, the
$geoWithin
operator returns geospatial objects that exist entirely within a specified shape. We can set this shape to be a circle using the $center
operator.Let's create a
GeoWithinCenterExample()
method in our program. This method will search for all restaurants that exist within a circle that we have centered on the Brooklyn Bridge:1 private static void GeoWithinCenterExample(IMongoCollection<Restaurant> collection) 2 { 3 var builder = Builders<Restaurant>.Filter; 4 5 // Set center point to Brooklyn Bridge 6 var point = GeoJson.Point(GeoJson.Position(-73.99631, 40.705396)); 7 8 // Create geospatial query that searches for restaurants that fall within a radius of 20 (units used by the coordinate system) 9 var filter = builder.GeoWithinCenter(x => x.Location, point.Coordinates.X, point.Coordinates.Y, 20); 10 Log("$geoWithin.$center", filter); 11 }
Another way to query for places is by combining the
$geoWithin
and $centerSphere
geospatial query operators. This differs from the $center
operator in a few ways:$centerSphere
uses spherical geometry while$center
uses flat geometry for calculations.$centerSphere
works with both GeoJSON objects and legacy coordinate pairs while$center
only works with and returns legacy coordinate pairs.$centerSphere
uses radians for distance, which requires additional calculations to produce an accurate query.$center
uses the units used by the coordinate system and may be less accurate for some queries.
We'll get to our example method in a moment, but first, a little context on how to calculate radians for spherical geometry!
💡 An important thing about working with
$centerSphere
(and any other geospatial operators that use spherical geometry), is that it uses radians for distance. This means the distance units used in queries (miles or kilometers) first need to be converted to radians. Using radians properly considers the spherical nature of the object we're measuring (usually Earth) and let's the $centerSphere
operator calculate distances correctly.Use this handy chart to convert between distances and radians:
Conversion | Description | Example Calculation |
---|---|---|
distance (miles) to radians | Divide the distance by the radius of the sphere (e.g., the Earth) in miles. The equitorial radius of the Earth in miles is approximately 3,963.2 . | Search for objects with a radius of 100 miles: 100 / 3963.2 |
distance (kilometers) to radians | Divide the distance by the radius of the sphere (e.g., the Earth) in kilometers. The equitorial radius of the Earth in kilometers is approximately 6,378.1 . | Search for objects with a radius of 100 kilometers: 100 / 6378.1 |
radians to distance(miles) | Multiply the radian measure by the radius of the sphere (e.g., the Earth). The equitorial radius of the Earth in miles is approximately 3,963.2 . | Find the radian measurement of 50 in miles: 50 * 3963.2 |
radians to distance(kilometers) | Multiply the radian measure by the radius of the sphere (e.g., the Earth). The equitorial radius of the Earth in kilometers is approximately 6,378.1 . | Find the radian measurement of 50 in kilometers: 50 * 6378.1 |
For our program, let's create a
GeoWithinCenterSphereExample()
that searches for all restaurants within a three-mile radius of Apollo Theater in Harlem:1 private static void GeoWithinCenterSphereExample(IMongoCollection<Restaurant> collection) 2 { 3 var builder = Builders<Restaurant>.Filter; 4 5 // Set center point to Apollo Theater in Harlem 6 var point = GeoJson.Point(GeoJson.Position(-73.949995, 40.81009)); 7 8 // Create geospatial query that searches for restaurants that fall within a 3-mile radius of Apollo Theater. 9 // Notice how we pass our 3-mile radius parameter as radians (3 / 3963.2). This ensures accurate calculations with the $centerSphere operator. 10 var filter = builder.GeoWithinCenterSphere(x => x.Location, point.Coordinates.X, point.Coordinates.Y, 3 / 3963.2); 11 12 // Log the filter we've built to the console using our helper method. 13 Log("$geoWithin.$centerSphere", filter); 14 }
As we've seen, working with MongoDB geospatial queries in C# is possible through its support for the geospatial query operators. In another tutorial, we'll take a look at how to visualize our geospatial query results on a map!
If you have any questions or get stuck, don't hesitate to post on our MongoDB Community Forums! And if you found this tutorial helpful, don't forget to rate it and leave any feedback. This helps us improve our articles so that they are awesome for everyone!