Using ASP.NET Core OData with MongoDB Atlas

techbelle

rachelle palmer

Posted on May 24, 2024

Using ASP.NET Core OData with MongoDB Atlas

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 which is a standard that allows developers to simplify the process of building and consuming RESTful APIs. MongoDB 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

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

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

Now we are going to add the MongoDB OData extension 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 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; }
    }
}


Enter fullscreen mode Exit fullscreen mode

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; }
    }
}


Enter fullscreen mode Exit fullscreen mode

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

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 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());
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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.


 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>();
            });
}


Enter fullscreen mode Exit fullscreen mode

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();
            });
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

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>"
  },


Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
} 


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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"
    }
  },
  .
  .
  .
}


Enter fullscreen mode Exit fullscreen mode

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"
    }
  },
  .
  .
  .
}


Enter fullscreen mode Exit fullscreen mode

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"
},
.
.
.
}
Enter fullscreen mode Exit fullscreen mode




Conclusion

The full list of queries supported by OData can be seen here. For more query options, you can refer here.

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 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 and Rachelle Palmer from MongoDB

💖 💪 🙅 🚩
techbelle
rachelle palmer

Posted on May 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related