1

We recently started using repository pattern and aggregate root pattern. This is all working super well when I am tracking the changes using EntityFramework, and I am able to call SaveChanges, when I am done with the work on my aggregate.

Now my problem is that I also want to use this pattern with my mongodb, but by nature, mongodb does not support change tracking. Rolling my own change tracking seems a bit of an overkill.

My problem is that I cannot do work on my entity in the associated aggregate, and then use the repository to save it back to mongodb. I can only do this if I use ReplaceOneAsync, which does not seem to be the best fit, since it is a chat I am creating and therefor might have several write operations from different clients.

I would like to have some kind of change tracking that will allow me to call SaveChanges on the repository with the specified aggregate.

Her is some pseudo-code that tries to explain what I want to do:

public abstract class BaseMongoRepository<TAggregate, TCollection> : IRepository<TAggregate> where TAggregate : BaseAggregateRoot, IMongoAggregate<TCollection>
{
    private readonly IMongoCollection<TCollection> _mongoCollection;

    protected BaseMongoRepository(IMongoCollection<TCollection> mongoCollection)
    {
        _mongoCollection = mongoCollection;
    }

    public async Task<bool> SaveAsync(TAggregate aggregate)
    {
        var state = aggregate.GetState();
        foreach (var stateChange in state.Changes)
        {
            var change = stateChange.ToUpdateDefinition();
            await _mongoCollection.UpdateOneAsync<TCollection>(aggregate.GetSelector(), change);
        }

        return true;
    }

    public TAggregate GetMongoAggregate(Expression<Func<TCollection, bool>> selector)
    {
        var state = _mongoCollection.AsQueryable().SingleOrDefault(selector);
        return new AggregateWithSelector(state, selector);
    }
}

The GetMongoAggregate would be implemented in the specific versions of the repository, but is here for pseudo purpose.

Hopefully somebody can send me in the right direction or give me some advice on how to model this.

Kristian Barrett
  • 3,574
  • 2
  • 26
  • 40
  • SaveChanges in EntityFramework sends set of command in a transaction to Sql DB (it's Unit of work pattern). Key difference of MongoDB is it doesn't support transactions. I think it's better to find approach which works well with MongoDB. – rnofenko Nov 04 '16 at 20:11

1 Answers1

1

Here is a simple example of my implementation (if you need real example you can write me in private)

I have a MongoDB collection:

public class TestCollection
{
    public string Id { get; set; }
    public string Name { get; set; }
}

And I made following aggregate root for the collection:

public class TestCollectionAggregateRoot : IAgregateRoot, 
    IHasObserver<IMongoObserver<TestCollection>>,
    IHasSelector<IMongoSelector<TestCollection>>
{
    private readonly IMongoSelector<TestCollection> _selector;
    private readonly IMongoObserver<TestCollection> _observer;
    private string _name;

    public TestCollectionAggregateRoot(TestCollection root, IMongoSelector<TestCollection> selector, IMongoObserver<TestCollection> observer)
    {
        _selector = selector;
        _observer = observer;
        _name = root.Name;
    }

    public string Name
    {
        get { return _name; }
        set
        {
            if (_name != value)
            {
                _name = value;
                _observer.OnPropertyChanged(x=>x.Name, _name);
            }
        }
    }

    public IMongoObserver<TestCollection> Observer => _observer;
    public IMongoSelector<TestCollection> Selector => _selector;
}

where

public interface IMongoObserver<TCollection>
{
    void OnPropertyChanged<TField>(Expression<Func<TCollection, TField>> func, TField value);

    UpdateDefinition<TCollection> Definition { get; }
}

public interface IMongoSelector<TCollection>
{
    FilterDefinition<TCollection> Definition { get; }
}

Simple implementation for IMongoObserver:

public class MongoObserver<TCollection> : IMongoObserver<TCollection>
{
    private volatile UpdateDefinition<TCollection> _definition;

    public void OnPropertyChanged<TField>(Expression<Func<TCollection, TField>> func, TField value)
    {
        if (Definition != null)
        {
           _definition = Definition.Set(func, value);
        }
        else
        {
            _definition = Builders<TCollection>.Update.Set(func, value);
        }
    }
    public UpdateDefinition<TCollection> Definition => _definition;
}

Simple example Repository.SaveAsync (without InsertOneAsync)

public class Repository : IRepository<TestCollectionAggregateRoot>
{
    private readonly IMongoCollection<TestCollection> _mongoCollection;

    public Repository(IMongoCollection<TestCollection> mongoCollection)
    {
        _mongoCollection = mongoCollection;
    }

    public async Task<bool> SaveAsync(TestCollectionAggregateRoot aggregate)
    {
        var changes = aggregate.Observer.Definition;
        var selector = aggregate.Selector.Definition;

        await _mongoCollection.UpdateOneAsync(selector, changes);
        return true;
    }
}
R.Titov
  • 3,115
  • 31
  • 35
  • I thought about something like this as well, but then there is the problem with complex types, such as arrays and objects. How do you handle updating those without replacing the whole array or object? (I'm on my mobile, so excuse me if I'm overlooking something) – Kristian Barrett Dec 28 '16 at 16:10
  • The Aggregator knows about UpdateDefinition inside, and we can use it for example the method Push to modification of items in array of the root. The main idea, that aggregator should return UpdateDefinition in SaveAsync to apply all updates. – R.Titov Dec 28 '16 at 16:33
  • But then you are tying your aggregate to the db implementation, which was what I tried to avoid. Your aggregate now needs to both know about the unit of work and an observer. Unless you are rolling your own db updates based on an observer, this does not help me with the problem of the aggregate for SQL and mongo having different implementations. – Kristian Barrett Dec 28 '16 at 16:54
  • Another problem with this approach is that you are not able to update an object of an array using the positional operator as described here http://stackoverflow.com/a/33720549/1958344 – Kristian Barrett Dec 28 '16 at 16:59
  • As I told in my answer, I have provided just most simple example. In my project logic from IMongoObserver segregated between different interfaces. All Mongo related are internal. Additionally I don't use collection in my aggregators. I have a a lot of mapping logic, and more 'powerful' observers. – R.Titov Dec 28 '16 at 17:10
  • Sure and I appreciate your answer. But in my opinion this is more about rolling your own change tracking system compared to the separation of concern I am after in my initial question. I also broke my models down, so that I can just use replaceoneasync without having to think about concurrency (each object is self contained). This was the best way I could cheaply replicate saveasync without having to roll my own change tracking. – Kristian Barrett Dec 28 '16 at 17:15
  • 1
    If you would like to be independent on EntityFramework MondoDB Driver etc... I would recommend to look on CQRS + Event Sourcing + DDD. Here is a good example of 'common' aggregate root for 'different' data bases https://github.com/Lokad/lokad-iddd-sample/tree/master/Sample/Domain/CustomerAggregate – R.Titov Dec 28 '16 at 17:33