Query Your Data With ASP.NET Core, OData, and the MongoDB Entity Framework Core Provider
Rate this tutorial
Among the good news of MongoDB.local NYC '24, was the announcement that MongoDB Entity Framework Core Provider has been made globally available. The provider integrates with Entity Framework and empowers .NET developers to easily access data in MongoDB databases with a well known framework.
This post shows how you can set up the Provider in a C# project and build an API with ASP.NET Core that offers an OData compatible endpoint. For those new to OData, this is a protocol that is used to build queryable REST APIs. User interfaces can access these APIs and use them to provide tabular views that offer a rich set of user interactions, like filtering, sorting or paging. As OData is an open standard, there is a wide variety of tools that can access OData compatible endpoints, e.g. Microsoft Excel or Microsoft Power BI.
If you want to follow along with the samples in this post, we create an endpoint that queries the movies in the sample_flix-database. The easiest way to get an instance of this sample database is to load the sample data in a MongoDB Atlas cluster. If you do not have a cluster already, you can set up a free MongoDB Atlas M0 cluster (see this link for a guide).
While this post will use a newly created ASP.NET Core Web API project, it is perfectly fine to add OData to offer the endpoints in addition to existing REST endpoints. When creating a new project, you might want to enable OpenAPI support for an easy way to test the API later on. Also, the new project should use controllers instead of a minimal API.
After setting up the ASP.NET Core project, we will first define the Entity Framework DbContext and then set it up with the MongoDB Entity Framework Core Provider.
The first step is to add the MongoDB Entity Framework Core Provider package to the project either through the UI or the
dotnet
CLI:1 dotnet add package MongoDB.EntityFrameworkCore
Then we define the entity class that contains the properties that we want to read from the database:
1 // requires using MongoDB.Bson; 2 public class Movie 3 { 4 public string Id { get; set; } = ObjectId.GenerateNewId().ToString(); 5 public string Title { get; set; } = string.Empty; 6 public string? Plot { get; set; } 7 public IEnumerable<string>? Genres { get; set; } 8 public IEnumerable<string>? Cast { get; set; } 9 // additional properties as required 10 }
Some of the properties (e.g.
Plot
) are nullable reference types. This asserts that missing values in the database do not result in an error when reading the documents.The next step is to set up the DbContext and add the configuration for the entity. This includes binding the entity to a collection and, as we are working with existing documents that follow a different naming convention, mapping the property names of the document to the ones of the .NET POCO:
1 // requires using Microsoft.EntityFrameworkCore; 2 // requires using MongoDB.Bson; 3 // requires using MongoDB.EntityFrameworkCore.Extensions; 4 5 public class MflixDbContext : DbContext 6 { 7 public MflixDbContext(DbContextOptions options) 8 : base(options) 9 { 10 } 11 12 public DbSet<Movie> Movies { get; init; } 13 14 protected override void OnModelCreating(ModelBuilder modelBuilder) 15 { 16 base.OnModelCreating(modelBuilder); 17 var movieEntity = modelBuilder.Entity<Movie>(); 18 movieEntity.ToCollection("movies"); 19 movieEntity.Property(x => x.Id) 20 .HasElementName("_id") 21 .HasConversion<ObjectId>(); 22 movieEntity.Property(x => x.Title).HasElementName("title"); 23 movieEntity.Property(x => x.Plot).HasElementName("plot"); 24 movieEntity.Property(x => x.Genres).HasElementName("genres"); 25 movieEntity.Property(x => x.Cast).HasElementName("cast"); 26 } 27 }
The
Id
property deserves special attention: in the MongoDB document, the _id
property is stored as an ObjectId. In an OData interface, using strings instead of an ObjectId
as the identifier value is much easier. By adding value conversion, we can have the best of both worlds: ObjectIds in MongoDB and strings in .NET.To finalize the set up of the MongoDB Entity Framework Core Provider, we register the client, database, and
MflixDbContext
in the IoC container:1 // requires using Microsoft.EntityFrameworkCore; 2 // requires using MongoDB.Driver; 3 // requires using MongoDBEFODataVerify; 4 5 builder.Services.AddSingleton<IMongoClient>(prov => 6 { 7 // Retrieve connection string from configuration 8 return new MongoClient("<your MongoDB connection string>"); 9 }); 10 builder.Services.AddSingleton(prov => 11 { 12 var client = prov.GetRequiredService<IMongoClient>(); 13 // Retrieve database name from configuration 14 return client.GetDatabase("sample_mflix"); 15 }); 16 builder.Services.AddDbContext<MflixDbContext>((prov, options) => 17 { 18 var database = prov.GetRequiredService<IMongoDatabase>(); 19 options.UseMongoDB( 20 database.Client, 21 database.DatabaseNamespace.DatabaseName); 22 });
If you are only using Entity Framework, it would be sufficient to register
MflixDbContext
and create the client in the factory method. The above sample code registers the client and database instance separately so that you can inject them independently if required.With the
DbContext
set up, we could also use controller scaffolding in order to generate the code for REST controllers if needed. However, for the sake of this post we will now create an OData endpoint from scratch.The first step when adding OData to a project is to reference the
Microsoft.AspNetCore.OData
package, either through the UI or using the dotnet
CLI:1 dotnet add package Microsoft.AspNetCore.OData
Next, we add a method that defines the EDM model for the OData interface in
Program.cs
:1 // requires using Microsoft.OData.Edm; 2 // requires using Microsoft.OData.ModelBuilder; 3 4 IEdmModel GetEdmModel() 5 { 6 var model = new ODataConventionModelBuilder(); 7 model.EnableLowerCamelCase(); 8 model.EntitySet<Movie>("Movies"); 9 return model.GetEdmModel(); 10 }
EnableLowerCamelCase
changes the naming convention of the properties to start with a lowercase character. This matches the behavior of JSON serialization in a standard REST API.We then register the OData components with the IoC container by adding the following code in
Program.cs
:1 // requires using Microsoft.OData; 2 3 // This should be already in the code 4 builder.Services 5 .AddControllers() 6 // This is new 7 .AddOData(options => options 8 .Select() 9 .Filter() 10 .OrderBy() 11 .Count() 12 .SetMaxTop(100) 13 .AddRouteComponents("odata", GetEdmModel()));
The code above enables various OData operations and restricts the maximum number of entities a client can request to prevent an overly large result set.
Lastly, we add a controller that provides the OData endpoint for our query. The name of the controller should be
MoviesController
to match the registration of the entity set in GetEdmModel
:1 // requires using Microsoft.AspNetCore.OData.Query; 2 // requires using Microsoft.AspNetCore.OData.Routing.Controllers; 3 4 public class MoviesController : ODataController 5 { 6 private readonly MflixDbContext _dbContext; 7 8 public MoviesController(MflixDbContext dbContext) 9 { 10 _dbContext = dbContext; 11 } 12 13 [ 14 15 ] 16 public IQueryable<Movie> Get() 17 { 18 return _dbContext.Movies; 19 } 20 }
We inject an instance of
MflixDbContext
into the controller to retrieve the movies and mark the Get
method with the EnableQuery
attribute. The PageSize
property allows us to set the maximum number of results sent to the client. Using AllowedOrderByProperties
restricts the properties that can be used in sort operations.The name must match the name that is serialized - lower camel case in our sample as we called
EnableLowerCamelCase
in GetEdmModel
earlier. There are various other options that you can use to adjust the behavior of the query endpoint. Please see the ASP.NET Core OData documentation for details.Now we are ready to test the OData endpoint and start it in the debugger. If you have enabled OpenAPI and Swagger UI in your project, you can use Swagger UI for a simple test. This is enough to check basic readiness of the endpoint and serves as a good starting point.
For more sophisticated requests, either enter the OData URL in a browser or use a tool like Postman or
curl
with OData URLs.A simple request to
https://localhost:\<YOUR PORT\>/odata/Movies
should return 10 movies in the following format:1 { 2 "@odata.context": "https://localhost:7104/odata/$metadata#Movies", 3 "value": [ 4 { 5 "id": "573a1390f29313caabcd4135", 6 "title": "Blacksmith Scene", 7 "plot": "Three men hammer on an anvil...", 8 "genres": [ 9 "Short" 10 ], 11 "cast": [ 12 "Charles Kayser", 13 "John Ott" 14 ] 15 }, 16 // 999 more results 17 ], 18 "@odata.nextLink": "https://localhost:7104/odata/Movies?$skip=10" 19 }
In order to make use of OData features like paging and filtering, you can also try the following requests:
- Filter by title:
https://localhost:\<YOUR PORT\>/odata/Movies?$filter=title eq 'The Godfather'
- Filter by part of title - case insensitive:
https://localhost:\<YOUR PORT\>/odata/Movies?$filter=contains(tolower(title), 'godfather')
- Paging with
$skip
,$top
and$count
:https://localhost:\<YOUR PORT\>/odata/Movies?$count=true&$top=10&$skip=10
- Sorting with
$orderBy
:https://localhost:\<YOUR PORT\>/odata/Movies?$orderBy=title ASC
What if you already have an API and are using MongoDB C# Driver without Entity Framework? Plain and simple, all you need for OData to work is an
IQueryable<T>
.This can be provided easily by
IMongoCollection<T>.AsQueryable()
so that we could also build the OData endpoint like this:1 // requires using Microsoft.AspNetCore.OData.Query; 2 // requires using Microsoft.AspNetCore.OData.Routing.Controllers; 3 // requires using MongoDB.Driver; 4 5 public class MoviesController : ODataController 6 { 7 private readonly IMongoCollection<Movie> _collMovies; 8 9 public MoviesController(IMongoDatabase db) 10 { 11 _collMovies = db.GetCollection<Movie>("movies"); 12 } 13 14 [ 15 16 ]17 public IQueryable<Movie> Get() 18 { 19 return _collMovies.AsQueryable(); 20 } 21 }
Please note that you also need to configure the mapping for the
Movie
class either by adding attributes or defining the class map imperatively.Up to now, we have built an OData endpoint that supports basic options for filtering, sorting and paging with the Microsoft.AspNetCore.OData package. Unfortunately, this package does not support
$select
and $expand
in your requests. If you want to go one step further and also add support for these operations, you need to rely on a package provided by MongoDB and reference MongoDB.AspNetCore.OData instead of the Microsoft package. All you need to do is replace EnableQuery
with MongoEnableQuery
and your endpoint will support these operations.1 // requires using Microsoft.AspNetCore.OData.Routing.Controllers; 2 // requires using MongoDB.AspNetCore.OData.Query; 3 // requires using MongoDB.Driver; 4 5 public class MoviesController : ODataController 6 { 7 private readonly IMongoCollection<Movie> _collMovies; 8 9 public MoviesController(IMongoDatabase db) 10 { 11 _collMovies = db.GetCollection<Movie>("movies"); 12 } 13 14 // Change this from EnableQuery to MongoEnableQuery 15 [ 16 17 ]18 public IQueryable<Movie> Get() 19 { 20 return _collMovies.AsQueryable(); 21 } 22 }
At the time of this writing, you need to follow the approach described in the section OData using MongoDB C# Driver above and cannot use a
DbSet<T>
provided by MongoDB Entity Framework Core Provider.In this post we showed how easy it is to add powerful query options to your API, with an OData endpoint that builds on Entity Framework and MongoDB. This API can be used to provide rich query options in the UI of your apps or can be accessed from popular tools like Microsoft Excel. As both frameworks are very powerful and offer a lot of options, be sure to check out the documentation on MongoDB Entity Framework Core Provider and ASP.NET Core OData.
Top Comments in Forums
There are no comments on this article yet.