ASP.NET MongoDB Example

Here's a working example of using MongoDB with ASP.NET / C# that demonstrates all the basic operations in MongoDB and encapsulates them in Web API. The example application is a simple Todo list, and the full source code is on GitHub. The example is pretty simplistic, but it's more or less the pattern I'm using right now for data access against MongoDB. MongoDB is a nice option for a backend if you don't need something SQL based, because it's very fast, easy to use, and quite reliable.

Prerequisites

You'll obviously need to install MongoDB before you can run the example. The official MongoDB documentation has good instructions per platform on how to install the free Community Edition.

After you've got MongoDB installed, you can use the shell to access MongoDB directly, or you can use a GUI. I personally like the free tool Robo 3T, although it's somewhat limited. MongoDB's Compass is probably the most popular free GUI option.

Note that for the example below, you don't need to create any Databases or Collections. They'll get automatically created for you.

Model

On to the actual .NET implementation. The first thing you're going to want to do is add the NuGet package for the MongoDB driver, and there's only one: MongoDB.Driver.

After that, we can define our model, which will be used in our actual application and serialize to/from MongoDB as well as in ASP.NET all the way up to the browser. We're making a simple Todo list application, so here's our model to hold a todo item using some of the decorative attributes from the MongoDB driver library:

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

	public string? Name { get; set; }

	public bool Done { get; set; }

	public int? Order { get; set; }

	[JsonIgnore]
	public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;

	[JsonIgnore]
	public DateTime LastModifiedUtc { get; set; } = DateTime.UtcNow;
}

Couple notes:

  • [BsonIgnoreExtraElements] is something you'll likely want to use, so that as you change models (e.g. if you remove a property), retrieving documens won't throw an exception if there's an extra element in the actual document in MongoDB.
  • [BsonId] marks that this is going to be our _id field in the collection in MongoDB.
  • [BsonRepresentation(BsonType.ObjectId)] is the most important attribute here, since it lets us use the Id as a string in C#/.NET code, including in serialization without adding extra code. And, it treats it like a MongoDB ObjectId (including autogenerating new values on inserts) when dealing with MongoDB.
  • We have [JsonIgnore] on some of our tracking fields that we want to keep in MongoDB, but we really don't want to pass up to the client when we serialize this object in our API controller.

Data Access

The pattern I like to use has a Context, which is then used by various Repository classes. The actual MongoClient object in this pattern in the Context, and is what is actually used to connect to MongoDB. Typically, when using dependency injection, you'd register the container for MongoClient as a singleton, and register any classes that use that as transient. The benefit of using the separate Context class, as opposed to mixing everything together is that you only have one singleton for MongoDB, and a bunch of transients. (Because in a production application, you'll typically have a number of Repository classes that are split roughly between types/areas.)

In this pattern, your interface has properties for each of your collections:

public interface IExampleMongoContext
{
	IMongoCollection<Todo> TodoItems { get; }
}

And our implementation of the interface has the actual client:

public class ExampleMongoContext : IExampleMongoContext
{
	private readonly IMongoDatabase _db;

	public IMongoCollection<Todo> TodoItems => _db.GetCollection<Todo>("TodoItems");

	public ExampleMongoContext(IOptions<MongoDbSettings> options)
	{
		var client = new MongoClient(options.Value.ConnectionString);
		_db = client.GetDatabase(options.Value.DatabaseName);
	}
}

Then, we write the interface for a Repository that has responsibilities for data access in a model area, such as our basic Todo, such as below:

public interface ITodoRepository
{
	Task<Todo> Get(string id);
	Task<IEnumerable<Todo>> GetAll();
	Task Insert(Todo value);
	Task<bool> Update(Todo value);
	Task<bool> UpdateOrder(Todo value);
	Task<bool> Delete(Todo value);
}

And our implementation of the Repository:

public class TodoRepository : ITodoRepository
{
	private readonly IExampleMongoContext _context;

	public TodoRepository(IExampleMongoContext context)
	{
		_context = context;
	}

	public Task<Todo> Get(string id)
    {
		var filter = Builders<Todo>.Filter.Eq(t => t.Id, id);

		return _context.TodoItems.Find(filter: filter).FirstOrDefaultAsync();
	}

	public async Task<IEnumerable<Todo>> GetAll()
	{
		var values = await _context
				.TodoItems
				.Find(_ => true)
				.ToListAsync();

		// Note, we want to sort first by the nullable Order field
		// with nulls after values, thus we're doing the sort
		// in memory vs. in MongoDB in the statement above.
		return values
			.OrderBy(t => t.Order == null)
			.ThenBy(t => t.Order)
			.ThenBy(t => t.CreatedUtc);
	}

	public Task Insert(Todo value)
	{
		return _context.TodoItems.InsertOneAsync(value);
	}

	public async Task<bool> Update(Todo value)
    {
		if (string.IsNullOrEmpty(value.Id))
			throw new ArgumentOutOfRangeException(nameof(value));

		var filter = Builders<Todo>.Filter.Eq(t => t.Id, value.Id);

		var updateDefinition =
			Builders<Todo>.Update
				.Set(t => t.Name, value.Name)
				.Set(t => t.Done, value.Done)
				.Set(t => t.LastModifiedUtc, DateTime.UtcNow);

		var updateResult =
			await _context
					.TodoItems
					.UpdateOneAsync(
						filter: filter,
						update: updateDefinition);

		return updateResult.IsAcknowledged && updateResult.ModifiedCount > 0;
	}

	public async Task<bool> UpdateOrder(Todo value)
	{
		if (string.IsNullOrEmpty(value.Id))
			throw new ArgumentOutOfRangeException(nameof(value));

		var filter = Builders<Todo>.Filter.Eq(t => t.Id, value.Id);

		var updateDefinition =
			Builders<Todo>.Update
				.Set(t => t.Order, value.Order)
				.Set(t => t.LastModifiedUtc, DateTime.UtcNow);

		var updateResult =
			await _context
					.TodoItems
					.UpdateOneAsync(
						filter: filter,
						update: updateDefinition);

		return updateResult.IsAcknowledged && updateResult.ModifiedCount > 0;
	}

	public async Task<bool> Delete(Todo value)
    {
		if (string.IsNullOrEmpty(value.Id))
			throw new ArgumentOutOfRangeException(nameof(value));

		var filter = Builders<Todo>.Filter.Eq(t => t.Id, value.Id);

		var deleteResult =
			await _context
					.TodoItems
					.DeleteOneAsync(filter: filter);

		return deleteResult.IsAcknowledged && deleteResult.DeletedCount > 0;
	}
}

Most of this is mostly straightforward and shows how to implement basic CRUD operations, but you might want to read the official documentation on the MongoDB C# driver for more detailed information. One thing we're doing above that is a little different than a stock example is that our updates are not just updating the whole document in MongoDB -- we're building update definitions that update just specific fields.

After we have our actual data access, we'll set up our class to hold our connection information:

public class MongoDbSettings
{
	public string? ConnectionString { get; set; }
	public string? DatabaseName { get; set; }
}

And then in our appsettings.json/appsettings.Development.json configuration files, we store our actual settings:

{
  "MongoDbSettings": {
    "ConnectionString": "mongodb://localhost:27017",
    "DatabaseName": "Example"
  }
}

Note that our settings here are pretty basic, and you'll likely want to have authentication and a replica set set up in your production MongoDB instance and reflected in your connection string.

Then, in our Program.cs (since we're using the simple file and this is a .NET 6 sample), we use our settings and set up dependency injection for our data access classes:

// We're setting up the settings, which will come from appsettings.json/appsettings.Development.json
builder.Services.Configure<MongoDbSettings>(builder.Configuration.GetSection("MongoDbSettings"));

// Then we're setting up the dependency injection for our context (which has the actual MongoClient,
// so it's Singleton) and our repository, which uses the context, as transient.
builder.Services.AddSingleton<IExampleMongoContext, ExampleMongoContext>();
builder.Services.AddTransient<ITodoRepository, TodoRepository>();

And a couple notes here:

  • Our IExampleMongoContext is set as a singleton, since it contains our MongoClient.
  • Our ITodoRepository is set as transient, since it depends on our IExampleMongoContext/singleton, and you might have a lot of these in a production application.

API Controller in ASP.NET

Then, our API controller that's exposed to the web depends on just our ITodoRepository, and our basic methods are mostly just a little more than passthroughs to our ITodoRepository. The main exception is PutOrder, which sets the order of our Todos in the list. Our example here is wide open has no authentication or authorization, and you'd really want to implement something to that effect in a production app.

[ApiController]
	[Route("api/todo")]
	public class TodoAPIController : Controller
	{
		private readonly ITodoRepository _todoRepository;

		public TodoAPIController(ITodoRepository todoRepository)
		{
			_todoRepository = todoRepository;
		}

        [HttpGet]
        public async Task<IActionResult> Get(string id)
        {
            var todo = await _todoRepository.Get(id);

            if (todo == null)
                return NotFound();

            return Ok(todo);
        }

        [HttpGet("list")]
        public async Task<IActionResult> GetAll()
        {
            var todos = await _todoRepository.GetAll();

            return Ok(todos);
        }

        [HttpPost]
        public async Task<IActionResult> Post(Todo todo)
        {
            await _todoRepository.Insert(todo);

            return CreatedAtAction(nameof(Get), new { id = todo.Id }, todo);
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> Put(string id, Todo todo)
        {
            var existingTodo = await _todoRepository.Get(id);

            if (existingTodo == null)
                return NotFound();

            todo.Id = existingTodo.Id;

            var result = await _todoRepository.Update(todo);

            // You probably want a better exception/logging than this
            // in a real application.
            if (!result)
                throw new Exception("Error updating Todo item");

            return Ok(todo);
        }

        [HttpPut("order")]
        public async Task<IActionResult> PutOrder(List<Todo> todos)
        {
            if (todos == null || todos.Count < 1)
                return Ok();

            int currentOrder = 0;

            // In a production application, you might want to perform
            // the logic below in a batch rather than individually.
            foreach (var todo in todos)
            {
                if (!string.IsNullOrEmpty(todo.Id))
                {
                    var existingTodo = await _todoRepository.Get(todo.Id);

                    if (existingTodo == null)
                        return NotFound();

                    existingTodo.Order = currentOrder;
                    var result = await _todoRepository.UpdateOrder(existingTodo);

                    // You probably want a better exception/logging than this
                    // in a real application.
                    if (!result)
                        throw new Exception("Error updating Todo item order!");

                    currentOrder += 1;
                }
            }

            return Ok();
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(string id)
        {
            var existingTodo = await _todoRepository.Get(id);

            if (existingTodo == null)
                return NotFound();

            var result = await _todoRepository.Delete(existingTodo);

            // You probably want a better exception/logging than this
            // in a real application.
            if (!result)
                throw new Exception("Error trying to delete Todo item");

            return Ok();
        }
    }

Example UI

Below is a quick preview of the simple UI that's in the example. The one below is dummy data with no API -- the version on GitHub is linked to an API that writes MongoDB.

No Todo items. Click Add to add one.