Condividi tramite


How to: Add Custom Methods for LINQ Queries

You can extend the set of methods that you can use for LINQ queries by adding extension methods to the IEnumerable<T> interface. For example, in addition to the standard average or maximum operations, you can create a custom aggregate method to compute a single value from a sequence of values. You can also create a method that works as a custom filter or a specific data transform for a sequence of values and returns a new sequence. Examples of such methods are Distinct, Skip<TSource>, and Reverse<TSource>.

When you extend the IEnumerable<T> interface, you can apply your custom methods to any enumerable collection. For more information, see Extension Methods (C# Programming Guide) or Extension Methods (Visual Basic).

Adding an Aggregate Method

An aggregate method computes a single value from a set of values. LINQ provides several aggregate methods, including Average, Min, and Max. You can create your own aggregate method by adding an extension method to the IEnumerable<T> interface.

The following code example shows how to create an extension method called Median to compute a median for a sequence of numbers of type double.

Imports System.Runtime.CompilerServices

Module LINQExtension

    ' Extension method for the IEnumerable(of T) interface.  
    ' The method accepts only values of the Double type.
    <Extension()> 
    Function Median(ByVal source As IEnumerable(Of Double)) As Double 
        If source.Count = 0 Then 
            Throw New InvalidOperationException("Cannot compute median for an empty set.")
        End If 

        Dim sortedSource = From number In source 
                           Order By number

        Dim itemIndex = sortedSource.Count \ 2

        If sortedSource.Count Mod 2 = 0 Then 
            ' Even number of items in list. 
            Return (sortedSource(itemIndex) + sortedSource(itemIndex - 1)) / 2
        Else 
            ' Odd number of items in list. 
            Return sortedSource(itemIndex)
        End If 
    End Function 
End Module
public static class LINQExtension
{
    public static double Median(this IEnumerable<double> source)
    {
        if (source.Count() == 0)
        {
            throw new InvalidOperationException("Cannot compute median for an empty set.");
        }

        var sortedList = from number in source
                         orderby number
                         select number;

        int itemIndex = (int)sortedList.Count() / 2;

        if (sortedList.Count() % 2 == 0)
        {
            // Even number of items. 
            return (sortedList.ElementAt(itemIndex) + sortedList.ElementAt(itemIndex - 1)) / 2;
        }
        else
        {
            // Odd number of items. 
            return sortedList.ElementAt(itemIndex);
        }
    }
}

You call this extension method for any enumerable collection in the same way you call other aggregate methods from the IEnumerable<T> interface.

Note

   In Visual Basic, you can either use a method call or standard query syntax for the Aggregate or Group By clause. For more information, see Aggregate Clause (Visual Basic) and Group By Clause (Visual Basic).

The following code example shows how to use the Median method for an array of type double.

Dim numbers1() As Double = {1.9, 2, 8, 4, 5.7, 6, 7.2, 0}

        Dim query1 = Aggregate num In numbers1 Into Median()

        Console.WriteLine("Double: Median = " & query1)



...


        ' This code produces the following output: 
        ' 
        ' Double: Median = 4.85
double[] numbers1 = { 1.9, 2, 8, 4, 5.7, 6, 7.2, 0 };

        var query1 = numbers1.Median();

        Console.WriteLine("double: Median = " + query1);



...


/*
 This code produces the following output:

 Double: Median = 4.85
*/

Overloading an Aggregate Method to Accept Various Types

You can overload your aggregate method so that it accepts sequences of various types. The standard approach is to create an overload for each type. Another approach is to create an overload that will take a generic type and convert it to a specific type by using a delegate. You can also combine both approaches.

To create an overload for each type

You can create a specific overload for each type that you want to support. The following code example shows an overload of the Median method for the integer type.

' Integer overload

<Extension()> 
Function Median(ByVal source As IEnumerable(Of Integer)) As Double 
    Return Aggregate num In source Select CDbl(num) Into med = Median()
End Function
//int overload 

public static double Median(this IEnumerable<int> source)
{
    return (from num in source select (double)num).Median();
}

You can now call the Median overloads for both integer and double types, as shown in the following code:

Dim numbers1() As Double = {1.9, 2, 8, 4, 5.7, 6, 7.2, 0}

        Dim query1 = Aggregate num In numbers1 Into Median()

        Console.WriteLine("Double: Median = " & query1)



...


        Dim numbers2() As Integer = {1, 2, 3, 4, 5}

        Dim query2 = Aggregate num In numbers2 Into Median()

        Console.WriteLine("Integer: Median = " & query2)



...


' This code produces the following output: 
' 
' Double: Median = 4.85 
' Integer: Median = 3
double[] numbers1 = { 1.9, 2, 8, 4, 5.7, 6, 7.2, 0 };

        var query1 = numbers1.Median();

        Console.WriteLine("double: Median = " + query1);



...


        int[] numbers2 = { 1, 2, 3, 4, 5 };

        var query2 = numbers2.Median();

        Console.WriteLine("int: Median = " + query2);



...


/*
 This code produces the following output:

 Double: Median = 4.85
 Integer: Median = 3
*/

To create a generic overload

You can also create an overload that accepts a sequence of generic objects. This overload takes a delegate as a parameter and uses it to convert a sequence of objects of a generic type to a specific type.

The following code shows an overload of the Median method that takes the Func<T, TResult> delegate as a parameter. This delegate takes an object of generic type T and returns an object of type double.

' Generic overload.

<Extension()> 
Function Median(Of T)(ByVal source As IEnumerable(Of T), 
                      ByVal selector As Func(Of T, Double)) As Double 
    Return Aggregate num In source Select selector(num) Into med = Median()
End Function
// Generic overload. 

public static double Median<T>(this IEnumerable<T> numbers,
                       Func<T, double> selector)
{
    return (from num in numbers select selector(num)).Median();
}

You can now call the Median method for a sequence of objects of any type. If the type does not have its own method overload, you have to pass a delegate parameter. In Visual Basic and C#, you can use a lambda expression for this purpose. Also, in Visual Basic only, if you use the Aggregate or Group By clause instead of the method call, you can pass any value or expression that is in the scope this clause.

The following example code shows how to call the Median method for an array of integers and an array of strings. For strings, the median for the lengths of strings in the array is calculated. The example shows how to pass the Func<T, TResult> delegate parameter to the Median method for each case.

Dim numbers3() As Integer = {1, 2, 3, 4, 5}

' You can use num as a parameter for the Median method  
' so that the compiler will implicitly convert its value to double. 
' If there is no implicit conversion, the compiler will 
' display an error message. 

Dim query3 = Aggregate num In numbers3 Into Median(num)

Console.WriteLine("Integer: Median = " & query3)

Dim numbers4() As String = {"one", "two", "three", "four", "five"}

' With the generic overload, you can also use numeric properties of objects. 

Dim query4 = Aggregate str In numbers4 Into Median(str.Length)

Console.WriteLine("String: Median = " & query4)

' This code produces the following output: 
' 
' Integer: Median = 3 
' String: Median = 4
int[] numbers3 = { 1, 2, 3, 4, 5 };

/* 
  You can use the num=>num lambda expression as a parameter for the Median method 
  so that the compiler will implicitly convert its value to double.
  If there is no implicit conversion, the compiler will display an error message.          
*/ 

var query3 = numbers3.Median(num => num);

Console.WriteLine("int: Median = " + query3);

string[] numbers4 = { "one", "two", "three", "four", "five" };

// With the generic overload, you can also use numeric properties of objects. 

var query4 = numbers4.Median(str => str.Length);

Console.WriteLine("String: Median = " + query4);

/*
 This code produces the following output:

 Integer: Median = 3
 String: Median = 4
*/

Adding a Method That Returns a Collection

You can extend the IEnumerable<T> interface with a custom query method that returns a sequence of values. In this case, the method must return a collection of type IEnumerable<T>. Such methods can be used to apply filters or data transforms to a sequence of values.

The following example shows how to create an extension method named AlternateElements that returns every other element in a collection, starting from the first element.

' Extension method for the IEnumerable(of T) interface.  
' The method returns every other element of a sequence.

<Extension()> 
Function AlternateElements(Of T)(
    ByVal source As IEnumerable(Of T)
    ) As IEnumerable(Of T)

    Dim list As New List(Of T)
    Dim i = 0
    For Each element In source
        If (i Mod 2 = 0) Then
            list.Add(element)
        End If
        i = i + 1
    Next 
    Return list
End Function
// Extension method for the IEnumerable<T> interface.  
// The method returns every other element of a sequence. 

public static IEnumerable<T> AlternateElements<T>(this IEnumerable<T> source)
{
    List<T> list = new List<T>();

    int i = 0;

    foreach (var element in source)
    {
        if (i % 2 == 0)
        {
            list.Add(element);
        }

        i++;
    }

    return list;
}

You can call this extension method for any enumerable collection just as you would call other methods from the IEnumerable<T> interface, as shown in the following code:

Dim strings() As String = {"a", "b", "c", "d", "e"}

Dim query = strings.AlternateElements()

For Each element In query
    Console.WriteLine(element)
Next 

' This code produces the following output: 
' 
' a 
' c 
' e
string[] strings = { "a", "b", "c", "d", "e" };

var query = strings.AlternateElements();

foreach (var element in query)
{
    Console.WriteLine(element);
}
/*
 This code produces the following output:

 a
 c
 e
*/

See Also

Reference

IEnumerable<T>

Extension Methods (C# Programming Guide)

Concepts

Extension Methods (Visual Basic)