用于Linq-to-Entities的可重用函数

时间:2015-04-08 10:15:48

标签: c# linq linq-to-sql linq-to-entities

我有一些我想在不同地方使用的统计代码来计算时间表Results的成功/失败百分比。我最近在代码中发现了一个错误,这是因为它在每个LINQ语句中被复制,然后我决定使用通用代码执行此操作会更好。当然,问题在于,在SQL服务器上执行时,正常函数会抛出NotSupportedException,因为SQL Server中不存在这种功能。

如何编写可在SQL服务器上执行的可重用统计代码,或者这是不可能的?

以下是我为Result

撰写的代码
public class Result
{
    public double CalculateSuccessRatePercentage()
    {
        return this.ExecutedCount == 0 ? 100 : ((this.ExecutedCount - this.FailedCount) * 100.0 / this.ExecutedCount);
    }

    public double CalculateCoveragePercentage()
    {
        return this.PresentCount == 0 ? 0 : (this.ExecutedCount * 100.0 / this.PresentCount);
    }
}

它的使用方式如此(结果为IQueryable,并抛出异常):

schedule.SuccessRatePercentage = (int)Math.Ceiling(results.Average(r => r.CalculateSuccessRatePercentage()));
schedule.CoveragePercentage = (int)Math.Ceiling(results.Average(r => r.CalculateCoveragePercentage()));

或者像这样(有效,因为我们在单个结果上这样做)

retSchedule.SuccessRatePercentage = (byte)Math.Ceiling(result.CalculateSuccessRatePercentage());
retSchedule.CoveragePercentage = (byte)Math.Ceiling(result.CalculateCoveragePercentage());

修改

根据@ Fred的回答,我现在有以下代码,适用于IQueryable

schedule.SuccessRatePercentage = (int)Math.Ceiling(scheduleResults.Average(ScheduleResult.CalculateSuccessRatePercentageExpression()));
schedule.CoveragePercentage = (int)Math.Ceiling(scheduleResults.Average(ScheduleResult.CalculateCoveragePercentageExpression()));

唯一的问题,虽然是一个小问题,但这个代码不适用于个别结果,即

retSchedule.SuccessRatePercentage = (byte)Math.Ceiling(/* How do I use it here for result */);

1 个答案:

答案 0 :(得分:4)

您无法将函数传递给SQL - 您需要在实际的SQL数据库上声明该函数,然后从您的代码中调用该函数。

你可以做/尝试的是:

Expression<Func<Result, double>> CalculateCoveragePercentage()
{
    return r => r.PresentCount == 0 ? 0 : (r.ExecutedCount * 100.0 / r.PresentCount);
}

需要解释而不是执行,以便EF可以将其转换为SQL。问题是,当它直接传递到where子句时,我只听说过这种情况。

由于您可以在LINQ查询中直接应用它们时进行这些计算,我倾向于认为应该可以将这些计算声明为Expression<Func<..., ...>>并将它们传递给它们。

唯一可以确定的方法是尝试(除非你想看看EF的ExpressionBuilder

<强>更新

我应该提到,如果这样做,你需要将这个表达式传递给Select语句:

// Assuming you have Results declared as a DbSet or IDbSet, such as:
DbSet<Result> Results

// You could do something like this (just to illustrate that
// it would be interpreted rather than executed):
List<double> allCoveragePercentages = Results.Select(CalculateCoveragePercentage)
                                             .ToList();

更新#2

为了使其能够处理单个结果(或者无论如何),您需要将其传递给接受表达式的子句。示例包括SelectWhereAverage(显然),任何都会返回结果。

从头顶(我确定我错过了一些):

  • 列表:ToArrayToDictionaryToListToLookup

  • 单个结果:FirstFirstOrDefaultSingleSingleOrDefaultLastLastOrDefault

  • 计算:CountSumMaxMin

由于上述条款返回结果,因此(据我所知)它们只接受Predicates(只能返回'true'或'false'的函数)

您可能巧合地使用了.Average(CalculateCoveragePercentage)

因此,如果您要使用.FirstOrDefault()获得单个结果,则应在其前面的select子句中传递表达式:.Select(CalculateCoveragePercentage).FirstOrDefault()。也就是说,如果您不需要实际实体而只需要计算。请注意,如果没有0结果,此特定示例将返回Result。您可能想要也可能不想要这种行为。

当然,如果你已经有了结果(它不再是IQueryable)那么你可以这么做:

var coveragePercentage = CalculateCoveragePercentage().Compile().Invoke(result);

但是这会破坏表达式的目的 - 对于这种情况,你应该只为你的Result类添加一个计算给定实例的CoveragePercentage的方法。