4 minute read

This is a continuation of an earlier post REST API with ASP.NET Core 7 and InMemory Store. In this tutorial I will extend the service to store data in MongoDB, I will be using MongoDB Community Server Docker Image for this sample. I will use Docker to run MongoDB.

Setup Database Server

I will be using a docker-compose to run MongoDB in a docker container. This would allow us the add more services that our rest api is depenedent on e.g. redis server for distributed caching.

Let’s start by adding a new file by right clicking on Solution name in Visual Studio and Add New File. I like to name file as docker-compose.dev-env.yml, feel free to name it as you like. Add following content to add a database instance for movies rest api.

version: '3.7'

services:
  movies.db:
    image: mongodb/mongodb-community-server:6.0-ubi8
    environment:
      - MONGODB_INITDB_ROOT_USERNAME=root
      - MONGODB_INITDB_ROOT_PASSWORD=Password123
      - MONGO_INITDB_DATABASE=Movies
    volumes:
      - moviesdbdata:/data/db
    ports:
      - "27017:27017"

volumes:
  moviesdbdata:

Open a terminal at the root of the solution where docker-compose file is location and execute following command to start database server.

docker-compose -f docker-compose.dev-env.yml up -d

Database Migrations

We would not do any database/schema migrations for MongoDB as its a NoSQL database, here is an excellent discussion on Stackoverflow on this topic. We don’t need any migration for this sample however if the need arise and there is no strong use case of a schema migration script I would prefer to opt the route of supporting multiple schemas conconcurrently and update when required.

MongoDB Movies Store

Setup

  • Lets start by adding nuget packages
    dotnet add package MongoDB.Driver --version 2.19.2
    dotnet add package MongoDB.Bson --version 2.19.2
    
  • Update IMovieStore and make all methods async.
  • Update Controller to make methods async and await calls to store methods
  • Update InMemoryMoviesStore to make methods async

Configuration

Add a new folder Configuration and add MoviesStoreConfiguration.cs file.

public class MoviesStoreConfiguration
{
    public string ConnectionString { get; set; } = null!;
    public string DatabaseName { get; set; } = null!;
    public string MoviesCollectionName { get; set; } = null!;
}

Add following to the appsettings.json

"MoviesStoreConfiguration": {
    "ConnectionString": "mongodb://localhost:27017",
    "DatabaseName": "MoviesStore",
    "BooksCollectionName": "Movies"
  }

Register the configuration in Dependency Injection container in Program.cs

// Add services to the container.
builder.Services.Configure<MoviesStoreConfiguration>(
    builder.Configuration.GetSection(nameof(MoviesStoreConfiguration)));

Class and Constructor

Add a new folder under Store, I named it as Mongo and add a file named MongoMoviesStore.cs. This class would accept an IOptions<MoviesStoreConfiguration> as parameter that we would use to connect to MongoDB, get database and MongoCollection.

private readonly IMongoCollection<Movie> moviesCollection;

public MongoMoviesStore(IOptions<MoviesStoreConfiguration> moviesStoreConfiguration)
{
    var mongoClient = new MongoClient(moviesStoreConfiguration.Value.ConnectionString);
    var mongoDatabase = mongoClient.GetDatabase(moviesStoreConfiguration.Value.DatabaseName);

    moviesCollection = mongoDatabase.GetCollection<Movie>(moviesStoreConfiguration.Value.MoviesCollectionName);
}

I have specified this in appsettings.json configuration file. This is acceptable for development but NEVER put a production/stagging connection string in a configuration file. This can be put in secure vault e.g. AWS Parameter Store or Azure KeyVault and can be accessed from the application. CD pipeline can also be configured to load this value from a secure location and set as an environment variable for the container running the application.

Create

We create a new instance of Movie, then we use moviesCollection to insert a new record, we are handling a MongoWriteException and throw our custom DuplicateKeyException if WriteError.Code of exception is 11000.

Create function looks like

public async Task Create(CreateMovieParams createMovieParams)
{
    var movie = new Movie(
        createMovieParams.Id,
        createMovieParams.Title,
        createMovieParams.Director,
        createMovieParams.TicketPrice,
        createMovieParams.ReleaseDate,
        DateTime.UtcNow,
        DateTime.UtcNow);
    try
    {
        await moviesCollection.InsertOneAsync(movie);
    }
    catch (MongoWriteException ex)
    {
        if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey &&
            ex.WriteError.Code == 11000)
        {
            throw new DuplicateKeyException();
        }

        throw;
    }
}

GetAll

We use moviesCollection to find all records.

public async Task<IEnumerable<Movie>> GetAll()
{
    return await moviesCollection.Find(_ => true).ToListAsync();
}

GetById

We use moviesCollection and filter on the Id property of the documents to get first or default instance of collection matching with passed parameter.

public async Task<Movie?> GetById(Guid id)
{
    return await moviesCollection.Find(x => x.Id == id).FirstOrDefaultAsync();
}

Update

We use moviesCollection and use UpdateOneAsync method filtering record with id parameter and passing all updateable properties as UpdateDefinition.

Update function looks like

public async Task Update(Guid id, UpdateMovieParams updateMovieParams)
{
    await moviesCollection.UpdateOneAsync(
        x => x.Id == id,
        Builders<Movie>.Update.Combine(
            Builders<Movie>.Update.Set(x => x.Title, updateMovieParams.Title),
            Builders<Movie>.Update.Set(x => x.Director, updateMovieParams.Director),
            Builders<Movie>.Update.Set(x => x.TicketPrice, updateMovieParams.TicketPrice),
            Builders<Movie>.Update.Set(x => x.ReleaseDate, updateMovieParams.ReleaseDate),
            Builders<Movie>.Update.Set(x => x.UpdatedAt, DateTime.UtcNow)
        ));
}

Delete

We use moviesCollection and use DeleteOneAsync method of collection to delete the record using id.

public async Task Delete(Guid id)
{
    await moviesCollection.DeleteOneAsync(x => x.Id == id);
}

Please note we don’t throw RecordNotFoundException exception as we were doing in InMemoryMoviesStore, reason for that is trying to delete a record with a non existent key is not considered an error.

Setup Dependency Injection

Final step is to setup the Dependency Injection container to wireup the new created store. Update Program.cs as shown below

// builder.Services.AddSingleton<IMoviesStore, InMemoryMoviesStore>();
builder.Services.AddSingleton<IMoviesStore, MongoMoviesStore>();

For simplicity I have disabled InMemoryMoviesStore, we can add a configuration and based on that decide which service to use at runtime. That can be a good exercise however we don’t do that practically. However for traffic heavy services InMemory or Distributed Cache is used to cache results to improve performance.

Test

I am not adding any unit or integration tests for this tutorial, perhaps a following tutorial. But all the endpoints can be tested either by the Swagger UI by running the application or using Postman.

Source

Source code for the demo application is hosted on GitHub in blog-code-samples repository.

References

In no particular order

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...