【问题标题】:How to write a FirstOrException() extension method compatible with LINQ to SQL (and high performing)?如何编写与 LINQ to SQL(和高性能)兼容的 FirstOrException() 扩展方法?
【发布时间】:2012-08-11 00:59:03
【问题描述】:

我发现自己经常写这样的代码:

Dim oRenewalOrder = (From c in dc.Orders _
                     Where c.CustomerID = iCustomerID _
                     And c.Type = "RENEWAL").FirstOrDefault()

If oRenewalOrder Is Nothing Then
   Throw New Exception("Can't find renewal order for customer " & iCustomerID) ' or assert, if you prefer
End If

' Continue processing...

我更喜欢使用 First() 方法,因为它在没有元素时已经抛出异常。但是这个异常并没有告诉你是什么原因造成的,这使得调试变得更加困难。所以我想写一个 FirstOrException() 扩展方法,我可以像这样使用:

Dim oRenewalOrder = (From c in dc.Orders _
                     Where c.CustomerID = iCustomerID _
                     And c.Type = "RENEWAL").FirstOrException("Can't find renewal order for customer " & iCustomerID)

' Continue processing...

问题在于很难以通用方式编写这样的方法,同时使用 LINQ to Objects 利用 LINQ to SQL 提供的查询优化。我能想到的最好的是:

    ''' <summary>
    ''' Returns the first element of a sequence.
    ''' If the sequence is empty, an InvalidOperationException is thrown with the specified message.
    <Extension()> _
    Function FirstOrException(Of T)(ieThis As IEnumerable(Of T), sMessage As String) As T
        Try
            Return ieThis.First()
        Catch ex As InvalidOperationException
            Throw New InvalidOperationException(sMessage, ex)
        End Try
    End Function

我还为 IEnumerable(Of T) 编写了另一个等效的扩展方法,以涵盖 LINQ to Objects 的情况。这些工作良好,但必须捕获异常并重新抛出它似乎很糟糕。我尝试了一种不同的方法,使用 Take(1) 和 AsEnumerable(),但是当我分析它时,它运行了两个单独的 SELECT TOP 1 语句:

    <Extension()> _
    Function FirstOrException(Of T)(iqThis As IQueryable(Of T), sMessage As String) As T
        Dim aFirst = iqThis.Take(1).AsEnumerable()
        If aFirst.Count() <= 0 Then
            Throw New InvalidOperationException(sMessage)
        Else
            Return aFirst(0)
        End If
    End Function

所以我回到异常处理方法。我不喜欢它,因为当出现不同的问题时,集合底层的任何 LINQ 提供程序都有可能抛出 InvalidOperationException —— 不是缺少结果,而是其他问题。这会导致我的代码错误地认为没有结果,而实际上这是完全不同的问题。

...

好吧,正如您输入详细问题时经常发生的那样,我想我找到了更好的解决方案,我将在下面的答案中发布。但如果有人发现更好的东西,我会留下这个问题:-)

【问题讨论】:

    标签: .net vb.net linq-to-sql extension-methods linq-to-objects


    【解决方案1】:

    我发现Take(1).ToArray() 是最干净的解决方案。它只导致将单个查询发送到数据库。请注意,在下面的代码中,我正在为 IEnumerable 和 IQueryable 执行单独的扩展方法,以支持 LINQ to SQL 和 LINQ to Objects。

        ''' <summary>
        ''' Returns the first element of a sequence.
        ''' If the sequence is empty, an InvalidOperationException is thrown with the specified message.
        <Extension()> _
        Function FirstOrException(Of T)(ieThis As IEnumerable(Of T), sMessage As String) As T
            Dim aFirst = ieThis.Take(1).ToArray()
            If aFirst.Length <= 0 Then
                Throw New InvalidOperationException(sMessage)
            Else
                Return aFirst(0)
            End If
        End Function
    
         ''' <summary>
         ''' Returns the first element of a queryable (e.g. LINQ to SQL) sequence.
         ''' If the sequence is empty, an InvalidOperationException is thrown with the specified message.
        <Extension()> _
        Function FirstOrException(Of T)(iqThis As IQueryable(Of T), sMessage As String) As T
            Dim aFirst = iqThis.Take(1).AsEnumerable()
            If aFirst.Length <= 0 Then
                Throw New InvalidOperationException(sMessage)
            Else
                Return aFirst(0)
            End If
        End Function
    

    【讨论】:

    • 我会接受我自己的答案,因为我没有收到除 devundef 之外的任何建议,这对于引用类型的集合虽然快速简单,但不适用于值类型的集合。
    【解决方案2】:

    您已经在没有更改 Linq-to-sql 优化的情况下进行了扩展。您需要至少访问数据库一次以检查那里是否有东西。您不需要使用Take(1),只需调用FirstOrDefault() 并检查它是否返回Nothing

    Function GetFirstOrException(Of T)(sMessage As String, sFirst as T) as T
       if (T is Nothing) 
          Throw New InvalidOperationException(sMessage)
       Return T
    End Function
    
     <Extension()> _
    Function FirstOrException(Of T)(iqThis As IQueryable(Of T), sMessage As String) As T
        Return GetFirstOrException(sMessage, idThis.FirstOrDefault())
    End Function
    
     <Extension()> _
    Function FirstOrException(Of T)(iqThis As IEnumerable(Of T), sMessage As String) As T
        Return GetFirstOrException(sMessage, idThis.FirstOrDefault())
    End Function
    

    【讨论】:

    • 我没想到从我的扩展方法中调用辅助方法。这甚至更好。谢谢。
    • 实际上我们甚至不需要使用辅助方法,不是吗?我们可以在扩展方法内部进行任何测试......现在我记得为什么我没有这样做:我希望这些方法适用于值类型和引用类型。值类型不能为 null/Nothing。例如,在 Int32 的列表中,无法通过 FirstOrDefault() 判断列表中是否没有项目,或者第一项是否等于零(Int32 的默认值)。这就是我需要 Take() 的原因。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多