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
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();
}
}
}