0

I'm trying to implement a BaseRepository like this:

public interface IRepository<T>
{    
   Task<T> Update(T entity, IEnumerable<UpdateFieldDefinition> update);
}
public class BaseRepository<T> : IRepository<T> where T : BaseEntity{
}

With BaseEntity a simple class that is used for all the entities stored in mongo. Since the Repository wil be called inside the core project by the interface, and the core project must not know that the implementation of the DB is MongoDb, I used an object to pass to Update called UpdateFieldDefinition defined like this:

public class UpdateFieldDefinition
    {
        public UpdateFieldDefinition(string propertyName, object propertyValue)
        {
            PropertyName = propertyName;
            PropertyValue = propertyValue;
        }
        public string PropertyName { get; set; }
        public object PropertyValue { get; set; }
    }

So, when I need to update a Role, i'll do something like this:

    var updateFields = new List<UpdateFieldDefinition>();
    var newNameValue = "Test";
    var newListValue = new List<string> { "1", "2" };
    updateFields.Add(new UpdateFieldDefinition("Name", newNameValue));
    updateFields.Add(new UpdateFieldDefinition("ListValues", newListValue));
    var testObj = await testRepository.Update(updateEntity, updateFields);

And the implementation of Update is this

public async Task<T> Update(T entity, IEnumerable<UpdateFieldDefinition> updateFieldDefinitions)
{
    var builder = new UpdateDefinitionBuilder<T>();
    var options = new FindOneAndUpdateOptions<T>
    {
        ReturnDocument = ReturnDocument.After,
        IsUpsert = false
    };
    //setting the fields to update based on what has been set from outside based on the Implemented BaseEntity T
    var updates = updateFieldDefinitions
        .Select(updateFieldDefinition =>
            builder.Set(updateFieldDefinition.PropertyName, updateFieldDefinition.PropertyValue))
        .ToList();
    //add update for LastModifiedDate => 
    updates.Add(builder.Set(x => x.LastModifiedDate, DateTime.Now));

    var filter = Builders<T>.Filter.Eq(en => en.Id, entity.Id);
    var updateCmd = builder.Combine(updates);
    var result = await DbContext.GetCollection<T>().FindOneAndUpdateAsync(filter, updateCmd, options);
    return result;
}

But this is not working, because when it meet the "ListValue" which is a list, but inside the UpdateFieldDefinition is stored as Object, the system is trying to convert it to a string, giving this error : Cannot deserialize a 'List<String>' from BsonType 'String'.

Is there any way to handle this? The code given is a simplified version of the problem, since I created an extension method for BaseEntity that will create the list of UpdateFieldDefinition using reflection on all properties of the type T.

//EDIT1 adding the example of how i retrieve for an entity the list of UpdateFieldDefinitions

public static class EntityHelper
    {
        public static IEnumerable<UpdateFieldDefinition> GetUpdateDefinition<T>(this T entity)
        {
            return entity.GetType().GetProperties()
                .Where(x =>
                {
                    switch (x.Name)
                    {
                        case nameof(BaseEntity.Id):
                        case nameof(BaseEntity.CreationDate):
                        case nameof(BaseEntity.LastModifiedDate):
                        case nameof(BaseEntity.CreatorId):
                        case nameof(BaseEntity.LastModifierUserId):
                            return false;
                        default:
                            return true;
                    }
                })
                .Select(x => new UpdateFieldDefinition(x.Name, x.GetValue(entity)));
        }
    }
Davide Quaglio
  • 751
  • 2
  • 11
  • 31
  • Why not `public UpdateFieldDefinition(Expression> field, TFieldType value)`? – ProgrammingLlama Oct 13 '18 at 14:20
  • 1
    Imho you should either design your repository to be CRUD (which is one of the main reasons for doing that), so you only update/insert/delete whole entities/documents (i.e. get entity, change its properties, then update it) or use CQRS if you have more complex things and a generic solution isn't possible or practical, this way you can also add logic to the command handlers (i.e. `DisableInactiveCustomersComandHandler` which disables all customers who did not place an order in the past 18 months etc. – Tseng Oct 13 '18 at 14:28
  • For me it looks like you want do partial updates (i.e. only update a specific field or two) based on some criteria. For that repositories are less then suitable since its either to generic (i.e. you lose strong typing/type-safety) or to specific (and you have to leak persistence knowledge to your domain/business logic) – Tseng Oct 13 '18 at 14:33
  • I added how I'm retrieving the list of fields to update for the entities @John maybe what you said is the correct approach but I'm having some problems to implement it, maybe it is not really possible. – Davide Quaglio Oct 13 '18 at 14:39
  • @Tseng yeah I'd like to to partial updates and not having to handle the retrieve of the old document and keeping the fields that does not need to be updated as they were – Davide Quaglio Oct 13 '18 at 14:40
  • What I'm trying to do now is use [this](https://stackoverflow.com/questions/232535/how-do-i-use-reflection-to-call-a-generic-method) inside to execute the generic method inside UpdateDefinitionBuilder passing each time the correct type, but having some problems since the Set method on the mongoDb driver has an overload and I'm having some problems to select the correct one. – Davide Quaglio Oct 13 '18 at 15:10

1 Answers1

1
    public static UpdateDefinition<T> GetUpdateDefinition<T>(this T entity)
            {
                var updateFieldDefinitions = entity.GetType().GetProperties()
                    .Where(x => !new string[] { "Id", "_id" }.Contains(x.Name))
                    .Select(x => new UpdateFieldDefinition(x.Name, x.GetValue(entity)));
    
                var builder = new UpdateDefinitionBuilder<T>();
                var updates = updateFieldDefinitions.Select(TField => builder.Set(TField.Name, TField.Value));
                return builder.Combine(updates);
            }
    

...

    public async static Task<T> UpsertAsync<T>(Expression<Func<T, bool>> expression, T entity)
            {
                var collection = Database.GetCollection<T>(typeof(T).Name);
    
                var updates = entity.GetUpdateDefinition();
    
                var options = new FindOneAndUpdateOptions<T>
                {
                    ReturnDocument = ReturnDocument.After,
                    IsUpsert = true
                };
    
                return await collection.FindOneAndUpdateAsync(expression, updates, options);
            }
emdlab
  • 11
  • 2