Using ASP.NET Core OData with MongoDB Atlas
Tags: odata, dotnetcore, webapi, csharp
Published: 2024-05-24 00:42:35

# Overview 
This article focuses on how you can quickly get up and running using Microsoft's ASP.NET Core OData extension with MongoDB Atlas. 

OData stands for [Open Data Protocol](https://www.odata.org/) which is a standard that allows developers to simplify the process of building and consuming RESTful APIs. [MongoDB Atlas](https://www.mongodb.com/docs/atlas/) is MongoDB's cloud offering that allows you to leverage the full potential of MongoDB's developer data platform without any cloud vendor lock-in. 

# Tutorial

You need to have a MongoDB Atlas Cluster set up with sample data loaded as shown [here](https://www.mongodb.com/docs/atlas/getting-started/)

I'm using Visual Studio 2022 Community Edition in this tutorial. You can choose to use any IDE of your choice. Open up a new project and choose the ASP.NET Core Empty Project template.
    ![VS ASP.NET Core Empty Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6dujbgypsln1wimmi03.png)
  
On the following screen, name your project and click Next. Choose the latest LTS .NET Framework and make sure to disable the 'Configure for HTTPS checkbox.  
    ![Configure for LTS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nq9mjh8ccpqq0eoon0hm.png)

Now we are going to add the [MongoDB OData extension](https://www.nuget.org/packages/MongoDB.AspNetCore.OData/#readme-body-tab) from the Nuget package manager as seen below. This uses the MongoDB .NET/C# Driver and Microsoft's `AspNetCore.OData` package as its dependencies. 

I'm going to use the `sample_restaurants` database from the [sample restaurants dataset](https://www.mongodb.com/docs/atlas/sample-data/sample-restaurants/) loaded into my MongoDB Atlas cluster. We need some model classes as shown below in our project for this. 
First we'll create the `Restaurant` class.

``` csharp
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace ODataTutorial.Models
{
    [BsonIgnoreExtraElements]
    public class Restaurant
    {
        [BsonRepresentation(BsonType.ObjectId)]
        public string Id { get; set; }

        [BsonElement("name")]
        public string Name { get; set; }

        [BsonElement("restaurant_id")]
        public string? RestaurantId { get; set; }

        [BsonElement("cuisine")]
        public string Cuisine { get; set; }

        [BsonElement("address")]
        public Address Address { get; set; }

        [BsonElement("borough")]
        public string Borough { get; set; }
    }
}
```

Next we’ll create the `Address` model class. 

``` csharp
using MongoDB.Bson.Serialization.Attributes;

namespace ODataTutorial.Models
{
    public class Address

  {
        [BsonElement("building")]
        public string Building { get; set; }

        [BsonElement("coord")]
        public double[] Coordinates { get; set; }

        [BsonElement("street")]
        public string Street { get; set; }

        [BsonElement("zipcode")]
        public string ZipCode { get; set; }
    }
}
```

The `Address` class does not require an `_id` because it is only stored as a nested subdocument.

Note that we are using the `[BsonElement]` [attribute](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/attributes) to tell the MongoDB C# driver to map the names in our model classes to the field names in the database. You could also choose to use a [convention pack] (https://www.mongodb.com/docs/drivers/csharp/upcoming/fundamentals/serialization/poco/#set-field-names) to do the same.

The `[BsonIgnoreExtraElements]` tells the driver to ignore other fields in the database like 'grades' so that it doesn't throw an error when deserializing such fields. You can read more about serialization using the MongoDB C# driver [here](https://www.mongodb.com/docs/drivers/csharp/current/fundamentals/serialization/poco/).

We need the `[BsonRepresentation(BsonType.ObjectId)]` attribute to tell the Driver that the Id is represented as a string in our model class but as an ObjectId in the database. 


Now we'll create a simple Controller that inherits from [ODataController](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnet.odata.odatacontroller) and has a Get action with the `[MongoEnableQuery]` attribute. This enables the use of URI query options in our API calls.

``` csharp 
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using MongoDB.AspNetCore.OData;
using MongoDB.Driver;
using ODataTutorial.Models;

namespace ODataTutorial.Controllers
{
    public class RestaurantsController : ODataController
    {
        private readonly IMongoCollection<Restaurant> _restaurants;

        public RestaurantsController(IMongoClient mongoClient)
        {
            var database = 
mongoClient.GetDatabase("sample_restaurants");
            _restaurants = database.GetCollection<Restaurant> 
("restaurants");
        }

        [MongoEnableQuery]
        public ActionResult Get()
        {
            return Ok(_restaurants.AsQueryable());
        }
    }
}
```

Our `Program.cs` file which is the entry point of this Web Application will be quite simple and we will use the `Startup.cs` file to connect to our database and set up the [Model Builder](https://learn.microsoft.com/en-us/odata/webapi/model-builder-abstract).

``` csharp
using ODataTutorial;

public class Program
{
    public static void Main(string[] args)
    {
        var app = CreateHostBuilder(args).Build();
        app.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder => 
            {
                webBuilder.UseStartup<Startup>();
            });
}
```
 
While setting up the modelBuilder we need to tell it to use our `Id` field as the primary key instead of the `RestaurantID` and this is done through `model.HasKey(e => e.Id)` 

``` csharp
using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;
using MongoDB.Driver;
using ODataTutorial.Models;

namespace ODataTutorial
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services) 
        {
            var connectionString = Configuration.GetSection("MongoDB").GetValue<string>("Uri");
            if (string.IsNullOrEmpty(connectionString))
            {
                throw new InvalidOperationException("Cannot read MongoDB connection settings");
            }

            services.AddSingleton<IMongoClient>(new MongoClient(connectionString));

            var modelBuilder = new ODataConventionModelBuilder();
            var model = modelBuilder.EntitySet<Restaurant>("Restaurants").EntityType;
            model.HasKey(e => e.Id);

            services.AddControllers().AddOData(
            options =>
            {
                options.Select().Filter().OrderBy().Expand().Count().SetMaxTop(1000).AddRouteComponents(
                    "odata",
                    modelBuilder.GetEdmModel());
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

```

Lastly, we need to make sure we add our MongoDB Atlas connection URL in our `appsettings.json` file as seen below

``` json
  "MongoDB": {
    "Uri": "<Enter MongoDB Atlas Connection URL here>"
  },
```

Voila! That's it, we are now ready to test our OData endpoints with MongoDB Atlas. 

# Testing 
Once you hit Build and let our application run, you should see it fire up in your chosen browser. We are using a browser in this example but we could use other tools like Postman to test the same. 

The default endpoint based on the port configured (E.g `http://localhost:5279/` in my project. Please note that this port will be randomly generated when creating a new project) will not show anything since we don't have it configured. However once you change it to `http://localhost:5279/odata/`, you should see a response like this 

``` json
{
  "@odata.context": "http://localhost:5279/odata/$metadata",
  "value": [
    {
      "name": "Restaurants",
      "kind": "EntitySet",
      "url": "Restaurants"
    }
  ]
} 
```

You can also see the metadata of our data model by going to `http://localhost:5279/odata/$metadata` Note that our `Id` field is shown as the Key as intended. 

``` XML
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0">
<edmx:DataServices>
  <Schema
    xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="ODataTutorial.Models">
    <EntityType Name="Restaurant">
      <Key>
        <PropertyRef Name="Id"/>
      </Key>
      <Property Name="Id" Type="Edm.String" Nullable="false"/>
      <Property Name="Name" Type="Edm.String" Nullable="false"/>
      <Property Name="RestaurantId" Type="Edm.String"/>
      <Property Name="Cuisine" Type="Edm.String" Nullable="false"/>
      <Property Name="Address" Type="ODataTutorial.Models.Address" Nullable="false"/>
      <Property Name="Borough" Type="Edm.String" Nullable="false"/>
    </EntityType>
    <ComplexType Name="Address">
      <Property Name="Building" Type="Edm.String" Nullable="false"/>
      <Property Name="Coordinates" Type="Collection(Edm.Double)" Nullable="false"/>
      <Property Name="Street" Type="Edm.String" Nullable="false"/>
      <Property Name="ZipCode" Type="Edm.String" Nullable="false"/>
    </ComplexType>
  </Schema>
  <Schema
    xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default">
    <EntityContainer Name="Container">
      <EntitySet Name="Restaurants" EntityType="ODataTutorial.Models.Restaurant"/>
    </EntityContainer>
  </Schema>
</edmx:DataServices>
</edmx:Edmx>
```

Now let's try to fetch data with some queries. We'll only see the first 1000 restaurants since we had `SetMaxTop(1000)` in our `Startup.cs` file. You can change this as needed. If we go to the endpoint `http://localhost:5279/odata/Restaurants`, we should be able to see our list of Restaurants.

``` json
{
"@odata.context": "http://localhost:5279/odata/$metadata#Restaurants",
"value": [
  {
    "Id": "5eb3d668b31de5d588f42930",
    "Name": "Brunos On The Boulevard",
    "RestaurantId": "40356151",
    "Cuisine": "American",
    "Borough": "Queens",
    "Address": {
      "Building": "8825",
      "Coordinates": [ -73.8803827, 40.7643124 ],
      "Street": "Astoria Boulevard",
      "ZipCode": "11369"
    }
  },
  {
    "Id": "5eb3d668b31de5d588f42932",
    "Name": "Taste The Tropics Ice Cream",
    "RestaurantId": "40356731",
    "Cuisine": "Ice Cream, Gelato, Yogurt, Ices",
    "Borough": "Brooklyn",
    "Address": {
      "Building": "1839",
      "Coordinates": [ -73.9482609, 40.6408271 ],
      "Street": "Nostrand Avenue",
      "ZipCode": "11226"
    }
  },
  {
    "Id": "5eb3d668b31de5d588f42934",
    "Name": "C & C Catering Service",
    "RestaurantId": "40357437",
    "Cuisine": "American",
    "Borough": "Brooklyn",
    "Address": {
      "Building": "7715",
      "Coordinates": [ -73.9973325, 40.6117489 ],
      "Street": "18 Avenue",
      "ZipCode": "11214"
    }
  },
  .
  .
  .
}
```

Let's try to play around with some filters. This will allow us to query data without any additional logic in our application. We can search for all Italian restaurants using `http://localhost:5279/odata/Restaurants?$filter=Cuisine eq 'Italian'` or even better, we can search for all Italian restaurants in Queens by `http://localhost:5279/odata/Restaurants?$filter=Cuisine eq 'Italian' and Borough eq 'Queens'` This should give us all the restaurants satisfying that query filter.
 
``` json
{
"@odata.context": "http://localhost:5279/odata/$metadata#Restaurants",
"value": [
  {
    "Id": "5eb3d668b31de5d588f429ed",
    "Name": "Piccola Venezia",
    "RestaurantId": "40367540",
    "Cuisine": "Italian",
    "Borough": "Queens",
    "Address": {
      "Building": "42-01",
      "Coordinates": [ -73.911784, 40.764766 ],
      "Street": "28 Avenue",
      "ZipCode": "11103"
    }
  },
  {
    "Id": "5eb3d668b31de5d588f429b8",
    "Name": "Don Peppe",
    "RestaurantId": "40366230",
    "Cuisine": "Italian",
    "Borough": "Queens",
    "Address": {
      "Building": "13558",
      "Coordinates": [ -73.8216767, 40.6689548 ],
      "Street": "Lefferts Boulevard",
      "ZipCode": "11420"
    }
  },
  {
    "Id": "5eb3d668b31de5d588f429cc",
    "Name": "Cara Mia",
    "RestaurantId": "40366812",
    "Cuisine": "Italian",
    "Borough": "Queens",
    "Address": {
      "Building": "220-20",
      "Coordinates": [ -73.7429218, 40.7305714 ],
      "Street": "Hillside Avenue",
      "ZipCode": "11427"
    }
  },
  {
    "Id": "5eb3d668b31de5d588f42a7e",
    "Name": "Aunt Bella'S Rest Of Little Neck",
    "RestaurantId": "40371807",
    "Cuisine": "Italian",
    "Borough": "Queens",
    "Address": {
      "Building": "4619",
      "Coordinates": [ -73.7363139, 40.767005 ],
      "Street": "Marathon Parkway",
      "ZipCode": "11362"
    }
  },
  .
  .
  .
}
```
 
If we only want to see selected fields, we can use the select filter as follows `http://localhost:5279/odata/Restaurants?$select=Name`

``` json
{
"@odata.context": "http://localhost:5279/odata/$metadata#Restaurants(Name)",
"value": [
  {
    "Name": "Brunos On The Boulevard"
  },
  {
    "Name": "Taste The Tropics Ice Cream"
  },
  {
    "Name": "C & C Catering Service"
  },
  {
    "Name": "Carvel Ice Cream"
  },
  .
  .
  .
} 
```

# Conclusion 

The full list of queries supported by OData can be seen [here](https://www.odata.org/getting-started/basic-tutorial/). For more query options, you can refer [here](https://learn.microsoft.com/en-us/odata/concepts/queryoptions-overview).

The OData protocol makes it simple to power your REST APIs but it may have some disadvantages like lack of control over what user's are trying to access and difficulties in optimizing specific queries which should be considered before deciding whether that suits your architectural needs.

We were able to quickly get up and running with a project that uses the [MongoDB extension for OData](https://www.nuget.org/packages/MongoDB.AspNetCore.OData/) with MongoDB Atlas. You can use the OData provider too to power you REST based applications. 

**Are you using odata with MongoDB? We want to hear from you! please look me up on linkedin and reach out!**

_This article contributed by [Rishit Bhatia](https://www.linkedin.com/in/rishit-bhatia-65ba5437/) and [Rachelle Palmer](https://www.linkedin.com/in/rachellepalmer/) from MongoDB_