For many reasons—including scalability, flexibility, and modernization—more and more development teams are looking to migrate away from PostgreSQL to MongoDB. In the past, these teams might have been hesitant if they used Entity Framework Core (EF Core) as their object relational mapper (ORM).
With EF Core being one of the most popular ORMs for .NET development, we received a lot of requests from the community for a provider, and so did Microsoft! So it made sense for MongoDB to develop a provider for EF Core. This went generally available (GA) in May 2024 and has been greatly received by the community.
Plus, developers can now use MongoDB's Relational Migrator, a free tool that uses smart algorithms and GenAI to streamline migrating from legacy relational databases to MongoDB, accelerating application modernization initiatives. So now is a great time to take a look at how you too can reap the benefits of all this by migrating your .NET application that uses PostgreSQL with EF Core, to use the MongoDB EF Core Provider.
In this tutorial, we will start off with a simple application that uses PostgreSQL as the database under the hood, and update it to still take advantage of EF Core but use MongoDB instead. So let's get started!
Preparing the data
For this tutorial, we won’t be going through the data migration process as we already have content on that with our getting started documentation and even a short but great demo video.
However, Relational Migrator was used to migrate the content, even making sure to take advantage of the benefits of the MongoDB data model, such as embedding documents.
In the original PostgreSQL database, there are seven tables:
- movies
- actors
- theaters
- users
- comments
- movie_cast: This table contains two foreign keys, movie_id and actor_id, to show the actors in a film.
- movie_genres: This table contains one foreign key, movie_id, and a column for genre. This is to show what genres belong to which film in the movies table.
Here at MongoDB, we have a mantra to help you understand how to think about your data from an application usage perspective: “Data that is accessed together should be stored together.”
So when connecting Relational Migrator to my PostgreSQL database, I selected the option to follow recommendations on how to model the data in collections for MongoDB.
This means we have gone from seven tables to four collections, as some fields have been embedded.
The four collections it suggested are:
- Movies: This contains all the movie information and embeds the comments as an array of comment documents inside that document, the same for movie_casts and movie_genre.
- Actors.
- Theaters.
- Users.
Note: It is highly recommended that you refine any schemas within Relational Migrator to optimize your data model to suit your needs before migrating. The recommendations you can select within Relational Migrator are a fantastic start but not perfect.
However, you will need to have the data available in your own MongoDB cluster that will be used in the tutorial.
You can find the data stored as a ZIP file on Azure that you will need to download, unzip, and then restore to your own cluster (M0 is sufficient) with mongorestore.
Once you have mongorestore on your machine, you can run the following command from the terminal from within the folder where you extracted the ZIP file.
mongorestore <your connection string> MigratedMflix/
The MigratedMflix folder will contain the database and collections within that database that contain all the documents we will make use of in this tutorial.
Prerequisites
In order to follow along with this tutorial, you will need a few things in place:
- An M0 or above cluster with the data loaded, as mentioned in the previous section
- The GitHub repo forked and cloned to your machine with the
with-postgresql
branch checked out - The connection string updated with your own value inappsettings.json
andappsettings.Development.json
"ConnectionStrings": {
"MongoDBAtlasConnectionString": "mongodb+srv://<username>:<password>@<your cluster url>/migrated_mflix"
}
- .NET 9 SDK installed IDE/editor of choice
If you choose to use VS Code, ensure you have the REST Client extension installed as we will use this later to test the endpoints.
Note: If you want to see the finished result, there is a branch on the repo called with-mongodb
Updating the NuGet package
The first thing we want to do in our code is update the NuGet packages from using Npgsql.EntityFrameworkCore.PostgreSQL
to using MongoDB.EntityFrameworkCore
.
You can do this either from the NuGet package manager inside your IDE of choice, or from the terminal with the .NET CLI:
dotnet remove package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package MongoDB.EntityFrameworkCore
This will of course lead to errors in the code because we removed the package, but we are going to update the code in the next section so it's fine to see errors for now.
Updating the models
The fields in our migrated documents don’t map directly to the columns in the tables in PostgreSQL. Plus, we don’t have as many collections compared to the number of tables in the relational database. So for this reason, the first step we are going to take is deleting model classes that are no longer required, and updating the existing ones to use MongoDB instead.
Delete old models
As mentioned, we don’t need some of the classes anymore as they are not collections in our new database, but instead embedded in the collections where they are accessed. So go ahead and delete the following files:
- Comment.cs
- MovieCast.cs
- MovieGenre.cs
Updating existing models
Now, we can go ahead and update the remaining models to use the properties we care about now and add the MongoDB related attributes, available from the provider, that tell it how to work with our data.
-
Actor.cs
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.EntityFrameworkCore;
namespace MigratedMoviesEFCore.Models;
[Collection("actors")]
public class Actor
{
[BsonId]
[BsonElement("_id")]
public ObjectId Id { get; set; }
[BsonElement("name")]
public required string Name { get; set; }
[BsonElement("dateOfBirth")]
public required DateTime DateOfBirth { get; set; }
[BsonElement("placeOfBirth")]
public required string PlaceOfBirth { get; set; }
}
-
Movie.cs
using System.ComponentModel.DataAnnotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.EntityFrameworkCore;
namespace MigratedMoviesEFCore.Models;
[Collection("movies")]
public class Movie
{
[BsonId] [BsonElement("_id")] public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
[BsonElement("title")]
public required string Title { get; set; }
[BsonElement("year")]
public required int Year { get; set; }
[BsonElement("runtime")]
public required int Runtime { get; set; }
[BsonElement("plot")]
public required string Plot { get; set; }
[BsonElement("fullplot")]
public string FullPlot { get; set; }
[BsonElement("released")]
public required DateTime Released { get; set; }
[BsonElement("rated")]
public required string Rated { get; set; }
[BsonElement("poster")]
public required string Poster { get; set; }
[BsonElement("comments")]
public List<Comment>? Comments { get; set; } = new List<Comment>();
}
public class Comment
{
[BsonElement("text")]
public required string Text { get; set; }
[BsonElement("commentDate")]
public required DateTime CommentDate { get; set; }
}
-
Theater.cs
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.EntityFrameworkCore;
namespace MigratedMoviesEFCore.Models;
[Collection("theaters")]
public class Theater
{
[BsonId]
[BsonElement("_id")]
public ObjectId Id { get; set; }
[BsonElement("theaterName")]
public required string TheaterName { get; set; }
[BsonElement("street1")]
public string? Street1 { get; set; }
[BsonElement("street2")]
public string? Street2 { get; set; }
[BsonElement("city")]
public string? City { get; set; }
[BsonElement("state")]
public string? State { get; set; }
[BsonElement("zipcode")]
public string? Zipcode { get; set; }
[BsonElement("location_lat")]
public decimal? LocationLat { get; set; }
[BsonElement("location_lon")]
public decimal? LocationLon { get; set; }
}
-
User.cs
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.EntityFrameworkCore;
namespace MigratedMoviesEFCore.Models;
[Collection("users")]
public class User
{
[BsonId]
[BsonElement("_id")]
public ObjectId Id { get; set; }
[BsonElement("name")]
public string? Name { get; set; }
[BsonElement("email")]
public string? Email { get; set; }
[BsonElement("password")]
public string? Password { get; set; }
[BsonElement("created_at")]
public DateTime? CreatedAt { get; set; }
}
Updating DbContext
Next, we need to update our MoviesDbContext.cs
class inside the Services folder so it no longer holds DbSet
properties for our deleted classes, as well as update the overridden method for our new data model. As you will see, not much is going to change as it is still going to use the EF Core paradigms and interaction patterns that you are used to.
Delete the DbSet property entries for the following:
- Comments
- MovieCasts
- MovieGenres
The next thing we need to do is update the code for OnModelCreating
.
This is to handle instances where the comments field is missing on a document. Going forward, if a document is created without passing a comments array, it will create an empty one. But this may not be the case in migrated data. You may also experience this in the future with error handling arrays in your own migrated data so it is a best practice worth learning.
protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.Entity<Movie>().HasMany(m => m.Comments).WithOne().IsRequired(false);}
Updating MovieService
Next, we need to remove the redundant methods from our IMovieService.cs
interface and the corresponding Movie.cs
class and make some adjustments to the data type for the id from int to string and add some return statements.
Update your IMovieService.cs
class to contain the following:
public interface IMovieService
{
IEnumerable<Movie> GetAllMovies();
Movie GetMovieById(string id);
void UpdateMovie(string movieId, Movie movie);
void DeleteMovie(Movie movieToDelete);
string AddMovie(Movie movie);
IEnumerable<Actor> GetAllActors();
Actor GetActorById(string id);
void UpdateActor(string id, Actor actor);
void DeleteActor(Actor actorToDelete);
string AddActor(Actor actor);
IEnumerable<Theater> GetAllTheaters();
IEnumerable<User> GetAllUsers();
}
We then need to make some changes to existing methods within MovieServices.cs
.
-
GetAllMovies
- Change the m.MovieId in the OrderBy call to bem.Title
instead. -
GetMovieById
andGetActorById
- Change the passed in parameter from int to string. We also need to parse from string to ObjectId (a special data type in MongoDB for the unique _id field in all documents.
public Movie GetMovieById(string id)
{
return _moviesDbContext.Movies.Find(ObjectId.Parse(id));
}
Make sure you have
using MongoDB.Bson;
added at the top if your editor doesn’t automatically add it.
public Actor GetActorById(string id)
{
return _moviesDbContext.Actors.Find(ObjectId.Parse(id));
}
-
AddMovie
andAddActor
: We want to change the return type from void to string and return the id of the movie and actors we just added, respectively.
public string AddMovie(Movie movie)
{
_moviesDbContext.Movies.Add(movie);
// Outputting for debugging purposes
_moviesDbContext.ChangeTracker.DetectChanges();
Console.WriteLine(_moviesDbContext.ChangeTracker.DebugView.LongView);
_moviesDbContext.SaveChanges();
return movie.Id.ToString();
}
public string AddActor(Actor actor)
{
_moviesDbContext.Actors.Add(actor);
// Outputting for debugging purposes
_moviesDbContext.ChangeTracker.DetectChanges();
Console.WriteLine(_moviesDbContext.ChangeTracker.DebugView.LongView);
_moviesDbContext.SaveChanges();
return actor.Id.ToString();
}
-
UpdateMovie
andUpdateActor
now take an additional parameter for the id and the contents of each method have been changed slightly:
public void UpdateMovie(string movieId, Movie movie)
{
var movieToUpdate = _moviesDbContext.Movies.FirstOrDefault((m => m.Id == ObjectId.Parse(movieId)));
_moviesDbContext.Movies.Update(movieToUpdate);
_moviesDbContext.ChangeTracker.DetectChanges();
Console.WriteLine(_moviesDbContext.ChangeTracker.DebugView.LongView);
_moviesDbContext.SaveChanges();
}
public void UpdateActor(string actorId, Actor actor)
{
var actorToUpdate = _moviesDbContext.Actors.FirstOrDefault(a => a.Id == ObjectId.Parse(actorId));
_moviesDbContext.Actors.Update(actorToUpdate);
// Outputting for debugging purposes
_moviesDbContext.ChangeTracker.DetectChanges();
Console.WriteLine(_moviesDbContext.ChangeTracker.DebugView.LongView);
_moviesDbContext.SaveChanges();
}
Finally, if you haven’t already, make sure to delete the methods we removed from the interface for comments, moviecasts, and moviegenres.
Updating Program.cs
Phew! We are almost there. The last couple of steps involve updates in the Program.cs
file. First, we need to change the code that configures EF Core to use MongoDB instead of PostgreSQL.
builder.Services.AddDbContext<MoviesDbContext>(options =>
options.UseMongoDB(builder.Configuration.GetConnectionString("MongoDBAtlasConnectionString"), "migrated_mflix").EnableSensitiveDataLogging());
This project uses the Minimal API’s feature, so all our endpoints are defined within Program.cs
. The final step we are going to take is to update our endpoints to remove the ones we no longer need and update some existing ones that currently have errors.
For simplicity, I will paste the final code here and then explain what has changed.
using Microsoft.EntityFrameworkCore;
using MigratedMoviesEFCore.Models;
using MigratedMoviesEFCore.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddOpenApi();
builder.Services.AddDbContext<MoviesDbContext>(options =>
options.UseMongoDB(builder.Configuration.GetConnectionString("MongoDBAtlasConnectionString"), "migrated_mflix")
.EnableSensitiveDataLogging());
builder.Services.AddScoped<IMovieService, MovieService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
#region movies
app.MapGet("/movies", (IMovieService movieService) =>
{
return movieService.GetAllMovies();
})
.WithName("GetMovies");
app.MapGet("/movies/{id}", (IMovieService movieService, string id) =>
{
return movieService.GetMovieById(id);
}).WithName("GetMovieById");
app.MapPost("/movies", (IMovieService movieService, Movie movie) =>
{
string newId = movieService.AddMovie(movie);
return newId;
}).WithName("AddMovie");
app.MapPut("/movies/{id}", (IMovieService movieService, string id, Movie movieToUpdate) =>
{
movieService.UpdateMovie(id, movieToUpdate);
}).WithName("UpdateMovie");
app.MapDelete("/movies/{id}", (IMovieService movieService, string id) =>
{
var movieToDelete = movieService.GetMovieById(id);
movieService.DeleteMovie(movieToDelete);
}).WithName("DeleteMovie");
#endregion
#region actors
app.MapGet("/actors", (IMovieService movieService) =>
{
return movieService.GetAllActors();
}).WithName("GetActors");
app.MapGet("/actors/{id}", (IMovieService movieService, string id) =>
{
return movieService.GetActorById(id);
}).WithName("GetActorById");
app.MapPost("/actors", (IMovieService movieService, Actor actor) =>
{
string newId = movieService.AddActor(actor);
return newId;
}).WithName("AddActor");
app.MapPut("/actors/{id}", (IMovieService movieService, string id, Actor actorToUpdate) =>
{
movieService.UpdateActor(id, actorToUpdate);
}).WithName("UpdateActor");
app.MapDelete("/actors/{id}", (IMovieService movieService, string id) =>
{
var actorToDelete = movieService.GetActorById(id);
movieService.DeleteActor(actorToDelete);
}).WithName("DeleteActor");
#endregion
app.MapGet("/theaters", (IMovieService movieService) =>
{
return movieService.GetAllTheaters();
}).WithName("GetTheaters");
app.MapGet("/users", (IMovieService movieService) =>
{
return movieService.GetAllUsers();
}).WithName("GetUsers");
app.Run();
- Updated the query parameter type from int to string in the endpoint for /movies/id and /actors/id
- Removed the parsing to Int32 of the string id in the DELETE endpoints
- Removed the endpoints that are no longer applicable
Just like that, we are ready to test it!
Testing with .http file
A feature that has been around for a few years in .NET, but that I always forget about, is .http files (shout-out to our amazing Champion Kev Smith who taught me they existed). These files act as a way to test your own APIs, or even endpoints on someone else’s API, without even needing to leave your IDE or VS Code (with an extension).
Note: If you would rather use Swagger, you can run the application in debug mode and access /swagger.
However, for this tutorial, we are going to test our endpoints by writing calls in the MigratedMoviesEFCore.http
file in the root of the project.
Make sure to update appsettings.Development.json
and appsettings.json
with your connection string.
"ConnectionStrings": {
"MongoDBAtlasConnectionString": "<Your Atlas URL>/migrated_mflix"
}
From the terminal, run your application by calling dotnet run
as the application needs to be running and available to test the endpoints.
Testing /movies
Inside the .http file, replace the existing GET with the following code to test the endpoint to get all movies:
GET {{MigratedMoviesEFCore_HostAddress}}/movies
Accept: application/json
###
The existence of this declaration, plus the three # symbols, will tell it that this is a complete test and you will see a play button appear, or perhaps you will see ‘Send Request’ appear above it, depending on your IDE/editor of choice.
Send the request and you should get back a list of all the available movies.
Next, we are going to create a new movie. This will look very similar to GET but instead uses POST and passes a JSON object to use in the request.
POST {{MigratedMoviesEFCore_HostAddress}}/movies
Content-Type: application/json
{
"title": "The Shawshank Redemption",
"year": 1994,
"released": "1994-09-23T00:00:00Z",
"rated": "R",
"runtime": 142,
"plot": "Two imprisoned men plot an escape",
"fullplot": "Andy Dufresne is sentenced to life in prison...",
"poster": "https://www.imdb.com/title/tt0111161/mediaviewer/rm10105600.jpg"
}
###
If you run that, you will see in the response that there is an id value returned. Copy that to clipboard as we will use that to test getting a movie by its Id.
GET {{MigratedMoviesEFCore_HostAddress}}/movies/<your copied movie id>
###
Make sure to update the id with the value that you copied. This will return a document containing the data of the movie you added in the previous step.
We can create and read the document, so now, let’s test updating it:
PUT {{MigratedMoviesEFCore_HostAddress}}/movies/<your copied id>
Content-Type: application/json
{
"title": "The Shawshank Redemption",
"year": 1994,
"released": "1994-09-23T00:00:00Z",
"rated": "18",
"runtime": 142,
"plot": "Two imprisoned men plot an escape",
"fullplot": "Andy Dufresne is sentenced to life in prison...",
"poster": "https://www.imdb.com/title/tt0111161/mediaviewer/rm10105600.jpg"
}
###
Again, make sure to add your id value. This doesn't return anything but you should see a 200 OK value to show it updated.
You could always run the test for getting that specific movie again if you would like to see that your changes have been applied.
Finally, we have DELETE:
DELETE {{MigratedMoviesEFCore_HostAddress}}/movies/<your copied id>
###
Testing /actors
Now that we have tested the /movies endpoints, it’s time to move on to /actors. Like the /movies endpoint, we are going to write two GET tests, a POST, a PUT, and a DELETE.
GET {{MigratedMoviesEFCore_HostAddress}}/actors
###
POST {{MigratedMoviesEFCore_HostAddress}}/actors
Content-Type: application/json
{
"name": "Morgan Freeman",
"dateOfBirth": "1937-06-02",
"placeOfBirth": "Memphis, Tennessee, USA"
}
###
Copy the returned id of the new actor on insertion for use in the next tests.
GET {{MigratedMoviesEFCore_HostAddress}}/actors/<your copied actor document id>
###
PUT {{MigratedMoviesEFCore_HostAddress}}/actors/<your copied actor document id>
Content-Type: application/json
{
"name": "Leonardo DiCaprio",
"dateOfBirth": "1974-11-11T00:00:00.000Z",
"placeOfBirth": "Los Angeles, California, USA"
}
###
Lastly, we can finish off this endpoint by deleting the recently added document.
DELETE {{MigratedMoviesEFCore_HostAddress}}/actors/<your copied actor document id>
###
Testing /theaters and /users
We are going to combine the last two endpoints as these have been kept simple and only have a single GET endpoint for both to fetch all documents.
GET {{MigratedMoviesEFCore_HostAddress}}/theaters
###
GET {{MigratedMoviesEFCore_HostAddress}}/users
###
Summary
Amazing! We have been able to update from using PostgreSQL as the database under the hood, to the flexible, scalable, and modern solution of MongoDB, while still enjoying the paradigms from EF Core that we are used to.
You can find the final full code to this on GitHub.
Give it a try today and see how easy it is to migrate over, with the great partnership of the Relational Migrator and our EF Core Provider. If you have any questions or want to share how you got on, you can visit our Community Forums.
Top comments (0)