1
\$\begingroup\$

Backstory

The company I work, we have the code below that acts as an abstraction of our database layer, which is mongoDB. This ensures our other code is not too dependent on the database. This is part of a large product that is already in use. However due to upcoming functionality changes we will be making some (large) changes to the codebase.

In light of these changes I'm turning to Code Review. I interested in hearing if any improvements can be made to this abstraction. This would be the ideal time to incorporate them, together with the other changes.

Design

Database interaction happens via stores. For these stores we have an Interface, the abstraction, and an implementation specific for mongoDB.

We have a base interface with some common methods, and other store interfaces extend that base interface (see code below). Same way we have a base class that implements this base interface, and all concrete stores inherit from that base.

These interfaces are generic and accept a concrete implementation. This is a POCO representing the object in the database, decorated with mongoDB specific attributes.

The code

Here I'll walk you through the code for the user store, we use for user management. Here we have the base interface and the UserStore{T} interface:

(we made interfaces for filters and update statements as to be independent from an implementation in the abstraction layer)

public interface IStore<TEntity>
{
    /// <param name="entity">The entity to create.</param>
    Task<OperationResponse> CreateAsync(TEntity entity);

    /// <param name="filterExpressions">The filters to find the entity to update.</param>
    /// <param name="updateExpressions">The definition of the fields to update</param>
    Task<OperationResponse> UpdateOneAsync(IDynamicFilterExpression filterExpressions, IDynamicUpdateExpression updateExpressions);

    /// <param name="filters">The filters to define the entity to replace.</param>
    /// <param name="replacement">The replacement.</param>
    /// <param name="isUpsert">Should the item be inserted if no entity was found to replace?</param>
    Task<OperationResponse> ReplaceOneAsync(IDynamicFilterExpression filters, TEntity replacement, bool isUpsert = false);

    /// <snip>... more methods ...</snip>
}

public interface IUserStore<TUser> : IStore<TUser>
{
    /// <param name="email">The normalized email to search for.</param>
    Task<TUser> FindByEmailAsync(string email);

    /// <param name="securityStamp">The normalized security stamp to search for.</param>
    Task<TUser> FindBySecurityStampAsync(string securityStamp);

    /// <snip>... more methods ...</snip>
}

Here we have the actual store for mongoDB:

public abstract class MongoDbStore<TDocument> : IStore<TDocument>
    where TDocument : class, IDocument
{
    protected MongoDbStore(DatabaseContext context, string collectionName)
    {
        // sets collection
    }

    public IMongoCollection<TDocument> Collection { get; set; }

    /// <param name="entity">The entity to create.</param>
    public virtual async Task<OperationResponse> CreateAsync(TDocument entity)
    {
        await Collection.InsertDocumentWithDefaultInsertOptionsAsync(entity, _cancellationToken);
        return true;
    }

    /// <param name="filterExpressions">The filters to find the entity to update.</param>
    /// <param name="updateExpressions">The definition of the fields to update</param>
    public virtual async Task<OperationResponse> UpdateOneAsync(IDynamicFilterExpression filterExpressions, IDynamicUpdateExpression updateExpressions)
    {
        var result = await Collection.UpdateOneDynamicAsync(filterExpressions, updateExpressions, _cancellationToken);

        if (!result.IsAcknowledged)
            return new OperationResponse(false, new string[] { "DB_OPERATION_NOT_ACKNOWLEDGED" });

        if (result.MatchedCount == 0)
            return new OperationResponse(false, new string[] { "ENTITY_NOT_FOUND" });

        return true;
    }

    /// <param name="filterExpressions">The filters to define the entity to replace.</param>
    /// <param name="replacement">The replacement.</param>
    /// <param name="isUpsert">Should the item be inserted if no entity was found to replace?</param>
    public virtual async Task<OperationResponse> ReplaceOneAsync(IDynamicFilterExpression filterExpressions, TDocument replacement, bool isUpsert = false)
    {
        // Create the mongoDB filter from abstraction.
        var filter = filterExpressions.BuildFilterDefinition<TDocument>();

        var result = await Collection.ReplaceOneAsync(filter, replacement, new UpdateOptions(), _cancellationToken);

        if (!result.IsAcknowledged)
            return new OperationResponse(false, new string[] { "DB_OPERATION_NOT_ACKNOWLEDGED" });

        if (result.MatchedCount == 0)
            return new OperationResponse(false, new string[] { "ENTITY_NOT_FOUND" });

        return true;
    }

    /// <snip>... more methods ...</snip>
}

public class UserStore : MongoDBStore<User>, IUserStore<User>
{
    public UserStore(DatabaseContext context)
        : base(context, "[core].users")
    {
    }

    /// <param name="email">The normalized email to search for.</param>
    public async Task<User> FindByNameAsync(string email)
    {
        var expression = new DynamicStringEqualFilterExpression<User>(u => u.Email, email);

        return await base.FindFirstAsync(expression, false);
    }

    /// <param name="securityStamp">The normalized security stamp to search for.</param>
    public async Task<User> FindBySecurityStampAsync(string securityStamp)
    {
        var expression = new DynamicStringEqualFilterExpression<User>(u => u.SecurityStamp, securityStamp);

        return await base.FindFirstAsync(expression, false);
    }

    /// <snip>... more methods ...</snip>
}

To conclude we have the user object itself, with the IDocument interface we use as a base requirement:

public interface IDocument : IDocument<String>
{
}

public interface IDocument<TKey>
{
    TKey Id { get; set; }
}

[BsonIgnoreExtraElements]
public class AppServerUser : IDocument
{
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }

    [BsonElement("email")]
    public string Email { get; set; }

    [BsonElement("pwdHash")]
    public string PasswordHash { get; set; }

    /// <summary>
    ///     A random value that should change whenever a user credentials change (password changed, login removed).
    /// </summary>
    [BsonElement("secStamp")]
    public string SecurityStamp { get; set; }

    /// <snip>... more props ...</snip>
}

The question

We have numerous stores that follow this same pattern. This code works, but I have a feeling this isn't really 'abstraction' in the correct way.

That's because we have these attributes on our classes that make us dependent on MongoDB. The idea is that we could simply remove them if we decide to move, but there is no guarantee that the properties would stay identical in another database. Also if we use multiple databases at the same time, with every new database there would be the possibility of additional annotations. So this feels wrong to me.

I'd love to hear what improvements will be suggested! :-)

I assume this ventures more towards design, but I'm under the impression that this is no problem. I'm open to suggestions if this is deemed off-topic. Happy weekend to all.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.