LINQ-to-SQL:将Func<T, T, bool>转换为Expression<Func<T, T, bool>>

6
LINQ-to-SQL对我来说一直是个大麻烦。我们使用它与数据库通信,然后通过WCF将实体发送到Silverlight应用程序。一切都很好,直到开始编辑(CUD)实体及其相关数据时出现问题。
最终,我设计出两个for循环允许进行CUD操作。我试图对它们进行重构,但我很快就发现我不能总是在L2S中使用Lambda表达式。
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);
        }
    }
}

调用者:

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

这几乎行得通。然而,我的Func需要是一个Expression>,这就是我卡住的地方。
有人能告诉我这是否可能吗?由于SharePoint 2010,我们必须使用.NET 3.5。
1个回答

11

只需要将参数从以下内容更改:

 Func<T, T, bool> predicate

致:

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

这个表达式是由编译器生成的。

现在问题是如何使用它。

在您的情况下,您需要同时使用一个 Func 和一个 Expression,因为您既在基于 Func 的可枚举型 LINQ 查询中使用它,又在基于表达式的 SQL LINQ 查询中使用它。

在:

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

“old”参数是固定的。因此,我们可以将参数更改为:

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

这意味着我们可以提供一个参数(即“固定”的参数),然后得到一个表达式。
foreach (var old in oldCollection)
{
    var condition = predicate(old);
    // ...
    {
        ctx.GetTable<T>().DeleteAllOnSubmit(ctx.GetTable<T>().Where(condition));
    }
}

我们还需要在Any中使用这个功能。为了从表达式中获取一个Func,我们可以调用Compile()

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

您可以用相同的方式处理下一部分内容。

有两个问题:

  1. 使用 Compile() 可能会影响性能。我不确定它会产生多少影响,但我建议进行剖析以进行检查。
  2. 现在使用有点奇怪,因为这是一个柯里化的 lambda 表达式。而不是传递 (x,y) => ...,你将传递 x => y => ...。我不确定对您来说是否很重要。

可能有更好的方法来解决这个问题 :)

以下是替代方法,它应该更快,因为表达式只需要编译一次。创建一个重写器,将“应用”一个参数,如下所示:

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

然后像这样使用它:
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);
        }
    }
}

Porges,感谢您的快速回复。但是,当您这样做,并将我的.Where子句更改为ctx.GetTable<T>().Where(predicate)时,我无法编译。我需要将两个变量(需要比较它们的属性)传递给Expression/Func。 - DaleyKD
啊,我明白了,你实际的问题在于这个方法中。我没有注意到 - 我会更新答案。 - porges
天哪!那绝对有效。也许我们会在1.1版本中重新研究它的性能。 ;) 谢谢! - DaleyKD
只是为了好玩,我添加了另一个版本,它将仅基于使用“固定”值重写表达式来编译表达式一次。 - porges
Porges,谢谢!但是,我无法测试这个,因为ExpressionVisitor仅适用于.NET 4,而我被困在SharePoint 2010(.NET 3.5)中。 - DaleyKD
我正在使用.NET 4,这篇文章让我的一天变得更美好!+1 - Mathieu Guindon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接