Escritura de consultas LINQ de C# para consultar datos

La mayoría de las consultas de la documentación introductoria de Language Integrated Query (LINK) se escribe con la sintaxis de consulta declarativa de LINQ. Pero la sintaxis de consulta debe traducirse en llamadas de método para .NET Common Language Runtime (CLR) al compilar el código. Estas llamadas de método invocan los operadores de consulta estándar, que tienen nombres tales como Where, Select, GroupBy, Join, Max y Average. Puede llamarlas directamente con la sintaxis de método en lugar de la sintaxis de consulta.

La sintaxis de consulta y la sintaxis de método son idénticas desde el punto de vista semántico, pero la sintaxis de consulta suele ser mucho más sencilla y fácil de leer. Algunos métodos deben expresarse como llamadas de método. Por ejemplo, debe usar una llamada de método para expresar una consulta que recupera el número de elementos que cumplen una condición especificada. También debe usar una llamada de método para una consulta que recupera el elemento que tiene el valor máximo de una secuencia de origen. La documentación de referencia de los operadores de consulta estándar del espacio de nombres System.Linq generalmente usa la sintaxis de método. Debería familiarizarse con cómo usar la sintaxis del método en consultas y en las propias expresiones de consulta.

Métodos de extensión de operador de consulta estándar

En el ejemplo siguiente se muestra una expresión de consulta sencilla y la consulta equivalente desde el punto de vista semántico que se escribe como consulta basada en métodos.

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

El resultado de los dos ejemplos es idéntico. Como puede ver, el tipo de variable de consulta es el mismo en ambos formularios: IEnumerable<T>.

Para entender la consulta basada en métodos, vamos a examinarla más detenidamente. En el lado derecho de la expresión, observe que la cláusula where ahora se expresa como un método de instancia en el objeto numbers, que tiene un tipo de IEnumerable<int>. Si está familiarizado con la interfaz genérica IEnumerable<T>, sabrá que no tiene un método Where. Pero si se invoca la lista de finalización de IntelliSense en el IDE de Visual Studio, verá no solo un método Where, sino muchos otros métodos tales como Select, SelectMany, Join y Orderby. Estos métodos implementan los operadores de consulta estándar.

Captura de pantalla en la que se muestran todos los operadores de consulta estándar de IntelliSense.

Aunque parece que IEnumerable<T> incluye más métodos, no es así. Los operadores de consulta estándar se implementan como métodos de extensión. Los métodos de extensión "extienden" un tipo existente; se pueden llamar como si fueran métodos de instancia en el tipo. Los operadores de consulta estándar extienden IEnumerable<T> y esta es la razón por la que la puede escribir numbers.Where(...).

Para usar métodos de extensión, es necesario incluirlos en el ámbito de las directivas using. Desde el punto de vista de la aplicación, un método de extensión y un método de instancia normal son iguales.

Para obtener más información sobre los métodos de extensión, vea Métodos de extensión. Para obtener más información sobre los operadores de consulta estándar, vea Información general sobre operadores de consulta estándar (C#). Algunos proveedores LINQ, como Entity Framework y LINQ to XML, implementan sus propios operadores de consulta estándar y métodos de extensión para otros tipos además de IEnumerable<T>.

Expresiones lambda

En el ejemplo anterior, observe que la expresión condicional (num % 2 == 0) se pasa como argumento insertado al método Enumerable.Where: Where(num => num % 2 == 0). Esta expresión insertada se denomina expresión lambda. Es una manera cómoda de escribir código que, de lo contrario, tendría que escribirse de forma más complicada. La num situada a la izquierda del operador es la variable de entrada que corresponde a num en la expresión de consulta. El compilador puede deducir el tipo de num porque sabe que numbers es un tipo IEnumerable<T> genérico. El cuerpo de la expresión lambda es exactamente igual que la expresión de la sintaxis de consulta o de cualquier otra expresión o instrucción de C#. Puede incluir llamadas de método y otra lógica compleja. El "valor devuelto" es simplemente el resultado de la expresión. Determinadas consultas solo se pueden expresar en sintaxis de método y algunas requieren expresiones lambda. Las expresiones lambda son una herramienta eficaz y flexible en el cuadro de herramientas de LINQ.

Capacidad de composición de consultas

En el ejemplo de código anterior, el método Enumerable.OrderBy se invoca mediante el operador de punto en la llamada a Where. Where genera una secuencia filtrada y, a continuación, Orderby ordena la secuencia generada por Where. Dado que las consultas devuelven un IEnumerable, redáctelas con la sintaxis de método encadenando las llamadas de método. El compilador realiza esta composición al escribir consultas mediante la sintaxis de consulta. Dado que una variable de consulta no almacena los resultados de la consulta, es posible modificarla o usarla como base para una nueva consulta en cualquier momento, incluso después de ejecutarla.

Los ejemplos siguientes muestran algunas consultas LINQ sencillas mediante cada enfoque enumerado anteriormente.

Nota:

Estas consultas funcionan en colecciones en memoria simples, pero la sintaxis básica es idéntica a la empleada en LINQ to Entities y LINQ to XML.

Ejemplo: Sintaxis de consulta

La mayoría de las consultas se escriben con sintaxis de consulta para crear expresiones de consulta. En el siguiente ejemplo se muestran tres expresiones de consulta. La primera expresión de consulta muestra cómo filtrar o restringir los resultados mediante la aplicación de condiciones con una cláusula where. Devuelve todos los elementos de la secuencia de origen cuyos valores sean mayores que 7 o menores que 3. La segunda expresión muestra cómo ordenar los resultados devueltos. La tercera expresión muestra cómo agrupar los resultados según una clave. Esta consulta devuelve dos grupos en función de la primera letra de la palabra.

List<int> numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

El tipo de las consultas es IEnumerable<T>. Todas estas consultas podrían escribirse mediante var como se muestra en el ejemplo siguiente:

var query = from num in numbers...

En cada ejemplo anterior, las consultas no se ejecutan realmente hasta que se recorre en iteración la variable de consulta en una instrucción foreach o cualquier otra instrucción.

Ejemplo: Sintaxis de método

Algunas operaciones de consulta deben expresarse como una llamada a método. Los más comunes de dichos métodos son aquellos que devuelven valores numéricos de singleton, como Sum, Max, Min, Average y así sucesivamente. A estos métodos siempre se los debe llamar en último lugar en cualquier consulta porque devuelven un solo valor y no pueden servir como origen para una operación de consulta adicional. En el ejemplo siguiente se muestra una llamada a método en una expresión de consulta:

List<int> numbers1 = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
List<int> numbers2 = [15, 14, 11, 13, 19, 18, 16, 17, 12, 10];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

Si el método tiene parámetros System.Action o System.Func<TResult>, se proporcionan estos argumentos en forma de expresión lambda, tal y como se muestra en el ejemplo siguiente:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

En las consultas anteriores, solo la consulta n.º 4 se ejecuta inmediatamente, ya que devuelve un solo valor y no una colección IEnumerable<T> genérica. El propio método usa foreach o código similar para procesar su valor.

Cada una de las consultas anteriores puede escribirse mediante tipos implícitos con `var``, tal y como se muestra en el ejemplo siguiente:

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

Ejemplo: Sintaxis de consulta y método combinada

En este ejemplo se muestra cómo usar la sintaxis de método en los resultados de una cláusula de consulta. Simplemente escriba entre paréntesis la expresión de consulta y luego aplique el operador punto y llame al método. En el ejemplo siguiente la consulta número 7 devuelve un recuento de los números cuyo valor está comprendido entre 3 y 7. Sin embargo, en general es mejor usar una segunda variable para almacenar el resultado de la llamada de método. De esta manera es menos probable que la consulta se confunda con los resultados de la consulta.

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

Dado que la consulta número 7 devuelve un solo valor y no una colección, se ejecuta inmediatamente.

La consulta anterior puede escribirse mediante tipos implícitos con var como sigue:

var numCount = (from num in numbers...

Puede escribirse en sintaxis de método como sigue:

var numCount = numbers.Count(n => n is > 3 and < 7);

Puede escribirse mediante tipos explícitos como sigue:

int numCount = numbers.Count(n => n is > 3 and < 7);

Especificación de filtros con predicado de forma dinámica en tiempo de ejecución

En algunos casos, no se conoce cuántos predicados hay que aplicar a los elementos de origen de la cláusula where hasta el tiempo de ejecución. Una forma de especificar dinámicamente varios filtros con predicado es usar el método Contains, como se muestra en el ejemplo siguiente. La consulta devuelve resultados distintos en función del valor de id al ejecutarse la consulta.

int[] ids = [111, 114, 112];

var queryNames =
    from student in students
    where ids.Contains(student.ID)
    select new
    {
        student.LastName,
        student.ID
    };

foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Garcia: 114
    O'Donnell: 112
    Omelchenko: 111
 */

// Change the ids.
ids = [122, 117, 120, 115];

// The query will now return different results
foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Adams: 120
    Feng: 117
    Garcia: 115
    Tucker: 122
 */

Puede usar instrucciones de flujo de control, como if... else o switch, para seleccionar entre consultas alternativas predeterminadas. En el ejemplo siguiente, studentQuery usa una cláusula where diferente si el valor del tiempo de ejecución de oddYear es true o false.

void FilterByYearType(bool oddYear)
{
    IEnumerable<Student> studentQuery = oddYear
        ? (from student in students
           where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
           select student)
        : (from student in students
           where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
           select student);
    var descr = oddYear ? "odd" : "even";
    Console.WriteLine($"The following students are at an {descr} year level:");
    foreach (Student name in studentQuery)
    {
        Console.WriteLine($"{name.LastName}: {name.ID}");
    }
}

FilterByYearType(true);

/* Output:
    The following students are at an odd year level:
    Fakhouri: 116
    Feng: 117
    Garcia: 115
    Mortensen: 113
    Tucker: 119
    Tucker: 122
 */

FilterByYearType(false);

/* Output:
    The following students are at an even year level:
    Adams: 120
    Garcia: 114
    Garcia: 118
    O'Donnell: 112
    Omelchenko: 111
    Zabokritski: 121
 */

Controlar valores nulos en expresiones de consulta

En este ejemplo se muestra cómo controlar los posibles valores nulos en colecciones de origen. Una colección de objetos como IEnumerable<T> puede contener elementos cuyo valor es NULL. Si una colección de origen es null, o contiene un elemento cuyo valor es null, y la consulta no controla los valores null, se inicia un elemento NullReferenceException cuando se ejecute la consulta.

Se pueden codificar de forma defensiva para evitar una excepción de referencia nula, tal y como se muestra en el ejemplo siguiente:

var query1 =
    from c in categories
    where c != null
    join p in products on c.ID equals p?.CategoryID
    select new
    {
        Category = c.Name,
        Name = p.Name
    };

En el ejemplo anterior, la cláusula where filtra todos los elementos nulos de la secuencia de categorías. Esta técnica es independiente de la comprobación de null en la cláusula join. La expresión condicional con NULL de este ejemplo funciona porque Products.CategoryID es de tipo int?, que es una abreviatura de Nullable<int>.

En una cláusula join, si solo una de las claves de comparación es de un tipo que acepta valores NULL, puede convertir la otra en un tipo que acepta valores NULL en la expresión de consulta. En el ejemplo siguiente, suponga que EmployeeID es una columna que contiene valores de tipo int?:

var query =
    from o in db.Orders
    join e in db.Employees
        on o.EmployeeID equals (int?)e.EmployeeID
    select new { o.OrderID, e.FirstName };

En cada uno de los ejemplos, se usa la palabra clave de consulta equals. También puede usar coincidencia de patrones, que incluye patrones para is null y is not null. Estos patrones no se recomiendan en las consultas de LINQ, ya que es posible que los proveedores de consultas no interpreten correctamente la sintaxis nueva de C#. Un proveedor de consultas es una biblioteca que traduce expresiones de consulta de C# a un formato de datos nativo, como Entity Framework Core. Los proveedores de consultas implementan la interfaz System.Linq.IQueryProvider para crear orígenes de datos que implementan la interfaz System.Linq.IQueryable<T>.

Controlar excepciones en expresiones de consulta

Es posible llamar a cualquier método en el contexto de una expresión de consulta. En las expresiones de consulta, no llame a cualquier método que pueda crear un efecto secundario, como modificar el contenido del origen de datos o producir una excepción. En este ejemplo se muestra cómo evitar que se produzcan excepciones al llamar a métodos en una expresión de consulta sin infringir las instrucciones generales de .NET sobre el control de excepciones. En esas instrucciones se indica que es aceptable detectar una excepción concreta cuando se comprende por qué se produce en un contexto determinado. Para obtener más información, vea Procedimientos recomendados para excepciones.

En el último ejemplo se muestra cómo controlar los casos en los que se debe producir una excepción durante la ejecución de una consulta.

En el ejemplo siguiente se muestra cómo mover código de control de excepciones fuera de una expresión de consulta. Esta refactorización solo es posible cuando el método no depende de ninguna variable local de la consulta. Es más fácil tratar las excepciones fuera de la expresión de consulta.

// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();

// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
    dataSource = GetData();
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null)
{
    // If we get here, it is safe to proceed.
    var query =
        from i in dataSource
        select i * i;

    foreach (var i in query)
    {
        Console.WriteLine(i.ToString());
    }
}

En el bloque catch (InvalidOperationException) del ejemplo anterior, controle (o no controle) la excepción de la manera adecuada para la aplicación.

En algunos casos, la mejor respuesta a una excepción que se produce dentro de una consulta podría ser detener la ejecución de la consulta inmediatamente. En el ejemplo siguiente se muestra cómo controlar las excepciones que pueden producirse desde el cuerpo de una consulta. Supongamos que SomeMethodThatMightThrow puede producir una excepción que requiere que se detenga la ejecución de la consulta.

El bloque try encierra el bucle foreach, no la propia consulta. El bucle foreach es el punto en el que se ejecuta la consulta. Las excepciones en tiempo de ejecución se producen cuando se ejecuta la consulta. Por lo tanto, deben controlarse en el bucle foreach.

// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
    s[4] == 'C' ?
        throw new InvalidOperationException() :
        @"C:\newFolder\" + s;

// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];

// Demonstration query that throws.
var exceptionDemoQuery =
    from file in files
    let n = SomeMethodThatMightThrow(file)
    select n;

try
{
    foreach (var item in exceptionDemoQuery)
    {
        Console.WriteLine($"Processing {item}");
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

/* Output:
    Processing C:\newFolder\fileA.txt
    Processing C:\newFolder\fileB.txt
    Operation is not valid due to the current state of the object.
 */

Recuerde detectar cualquier excepción que espere generar o realice cualquier limpieza necesaria en un bloque finally.

Consulte también