Kristian Barrett
Kristian Barrett

Reputation: 3762

C# Mongodb driver: Translate Action into an UpdateDefinition

I am trying to generalise updates to my mongodb, so I can make updating my data - storage agnostic. With EntityFramework, this is super easy as it has change tracking and I therefore can seperate business logic and database specific operations. When it comes to MongoDb, it is not as easy, since I have to create these UpdateDefinitions, to do updates on documents.

An easy way to mimic this would be to use ReplaceOneAsync, but this is not an option, since more than one party could be writing to my collections at the same time. I am for example using it for a chat system, within my application.

I wanted to do something like this:

public class MongoActionWrapper<TCollection>
{
    public MongoActionWrapper(Action<TCollection> updateAction)
    {
        UpdateAction = updateAction;
    }

    public Action<TCollection> UpdateAction { get; }
}

And then I have my MongoRepository:

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 void SaveAsync(TAggregate aggregate)
    {
        var state = aggregate.GetState();
        foreach (var action in state.ActionsToExecuteOnCollection)
        {
            await _mongoCollection.UpdateOneAsync<TCollection>(aggregate.GetSelector(), action.UpdateAction);
        }
    }
}

Where I would like to execute the action on the collection instead of an UpdateDefinition. Or somehow translate my Action into an UpdateDefinition.

This would make me able to apply the same updates on a list (or whatever other collection, where I want to store my data), so that I am not forced to stay with mongodb.

My only solution so far, would be to make my UpdateAction more like an object that describes the updates, and then translate these objects, when I want to persist it in mongodb. This way I can translate them into Actions and apply them to my collection or whatever other DB I would choose.

I hope somebody has an idea, since I am running out of options here.

Upvotes: 3

Views: 953

Answers (1)

user2127981
user2127981

Reputation: 51

I am not sure how you do this with Action delegate, but if you want to reproduce EntityFramework like Read-Through, Write-Through cache with multiple concurrent writes on single AggregateRoot, you can try somthing like this:

public abstract class AggregateRoot<T> where T : AggregateRoot<T>
{
    internal UpdateDefinition<T> UpdateDefinition { get; internal set; }
    internal void Aggregate(UpdateDefinition<T> component)
    {
        if (component == null)
        {
            throw new ArgumentNullException("component");
        }

        if (this.UpdateDefinition == null)
        {
            this.UpdateDefinition = component;
        }
        else
        {
            this.UpdateDefinition = Builders<T>.Update.Combine
                (this.UpdateDefinition, component);
        }
    }
}

and than in the repository base class:

public class Repository<T> where T : AggregateRoot<T>
{
    private ConcurrentDictionary<ObjectId, T> cache 
        = new ConcurrentDictionary<ObjectId, T>();

    private IMongoCollection<T> collection;

    public Repository(string connectionString, string databaseName, string collectionName)
    {
        collection = new MongoClient(connectionString)
            .GetDatabase(databaseName)
            .GetCollection<T>(collectionName);
    }

    public T Find(ObjectId aggregateRootId)
    {
        return cache.GetOrAdd(
            key: aggregateRootId, 
            valueFactory: key => findById(key));
    }

    private T findById(ObjectId aggregateRootId)
    {
        return collection
            .Find(Builders<T>.Filter
                .Eq(x => x.Id, aggregateRootId))
            .SingleOrDefault();
    }

    public void SaveChanges()
    {   
        var writeModels = generateWriteModels();
        collection.BulkWrite(writeModels);
    }        

    private IEnumerable<WriteModel<T>> generateWriteModels()
    {
        List<WriteModel<T>> writeModels = new List<WriteModel<T>>();

        foreach (var cached in cache)
        {
            if (cached.Value != null)
            {
                if (cached.Value.UpdateDefinition != null)
                {   
                    writeModels.Add(new UpdateOneModel<T>(
                        filter: Builders<T>.Filter.Eq(x => x.Id, cached.Value.Id),
                        update: cached.Value.UpdateDefinition){ IsUpsert = true });
                    cached.Value.UpdateDefinition = null;
                }
            }
        }

        return writeModels;
    }
}

With this implementation you can interact with Aggregate(UpdateDefinition<T> component) method of AggregateRoot<T> base class directly in the implementation (watch out of using it on setters - BsonSerializer use setters as well). Changes will be consumed by Repository<T>.

If you want to make your domain models as much decoupled from store implementation as possible, you can build an equivalents of MongoActionWrapper<TCollection> as a wrappers on aggregates. Those wrappers can use aggregates methods along with Aggregate(UpdateDefinition<T> component) to keep update definition syncronized with changes done to the aggregate.

Upvotes: 3

Related Questions