Create Web APIs with ASP.NET Core 6 and MongoDB with multiple collections

Aristoteles Joel Nici
carpediem-tech
Published in
6 min readJul 26, 2023

Today we want to create web APIs for an application using ASP.NET Core 6 and MongoDb database.

The purpose will be to create the necessary services to build APIs for interacting with multiple object models and saving them to a NoSQL database.

To achieve this, we will still follow the documentation provided by Microsoft. Specifically, you can find the information at this link.

You can comfortably follow the documentation until the section “Add a configuration model.” From this point onwards, we will make some modifications to facilitate the transition from a single collection to multiple collections.

Personally, I have already written the base Book.cs class without the[BsonElement("Name”)] attribute. For what we want to see, it doesn’t matter much.

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace BookStoreApi.Models;

public class Book
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }

public string BookName { get; set; } = null!;

public decimal Price { get; set; }

public string Category { get; set; } = null!;

public string Author { get; set; } = null!;
}

In the above class, the Id property is:

  • Required for mapping the Common Language Runtime (CLR) object to the MongoDB collection.
  • Annotated with [BsonId] to make this property the document’s primary key.
  • Annotated with [BsonRepresentation(BsonType.ObjectId)] to allow passing the parameter as type string instead of an ObjectId structure. Mongo handles the conversion from string to ObjectId.

We create a DatabaseSettings class inside the Models folder.

namespace BookStoreApi.Models
{
public class DatabaseSettings
{
public string ConnectionString { get; set; } = null!;

public string DatabaseName { get; set; } = null!;

}
}

The above class is used to store database connection and name inside the appsettings.json file. These connection properties will be used to set up connection between MongoDB database and .NET application.

We can add connection properties inside the appsettings.json file as well.

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"DatabaseSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BookStore"
}
}

We can create an BookService.cs inside a new folder “Services”

using Microsoft.Extensions.Options;
using MongoDB.Driver;
using Book.Models;

namespace BookStoreApi.Services
{
public class BookService
{
private readonly IMongoCollection<Book> _bookCollection;

public BookService(
IOptions<DatabaseSettings> dabaseSettings)
{
var mongoClient = new MongoClient(dabaseSettings.Value.ConnectionString);

var mongoDatabase = mongoClient.GetDatabase(dabaseSettings.Value.DatabaseName);

string className = typeof(T).Name;
_bookCollection= myRepository.mongoDatabase.GetCollection<T>(className);
}

public async Task<List<Book>> GetAsync() =>
await _bookCollection.Find(_ => true).ToListAsync();

public async Task<Book?> GetAsync(string id) =>
await _bookCollection.Find(x => x.Id == id).FirstOrDefaultAsync();

public async Task CreateAsync(Book newBook) =>
await _bookCollection.InsertOneAsync(newBook);

public async Task UpdateAsync(string id, Book updatedBook) =>
await _bookCollection.ReplaceOneAsync(x => x.Id == id, updatedBook);

public async Task RemoveAsync(string id) => await _bookCollection.DeleteOneAsync(x => x.Id == id);
}
}

In the above service, we have implemented all the methods for CRUD operations with MongoDB database. We will use this service inside our API controller soon.

We change the Program.cs file with the code changes below.

using Book.Models;
using Book.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.Configure<DatabaseSettings>(builder.Configuration.GetSection("DatabaseSettings"));
builder.Services.AddSingleton<BookService>();

builder.Services.AddControllers();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddCors(x => x.AddPolicy("CorsPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
}));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.UseCors("CorsPolicy");

app.Run();

The above code is used to register dependency injection with appsettings.json file’s DatabaseSettings properties. Also, BookService class is registered with DI to support constructor injection in consuming classes. The singleton service lifetime is most proper because BookServicetakes a direct dependency on MongoClient.

We can create an API controller BookController inside the “Controllers” folder and add the code below.

using Microsoft.AspNetCore.Mvc;
using Book.Models;
using Book.Services;

namespace BookStoreApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
private readonly BookService _bookService;

public BookController(BookService bookService) => _bookService = bookService;

[HttpGet]
public async Task<List<Book>> Get() => await _bookService.GetAsync();

[HttpGet("{id:length(24)}")]
public async Task<ActionResult<Book>> Get(string id)
{
var book = await _bookService.GetAsync(id);

if (book is null)
{
return NotFound();
}

return book;
}

[HttpPost]
public async Task<IActionResult> Post(Book newBook)
{
await _bookService.CreateAsync(newBook);

return NoContent();
}

[HttpPut("{id:length(24)}")]
public async Task<IActionResult> Update(string id, Book updatedBook)
{
var book = await _bookService.GetAsync(id);

if (book is null)
{
return NotFound();
}

updatedBook.Id = book.Id;

await _bookService.UpdateAsync(id, updatedBook);

return NoContent();
}

[HttpDelete("{id:length(24)}")]
public async Task<IActionResult> Delete(string id)
{
var book= await _bookService.GetAsync(id);

if (bookis null)
{
return NotFound();
}

await _bookService.RemoveAsync(id);

return NoContent();
}
}
}

We have used BookService class to implement CRUD operations inside the API controller.

Controller has action methods to support GET, POST, PUT, and DELETE HTTP requests.

We have completed the Web API application. You can use swagger to test the CRUD operations if needed.

Now, however, we want to restructure the code so that we can create multiple objects, collections, and controllers.

First, let’s modify our Book.cs class. Or rather, we should create an interface and a base class that will group all the common and fundamental elements for interacting with the database.

In our example, we obtain these classes:

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace BookStoreApi.Models;

public interface IBaseEntity
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
string? Id { get; set; }
}
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace BookStoreApi.Models;

public class BaseEntity : IBaseEntity
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string? Id { get; set; }
}
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace BookStoreApi.Models;

public class Book : BaseEntity
{
public string BookName { get; set; } = null!;

public decimal Price { get; set; }

public string Category { get; set; } = null!;

public string Author { get; set; } = null!;
}

All classes that we want to save in our database as collections must inherit from the BaseEntity class.

We create a class MyMongoRepository.cs:

using MongoDB.Driver;

namespace BookStoreApi.Services;

public class MyMongoRepository
{
public IMongoDatabase mongoDatabase;

public MyMongoRepository(
IOptions<DatabaseSettings> dabaseSettings)
{
var mongoClient = new MongoClient(dabaseSettings.Value.ConnectionString);

mongoDatabase = mongoClient.GetDatabase(dabaseSettings.Value.DatabaseName);
}
}

In the above class, we isolate the behavior of the BookService class, which will be used for all the service classes we create.

Specifically, we have:

  • Defined a parameterized constructor that takes a parameter of type IOptions<DatabaseSetting>. This is a configuration class that contains the database settings. This parameter is injected through the dependency injection in ASP.NET Core.
  • Created an instance of MongoClient, which is an object provided by the official MongoDB driver for C#. This object represents a MongoDB client and manages the connection to the MongoDB server. We retrieve the database connection string from the DatabaseSettingsusing dabaseSettings.Value.ConnectionString and use it to create the MongoClient.
  • With the MongoClientcreated, we can obtain an instance of IMongoDatabase, which represents the specific database we want to interact with. We use the database name, also retrieved from the DatabaseSettingsusing dabaseSettings.Value.DatabaseName, to get the IMongoDatabaseinstance.

At this point, we no longer need to have BookServicein DI (Dependency Injection), but we need to register our MyMongoRepositoryclass in the DI container in Program.cs:

using Book.Models;
using Book.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.Configure<DatabaseSettings>(builder.Configuration.GetSection("DatabaseSettings"));
/builder.Services.AddSingleton<BookService>();
builder.Services.AddSingleton<MyMongoRepository>();

builder.Services.AddControllers();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddCors(x => x.AddPolicy("CorsPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
}));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.UseCors("CorsPolicy");

app.Run();

Now, let’s group all the methods for CRUD operations with the MongoDB database into a single class called BaseService.cs, which will serve as the parent class for all the service classes we create for various collections we want to have in our database:

using MongoDB.Driver;
using Book.Models;

namespace BookStoreApi.Services
{
public class BaseService<T> where T : class, IBaseEntity, new()
{
private readonly IMongoCollection<T> _collection;

public BaseService(MyMongoRepository myRepository)
{
string className = typeof(T).Name;
_collection = myRepository.mongoDatabase.GetCollection<T>(className);
}

public async Task<List<T>> GetAsync() =>
await _collection.Find(_ => true).ToListAsync();

public async Task<T?> GetAsync(string id) =>
await _collection.Find(x => x.Id == id).FirstOrDefaultAsync();

public async Task CreateAsync(T newItem) =>
await _collection.InsertOneAsync(newItem);

public async Task UpdateAsync(string id, T updatedItem) =>
await _collection.ReplaceOneAsync(x => x.Id == id, updatedItem);

public async Task RemoveAsync(string id) => await _collection.DeleteOneAsync(x => x.Id == id);
}
}

The above class is designed to work with a generic data type T, which will be specified when creating an instance of the BaseService class. The generic type T must satisfy 2 requirements:

  • T must be a reference to a class type (it cannot be a primitive type like int or double).
  • T must implement the IBaseIteminterface. This means that we cannot use the BaseSeriviceclass with objects that do not adhere to the rules defined in IBaseItem.
  • T must be instantiated directly, so it cannot be an abstract class or an interface.

At this point, our BookService.cs class becomes:

using MongoDB.Driver;
using Book.Models;

namespace BookStoreApi.Services
{
public class BookService : BaseService<Book>
{
public BookService(MyMongoRepository repository) : base(repository)
{

}
}
}

While the constructor of the BookControllerbecomes:

public BookController(MyMongoRepository repo)
{
_bookService = new BookService(repo);
}

Now we have completed our Web API application. You can use Swagger to test the CRUD operations if needed.

Feel free to create new model classes, services, and new controllers as well.

--

--