CRUD with MongoDB in C# and .Net 6

Search for a command to run...

No comments yet. Be the first to comment.
Building another To-Do list application whilst exploring many development tools, styles and architectural patterns.
Introduction This is the first in my series of articles, which aims to build a working ‘To-Do’ list application whilst exploring a whole range of different technologies and frameworks. A bit more background is given in my introduction page. To sta...
Introduction Semantic Kernel is an open-source framework by Microsoft that integrates machine learning and natural language processing to build AI applications. It offers tools for tasks like language understanding, text generation, and semantic sear...

Introduction In the earlier articles in my PDF Splitter series, we have relied on the inbuild .NET libraries to handle working with PDF files in MAUI, but these only support opening and displaying of PDF files. Now that we are looking to save our new...

Introduction In the previous article, we created a PDF model and service to allow us to work with and track PDF pages. The next step is to build the UI to allow us to interact with the service. To do this we will create a single 'view' and a 'view mo...

Introduction For the fourth article in my PDF Splitter series, we will start working with PDF files in .NET MAUI. Helpfully, MAUI contains a lot of useful PDF tools for reading PDF files out of the box, so we do not need to use any additional librari...

Introduction In this article, I'm going to introduce basic navigation into my .NET MAUI PDF Splitter application. If you're following my PDF Splitter Application series, you will have a simple application based on the out-the-box .NET template with a...

In the second part of the 'To-Do List' series, I'm going to introduce the basic functionality to read and write to MongoDB from C# in .Net 6. This will build upon the code from my previous article, which also explains how to get a free Atlas MongoDB set-up to host your database if you do not currently have a database set up.
We're going to use a pretty standard layered pattern here, separating our code into three layers:
One of my favourite articles around the layered approach is this one by Jeffrey Palermo on 'Onion Architecture'.
This may seem a bit overkill for now, but this project will grow, and we should always try to implement good practices from the start.
All source for this is available on GitHub here.
To store data in MongoDB, we need a data model. For this example, I have created a simple ToDoListItemModel.cs class that defines the basic element we will store in our database. I have also created a second ToDoItemNoteModel class that will allow me to add notes to the item at a later date. The notes will be stored in an array on the to-do list item. As MongoDB is not a relational database, we will store notes as an array on the parent to-do list item model.
public class ToDoItemModel
{
public string Id { get; set; }
public DateTime DateCreated { get; set; }
public DateTime? DateCompleted { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public ToDoItemNoteModel[] Notes { get; set; }
public bool Complete { get; set; }
}
public class ToDoItemNoteModel
{
public DateTime DateCreated { get; set; }
public string Note { get; set; }
}
To interact with the database, I have decided to use the factory pattern, which allows me to abstract away some configuration from the data access services. This will make unit testing of our services much more straightforward. Unit testing will be covered in a later article.
This factory can be injected into all of our data access services (we currently only have one) and be called upon to create a new connection to a database collection.
Note: I have also created interfaces for these services which will be used to add the services to the dependency injection container. For the interface code, you can refer to the GitHub repository if required.
public class MongoDbFactory : IMongoDbFactory
{
private readonly IMongoClient _client;
public MongoDbFactory(string connectionString)
{
var settings = MongoClientSettings.FromConnectionString(connectionString);
settings.ServerApi = new ServerApi(ServerApiVersion.V1);
_client = new MongoClient(settings);
}
public IMongoCollection<T> GetCollection<T>(string databaseName, string collectionNme)
{
return _client.GetDatabase(databaseName).GetCollection<T>(collectionNme);
}
}
Reading and writing operations for our collection are added to a data access service. There should be no business logic in here, as that will be added to our service layer. The sole responsibility of data access service is the basic database operations. For now, I have included some basic create, read, update and delete (CRUD) operations that we will need to get stared with our application.
I have left the name of our database and collection hard coded for now, but this could be moved to config if required. You will see how the factory is injected in the constructor and then called upon to create a new connection to our specific collection using generics, which I will cover in a later article.
public class ToDoItemDataAccessService : IToDoItemDataAccessService
{
private readonly IMongoCollection<ToDoItemModel> _toDoItems;
public ToDoItemDataAccessService(IMongoDbFactory mongoDbFactory)
{
_toDoItems = mongoDbFactory.GetCollection<ToDoItemModel>("ToDo", "ToDoItems");
}
public async Task CreateItemAsync(ToDoItemModel model)
{
await _toDoItems.InsertOneAsync(model);
}
public async Task<ToDoItemModel> GetToDoItemAsync(string id)
{
var results = await _toDoItems.FindAsync(x => x.Id == id);
return await results.SingleAsync();
}
public async Task<IEnumerable<ToDoItemModel>> GetOpenToDoItemsAsync()
{
var results = await _toDoItems.FindAsync(x => !x.Complete);
return await results.ToListAsync();
}
public async Task UpdateToDoItemAsync(ToDoItemModel model)
{
await _toDoItems.FindOneAndReplaceAsync(x => x.Id == model.Id, model);
}
public async Task DeleteToDoItemAsync(string id)
{
await _toDoItems.DeleteOneAsync(x => x.Id == id);
}
}
The service layer for this application should contain all our business logic. In this example, we don't yet have much business logic, so for now on the creation function we use it to set the ID and the creation date and make sure any other values are reset, and for the remaining functions it will act as a simple pass through. I have created the ID as a GUID and converted to a string, rather than using the out the box BSON ID functionality as this makes it easier for us to search against later. Notice the data access layer service is injected into the service layer service again through the constructor.
public class ToDoItemService : IToDoItemService
{
private readonly IToDoItemDataAccessService _toDoItemDataAccessService;
public ToDoItemService(IToDoItemDataAccessService toDoItemDataAccessService)
{
_toDoItemDataAccessService = toDoItemDataAccessService;
}
public async Task CreateItemAsync(ToDoItemModel model)
{
model.Id = Guid.NewGuid().ToString();
model.DateCreated = DateTime.Now;
model.DateCompleted = null;
await _toDoItemDataAccessService.CreateItemAsync(model);
}
public async Task<ToDoItemModel> GetToDoItemAsync(string id)
{
return await _toDoItemDataAccessService.GetToDoItemAsync(id);
}
public async Task<IEnumerable<ToDoItemModel>> GetOpenToDoItemsAsync()
{
return await _toDoItemDataAccessService.GetOpenToDoItemsAsync();
}
public async Task UpdateToDoItemAsync(ToDoItemModel model)
{
await _toDoItemDataAccessService.UpdateToDoItemAsync(model);
}
public async Task DeleteToDoItemAsync(string id)
{
await _toDoItemDataAccessService.DeleteToDoItemAsync(id);
}
}
For the top level, we have a simple controller that utilises the service layer services, so allow us to receive REST requests and respond accordingly. I have only included create and get for now, but this could easily be extended to hook up the remaining actions.
[Route("[controller]")]
public class ToDoItemController : ControllerBase
{
private readonly IToDoItemService _toDoItemService;
public ToDoItemController(IToDoItemService toDoItemService)
{
_toDoItemService = toDoItemService;
}
[HttpPost("Create")]
public async Task<IActionResult> CreateToDoItem([FromBody] ToDoItemModel model)
{
try
{
await _toDoItemService.CreateItemAsync(model);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet("Get")]
public async Task<IActionResult> GetToDoItem([FromQuery]string id)
{
try
{
var item = await _toDoItemService.GetToDoItemAsync(id);
return Ok(item);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpGet("GetOpenItems")]
public async Task<IActionResult> GetOpenItems()
{
try
{
var items = await _toDoItemService.GetOpenToDoItemsAsync();
return Ok(items);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
The only task remaining is to add the above services into the dependency injection container. We do this in the Program.cs.
builder.Services.AddTransient<IToDoItemDataAccessService, ToDoItemDataAccessService>();
builder.Services.AddTransient<IToDoItemService, ToDoItemService>();
builder.Services.AddSingleton<IMongoDbFactory>(new MongoDbFactory(builder.Configuration.GetValue<string>("ConnectionStrings:MongoDb")));
As you can see, we add the services as transient, which means they will be re-created for each request. However, as the factory is used to handle connectivity to the database, to ensure consistency, we add this as a singleton.
We're now ready to run the application and add and retrieve data. We can do this through the swagger page that's built into the default .Net 6 Web API templates.
We should be able to send a simple request with just the following fields, the rest have been removed. As we progress with this project we will create specific models to create, update etc. but for now we've reused the same one to keep it simple.

After executing the query, we should be able to see the entry in our MongoDB collection:

Note: If the collection didn't previously exist MongoDB will create it for you.
Calling the 'Get Open Items' endpoint should also return our single to-do item:

And if we take the GUID out of there and apply it to our 'Get' function we should also be able to find our single result:

In this tutorial we have now successfully been able to connect to our MongoDB instance, add records to a collection and retrieve those records from the collection. In further articles we will also start to add to and update the data in these records as we start to improve our application.