October 21st, 2024

MongoDB EF Core Provider: What’s New?

This is a guest post by Rishit Bhatia and Luce Carter. Rishit is a Senior Product Manager at MongoDB focusing on the .NET Developer Experience and has been working with C# since many years hands on before moving into Product Management. Luce is a Developer Advocate at MongoDB, Microsoft MVP and lover of code, sunshine and learning. This blog was reviewed by the Microsoft .NET team for EF Core.

The EF Core provider for MongoDB went GA in May 2024. We’ve come a long way since we initially released this package in preview six months ago. We wanted to share some interesting features that we’ve been working on which would not have been possible without the support of and collaboration with Microsoft’s .NET Data and Entity Framework team.

In this post, we will be using the MongoDB EF Core provider with MongoDB Atlas to showcase the following:

  • Adding a property to an entity and change tracking
  • Leveraging the escape hatch to create an index
  • Performing complex queries
  • Transactions and optimistic concurrency

The code related to this blog can be found on Github. The boilerplate code to get started is in the “start” branch. The full code with all the feature highlights mentioned below is in the “main” branch.

Prerequisites

We will be using a sample dataset — specifically, the movies collection from the sample_mflix database available for MongoDB Atlas in this example. To set up an Atlas cluster with sample data, you can follow the steps in the docs. We’ll create a simple .NET Console App to get started with the MongoDB EF Core provider. For more details on how to do that, you can check the quickstart guide.

At this point, you should be connected to Atlas and able to output the movie plot from the movie being read in the quickstart guide.

Features highlight

Adding properties and change tracking

One of the advantages of MongoDB’s document model is that it supports a flexible schema. This, coupled with EF Core’s ability to support a Code First approach, lets you add properties to your entities on the fly. To show this, we are going to add a new nullable boolean property called adapted_from_book to our model class. This will make our model class as seen below:

public class Movie
{
    public ObjectId Id { get; set; }

    [BsonElement("title")]
    public string Title { get; set; }

    [BsonElement("rated")]
    public string Rated { get; set; }

    [BsonElement("plot")]
    public string Plot { get; set; }

    [BsonElement("adaptedFromBook")]
    public bool? AdaptedFromBook { get; set; }
}

Now, we are going to set this newly added property for the movie entity we found and see EF Core’s Change Tracking in action after we save our changes. To do so, we’ll add the following lines of code after printing the movie plot:

movie.AdaptedFromBook = false;
await db.SaveChangesAsync();

Before we run our program, let’s go to our collection in Atlas and find this movie to make sure that this newly created field adapted_from_book does not exist in our database. To do so, simply go to your cluster in the Atlas Web UI and select Browse Collections.

Browse Collections button showing in Atlas UI

Then, choose the movies collection from the sample_mflix database. In the filter tab, we can find our movie using the query below:

{title: "Back to the Future"}

This should find our movie and we can confirm that the new field we intend to add is indeed not seen.

Example movie document

Next, let’s add a breakpoint to the two new lines we just added to make sure that we can track the changes live as we proceed. Select the Start Debugging button to run the app. When the first breakpoint is hit, we can see that the local field value has been assigned.

Contents of local movie field viewed from debugger

Let’s hit Continue and check the document in the database. We can see that the new field has not yet been added. Let’s step over the save changes call which will end the program. At this point, if we check our document in the database, we’ll notice that the new field has been added as seen below!

Previous document example with new field adapted from book added

Index management

The MongoDB EF Core provider is built on top of the existing .NET/C# driver. One advantage of this architecture is that we can reuse the MongoClient already created for the DbContext to leverage other capabilities exposed by MongoDB’s developer data platform. This includes but is not limited to features such as Index Management, Atlas Search, and Vector Search.

We’ll see how we can create a new index using the driver in this same application. First, we’ll list the indexes in our collection to see which indexes already exist. MongoDB creates an index on the _id field by default. We’re going to create a helper function to print the indexes:

var moviesCollection = client.GetDatabase("sample_mflix").GetCollection<Movie>("movies");
Console.WriteLine("Before creating a new Index:");
PrintIndexes();

void PrintIndexes()
{
    var indexes = moviesCollection.Indexes.List();
    foreach (var index in indexes.ToList())
    {
        Console.WriteLine(index);
    }
}

The expected output is as seen below:

{ "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }

Now, we’ll create a compound index on the title and rated fields in our collection and print the indexes again.

var moviesIndex = new CreateIndexModel<Movie>(Builders<Movie>.IndexKeys
    .Ascending(m => m.Title)
    .Ascending(x => x.Rated));
await moviesCollection.Indexes.CreateOneAsync(moviesIndex);

Console.WriteLine("After creating a new Index:");
PrintIndexes();

We can see that a new index with the name title_1_rated_1 has been created.

After creating a new Index:
{ "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" }
{ "v" : 2, "key" : { "title" : 1, "rated" : 1 }, "name" : "title_1_rated_1" }

Querying data

Since EF Core already supports Language Integrated Query (LINQ) Syntax, it becomes easy to write strongly typed queries in C#. Based on the fields available in our model classes, we can try to find some interesting movies from our collection. Let’s say I wanted to find all movies that are rated “PG-13” with their plot containing the word “shark” but I wanted them ordered by their title field. I can do so easily with the following query:

var myMovies = await db.Movies
    .Where(m => m.Rated == "PG-13" && m.Plot.Contains("shark"))
    .OrderBy(m => m.Title)
    .ToListAsync();

foreach (var m in myMovies)
{
    Console.WriteLine(m.Title);
}

We can then print out the queries using the code above and run the program using dotnet run to see the results. We should be able to see two movie names from the 20K+ movies in our collection printed in the console as seen below.

Jaws: The Revenge
Shark Night 3D

If you would like to see the query that is sent to the server, which in this case is the MQL, then you can enable logging in the Create function on the DbContext as seen below:

   public static MflixDbContext Create(IMongoDatabase database) =>
       new(new DbContextOptionsBuilder<MflixDbContext>()
           .UseMongoDB(database.Client, database.DatabaseNamespace.DatabaseName)
           .LogTo(Console.WriteLine)
           .EnableSensitiveDataLogging()
           .Options);

This way we can see the following as a part of our detailed logs when we run the program again:

Executed MQL query
sample_mflix.movies.aggregate([{ "$match" : { "rated" : "PG-13", "plot" : /shark/s } }, { "$sort" : { "title" : 1 } }])

Autotransactions and optimistic concurrency

Yes, you read that right! The MongoDB EF Core provider from its 8.1.0 release supports transactions and optimistic concurrency. What this means is that by default, SaveChanges and SaveChangesAsync are transactional. This will empower automatic rollback of operations in production grade workloads in case of any failures and ensure that all operations are fulfilled with optimistic concurrency.

If you want to turn off transactions, you can do so during the initialization phase before calling any SaveChanges operation.

db.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;

The provider supports two methods of optimistic concurrency depending on your requirements which are through a concurrency check or row versions. You can read more about it in the docs. We’ll be using the RowVersion to demonstrate this use case. This leverages the Version field in our model class which will be updated automatically by the MongoDB EF Provider. To add the version, we add the following to our model class.

 [Timestamp]
 public long? Version { get; set; }

First, let’s create a new movie entity called myMovie as seen below and add it to the DbSet, followed by SaveChangesAsync.

Movie myMovie1= new Movie {
    Title = "The Rise of EF Core 1",
    Plot = "Entity Framework (EF) Core is a lightweight, extensible, open source and cross-platform version of the popular Entity Framework data access technology.",
    Rated = "G"
};

db.Movies.Add(myMovie1);
await db.SaveChangesAsync();

Now, let’s create a new DbContext similar to the one we created above. We can move the database creation into a variable so we don’t have to define the name of the database again. With this new context, let’s add a sequel for our movie and add it to the DbSet. We’ll also add a third part (yes, it’s a trilogy) but use the same ID as our second movie entity to this new context and then save our changes.

var dbContext2 = MflixDbContext.Create(database);
dbContext2.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never;
var myMovie2 = new Movie { title = "The Rise of EF Core 2" };
dbContext2.Movies.Add(myMovie2);

var myMovie3 = new Movie { Id = myMovie2.Id,Title = "The Rise of EF Core 3" };
dbContext2.Movies.Add(myMovie3);
await dbContext2.SaveChangesAsync();

With transactions now being supported, the second set of operations for our latter two movie entities should not go through since we are trying to add them with an already existing _id. We should see an exception and the transaction should be rolled with only one movie being seen in our database. Let’s run and see if that is true.

We rightfully see an exception and we can confirm that we have only one movie (the first part) inserted into the database.

Exception being thrown by the transaction as the document being added has the same id as an existing one

The following shows only a single document in the database as the transaction was rolled back.

Only one document in the database as the transaction was rolled back

Don’t worry, we will correctly add our trilogy in the database. Let’s remove the _id assignment on our third entity and let MongoDB automatically insert it for us.

var myMovie3 = new Movie { Title = "The Rise of EF Core 3" };

Once we re-run the program, we can see that all our entities have been added to the database.

All three movies in the trilogy in the database due to fixing the duplicate id issue

Summary

We were able to use the MongoDB EF Core provider with MongoDB Atlas to showcase different capabilities like adding a property to an entity on the fly, leveraging the Escape Hatch to create an index, performing complex queries via LINQ, and demonstrating the newly added transactions and optimistic concurrency support.

Learn more

To learn more about EF Core and MongoDB:

0 comments