Using MongoDB .NET Driver with .NET Core WebAPI

What’s about

Problem / solution format brings an easier understanding on how to build things, giving an immediate feedback. Starting from this idea, the blog post I will present step by step how to build

a web application to store your ideas in an easy way, adding text notes, either from desktop or mobile, with few characteristics: run fast, save on the fly whatever you write, and be reasonably reliable and secure.

This article will implement just the backend, WebApi and the database access, in the most simple way.

A couple of updates done to the original article

  • Following Peter’s comment, I have simplified the documents returned, see HttpGet requests
  • Following Luciano’s comment, I have extend the update function, making update of the full MongoDB documents at once, not just to some of the properties. There is a new section below, describing this change
  • Trying to read from Angular 2, find the article here, I have ran into CORS problems. An error message was displayed “No ‘Access-Control-Allow-Origin’ header is present on the requested resource”. I have added a new section to describe the solution.
  • I have updated the project to .NET Core 1.1 as well to MongoDB .NET Driver 2.4
  • Added a basic level of exception management
  • Following Peter’s comment I have converted the solution to Visual Studio 2017
  • Updated to .NET Core 2.0

The GitHub project is updated and includes all these changes. You could directly download the sources or clone the project locally.

Topics covered

  • Technology stack
  • Configuration model
  • Options model
  • Dependency injection
  • MongoDb – Installation and configuration using MongoDB C# Driver v.2
  • Make a full ASP.NET WebApi project, connected async to MongoDB
  • Allowing Cross Domain Calls (CORS)
  • Update entire MongoDB documents
  • Exception management

You might be interested also

Technology stack

The ASP.NET Core Web API has the big advantage that it can be used as HTTP service and it can be subscribed by any client application, ranging from desktop to mobiles, and also be installed on Windows, macOS or Linux.

MongoDB is a popular NoSQL database that makes a great backend for Web APIs. These lend themselves more to document store type, rather than to relational databases. This blog will present how to build a .NET Core Web API connected asynchronously to MongoDB, with full support for HTTP GET, PUT, POST, and DELETE.

To install

Here are all the things needed to be installed:

Creating the ASP.NET WebApi project

Launch Visual Studio and then access File > New Project > .Net Core > ASP.NET Core Web Application.

and then

Configuration

There are multiple file formats, supported out of the box for the configuration (JSON, XML, or INI). By default, the WebApi project template comes with JSON format enabled. Inside the setting file, order matters, and include complex structures. Here is an example with a 2 level settings structure for database connection.
AppSettings.json – update the file:

{
  "MongoConnection": {
    "ConnectionString": "mongodb://admin:[email protected]",
    "Database": "NotesDb"
  },

  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  }
}

Dependency injection and Options model

Constructor injection is one of the most common approach to implementing Dependency Injection (DI), though not the only one. ASP.NET Core uses constructor injection in its solution, so we will also use it. ASP.NET Core project has a Startup.cs file, which configures the environment in which our application will run. The Startup.cs file also places services into ASP.NET Core’s Services layer, which is what enables dependency injection.

To map the custom database connection settings, we will add a new Settings class.

namespace NotebookAppApi.Model
{
    public class Settings
    {
        public string ConnectionString;
        public string Database;
    }
}

Here is how we modify Startup.cs to inject Settings in the Options accessor model:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();

    services.Configure<Settings>(options =>
    {
        options.ConnectionString = Configuration.GetSection("MongoConnection:ConnectionString").Value;
        options.Database = Configuration.GetSection("MongoConnection:Database").Value;
    });
}

Further in the project, settings will be access via IOptions interface:

IOptions<Settings>

MongoDB configuration

Once you have installed MongoDB, you would need to configure the access, as well as where the data is located.

To do this, create a file locally, named mongod.cfg. This will include setting path to the data folder for MongoDB server, as well as to the MongoDB log file, initially without any authentication. Please update these local paths, with your own settings:

systemLog:
  destination: file
  path: "C:\\tools\\mongodb\\db\\log\\mongo.log"
  logAppend: true
storage:
  dbPath: "C:\\tools\\mongodb\\db\\data"

Run in command prompt next line. This will start the MongoDB server, pointing to the configuration file already created (in case the server is installed in a custom folder, please update first the command)

"C:\Program Files\MongoDB\Server\3.2\bin\mongod.exe" --config C:\Dev\Data.Config\mongod.cfg

Once the server is started (and you could see the details in the log file), run mongo.exe in command prompt. The next step is to add the administrator user to the database. Run mongodb with the full path (ex: “C:\Program Files\MongoDB\Server\3.2\bin\mongo.exe”).
sketch

and then copy paste the next code in the console:

use admin
db.createUser(
  {
	user: "admin",
	pwd: "abc123!",
	roles: [ { role: "root", db: "admin" } ]
  }
);
exit;

Then stop the server and update the configuration file, including the security option.

systemLog:
  destination: file
  path: "C:\\tools\\mongodb\\db\\log\\mongo.log"
  logAppend: true
storage:
  dbPath: "C:\\tools\\mongodb\\db\\data"
security:
  authorization: enabled

From now on, we’ll connect to MongoDb using admin user. There is a good practice to not use the superuser role (in our case administrator) for normal operations, but in order to keep the things simple, we will continue to have just a single user.

MongoDB .NET Driver

To connect to MongoDB, add via Nuget the package named MongoDB.Driver. This is the new official driver for .NET, fully supporting the ASP.NET Core applications.

Model

The model class (POCO) associated with each entry in the notebook is included below:

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

namespace NotebookAppApi.Model
{
    public class Note
    {
        [BsonId]
        public string Id { get; set; }
        public string Body { get; set; } = string.Empty;
        public DateTime UpdatedOn { get; set; } = DateTime.Now;
        public DateTime CreatedOn { get; set; } = DateTime.Now;
        public int UserId { get; set; } = 0;
    }
}

Defining the database context

In order to keep the functions for accessing the database in a distinct place, we will add a NoteContext class. This will use the Settings defined above.

public class NoteContext
{
    private readonly IMongoDatabase _database = null;

    public NoteContext(IOptions<Settings> settings)
    {
        var client = new MongoClient(settings.Value.ConnectionString);
        if (client != null)
            _database = client.GetDatabase(settings.Value.Database);
    }

    public IMongoCollection<Note> Notes
    {
        get
        {
            return _database.GetCollection<Note>("Note");
        }
    }
}

Adding the repository

Using a repository interface, we will implement the functions needed to manage the Notes. These will also use Dependency Injection (DI) to be easily access from the application (e.g. controller section):

public interface INoteRepository
{
    Task<IEnumerable<Note>> GetAllNotes();
    Task<Note> GetNote(string id);
    Task AddNote(Note item);
    Task<DeleteResult> RemoveNote(string id);
    Task<UpdateResult> UpdateNote(string id, string body);

    // demo interface - full document update
    Task<ReplaceOneResult> UpdateNoteDocument(string id, string body);

    // should be used with high cautious, only in relation with demo setup
    Task<DeleteResult> RemoveAllNotes();
}

The access to database will be asynchronous. We are using here the new driver, which offers a full async stack.

Just as an example: to get all the Notes, we make an async request:

public async Task<IEnumerable<Note>> GetAllNotes()
{
    var documents = await _context.Notes.Find(_ => true).ToListAsync();
    return documents;
}

Here is the full implementation, for all basic CRUD operations:

public class NoteRepository : INoteRepository
{
    private readonly NoteContext _context = null;

    public NoteRepository(IOptions<Settings> settings)
    {
        _context = new NoteContext(settings);
    }

    public async Task<IEnumerable<Note>> GetAllNotes()
    {
        return await _context.Notes.Find(_ => true).ToListAsync();
    }

    public async Task<Note> GetNote(string id)
    {
        var filter = Builders<Note>.Filter.Eq("Id", id);
        return await _context.Notes
                             .Find(filter)
                             .FirstOrDefaultAsync();
    }

    public async Task AddNote(Note item)
    {
        await _context.Notes.InsertOneAsync(item);
    }

    public async Task<DeleteResult> RemoveNote(string id)
    {
        return await _context.Notes.DeleteOneAsync(
                     Builders<Note>.Filter.Eq("Id", id));
    }

    public async Task<UpdateResult> UpdateNote(string id, string body)
    {
        var filter = Builders<Note>.Filter.Eq(s => s.Id, id);
        var update = Builders<Note>.Update
                            .Set(s => s.Body, body)
                            .CurrentDate(s => s.UpdatedOn);
        return await _context.Notes.UpdateOneAsync(filter, update);
    }

    public async Task<ReplaceOneResult> UpdateNote(string id, Note item)
    {
        return await _context.Notes
                             .ReplaceOneAsync(n => n.Id.Equals(id)
                                                 , item
                                                 , new UpdateOptions { IsUpsert = true });
    }

    public async Task<DeleteResult> RemoveAllNotes()
    {
        return await _context.Notes.DeleteManyAsync(new BsonDocument());
    }
}

In order to access NoteRepository using DI model, we add a new line in ConfigureServices

services.AddTransient<INoteRepository, NoteRepository>();

where:

  • Transient: Created each time.
  • Scoped: Created only once per request.
  • Singleton: Created the first time they are requested. Each subsequent request uses the instance that was created the first time.

Adding the main controller

First we present the main controller. It provides all the CRUD interfaces, available to external applications.
The Get actions have NoCache directive, to ensure web clients make always requests to the server.

[Route("api/[controller]")]
public class NotesController : Controller
{
    private readonly INoteRepository _noteRepository;

    public NotesController(INoteRepository noteRepository)
    {
        _noteRepository = noteRepository;
    }

    [NoCache]
    [HttpGet]
    public Task<IEnumerable<Note>> Get()
    {
        return GetNoteInternal();
    }

    private async Task<IEnumerable<Note>> GetNoteInternal()
    {
        return await _noteRepository.GetAllNotes();
    }

    // GET api/notes/5
    [NoCache]
    [HttpGet("{id}")]
    public Task<Note> Get(string id)
    {
        return GetNoteByIdInternal(id);
    }

    private async Task<Note> GetNoteByIdInternal(string id)
    {
        return await _noteRepository.GetNote(id) ?? new Note();
    }

    // POST api/notes
    [HttpPost]
    public void Post([FromBody]string value)
    {
        _noteRepository.AddNote(new Note() 
                                { Body = value, 
                                  CreatedOn = DateTime.Now, 
                                  UpdatedOn = DateTime.Now });
    }

    // PUT api/notes/5
    [HttpPut("{id}")]
    public void Put(string id, [FromBody]string value)
    {
        _noteRepository.UpdateNote(id, value);
    }

    // DELETE api/notes/5
    public void Delete(string id)
    {
        _noteRepository.RemoveNote(id);
    }
}

Adding the admin controller

This will be a controller dedicated to administrative tasks (we use to initialize the database with some dummy data). In real projects, we should very cautiously use such interface. For development only and quick testing purpose, this approach may be convenient.

To use it, we will just add the url in the browser. Running the code below, the full setup will be automatically created (e.g. new database, new collection, sample records). We can use either http://localhost:5000/api/system/init or http://localhost:53617/api/system/init(when using IIS). We could even extend the idea, adding more commands. However, as mentioned above, these kind of scenarios should be used just for development, and be never deployed to a production environment.

[Route("api/[controller]")]
public class SystemController : Controller
{
    private readonly INoteRepository _noteRepository;

    public SystemController(INoteRepository noteRepository)
    {
        _noteRepository = noteRepository;
    }

    // Call an initialization - api/system/init
    [HttpGet("{setting}")]
    public string Get(string setting)
    {
        if (setting == "init")
        {
            _noteRepository.RemoveAllNotes();
            _noteRepository.AddNote(new Note() { Id = "1", Body = "Test note 1", 
                          CreatedOn = DateTime.Now, UpdatedOn = DateTime.Now, UserId = 1 });
            _noteRepository.AddNote(new Note() { Id = "2", Body = "Test note 2", 
                          CreatedOn = DateTime.Now, UpdatedOn = DateTime.Now, UserId = 1 });
            _noteRepository.AddNote(new Note() { Id = "3", Body = "Test note 3", 
                          CreatedOn = DateTime.Now, UpdatedOn = DateTime.Now, UserId = 2 });
            _noteRepository.AddNote(new Note() { Id = "4", Body = "Test note 4", 
                          CreatedOn = DateTime.Now, UpdatedOn = DateTime.Now, UserId = 2 });

            return "Done";
        }

        return "Unknown";
    }
}

Launch settings

In order to have a quick display of the values, once the project will run, please update the file launchSettings.json.

sketch

Here is the full file content, pointing by default to api/notes url.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:53617/",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/notes",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "NotebookAppApi": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "http://localhost:5000/api/notes",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Running the project

Before running the project, please make sure the MongoDB is running (either as an Windows Service, or via console application, as presented above).

Run first the initialization link:
http://localhost:53617/system/init

and then run the default application link
http://localhost:53617/api/notes

sketch

Use Robomongo

Using Robomongo we could check the actual entries inside the database. Connecting to the database, using the credentials, we could see all 4 records.

sketch

Running project on GitHub

Full source for this example is available on GitHub -> https://github.com/fpetru/WebApiMongoDB

Allowing Cross Domain Calls (CORS)

Being different applications, running on separate domains, all calls back to ASP.NET WebAPI site are effectively cross domain calls. With Angular 2, there is first a pre-flight request, before the actual request, (an OPTIONS request). Doing this pre-check, we verify first that cross domain calls are allowed (CORS).

I have enabled CORS by applying two changes:

  • First register CORS functionality in ConfigureServices() of Startup.cs:
  •  public void ConfigureServices(IServiceCollection services) 
     {
          // Add service and create Policy with options 
          services.AddCors(options => { options.AddPolicy("CorsPolicy", 
                                          builder => builder.AllowAnyOrigin() 
                                                            .AllowAnyMethod() 
                                                            .AllowAnyHeader() 
                                                            .AllowCredentials()); 
                                      }); 
          // .... 
    
          services.AddMvc(); 
     }
    
  • and then enable the policy globally to every request in the application by calling app.useCors() in the Configure()method of Startup, before UseMVC.
  •  public void Configure(IApplicationBuilder app) 
     { 
        // ... 
    
        // global policy, if assigned here (it could be defined individually for each controller) 
        app.UseCors("CorsPolicy"); 
    
        // ... 
    
        // We define UseCors() BEFORE UseMvc, below just a partial call
        app.UseMvc(routes => {
     }
    

Even if this could be further and more selective applied, the rest of the article remains unchanged.

Fully update the MongoDB documents

Initially the sample project included only selective update of the properties. Using ReplaceOneAsync we could update the full document. Upsert creates the document, in case it doesn’t already exist.

public async Task<ReplaceOneResult> UpdateNote(string id, Note item)
{
     return await _context.Notes
                          .ReplaceOneAsync(n => n.Id.Equals(id)
                                            , item
                                            , new UpdateOptions { IsUpsert = true });
} 

Test the update

To be able to test the update, I have used Postman. It is an excellent tool to test APIs.

I have selected the command type POST, then entered the local URL, and added a new Header (Content-Type as application/json).

ASP.NET Core WebAPI Set-header

And then set the Body as raw and updated a dummy value.
ASP.NET Core WebAPI Make the request

Using RoboMongo we can see the value updated.
MongoDB .NET Driver Updated document in Robomongo

Exception management

Starting with C# 5.0 async and await were introduced into the language to simplify using the Task Parallel Library. We can simply use a try/catch block to catch an exception, like so:

public async Task<IEnumerable<Note>> GetAllNotes()
{
    try
    {
        return await _context.Notes.Find(_ => true).ToListAsync();
    }
    catch (Exception ex)
    {
        // log or manage the exception
        throw ex;
    }
}

In this way we handle a faulted task by asynchronously wait for it to complete, using await. This will rethrow the original stored exception.

Initially I have used void as return. Changing the return type, the exception raised in the async method will get safely saved in the returning Task instance. When we await the faulty method, the exception saved in the Task will get rethrown with its full stack trace preserved.

public async Task AddNote(Note item)
{
    try
    {
        await _context.Notes.InsertOneAsync(item);
    }
    catch (Exception ex)
    {
        // log or manage the exception
        throw ex;
    }
}

Software developer, solution architect and technical lead.

43 comments On Using MongoDB .NET Driver with .NET Core WebAPI

  • Does this work with .NET Core 2.0? Would I need to change anything?

  • Hello Petru,
    I have followed all the above steps but The only output i get on the browser is the empty braces ” [ ]” and I don’t find the database and collection in MongoDB. Any help would be greatly appreciated.
    Thanks

  • Youre awesome!

  • Hello,

    I am trying to setup a similar structure but within a .NET Core web application instead of an API. I have everything setup in a similar way, but now I am trying to call the equivalent to your NotesController. I am unable to create INotesRepository to be sent into the constructor function, I am not familiar with how web API’s work therefore I don’t see how that is being sent into your NotesController function as well. Any information is much appreciated! Thanks, Al

  • Hi,

    I want to connect to remote mongo db which is located in different server. Where i have to specify the mongo db server name

  • Why create all files under one structure. why not add different class libraries and import them?

    • I have tried to create the project as simple as possible. When you make something bigger than this concise example, yes, it would be a good idea to structure it and make it modular, and easier to maintain.

      Best regards
      Petru

  • Why Transient and not scoped?

    • Hi Nicolas,

      Thanks for your comment. I had an issue with the syntax highlighter. The code is now displayed correctly.

      Kind regards
      Petru

    • Hi Nicolas,

      I usually choose Transient, then scaling up to Scoped or Singleton, when the situation calls for it. There is usually a connection pooling, with an implicit re-using the actual database connections, behind the hood, and for such a simple example, Transient was a better fit.

      Scoped services are indeed good, since they can be used to maintain some context during the given request, and they are isolated from other requests happening at the same time. However, the logic of the app would need also to be more extended.

      Kind regards
      Petru

  • Hello. This article have a mistake. Instead
    services.Configure(options =>
    Should be
    services.Configure(options =>

  • Thanks for the post, Peter. I got one question: I was stuck at the creating user part for mongodb. How could you db.createUser after setting “security authentication” to “enabled”? It throws and error as “not authorized on admin to execute command” every time when I try to creatUser, but if I comment the security line in mongod.conf file, it allows me to create a user. I looked it up but none of the solutions work for me. Do you know why this is happening?

    • Hi Claire,

      Yes, it is true, and thanks for the hint. I have missed to present this step initially and now I have updated the article.

      At first we need to disable the authentication, and create a single user with a superuser role. Then we would stop the server, and enable the authentication. From there, we would connect with this user (“admin” in our case) and we would run any other tasks (query, insert, new user creation etc.).

      Kind regards
      Petru

  • Salve Petru,
    Felicitari pentru articol! Long time no see! Scrie-mi te rog pe email, chiar as vrea sa mai povestim!
    Best,
    John

  • Thank you for a really good and simple project. I greatly enjoyed building it in Visual Studio 15.
    However, there seems to be some problems with both building and running the finished project in Visual Studio 17.
    I get a lot of errors with references. I know Microsoft has decided to use *.csproj instead of project.json and all references are stored here from now on. But even when I follow your tutorial and add them manually via NuGet I still get errors. I also cannot use your completed version as it does not work in Visual Studio 17, so I was wondering if you could update your project to be compatable with Visual Studio 17 or maybe add a small section outlining how to do this. It is a really good tutorial and I would very much like to make it work with the latest version Visual Studio Community.

  • Great work , Thanks for the article

  • great job, thank you very much!

    Run first the initialization link:
    http://localhost:53617/system/init

    should be
    http://localhost:53617/api/system/init

  • How can I update the whole document (all properties) without set each property manually ?

    • I have updated the article, and included the code necessarily to make the updates of the MongoDB documents. Basically use ReplaceOneAsync. Please let me know if this worked for you.

  • should be

    [HttpGet]
    public Task<IEnumerable> Get()
    {
    return GetNoteInternal();
    }

  • by the way, why return the notes as a string from the controller. Why not return something strongly typed and let the framework do the serialization to JSON like so:

    [HttpGet]
    public Task<IEnumerable> Get()
    {
    return GetNoteInternal();
    }

    private async Task<IEnumerable> GetNoteInternal()
    {
    var notes = await _noteRepository.GetAllNotes();
    return notes;
    }

  • great job, keep it coming!

Leave a reply:

Your email address will not be published.

Site Footer