Search code examples
c#expression-treesdtoprojectionlinq2db

Project on update/create (set values from another object en masse) in LINQ2DB?


When using LINQ2DB for my application I tried to use entity-DTO mapping using Expression<Func<Entity, DTO>> and vice versa like described here: https://github.com/linq2db/linq2db/issues/1283#issuecomment-413509043

This works great for projecting using a select, but what do I do when I need to update/insert a new record? I've skimmed over Update and Set extension methods but couldn't find anything.

What I am trying to achieve is basically expression-based two-way mapping between an entity class and a DTO, kinda like AutoMapper's projection for EF but manually written per-DTO, in the form of two expressions for two-way conversion.

Sadly I am not an expert in expression trees and LINQ to SQL translation, so would appreciate if anyone suggests something that works like this:

Expression<Func<SomeDTO, SomeEntityTable>> projectExpr =
    x => new SomeEntity
    {
        ID = x.ID,
        Name = x.Name,
        // ...
    }; // this is just so that I can write two mapping expressions per DTO and don't ever repeat them, for stuff like CRUD

// ...

using var db = ConnectionFactory.Instance.GetMainDB();
await db.SomeEntityTable
    .Where(e => e.ID == dto.ID)
    .Set(dto, projectExpr) // dto is of SomeDTO type here; this will set ONLY the values that are written in the expression
    .Set(e => e.LastEditedAt, DateTime.Now()) // able to append some more stuff after 
    .UpdateAsync();


// similar for insert operation, using the same expression

Solution

  • These extension methods should provide needed mapping:

    using var db = ConnectionFactory.Instance.GetMainDB();
    
    await db.SomeEntityTable
        .Where(e => e.ID == dto.ID)
        .AsUpdatable()
        .Set(dto, projectExpr) // new extension method
        .Set(e => e.LastEditedAt, DateTime.Now())
        .UpdateAsync();
    
    await db.SomeEntityTable
        .AsValueInsertable()
        .Values(dto, projectExpr) // new extension method
        .Value(e => e.LastEditedAt, DateTime.Now())
        .InsertAsync();
    
    

    And implementation:

    public static class InsertUpdateExtensions
    {
        private static MethodInfo _withIUpdatable       = Methods.LinqToDB.Update.SetUpdatableExpression;
        private static MethodInfo _withIValueInsertable = Methods.LinqToDB.Insert.VI.ValueExpression;
    
        public static IUpdatable<TEntity> Set<TEntity, TDto>(
            this IUpdatable<TEntity>        updatable, 
            TDto                            obj,
            Expression<Func<TDto, TEntity>> projection)
        {
            var body = projection.GetBody(Expression.Constant(obj));
    
            var entityParam = Expression.Parameter(typeof(TEntity), "e");
    
            var pairs = EnumeratePairs(body, entityParam);
    
            foreach (var pair in pairs)
            {
                updatable = (IUpdatable<TEntity>)_withIUpdatable.MakeGenericMethod(typeof(TEntity), pair.Item1.Type)
                    .Invoke(null,
                        new object?[]
                        {
                            updatable, 
                            Expression.Lambda(pair.Item1, entityParam), 
                            Expression.Lambda(pair.Item2)
                        })!;
            }
    
            return updatable;
        }
    
        public static IValueInsertable<TEntity> Values<TEntity, TDto>(
            this IValueInsertable<TEntity>  insertable, 
            TDto                            obj,
            Expression<Func<TDto, TEntity>> projection)
        {
            var body = projection.GetBody(Expression.Constant(obj));
    
            var entityParam = Expression.Parameter(typeof(TEntity), "e");
    
            var pairs = EnumeratePairs(body, entityParam);
    
            foreach (var pair in pairs)
            {
                insertable = (IValueInsertable<TEntity>)_withIValueInsertable.MakeGenericMethod(typeof(TEntity), pair.Item1.Type)
                    .Invoke(null,
                        new object?[]
                        {
                            insertable, 
                            Expression.Lambda(pair.Item1, entityParam), 
                            Expression.Lambda(pair.Item2)
                        })!;
            }
    
            return insertable;
        }
    
        private static IEnumerable<Tuple<Expression, Expression>> EnumeratePairs(Expression projection, Expression entityPath)
        {
            switch (projection.NodeType)
            {
                case ExpressionType.MemberInit:
                {
                    var mi = (MemberInitExpression)projection;
                    foreach (var b in mi.Bindings)
                    {
                        if (b.BindingType == MemberBindingType.Assignment)
                        {
                            var assignment = (MemberAssignment)b;
                            foreach (var p in EnumeratePairs(Expression.MakeMemberAccess(entityPath, assignment.Member),
                                            assignment.Expression))
                            {
                                yield return p;
                            }
                        }
                    }
                    break;
                }
    
                case ExpressionType.New:
                {
                    var ne = (NewExpression)projection;
                    if (ne.Members != null)
                    {
                        for (var index = 0; index < ne.Arguments.Count; index++)
                        {
                            var expr   = ne.Arguments[index];
                            var member = ne.Members[index];
    
                            foreach (var p in EnumeratePairs(Expression.MakeMemberAccess(entityPath, member), expr))
                            {
                                yield return p;
                            }
                        }
                    }
                    break;
                }
    
                case ExpressionType.MemberAccess:
                {
                    yield return Tuple.Create(projection, entityPath);
                    break;
    
                }
                default:
                    throw new NotImplementedException();
            }
        }
    }