CRUD with MongoDB in C# and .Net 6

·

6 min read

Introduction

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.

Architecture

We're going to use a pretty standard layered pattern here, separating our code into three layers:

  • Presentation layer (This is where our controllers will go)
  • Application layer (Our business logic lives here)
  • Data access layer (Interaction with MongoDB)

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.

Data Access Layer

Data Model

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; }
}

MongoDB Factory

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);
    }
}

Data Access Service

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);
    }
}

Service Layer

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);
    }
}

Presentation Layer

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);
        }
    }
}

Putting it all Together

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.

Running the Application

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.

image.png

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

image.png

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:

image.png

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:

image.png

Summary

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.

Did you find this article valuable?

Support Dave K by becoming a sponsor. Any amount is appreciated!