DEV Community

Alex Alves
Alex Alves

Posted on

Getting started with .NET Core API, MongoDB, and Transactions

Image description

MongoDB is a kind of NoSQL database. NoSQL is a document-oriented database that is organized as a JSON. Some points about MongoDB:

  • Performance: It stores a majority of data in RAM, so the query performance is much better here than in a relational database. But it requires more RAM and precise indexes.
  • Simplicity: Some users consider the query syntax is simpler here than in relational databases. The installation, configuration, and execution are simple and quick to do. And the learning curve is shorter than in the others.
  • Flexibility: It’s dynamic because it doesn’t have a predefined schema.
  • Flexibility: It’s dynamic because it doesn’t have a predefined schema.
  • Transaction: v3.6 and beyond allow using the transaction concept. And v4.0 and beyond allow using the multi-document transaction concept.

Now that you know a little more about MongoDB, let’s go to our goal! This article proposes to teach you how you can build a .NET Core API connecting with MongoDB and use the Transactions with Dependency Injection.

#ShowMeTheCode!

For this example, don’t consider the structure and the architecture, it was built in this way just because I think that it’s easier to explain. So, I won’t explain the layers, folders, and other things in detail, except those needed by the use of MongoDB and the transactions.

The project

In this case, I used the Visual Studio 2019, Community version. So, with the VS installed, we select the “ASP.NET Core Web Application” option and select the API type.

Image description

Image description

About the base structure

After creating the project, we create five folders like below:
Image description

  • Controllers: responsible to answer the requests
  • Entities: Domain classes
  • Interfaces: like contracts, we’ll use it to do the DI and IoC
  • Models: Classes that we’ll use to receive and return data on controllers
  • Repositories: Classes with methods that contain the implementation of MongoDB operations

We’ll focus on just Controllers and Repositories folders and the Startup class. If you want to see the complete code, wait for the end of this article.

Installing and configuring MongoDB

Now, we need to install the more important package for our project, that is the MongoDB.Driver.

Image description

After installed, we need to modify the Startup class, on the ConfigureServices method, specifically.

Let’s analyze this code:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddSingleton<IMongoClient>(c =>
    {
        var login = "";
        var password = Uri.EscapeDataString("");
        var server = "";

        return new MongoClient(
            string.Format("mongodb+srv://{0}:{1}@{2}/test?retryWrites=true&w=majority", login, password, server));
    });

    services.AddScoped(c => 
        c.GetService<IMongoClient>().StartSession());

    // ...
}
Enter fullscreen mode Exit fullscreen mode

The ConfigureServices method is where we do our IoC. So, the first statement is where we make the MongoDB connection. Pay attention in this step because we make a Singleton, we do this because the MongoClient instance, in MongoDB, is already a pool connection, so if you don’t use a Singleton, a new pool connection will always be created.

And the second statement is where we declare the IoC, which we’ll use to start a session of the MongoDB Transaction. Notice that, in this case, we make a Scoped, because the transaction life cycle will be equal to the request life cycle.

Creating a base repository

So now, let’s make a base repository that we will use whenever we want to do some operations. First, the base repository class will receive a Generics to identify the entity in our code that represents a collection in the database, like below:

After creating the class, let’s go to declare some attributes that will be important to us:

public class BaseRepository<T> : IRepositoryBase<T> where T : BaseEntity
{
    // some code
}
Enter fullscreen mode Exit fullscreen mode

We have four attributes:

public class BaseRepository<T> : IRepositoryBase<T> where T : BaseEntity
{
    private const string DATABASE = "poc_dotnet_mongodb";
    private readonly IMongoClient _mongoClient;
    private readonly IClientSessionHandle _clientSessionHandle;
    private readonly string _collection;
}
Enter fullscreen mode Exit fullscreen mode
  • DATABASE: it’s the Constant that represents the name of our database.
  • _mongoClient: it’s the client interface to MongoDB. Using it we do some operations on the database.
  • _clientSessionHandle: it’s the interface handle for a client session. So, if you start a transaction, you should pass the handle when you do some operation.
  • _collection: it’s the name of the collection used by the Generic class.

All these attributes will receive a value on a class constructor:

public class BaseRepository<T> : IRepositoryBase<T> where T : BaseEntity
{
    private const string DATABASE = "poc_dotnet_mongodb";
    private readonly IMongoClient _mongoClient;
    private readonly IClientSessionHandle _clientSessionHandle;
    private readonly string _collection;

    public BaseRepository(IMongoClient mongoClient, IClientSessionHandle clientSessionHandle, string collection)
    {
        (_mongoClient, _clientSessionHandle, _collection) = (mongoClient, clientSessionHandle, collection);

        if (!_mongoClient.GetDatabase(DATABASE).ListCollectionNames().ToList().Contains(collection))
            _mongoClient.GetDatabase(DATABASE).CreateCollection(collection);
    }
}
Enter fullscreen mode Exit fullscreen mode

Look at the constructor parameters. We receive these parameters by dependency injection. We get the values of parameters and we assign them to the attributes that we created.

Another very important thing is the code below of the assignments. If you work with transactions, on MongoDB, and work with a multi-document transaction, we need to create the collection first of all. And for it, we verify if the collection exists in our database and, if not, we create it. If we try to create a collection that already exists, the flow will break and throw an exception.

Now, we will do an important code too, specifically a virtual property that we use to facilitate to do some operations:

public class BaseRepository<T> : IRepositoryBase<T> where T : BaseEntity
{
    // ...some attributes and the constructor...

    protected virtual IMongoCollection<T> Collection =>
        _mongoClient.GetDatabase(DATABASE).GetCollection<T>(_collection);

    // ...some other codes...
}
Enter fullscreen mode Exit fullscreen mode

From the _mongoClient attribute, we retrieve the database and, from the _collection attribute, we retrieve the collection and associate it with the Generic class.

Finally, we build some base operations to change the data in the database like Insert, Update, and Delete:

public class BaseRepository<T> : IRepositoryBase<T> where T : BaseEntity
{
    public async Task InsertAsync(T obj) =>
        await Collection.InsertOneAsync(_clientSessionHandle, obj);

    public async Task UpdateAsync(T obj)
    {
        Expression<Func<T, string>> func = f => f.Id;
        var value = (string)obj.GetType().GetProperty(func.Body.ToString().Split(".")[1]).GetValue(obj, null);
        var filter = Builders<T>.Filter.Eq(func, value);

        if (obj != null)
            await Collection.ReplaceOneAsync(_clientSessionHandle, filter, obj);
    }

    public async Task DeleteAsync(string id) =>
        await Collection.DeleteOneAsync(_clientSessionHandle, f => f.Id == id);
}
Enter fullscreen mode Exit fullscreen mode

Some important things:

  • We have to pass the _clientSessionHandle for all the methods that do, in fact, the operation, like the InsertOneAsync method.
  • To do an Update operation, we build a Lambda Expression, using the ID property. And then, we use a Reflection to retrieve the ID value and we make the filter that will use to do, in fact, the operation.

Example of a specifical repository

Now I show you how we can use the Base Repository to do some other specifically repository. In this case, we’ll build an AuthorRepository.

First, we build the class with the inheritances and the constructor:

public class AuthorRepository : BaseRepository<Author>, IRepositoryAuthor
{
    public AuthorRepository(
        IMongoClient mongoClient,
        IClientSessionHandle clientSessionHandle) 
        : base(mongoClient, clientSessionHandle, "author")
    {
    }

    // some code...
}
Enter fullscreen mode Exit fullscreen mode

When we inherit the BaseRepository class, we force it to make a constructor as shown. Look at how we pass the collection name using a string “author”.

Finally, we build some other MongoDB operations, here we build only specific query operations to get data. It happens because we can get specific data in each moment, but the operations that change the database data (like insert, update, delete) will be always the same in this example.

public class AuthorRepository : BaseRepository<Author>, IRepositoryAuthor
{
    // ... constructor code

    public async Task<Author> GetAuthorByIdAsync(string id)
    {
        var filter = Builders<Author>.Filter.Eq(s => s.Id, id);
        return await Collection.Find(filter).FirstOrDefaultAsync();
    }

    public async Task<IEnumerable<Author>> GetAuthorsAsync() =>
        await Collection.AsQueryable().ToListAsync();

    public async Task<IEnumerable<Book>> GetBooksAsync(string authorId)
    {
        var filter = Builders<Author>.Filter.Eq(s => s.Id, authorId);
        return await Collection.Find(filter).Project(p => p.Books).FirstOrDefaultAsync();
    }

    public async Task<IEnumerable<Author>> GetAuthorsByNameAsync(string name)
    {
        var filter = Builders<Author>.Filter.Eq(s => s.Name, name);
        return await Collection.Find(filter).ToListAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this code, I show you some query operations examples:

  • We can recover one author searching by id.
  • We can recover all authors.
  • We can recover all books of a specific author, searching by author’s id, using the Project method.
  • We can recover some authors searching by name.

Business rules examples with transactions

Let’s use these repositories to do some business rules and use, in fact, the transactions! For this, we’ll make all business rules in the classes located in the Controller folder.

So, we build a class called BusinessController, like below:

[Route("api/[controller]")]
[ApiController]
public class BusinessController : ControllerBase
{
    private readonly IRepositoryUser _repositoryUser;
    private readonly IRepositoryAuthor _repositoryAuthor;
    private readonly IClientSessionHandle _clientSessionHandle;

    public BusinessController(
        IRepositoryUser repositoryUser, 
        IRepositoryAuthor repositoryAuthor, 
        IClientSessionHandle clientSessionHandle) =>
        (_repositoryUser, _repositoryAuthor, _clientSessionHandle) = 
            (repositoryUser, repositoryAuthor, clientSessionHandle);
}
Enter fullscreen mode Exit fullscreen mode

I’ll don’t talk about the Annotations and the inheritances class, because it’s specific for the API. So, look that we use the repositories that we created before and a session _clientSessionHandle that we’ll use to make the transactions. All these attributes we’ll receive on the constructor parameters by dependency injection.

So now, let’s show how we can use a transaction, in fact:

[Route("api/[controller]")]
[ApiController]
public class BusinessController : ControllerBase
{
    // ... some code

    [HttpPost]
    [Route("user")]
    public async Task<IActionResult> InsertUser([FromBody] CreateUserModel userModel)
    {
        _clientSessionHandle.StartTransaction();

        try
        {
            var user = new User(userModel.Name, userModel.Nin);
            await _repositoryUser.InsertAsync(user);
            await _clientSessionHandle.CommitTransactionAsync();

            return Ok();
        }
        catch (Exception ex)
        {
            await _clientSessionHandle.AbortTransactionAsync();

            return BadRequest(ex);
        }
    }

    // ... some code
}
Enter fullscreen mode Exit fullscreen mode

In this case, it’s a POST method, which means that we want to insert a new record on the database. In the method beginning, we need to start a transaction (line 11), then we can make our business rules and, if there is nothing wrong, we can Commit all changes we did on the database. But if something break, an exception will be thrown and the flow will begin on the Catch statement, thereby, we’ll abort all the transaction and nothing in the database will be changed.

Below you can look another example using multi-document transactions. Where we use more than one operation on the database:

[Route("api/[controller]")]
[ApiController]
public class BusinessController : ControllerBase
{
    // ... some code

    [HttpPost]
    [Route("authorAndUser")]
    public async Task<IActionResult> InsertAuthorAndUser([FromBody] CreateAuthorAndUserModel authorAndUserModel)
    {
        _clientSessionHandle.StartTransaction();

        try
        {
            var author = new Author(authorAndUserModel.AuthorModel.Name, new List<Book>(authorAndUserModel.AuthorModel.Books.Select(s => new Book(s.Name, s.Year))));
            var user = new User(authorAndUserModel.UserModel.Name, authorAndUserModel.UserModel.Nin);

            await _repositoryAuthor.InsertAsync(author);
            await _repositoryUser.InsertAsync(user);
            await _clientSessionHandle.CommitTransactionAsync();

            return Ok();
        }
        catch (Exception ex)
        {
            await _clientSessionHandle.AbortTransactionAsync();

            return BadRequest(ex);
        }
    }

    // ... some code
}
Enter fullscreen mode Exit fullscreen mode

The logic is the same as the code shown before.

I expect that you liked this article! If yes, give a like to it. If you have any doubt or any questions comment below.
You can find the complete code here!

Thank you!

Image description


References:

Discussion (0)