Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

LINQ-to-SQL : Convert Func<T, T, bool> to an Expression<Func<T, T, bool>>

LINQ-to-SQL has been a PITA for me. We're using it to communicate to the database, and then send entities via WCF to a Silverlight app. Everything was working fine, until it came time to start editing (CUD) the entities, and their related data.

I was finally able to devise two for loops that allowed the CUD. I tried to refactor them, and I was so close, until I learned that I can't always do Lambda with L2S.

public static void CudOperation<T>(this DataContext ctx, IEnumerable<T> oldCollection, IEnumerable<T> newCollection, Func<T, T, bool> predicate)
    where T : class
{
    foreach (var old in oldCollection)
    {
        if (!newCollection.Any(o => predicate(old, o)))
        {
            ctx.GetTable<T>().DeleteAllOnSubmit(ctx.GetTable<T>().Where(o => predicate(old, o)));
        }
    }

    foreach (var newItem in newCollection)
    {
        var existingItem = oldCollection.SingleOrDefault(o => predicate(o, newItem));
        if (existingItem != null)
        {
            ctx.GetTable<T>().Attach(newItem, existingItem);
        }
        else
        {
            ctx.GetTable<T>().InsertOnSubmit(newItem);
        }
    }
}

Called by:

ctx.CudOperation<MyEntity>(myVar.MyEntities, newHeader.MyEntities,
    (x, y) => x.PkID == y.PkID && x.Fk1ID == y.Fk1ID && x.Fk2ID == y.FK2ID);

This almost worked. However, my Func needs to be an Expression>, and that's where I'm stuck.

Is there anyone who can tell me if this is possible? We have to be in .NET 3.5 due to SharePoint 2010.

like image 836
DaleyKD Avatar asked Feb 17 '12 00:02

DaleyKD


1 Answers

Just change the parameter from:

 Func<T, T, bool> predicate

To:

 Expression<Func<T, T, bool>> predicate

The Expression is generated by the compiler.

Now, the issue is how to use this.

In your case, you need both a Func and an Expression, since you're using it in Enumerable LINQ queries (func based) as well as the SQL LINQ queries (expression based).

In:

.Where(o => predicate(old, o))

The old parameter is fixed. So we could change the parameter to:

Func<T, Expression<Func<T, bool>>> predicate

This means we can supply one argument (the 'fixed' one) and get back an expression.

foreach (var old in oldCollection)
{
    var condition = predicate(old);
    // ...
    {
        ctx.GetTable<T>().DeleteAllOnSubmit(ctx.GetTable<T>().Where(condition));
    }
}

We also need to use this in Any. To get a Func from an Expression we can call Compile():

foreach (var old in oldCollection)
{
    var condition = predicate(old);
    if (!newCollection.Any(condition.Compile()))
    {
        ctx.GetTable<T>().DeleteAllOnSubmit(ctx.GetTable<T>().Where(condition));
    }
}

You can do the same thing with the next part.

There are two issues:

  1. The performance might be impacted by using Compile() lots. I'm not sure how much effect it would actually have, but I'd profile it to check.
  2. The usage is now a little weird, since this is a curried lambda. Instead of passing (x,y) => ... you will be passing x => y => .... I'm not sure if this is a big deal for you.

There might be a better way to do this :)

Here's an alternate method, which should be a bit faster, since the Expression only has to be compiled once. Create a rewriter that will 'apply' one argument, like this:

class PartialApplier : ExpressionVisitor
{
    private readonly ConstantExpression value;
    private readonly ParameterExpression replace;

    private PartialApplier(ParameterExpression replace, object value)
    {
        this.replace = replace;
        this.value = Expression.Constant(value, value.GetType());
    }

    public override Expression Visit(Expression node)
    {
        var parameter = node as ParameterExpression;
        if (parameter != null && parameter.Equals(replace))
        {
            return value;
        }
        else return base.Visit(node);
    }

    public static Expression<Func<T2,TResult>> PartialApply<T,T2,TResult>(Expression<Func<T,T2,TResult>> expression, T value)
    {
        var result = new PartialApplier(expression.Parameters.First(), value).Visit(expression.Body);

        return (Expression<Func<T2,TResult>>)Expression.Lambda(result, expression.Parameters.Skip(1));
    }
}

Then use it like this:

public static void CudOperation<T>(this DataContext ctx,
    IEnumerable<T> oldCollection,
    IEnumerable<T> newCollection,
    Expression<Func<T, T, bool>> predicate)
    where T : class
{

    var compiled = predicate.Compile();

    foreach (var old in oldCollection)
    {
        if (!newCollection.Any(o => compiled(o, old)))
        {
            var applied = PartialApplier.PartialApply(predicate, old);
            ctx.GetTable<T>().DeleteAllOnSubmit(ctx.GetTable<T>().Where(applied));
        }
    }

    foreach (var newItem in newCollection)
    {
        var existingItem = oldCollection.SingleOrDefault(o => compiled(o, newItem));
        if (existingItem != null)
        {
            ctx.GetTable<T>().Attach(newItem, existingItem);
        }
        else
        {
            ctx.GetTable<T>().InsertOnSubmit(newItem);
        }
    }
}
like image 88
porges Avatar answered Sep 28 '22 19:09

porges