29

I know that MongoDB is not supposed to support unit of work, etc. But I think it would be nice to implement the repository which would store only the intentions (similar to criteria) and then commit them to the DB. Otherwise in every method in your repository you have to create connection to DB and then close it. If we place the connection to DB in some BaseRepository class, then we tie our repository to concrete DB and it is really difficult to test repositories, to test IoC which resolve repositories.

Is creating a session in MongoDB a bad idea? Is there a way to separate the connection logic from repository?

Here is some code by Rob Conery. Is it a good idea to always connect to your DB on every request? What is the best practice?

There is one more thing. Imagine I want to provide an index for a collection. Previously I did in a constructor but with Rob's approach this seems out of logic to do it there.

 using Norm;
    using Norm.Responses;
    using Norm.Collections;
    using Norm.Linq;

    public class MongoSession {

        private string _connectionString;

        public MongoSession() {
            //set this connection as you need. This is left here as an example, but you could, if you wanted,
            _connectionString = "mongodb://127.0.0.1/MyDatabase?strict=false";
        }

        public void Delete<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new() {
            //not efficient, NoRM should do this in a way that sends a single command to MongoDB.
            var items = All<T>().Where(expression);
            foreach (T item in items) {
                Delete(item);
            }
        }

        public void Delete<T>(T item) where T : class, new() {
            using(var db = Mongo.Create(_connectionString))
            {
              db.Database.GetCollection<T>().Delete(item);
            }
        }

        public void DeleteAll<T>() where T : class, new() {
            using(var db = Mongo.Create(_connectionString))
            {
              db.Database.DropCollection(typeof(T).Name);
            }
        }

        public T Single<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new() {
            T retval = default(T);
            using(var db = Mongo.Create(_connectionString))
            {
              retval = db.GetCollection<T>().AsQueryable()
                         .Where(expression).SingleOrDefault();
            }
            return retval;
        }

        public IQueryable<T> All<T>() where T : class, new() {
            //don't keep this longer than you need it.
            var db = Mongo.Create(_connectionString);
            return db.GetCollection<T>().AsQueryable();
        }

        public void Add<T>(T item) where T : class, new() {
            using(var db = Mongo.Create(_connectionString))
            {
              db.GetCollection<T>().Insert(item);
            }
        }

        public void Add<T>(IEnumerable<T> items) where T : class, new() {
            //this is WAY faster than doing single inserts.
            using(var db = Mongo.Create(_connectionString))
            {
              db.GetCollection<T>().Insert(items);
            }
        }

        public void Update<T>(T item) where T : class, new() {
            using(var db = Mongo.Create(_connectionString))
            {
              db.GetCollection<T>().UpdateOne(item, item);
            }
        }

        //this is just some sugar if you need it.
        public T MapReduce<T>(string map, string reduce) {
            T result = default(T);
            using(var db = Mongo.Create(_connectionString))
            {
            var mr = db.Database.CreateMapReduce();
            MapReduceResponse response =
                mr.Execute(new MapReduceOptions(typeof(T).Name) {
                    Map = map,
                    Reduce = reduce
                });
            MongoCollection<MapReduceResult<T>> coll = response.GetCollection<MapReduceResult<T>>();
            MapReduceResult<T> r = coll.Find().FirstOrDefault();
            result = r.Value;
            }
            return result;
        }

        public void Dispose() {
            _server.Dispose();
        }
    }
Yurii Hohan
  • 4,021
  • 4
  • 40
  • 54
  • How about dependency injection via a framework (or self-written)? – DrColossos Aug 23 '11 at 12:10
  • @DrColossos, Actually I use a framework called Castle.Windsor. I've seen some code by Rob Conery simulating sessions but he connects to DB on every create/update/delete/search and I do not if it is optimal – Yurii Hohan Aug 23 '11 at 12:13
  • have a look at [MongoDB.Entities](https://github.com/dj-nitehawk/MongoDB.Entities) it is a generic repo with many conveniences. – Dĵ ΝιΓΞΗΛψΚ May 25 '19 at 15:41

4 Answers4

20

Don't worry too much about opening and closing connections. The MongoDB C# driver maintains an internal connection pool, so you won't suffer overheads of opening and closing actual connections each time you create a new MongoServer object.

You can create a repository interface that exposes your data logic, and build a MongoDB implementation that is injected where it's needed. That way, the MongoDB specific connection code is abstratced away from your application, which only sees the IRepository.

Be careful trying to implement a unit-of-work type pattern with MongoDB. Unlike SQL Server, you can't enlist multiple queries in a transaction that can be rolled back if one fails.

For a simple example of a repository pattern that has MongoDB, SQL Server and JSON implementations, check out the NBlog storage code. It uses Autofac IoC to inject concrete repositories into an ASP.NET MVC app.

Chris Fulstow
  • 41,170
  • 10
  • 86
  • 110
  • Chris, thank you for the answer. I've looked into your code for repository. In the constructor you create and keep throughout the objects lifetime an instance of MongoServer. For example, your Unit test code for IoC has to resolve some repository. It means your build server might fail because there is no mongos process in it. And even conceptually it does not seem OK – Yurii Hohan Aug 24 '11 at 05:25
  • @Hohhi those are integration tests rather than unit tests, they expect (and require) a mongod process running locally to pass. – Chris Fulstow Aug 25 '11 at 07:34
  • You could consider separating the responsibility for creating indexes away from the application logic, and think of it more as a manual DBA task. However, there's nothing to stop you running ensureIndex() each time a repository class is created, or having some other class that executes when the application starts. – Chris Fulstow Aug 25 '11 at 08:31
  • If I call ensure index each time repo is created, then I get the exception of no DB connection and I think separating it from app is a better solution – Yurii Hohan Aug 25 '11 at 08:50
3

While researching design patterns, I was creating a basic repository pattern for .Net Core and MongoDB. While reading over the MongoDB documentation I came across an article about transactions in MongoDB. In the article it specified that:

Starting in version 4.0, MongoDB provides the ability to perform multi-document transactions against replica sets.

Looking around the intertubes I came across a library that does a really good job of implementing the Unit of Work pattern for MongoDB.

bschreck
  • 183
  • 1
  • 9
0

If you are interested in an implementation similar to Rob Connery's and NBlog storage code but using the mongodb csharp driver 2.0 (that is asynchronous), you can look at:

https://github.com/alexandre-spieser/mongodb-generic-repository

You can then write a custom repository inheriting from BaseMongoRepository.

public interface ITestRepository : IBaseMongoRepository
{
    void DropTestCollection<TDocument>();
    void DropTestCollection<TDocument>(string partitionKey);
}

public class TestRepository : BaseMongoRepository, ITestRepository
{
    public TestRepository(string connectionString, string databaseName) : base(connectionString, databaseName)
    {
    }

    public void DropTestCollection<TDocument>()
    {
        MongoDbContext.DropCollection<TDocument>();
    }

    public void DropTestCollection<TDocument>(string partitionKey)
    {
        MongoDbContext.DropCollection<TDocument>(partitionKey);
    }
}
Darxtar
  • 2,022
  • 22
  • 21
0

Briefly

You can use this nuget-package UnitOfWork.MongoDb. This is a wrapper for MongoDb.Driver with some helpful functions and features. Also you can find sample code and video (ru).

Read settings for connection

// read MongoDb settings from appSettings.json
services.AddUnitOfWork(configuration.GetSection(nameof(DatabaseSettings)));

// --- OR ----

// use hardcoded
services.AddUnitOfWork(config =>
{
    config.Credential = new CredentialSettings { Login = "sa", Password = "password" };
    config.DatabaseName = "MyDatabase";
    config.Hosts = new[] { "Localhost" };
    config.MongoDbPort = 27017;
    config.VerboseLogging = false;
});

Injections

namespace WebApplicationWithMongo.Pages
{
    public class IndexModel : PageModel
    {
        private readonly IUnitOfWork _unitOfWork;
        private readonly ILogger<IndexModel> _logger;

        public IndexModel(IUnitOfWork unitOfWork, ILogger<IndexModel> logger)
        {
            _unitOfWork = unitOfWork;
            _logger = logger;
        }

        public IPagedList<Order>? Data { get; set; }
    }
}

After injection you can get repository.

Get repository

public async Task<IActionResult> OnGetAsync(int pageIndex = 0, int pageSize = 10)
{
    var repository = _unitOfWork.GetRepository<Order, int>();
    Data = await repository.GetPagedAsync(pageIndex, pageSize, FilterDefinition<Order>.Empty, HttpContext.RequestAborted);
    return Page();
}

GetPagedAsync one of some helpful implementations

Transactions

If you need ACID operations (transactions) you can use IUnitOfWork something like this. (Replicate Set should be correctly set up). For example:


await unitOfWork.UseTransactionAsync<OrderBase, int>(ProcessDataInTransactionAsync1, HttpContext.RequestAborted, session);

Method ProcessDataInTransactionAsync1 can be looks like this:

async Task ProcessDataInTransactionAsync1(IRepository<OrderBase, int> repositoryInTransaction, IClientSessionHandle session, CancellationToken cancellationToken)
{
    await repository.Collection.DeleteManyAsync(session, FilterDefinition<OrderBase>.Empty, null, cancellationToken);

    var internalOrder1 = DocumentHelper.GetInternal(99);
    await repositoryInTransaction.Collection.InsertOneAsync(session, internalOrder1, null, cancellationToken);
    logger!.LogInformation("InsertOne: {item1}", internalOrder1);

    var internalOrder2 = DocumentHelper.GetInternal(100);
    await repositoryInTransaction.Collection.InsertOneAsync(session, internalOrder2, null, cancellationToken);
    logger!.LogInformation("InsertOne: {item2}", internalOrder2);

    var filter = Builders<OrderBase>.Filter.Eq(x => x.Id, 99);
    var updateDefinition = Builders<OrderBase>.Update.Set(x => x.Description, "Updated description");
    var result = await repositoryInTransaction.Collection
        .UpdateOneAsync(session, filter, updateDefinition, new UpdateOptions { IsUpsert = false }, cancellationToken);

    if (result.IsModifiedCountAvailable)
    {
        logger!.LogInformation("Update {}", result.ModifiedCount);
    }

    throw new ApplicationException("EXCEPTION! BANG!");
}

This nuget is open-source Calabonga.UnitOfWork.MongoDb

Calabonga
  • 310
  • 3
  • 11