2012-08-11 88 views
2

很多时候我发现自己写这样的代码:如何编写与LINQ to SQL(和高性能)兼容的FirstOrException()扩展方法?

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到工作对象利用目前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到对象的情况下。这些工作很好,但似乎不好,必须赶上例外并重新抛出它。我尝试了不同的方法,通过采取(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 - 不是缺少结果,而是另一个问题。这会导致我的代码错误地认为没有结果,但实际上完全是另一个问题。

...

嗯,所以经常发生,当你键入了一个详细的问题,我想我找到了一个更好的解决方案,我将它张贴在下面的答案。但我会留下问题,以防万一有人发现更好的东西:-)

回答

0

我发现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 
+0

,因为我还没有收到超过devundef的,它们虽然快速,简单的引用类型的集合,将不会为值类型的集合的工作以外的任何建议,我会接受我自己的答案。 – 2012-08-14 17:29:52

0

您已经做了扩展而没有更改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 
+0

我没想过从我的扩展方法中调用辅助方法。这更好。谢谢。 – 2012-08-13 21:31:09

+0

其实我们甚至不需要使用辅助方法,是吗?我们可以在扩展方法内部测试任何东西......现在我记得为什么我没有这样做:我想要这些方法适用于值类型以及引用类型。值类型不能为null/Nothing。例如,在Int32列表中,如果列表中没有项目,或者第一个项目等于零(Int32的默认值),则不可能通过FirstOrDefault()来判断。这就是为什么我需要Take()。 – 2012-08-13 21:34:34