MongoDB Provider for EF Core Tutorial: Building an App with CRUD and Change Tracking
Rate this tutorial
Entity Framework (EF) has been part of .NET for a long time (since .NET 3.51) and is a popular object relational mapper (ORM) for many applications. EF has evolved into EF Core alongside the evolution of .NET. EF Core supports a number of different database providers and can now be used with MongoDB with the help of the MongoDB Provider for Entity Framework Core.
In this tutorial, we will look at how you can build a car booking application using the new MongoDB Provider for EF Core that will support create, read, update, and delete operations (CRUD) as well as change tracking, which helps to automatically update the database and only the fields that have changed.
A car booking system is a good example to explore the benefits of using EF Core with MongoDB because there is a need to represent a diverse range of entities. There will be entities like cars with their associated availability status and location, and bookings including the associated car.
As the system evolves and grows, ensuring data consistency can become challenging. Additionally, as users interact with the system, partial updates to data entities — like booking details or car specifications — will happen more and more frequently. Capturing and efficiently handling these updates is paramount for good system performance and data integrity.
In order to follow along with this tutorial, you are going to need a few things:
- .NET 7.0.
- Basic knowledge of ASP.NET MVC and C#.
ASP.NET Core is a very flexible web framework, allowing you to scaffold out different types of web applications that have slight differences in terms of their UI or structure.
For this tutorial, we are going to create an MVC project that will make use of static files and controllers. There are other types of front end you could use, such as React, but MVC with .cshtml views is the most commonly used.
To create the project, we are going to use the .NET CLI:
1 dotnet new mvc -o SuperCarBookingSystem
Because we used the CLI, although easier, it only creates the csproj file and not the solution file which allows us to open it in Visual Studio, so we will fix that.
1 cd SuperCarBookingSystem 2 dotnet new sln 3 dotnet sln .\SuperCarBookingSystem.sln add .\SuperCarBookingSystem.csproj
Now that we have the new project created, we will want to go ahead and add the required NuGet packages. Either using the NuGet Package Manager or using the .NET CLI commands below, add the MongoDB MongoDB.EntityFrameworkCore and Microsoft.EntityFrameworkCore packages.
1 dotnet add package MongoDB.EntityFrameworkCore --version 7.0.0-preview.1 2 dotnet add package Microsoft.EntityFrameworkCore
At the time of writing, the MongoDB.EntityFrameworkCore is in preview, so if using the NuGet Package Manager UI inside Visual Studio, be sure to tick the “include pre-release” box or you won’t get any results when searching for it.
Before we can start implementing the new packages we just added, we need to create the models that represent the entities we want in our car booking system that will of course be stored in MongoDB Atlas as documents.
In the following subsections, we will create the following models:
- Car
- Booking
- MongoDBSettings
First, we need to create our car model that will represent the cars that are available to be booked in our system.
- Create a new class in the Models folder called Car.
- Add the following code:
1 using MongoDB.Bson; 2 using MongoDB.EntityFrameworkCore; 3 using System.ComponentModel.DataAnnotations; 4 5 6 namespace SuperCarBookingSystem.Models 7 { 8 [ ] 9 public class Car 10 { 11 12 public ObjectId Id { get; set; } 13 14 [ ]15 [ ]16 public string? Model { get; set; } 17 18 19 20 [ ]21 [ ]22 public string NumberPlate { get; set; } 23 24 25 [ ]26 public string? Location { get; set; } 27 28 29 public bool IsBooked { get; set; } = false; 30 } 31 }
The collection attribute before the class tells the application what collection inside the database we are using. This allows us to have differing names or capitalization between our class and our collection should we want to.
We also need to create a booking class to represent any bookings we take in our system.
- Create a new class inside the Models folder called Booking.
- Add the following code to it:
1 using MongoDB.Bson; 2 using MongoDB.EntityFrameworkCore; 3 using System.ComponentModel.DataAnnotations; 4 5 6 namespace SuperCarBookingSystem.Models 7 { 8 [ ]9 public class Booking 10 { 11 public ObjectId Id { get; set; } 12 13 14 public ObjectId CarId { get; set; } 15 16 17 public string CarModel { get; set; } 18 19 20 [ ]21 [ ]22 public DateTime StartDate { get; set; } 23 24 25 [ ]26 [ ]27 public DateTime EndDate { get; set; } 28 } 29 }
Although it won’t be a document in our database, we need a model class to store our MongoDB-related settings so they can be used across the application.
- Create another class in Models called MongoDBSettings.
- Add the following code:
1 public class MongoDBSettings 2 { 3 public string AtlasURI { get; set; } 4 public string DatabaseName { get; set; } 5 }
This is the exciting part. We are going to start to implement EF Core and take advantage of the new MongoDB Provider. If you are used to working with EF Core already, some of this will be familiar to you.
- In a location of your choice, create a class called CarBookingDbContext. I placed it inside a new folder called Services.
- Replace the code inside the namespace with the following:
1 using Microsoft.EntityFrameworkCore; 2 using SuperCarBookingSystem.Models; 3 4 namespace SuperCarBookingSystem.Services 5 { 6 public class CarBookingDbContext : DbContext 7 { 8 public DbSet<Car> Cars { get; init; } 9 10 11 public DbSet<Booking> Bookings { get; init; } 12 13 14 public CarBookingDbContext(DbContextOptions options) 15 : base(options) 16 { 17 } 18 19 20 protected override void OnModelCreating(ModelBuilder modelBuilder) 21 { 22 base.OnModelCreating(modelBuilder); 23 24 25 modelBuilder.Entity<Car>(); 26 modelBuilder.Entity<Booking>(); 27 } 28 } 29 }
If you are used to EF Core, this will look familiar. The class extends the DbContext and we create DbSet properties that store the models that will also be present in the database. We also override the OnModelCreating method. You may notice that unlike when using SQL Server, we don’t call .ToTable(). We could call ToCollection instead but this isn’t required here as we specify the collection using attributes on the classes.
Earlier, we created a MongoDBSettings model, and now we need to add the values that the properties map to into our appsettings.
- In both appsettings.json and appsettings.Development.json, add the following new section:
1 "MongoDBSettings": { 2 "AtlasURI": "mongodb+srv://<username>:<password>@<url>", 3 "DatabaseName": "cargarage" 4 }
Now we have configured our models and DbContext, it is time to add them to our program.cs file.
After the existing line
builder.Services.AddControllersWithViews();
, add the following code:1 var mongoDBSettings = builder.Configuration.GetSection("MongoDBSettings").Get<MongoDBSettings>(); 2 builder.Services.Configure<MongoDBSettings>(builder.Configuration.GetSection("MongoDBSettings")); 3 4 builder.Services.AddDbContext<CarBookingDbContext>(options => 5 options.UseMongoDB(mongoDBSettings.AtlasURI ?? "", mongoDBSettings.DatabaseName ?? ""));
Now, it is time to add the services we will use to talk to the database via the CarBookingDbContext we created. For each service, we will create an interface and the class that implements it.
The first interface and service we will implement is for carrying out the CRUD operations on the cars collection. This is known as the repository pattern. You may see people interact with the DbContext directly. But most people use this pattern, which is why we are including it here.
- If you haven’t already, create a Services folder to store our new classes.
- Create an ICarService interface and add the following code for the methods we will implement:
1 using MongoDB.Bson; 2 using SuperCarBookingSystem.Models; 3 4 namespace SuperCarBookingSystem.Services 5 { 6 public interface ICarService 7 { 8 IEnumerable<Car> GetAllCars(); 9 Car? GetCarById(ObjectId id); 10 11 void AddCar(Car newCar); 12 13 void EditCar(Car updatedCar); 14 15 void DeleteCar(Car carToDelete); 16 } 17 } - Create a CarService class file.
- Update the CarService class declaration so it implements the ICarService we just created:
1 using Microsoft.EntityFrameworkCore; 2 using MongoDB.Bson; 3 using MongoDB.Driver; 4 using SuperCarBookingSystem.Models; 5 6 namespace SuperCarBookingSystem.Services 7 { 8 public class CarService : ICarService 9 { - This will cause a red squiggle to appear underneath ICarService as we haven’t implemented all the methods yet, but we will implement the methods one by one.
- Add the following code after the class declaration that adds a local CarBookingDbContext object and a constructor that gets an instance of the DbContext via dependency injection.
1 private readonly CarBookingDbContext _carDbContext; 2 public CarService(CarBookingDbContext carDbContext) 3 { 4 _carDbContext = carDbContext; 5 } - Next, we will implement the GetAllCars method so add the following code:
1 public IEnumerable<Car> GetAllCars() 2 { 3 return _carDbContext.Cars.OrderBy(c => c.Id).AsNoTracking().AsEnumerable<Car>(); 4 } The id property here maps to the _id field in our document which is a special MongoDB ObjectId type and is auto-generated when a new document is created. But what is useful about the _id property is that it can actually be used to order documents because of how it is generated under the hood.If you haven’t seen it before, theAsNoTracking()
method is part of EF Core and prevents EF tracking changes you make to an object. This is useful for reads when you know no changes are going to occur. - Next, we will implement the method to get a specific car using its Id property:
1 public Car? GetCarById(ObjectId id) 2 { 3 return _carDbContext.Cars.FirstOrDefault(c => c.Id == id); 4 } Then, we will add the AddCar implementation:1 public void AddCar(Car car) 2 { 3 _carDbContext.Cars.Add(car); 4 5 _carDbContext.ChangeTracker.DetectChanges(); 6 Console.WriteLine(_carDbContext.ChangeTracker.DebugView.LongView); 7 8 _carDbContext.SaveChanges(); 9 } In a production environment, you might want to use something like ILogger to track these changes rather than printing to the console. But this will allow us to clearly see that a new entity has been added, showing change tracking in action. - EditCar is next:
1 public void EditCar(Car car) 2 { 3 var carToUpdate = _carDbContext.Cars.FirstOrDefault(c => c.Id == car.Id); 4 5 if(carToUpdate != null) 6 { 7 carToUpdate.Model = car.Model; 8 carToUpdate.NumberPlate = car.NumberPlate; 9 carToUpdate.Location = car.Location; 10 carToUpdate.IsBooked = car.IsBooked; 11 12 _carDbContext.Cars.Update(carToUpdate); 13 14 _carDbContext.ChangeTracker.DetectChanges(); 15 Console.WriteLine(_carDbContext.ChangeTracker.DebugView.LongView); 16 17 _carDbContext.SaveChanges(); 18 19 } 20 else 21 { 22 throw new ArgumentException("The car to update cannot be found. "); 23 } 24 } Again, we add a call to print out information from change tracking as it will show that the new EF Core Provider, even when using MongoDB as the database, is able to track modifications. - Finally, we need to implement DeleteCar:
1 public void DeleteCar(Car car) 2 { 3 var carToDelete = _carDbContext.Cars.Where(c => c.Id == car.Id).FirstOrDefault(); 4 5 if(carToDelete != null) { 6 _carDbContext.Cars.Remove(carToDelete); 7 _carDbContext.ChangeTracker.DetectChanges(); 8 Console.WriteLine(_carDbContext.ChangeTracker.DebugView.LongView); 9 _carDbContext.SaveChanges(); 10 } 11 else { 12 throw new ArgumentException("The car to delete cannot be found."); 13 } 14 }
Next up is our IBookingService and BookingService.
- Create the IBookingService interface and add the following methods:
1 using MongoDB.Bson; 2 using SuperCarBookingSystem.Models; 3 namespace SuperCarBookingSystem.Services 4 { 5 public interface IBookingService 6 { 7 IEnumerable<Booking> GetAllBookings(); 8 Booking? GetBookingById(ObjectId id); 9 10 void AddBooking(Booking newBooking); 11 12 void EditBooking(Booking updatedBooking); 13 14 void DeleteBooking(Booking bookingToDelete); 15 } 16 } - Create the BookingService class, and replace your class with the following code that implements all the methods:
1 using Microsoft.EntityFrameworkCore; 2 using MongoDB.Bson; 3 using SuperCarBookingSystem.Models; 4 5 namespace SuperCarBookingSystem.Services 6 { 7 public class BookingService : IBookingService 8 { 9 private readonly CarBookingDbContext _carDbContext; 10 11 public BookingService(CarBookingDbContext carDBContext) 12 { 13 _carDbContext = carDBContext; 14 } 15 public void AddBooking(Booking newBooking) 16 { 17 var bookedCar = _carDbContext.Cars.FirstOrDefault(c => c.Id == newBooking.CarId); 18 if (bookedCar == null) 19 { 20 throw new ArgumentException("The car to be booked cannot be found."); 21 } 22 23 newBooking.CarModel = bookedCar.Model; 24 25 bookedCar.IsBooked = true; 26 _carDbContext.Cars.Update(bookedCar); 27 28 _carDbContext.Bookings.Add(newBooking); 29 30 _carDbContext.ChangeTracker.DetectChanges(); 31 Console.WriteLine(_carDbContext.ChangeTracker.DebugView.LongView); 32 33 _carDbContext.SaveChanges(); 34 } 35 36 public void DeleteBooking(Booking booking) 37 { 38 var bookedCar = _carDbContext.Cars.FirstOrDefault(c => c.Id == booking.CarId); 39 bookedCar.IsBooked = false; 40 41 var bookingToDelete = _carDbContext.Bookings.FirstOrDefault(b => b.Id == booking.Id); 42 43 if(bookingToDelete != null) 44 { 45 _carDbContext.Bookings.Remove(bookingToDelete); 46 _carDbContext.Cars.Update(bookedCar); 47 48 _carDbContext.ChangeTracker.DetectChanges(); 49 Console.WriteLine(_carDbContext.ChangeTracker.DebugView.LongView); 50 51 _carDbContext.SaveChanges(); 52 } 53 else 54 { 55 throw new ArgumentException("The booking to delete cannot be found."); 56 } 57 } 58 59 public void EditBooking(Booking updatedBooking) 60 { 61 var bookingToUpdate = _carDbContext.Bookings.FirstOrDefault(b => b.Id == updatedBooking.Id); 62 63 64 if (bookingToUpdate != null) 65 { 66 bookingToUpdate.StartDate = updatedBooking.StartDate; 67 bookingToUpdate.EndDate = updatedBooking.EndDate; 68 69 70 _carDbContext.Bookings.Update(bookingToUpdate); 71 72 _carDbContext.ChangeTracker.DetectChanges(); 73 _carDbContext.SaveChanges(); 74 75 Console.WriteLine(_carDbContext.ChangeTracker.DebugView.LongView); 76 } 77 else 78 { 79 throw new ArgumentException("Booking to be updated cannot be found"); 80 } 81 82 } 83 84 public IEnumerable<Booking> GetAllBookings() 85 { 86 return _carDbContext.Bookings.OrderBy(b => b.StartDate).AsNoTracking().AsEnumerable<Booking>(); 87 } 88 89 public Booking? GetBookingById(ObjectId id) 90 { 91 return _carDbContext.Bookings.AsNoTracking().FirstOrDefault(b => b.Id == id); 92 } 93 94 } 95 }
This code is very similar to the code for the CarService class but for bookings instead.
The final step for the services is to add them to the dependency injection container.
Inside Program.cs, add the following code after the code we added there earlier:
1 builder.Services.AddScoped<ICarService, CarService>(); 2 builder.Services.AddScoped<IBookingService, BookingService>();
Before we implement the front end, we need to add the view models that will act as a messenger between our front and back ends where required. Even though our application is quite simple, implementing the view model is still good practice as it helps decouple the pieces of the app.
The first one we will add is the CarListViewModel. This will be used as the model in our Razor page later on for listing cars in our database.
- Create a new folder in the root of the project called ViewModels.
- Add a new class called CarListViewModel.
- Add
public IEnumerable<Car> Cars { get; set; }
inside your class.
We also want a view model that can be used by the Add view we will add later.
- Inside the ViewModels folder, create a new class called CarAddViewModel.
- Add
public Car? Car { get; set; }
.
Now, we want to do something very similar for bookings, starting with BookingListViewModel.
- Create a new class in the ViewModels folder called BookingListViewModel.
- Add
public IEnumerable<Booking> Bookings { get; set; }
.
Finally, we have our BookingAddViewModel.
Create the class and add the property
public Booking? Booking { get; set; }
inside the class.Later on, we will be adding references to our models and viewmodels in the views. In order for the application to know what they are, we need to add references to them in the _ViewImports.cshtml file inside the Views folder.
There will already be some references in there, including TagHelpers, so we want to add references to our .Models and .ViewModels folders. When added, it will look something like below, just with your application name instead.
1 @using <YourApplicationName> 2 @using <YourApplicationName>.Models 3 @using <YourApplicationName>.ViewModels
Now we have the backend implementation and the view models we will refer to, we can start working toward the front end.
We will be creating two controllers: one for Car and one for Booking.
The first controller we will add is for the car.
- Inside the existing Controllers folder, add a new controller. If using Visual Studio, use the MVC Controller - Empty controller template.
- Add a local ICarService object and a constructor that fetches it from dependency injection:
1 private readonly ICarService _carService; 2 3 4 public CarController(ICarService carService) 5 { 6 _carService = carService; 7 } - Depending on what your scaffolded controller came with, either create or update the Index function with the following:
1 public IActionResult Index() 2 { 3 CarListViewModel viewModel = new() 4 { 5 Cars = _carService.GetAllCars(), 6 }; 7 return View(viewModel); 8 } For the other CRUD operations — so create, update, and delete — we will have two methods for each: one is for Get and the other is for Post. - The HttpGet for Add will be very simple as it doesn’t need to pass any data around:
1 public IActionResult Add() 2 { 3 return View(); 4 } - Next, add the Add method that will be called when a new car is requested to be added:
1 [ ]2 public IActionResult Add(CarAddViewModel carAddViewModel) 3 { 4 if(ModelState.IsValid) 5 { 6 Car newCar = new() 7 { 8 Model = carAddViewModel.Car.Model, 9 Location = carAddViewModel.Car.Location, 10 NumberPlate = carAddViewModel.Car.NumberPlate 11 }; 12 13 14 _carService.AddCar(newCar); 15 return RedirectToAction("Index"); 16 } 17 18 19 return View(carAddViewModel); 20 } - Now, we will add the code for editing a car:
1 public IActionResult Edit(string id) 2 { 3 if(id == null) 4 { 5 return NotFound(); 6 } 7 8 9 var selectedCar = _carService.GetCarById(new ObjectId(id)); 10 return View(selectedCar); 11 } 12 13 14 [ ]15 public IActionResult Edit(Car car) 16 { 17 try 18 { 19 if(ModelState.IsValid) 20 { 21 _carService.EditCar(car); 22 return RedirectToAction("Index"); 23 } 24 else 25 { 26 return BadRequest(); 27 } 28 } 29 catch (Exception ex) 30 { 31 ModelState.AddModelError("", $"Updating the car failed, please try again! Error: {ex.Message}"); 32 } 33 34 35 return View(car); 36 } - Finally, we have Delete:
1 public IActionResult Delete(string id) { 2 if (id == null) 3 { 4 return NotFound(); 5 } 6 7 8 var selectedCar = _carService.GetCarById(new ObjectId(id)); 9 return View(selectedCar); 10 } 11 12 13 [ ]14 public IActionResult Delete(Car car) 15 { 16 if (car.Id == null) 17 { 18 ViewData["ErrorMessage"] = "Deleting the car failed, invalid ID!"; 19 return View(); 20 } 21 22 23 try 24 { 25 _carService.DeleteCar(car); 26 TempData["CarDeleted"] = "Car deleted successfully!"; 27 28 29 return RedirectToAction("Index"); 30 } 31 catch (Exception ex) 32 { 33 ViewData["ErrorMessage"] = $"Deleting the car failed, please try again! Error: {ex.Message}"; 34 } 35 36 37 var selectedCar = _carService.GetCarById(car.Id); 38 return View(selectedCar); 39 }
Now for the booking controller. This is very similar to the CarController but it has a reference to both the car and booking service as we need to associate a car with a booking. This is because at the moment, the EF Core Provider doesn’t support relationships between entities so we can relate entities in a different way. You can view the roadmap on the GitHub repo, however.
- Create another empty MVC Controller called BookingController.
- Paste the following code replacing the current class:
1 public class BookingController : Controller 2 { 3 private readonly IBookingService _bookingService; 4 private readonly ICarService _carService; 5 6 public BookingController(IBookingService bookingService, ICarService carService) 7 { 8 _bookingService = bookingService; 9 _carService = carService; 10 } 11 12 public IActionResult Index() 13 { 14 BookingListViewModel viewModel = new BookingListViewModel() 15 { 16 Bookings = _bookingService.GetAllBookings() 17 }; 18 return View(viewModel); 19 } 20 21 public IActionResult Add(string carId) 22 { 23 var selectedCar = _carService.GetCarById(new ObjectId(carId)); 24 25 BookingAddViewModel bookingAddViewModel = new BookingAddViewModel(); 26 27 bookingAddViewModel.Booking = new Booking(); 28 bookingAddViewModel.Booking.CarId = selectedCar.Id; 29 bookingAddViewModel.Booking.CarModel = selectedCar.Model; 30 bookingAddViewModel.Booking.StartDate = DateTime.UtcNow; 31 bookingAddViewModel.Booking.EndDate = DateTime.UtcNow.AddDays(1); 32 33 return View(bookingAddViewModel); 34 } 35 36 [ ]37 public IActionResult Add(BookingAddViewModel bookingAddViewModel) 38 { 39 Booking newBooking = new() 40 { 41 CarId = bookingAddViewModel.Booking.CarId, 42 StartDate = bookingAddViewModel.Booking.StartDate, 43 EndDate = bookingAddViewModel.Booking.EndDate, 44 }; 45 46 _bookingService.AddBooking(newBooking); 47 return RedirectToAction("Index"); 48 } 49 50 public IActionResult Edit(string Id) 51 { 52 if(Id == null) 53 { 54 return NotFound(); 55 } 56 57 var selectedBooking = _bookingService.GetBookingById(new ObjectId(Id)); 58 return View(selectedBooking); 59 } 60 61 [ ]62 public IActionResult Edit(Booking booking) 63 { 64 try 65 { 66 var existingBooking = _bookingService.GetBookingById(booking.Id); 67 if (existingBooking != null) 68 { 69 _bookingService.EditBooking(existingBooking); 70 return RedirectToAction("Index"); 71 } 72 else 73 { 74 ModelState.AddModelError("", $"Booking with ID {booking.Id} does not exist!"); 75 } 76 } 77 catch (Exception ex) 78 { 79 ModelState.AddModelError("", $"Updating the booking failed, please try again! Error: {ex.Message}"); 80 } 81 82 return View(booking); 83 } 84 85 public IActionResult Delete(string Id) 86 { 87 if (Id == null) 88 { 89 return NotFound(); 90 } 91 92 var selectedBooking = _bookingService.GetBookingById(Id); 93 return View(selectedBooking); 94 } 95 96 [ ]97 public IActionResult Delete(Booking booking) 98 { 99 if(booking.Id == null) 100 { 101 ViewData["ErrorMessage"] = "Deleting the booking failed, invalid ID!"; 102 return View(); 103 } 104 105 try 106 { 107 _bookingService.DeleteBooking(booking); 108 TempData["BookingDeleted"] = "Booking deleted successfully"; 109 110 return RedirectToAction("Index"); 111 } 112 catch (Exception ex) 113 { 114 ViewData["ErrorMessage"] = $"Deleting the booking failed, please try again! Error: {ex.Message}"; 115 } 116 117 var selectedCar = _bookingService.GetBookingById(booking.Id.ToString()); 118 return View(selectedCar); 119 } 120 }
Now we have the back end and the controllers prepped with the endpoints for our car booking system, it is time to implement the views. This will be using Razor pages. You will also see reference to classes from Bootstrap as this is the CSS framework that comes with MVC applications out of the box.
We will be providing views for the CRUD operations for both listings and bookings.
First, we will provide a view that will map to the root of /Car, which will by convention look at the Index method we implemented.
ASP.NET Core MVC uses a convention pattern whereby you name the .cshtml file the name of the endpoint/method it uses and it lives inside a folder named after its controller.
- Inside the Views folder, create a new subfolder called Car.
- Inside that Car folder, add a new view. If using the available templates, you want Razor View - Empty. Name the view Index.
- Delete the contents of the file and add a reference to the CarListViewModel at the top
@model CarListViewModel
. - Next, we want to add a placeholder for the error handling. If there was an issue deleting a car, we added a string to TempData so we want to add that into the view, if there is data to display.
1 @if (TempData["CarDeleted"] != null) 2 { 3 <p class="text-success">@TempData["CarDeleted"]</p> 4 } - Next, we will handle if there are no cars in the database, by displaying a message to the user:
1 @if (!Model.Cars.Any()) 2 { 3 <p>No results</p> 4 } - The easiest way to display the list of cars and the relevant information is to use a table:
1 else 2 { 3 <table class="table table-condensed table-bordered"> 4 <tr> 5 <th> 6 Model 7 </th> 8 <th> 9 Number Plate 10 </th> 11 <th> 12 Location 13 </th> 14 <th> 15 Actions 16 </th> 17 </tr> 18 19 @foreach (var car in Model.Cars) 20 { 21 <tr> 22 <td>@car.Model</td> 23 <td>@car.NumberPlate</td> 24 <td>@car.Location</td> 25 <td> 26 <a asp-action="Edit" asp-route-id="@car.Id.ToString()">Edit</a> 27 <a asp-action="Delete" asp-route-id="@car.Id.ToString()">Delete</a> 28 @if(!car.IsBooked) 29 { 30 <a asp-controller="Booking" asp-action="Add" asp-route-carId="@car.Id.ToString()">Book</a> 31 } 32 </td> 33 </tr> 34 } 35 36 </table> 37 } 38 39 <p> 40 <a class="btn btn-primary" asp-action="Add">Add new car</a> 41 </p> It makes sense to have the list of cars as our home page so before we move on, we will update the default route from Home to /Car. - In Program.cs, inside
app.MapControllerRoute
, replace the pattern line with the following:1 pattern: "{controller=Car}/{action=Index}/{id?}");
If we ran this now, the buttons would lead to 404s because we haven’t implemented them yet. So let’s do that now.
We will start with the form for adding new cars.
- Add a new, empty Razor View inside the Car subfolder called Add.cshtml.
- Before adding the form, we will add the model reference at the top, a header, and some conditional content for the error message.
1 @model CarAddViewModel 2 3 4 <h2>Create a new car</h2> 5 <hr /> 6 7 8 @if (ViewData["ErrorMessage"] != null) 9 { 10 <p class="text-danger">@ViewData["ErrorMessage"]</p> 11 } - Now, we can implement the form.
1 <form method="post" asp-controller="Car" asp-action="Add"> 2 <div asp-validation-summary="All" class="text-danger"></div> 3 4 5 <div class="mb-3"> 6 <label asp-for="Car.Model" class="form-label"></label> 7 <input asp-for="Car.Model" class="form-control" /> 8 <span asp-validation-for="Car.Model" class="text-danger"></span> 9 </div> 10 11 12 <div class="mb-3"> 13 <label asp-for="Car.NumberPlate" class="form-label"></label> 14 <input asp-for="Car.NumberPlate" class="form-control" /> 15 <span asp-validation-for="Car.NumberPlate" class="text-danger"></span> 16 </div> 17 18 19 <div class="mb-3"> 20 <label asp-for="Car.Location" class="form-label"></label> 21 <input asp-for="Car.Location" class="form-control" /> 22 <span asp-validation-for="Car.Location" class="text-danger"></span> 23 </div> 24 25 26 <input type="submit" value="Add car" class="btn btn-primary" /> 27 </form>
Now, we want to add a button at the bottom to easily navigate back to the list of cars in case the user decides not to add a new car after all.
Add the following after the
</form>
tag:1 <div> 2 <a asp-controller="Car" asp-action="Index">Back to list</a> 3 </div>
The code for the Edit page is almost identical to Add, but it uses the Car as a model as it will use the car it is passed to pre-populate the form for editing.
- Add another view inside the Car subfolder called Edit.cshtml.
- Add the following code:
1 @model Car 2 3 <h2>Update @Model.Model</h2> 4 <hr /> 5 6 <form method="post" asp-controller="Car" asp-action="Edit"> 7 <div asp-validation-summary="ModelOnly" class="text-danger"></div> 8 <input type="hidden" asp-for="Id" /> 9 10 <div class="mb-3"> 11 <label asp-for="Model" class="form-label"></label> 12 <input asp-for="Model" class="form-control" /> 13 <span asp-validation-for="Model" class="text-danger"/> 14 </div> 15 <div class="mb-3"> 16 <label asp-for="NumberPlate" class="form-label"></label> 17 <input asp-for="NumberPlate" class="form-control" /> 18 <span asp-validation-for="NumberPlate" class="text-danger"/> 19 </div> 20 <div class="mb-3"> 21 <label asp-for="Location" class="form-label"></label> 22 <input asp-for="Location" class="form-control" /> 23 <span asp-validation-for="Location" class="text-danger"/> 24 </div> 25 <input type="submit" value="Update car" class="btn btn-primary" /> 26 </form> 27 <div> 28 <a asp-controller="Car" asp-action="Index">Back to list</a> 29 </div>
The final page we need to implement is the page that is called when the delete button is clicked for a car.
- Create a new empty View called Delete.cshtml.
- Add the following code to add the model, heading, and conditional error message:
1 @model Car 2 3 4 <h2>Deleting @Model.Model</h2> 5 <hr /> 6 7 8 @if(ViewData["ErrorMessage"] != null) 9 { 10 <p class="text-danger">@ViewData["ErrorMessage"]</p> 11 } Instead of a form like in the other views, we are going to add a description list to display information about the car that we are confirming deletion of.1 <div> 2 <dl class="row"> 3 <dt class="col-sm-2"> 4 <label asp-for="Model"></label> 5 </dt> 6 <dd class="col-sm-10"> 7 @Model?.Model 8 </dd> 9 <dt class="col-sm-2"> 10 <label asp-for="NumberPlate"></label> 11 </dt> 12 <dd class="col-sm-10"> 13 @Model?.NumberPlate 14 </dd> 15 <dt class="col-sm-2"> 16 <label asp-for="Location"></label> 17 </dt> 18 <dd class="col-sm-10"> 19 @Model?.Location 20 </dd> 21 22 23 </dl> 24 </div> - Below that, we will add a form for submitting the deletion and the button to return to the list:
1 <form method="post" asp-action="Delete"> 2 <input type="hidden" asp-for="Id" /> 3 <input type="submit" value="Delete car" class="btn btn-danger" onclick="javascript: return confirm('Are you sure you want to delete this car?');" /> 4 </form> 5 6 7 <div> 8 <a asp-controller="Car" asp-action="Index">Back to list</a> 9 </div>
We have added the views for the cars so now we will add the views for bookings, starting with listing any existing books.
- Create a new folder inside the Views folder called Booking.
- Create a new empty view called Index.
- Add the following code to display the bookings, if any exist:
1 @model BookingListViewModel 2 3 4 @if (TempData["BookingDeleted"] != null) 5 { 6 <p class="text-success">@TempData["BookingDeleted"]</p> 7 } 8 9 10 @if (!Model.Bookings.Any()) 11 { 12 <p>No results</p> 13 } 14 15 16 else 17 { 18 <table class="table table-condensed table-bordered"> 19 <tr> 20 <th> 21 Booked Car 22 </th> 23 <th> 24 Start Date 25 </th> 26 <th> 27 End Date 28 </th> 29 <th> 30 Actions 31 </th> 32 </tr> 33 34 35 @foreach(var booking in Model.Bookings) 36 { 37 <tr> 38 <td>@booking.CarModel</td> 39 <td>@booking.StartDate</td> 40 <td>@booking.EndDate</td> 41 <td> 42 <a asp-action="Edit" asp-route-id="@booking.Id.ToString()">Edit</a> 43 <a asp-action="Delete" asp-route-id="@booking.Id.ToString()">Delete</a> 44 </td> 45 </tr> 46 } 47 48 49 </table> 50 51 52 }
Adding bookings is next. This view will be available when the book button is clicked next to a listed car.
- Create an empty view called Add.cshtml.
- Add the following code:
1 @model BookingAddViewModel 2 3 4 @if (ViewData["ErrorMessage"] != null) 5 { 6 <p class="text-danger">@ViewData["ErrorMessage"]</p> 7 } 8 9 <form method="post" asp-controller="Booking" asp-action="Add"> 10 <div asp-validation-summary="All" class="text-danger"></div> 11 <input type="hidden" asp-for="Booking.Id" /> 12 <input type="hidden" asp-for="Booking.CarId" /> 13 14 <div class="mb-3"> 15 <label asp-for="Booking.StartDate" class="form-label"></label> 16 <input asp-for="Booking.StartDate" type="date" class="form-control" /> 17 <span asp-validation-for="Booking.StartDate" class="text-danger"></span> 18 </div> 19 <div class="mb-3"> 20 <label asp-for="Booking.EndDate" class="form-label"></label> 21 <input asp-for="Booking.EndDate" type="date" class="form-control" /> 22 <span asp-validation-for="Booking.EndDate" class="text-danger"></span> 23 </div> 24 25 <input type="submit" value="Book car" class="btn btn-primary" /> 26 </form>
Just like with cars, we also want to be able to edit existing books.
- Create an empty view called Edit.cshtml.
- Add the following code:
1 @model Booking 2 3 4 <h2>Editing booking for @Model.CarModel between @Model.StartDate and @Model.EndDate</h2> 5 <hr /> 6 7 8 <form method="post" asp-controller="Booking" asp-action="Edit"> 9 <div asp-validation-summary="ModelOnly" class="text-danger"></div> 10 <input type="hidden" asp-for="Id" /> 11 12 13 <div class="mb-3"> 14 <label asp-for="StartDate" class="form-label"></label> 15 <input asp-for="StartDate" class="form-control" /> 16 <span asp-validation-for="StartDate" class="text-danger" /> 17 </div> 18 <div class="mb-3"> 19 <label asp-for="EndDate" class="form-label"></label> 20 <input asp-for="EndDate" class="form-control" /> 21 <span asp-validation-for="EndDate" class="text-danger" /> 22 </div> 23 <input type="submit" value="Update booking" class="btn btn-primary" /> 24 </form> 25 <div> 26 <a asp-controller="Booking" asp-action="Index">Back to bookings</a> 27 </div>
The final view we need to add is to delete a booking. As with cars, we will display the booking information and deletion confirmation.
1 @model Booking 2 3 <h2>Delete booking</h2> 4 <hr /> 5 6 @if (ViewData["ErrorMessage"] != null) 7 { 8 <p class="text-danger">@ViewData["ErrorMessage"]</p> 9 } 10 11 <div> 12 <dl class="row"> 13 <dt class="col-sm-2"> 14 <label asp-for="CarModel"></label> 15 </dt> 16 <dd class="col-sm-10"> 17 @Model?.CarModel 18 </dd> 19 <dt class="col-sm-2"> 20 <label asp-for="StartDate"></label> 21 </dt> 22 <dd class="col-sm-10"> 23 @Model?.StartDate 24 </dd> 25 </dl> 26 <dt class="col-sm-2"> 27 <label asp-for="EndDate"></label> 28 </dt> 29 <dd class="col-sm-10"> 30 @Model?.EndDate 31 </dd> 32 </div> 33 34 <form method="post" asp-action="Delete"> 35 <input type="hidden" asp-for="Id" /> 36 <input type="hidden" asp-for="CarId" /> 37 <input type="submit" value="Delete booking" class="btn btn-danger" onclick="javascript: return confirm('Are you sure you want to delete this booking?');" /> 38 </form> 39 40 <div> 41 <a asp-controller="Booking" asp-action="Index">Back to list</a> 42 </div>
We now have a functioning application that uses the new MongoDB Provider for EF Core — hooray! Now is the time to test it all and visit our endpoints to make sure it all works.
It is not part of this tutorial as it is not required, but I chose to make some changes to the site.css file to add some color. I also updated the _Layout.cshtml file to add the Car and Bookings pages to the navbar. You will see this reflected in the screenshots in the rest of the article. You are of course welcome to make your own changes if you have ideas of how you would like the application to look.
Below are some screenshots I took from the app, showing the features of the Cars endpoint.
The bookings pages will look very similar to cars but are adapted for the bookings model that includes dates.
There we have it: a full stack application using ASP.NET MVC that takes advantage of the new MongoDB Provider for EF Core. We are able to do the CRUD operations and track changes.
EF Core is widely used amongst developers so having an official MongoDB Provider is super exciting. This library is in Preview, which means we are continuing to build out new features. Stay tuned for updates and we are always open to feedback. We can’t wait to see what you build!
You can view the Roadmap of the provider in the GitHub repository, where you can also find links to the documentation!
As always, if you have any questions about this or other topics, get involved at our MongoDB Community Forums.