LINQ: consulta de .NET Language-Integrated
Don Box, Anders Hejlsberg
Febrero de 2007
Se aplica a:
Visual Studio Code nombre "Orcas"
.NET Framework 3.5
Resumen: Las instalaciones de consulta de uso general agregadas a .NET Framework se aplican a todos los orígenes de información, no solo a los datos relacionales o XML. Esta instalación se denomina consulta de .NET Language-Integrated (LINQ). (32 páginas impresas)
Contenido
Consulta de Language-Integrated de .NET
Introducción con operadores de consulta estándar
Características de lenguaje compatibles con el proyecto LINQ
Más operadores de consulta estándar
Sintaxis de las consultas
LINQ to SQL: Integración de SQL
LINQ to XML: Integración XML
Resumen
Consulta de Language-Integrated de .NET
Después de dos décadas, la industria ha alcanzado un punto estable en la evolución de las tecnologías de programación orientadas a objetos (OO). Los programadores ahora toman características concedidas como clases, objetos y métodos. Al examinar la actual y la próxima generación de tecnologías, se ha vuelto evidente que el siguiente gran desafío en la tecnología de programación es reducir la complejidad del acceso e integración de la información que no está definida de forma nativa mediante la tecnología de OO. Los dos orígenes más comunes de información que no son de OO son bases de datos relacionales y XML.
En lugar de agregar características relacionales o específicas de XML a nuestros lenguajes de programación y tiempo de ejecución, con el proyecto LINQ hemos adoptado un enfoque más general y estamos agregando instalaciones de consulta de uso general a .NET Framework que se aplican a todos los orígenes de información, no solo a los datos relacionales o XML. Esta instalación se denomina consulta de .NET Language-Integrated (LINQ).
Usamos el término consulta integrada en lenguaje para indicar que la consulta es una característica integrada de los lenguajes de programación principales del desarrollador (por ejemplo, Visual C#, Visual Basic). La consulta integrada en lenguaje permite que las expresiones de consulta se beneficien de los metadatos enriquecidos , la comprobación de sintaxis en tiempo de compilación, la escritura estática e IntelliSense que anteriormente solo estaba disponible para el código imperativo. La consulta integrada en lenguaje también permite aplicar una única función de consulta declarativa de uso general a toda la información en memoria, no solo a la información de orígenes externos.
.NET Language-Integrated Query define un conjunto de operadores de consulta estándar de uso general que permiten expresar operaciones de recorrido, filtro y proyección de forma directa pero declarativa en cualquier . Lenguaje de programación basado en NET. Los operadores de consulta estándar permiten aplicar consultas a cualquier origen de información basado en T> IEnumerable<. LINQ permite a terceros aumentar el conjunto de operadores de consulta estándar con nuevos operadores específicos del dominio que son adecuados para el dominio o la tecnología de destino. Lo más importante es que los terceros también pueden reemplazar a los operadores de consulta estándar por sus propias implementaciones que proporcionan servicios adicionales, como la evaluación remota, la traducción de consultas, la optimización, etc. Al adherirse a las convenciones del patrón LINQ, estas implementaciones disfrutan de la misma integración de lenguaje y compatibilidad con herramientas que los operadores de consulta estándar.
La extensibilidad de la arquitectura de consulta se usa en el propio proyecto LINQ para proporcionar implementaciones que funcionan sobre datos XML y SQL. Los operadores de consulta a través de XML (LINQ to XML) usan una instalación XML eficaz, fácil de usar y en memoria para proporcionar funcionalidad XPath/XQuery en el lenguaje de programación host. Los operadores de consulta sobre datos relacionales (LINQ to SQL) se basan en la integración de definiciones de esquema basadas en SQL en el sistema de tipos de Common Language Runtime (CLR). Esta integración proporciona una escritura segura sobre los datos relacionales, a la vez que conserva la eficacia expresiva del modelo relacional y el rendimiento de la evaluación de consultas directamente en el almacén subyacente.
Introducción con operadores de consulta estándar
Para ver la consulta integrada en el lenguaje en el trabajo, comenzaremos con un sencillo programa de C# 3.0 que usa los operadores de consulta estándar para procesar el contenido de una matriz:
using System;
using System.Linq;
using System.Collections.Generic;
class app {
static void Main() {
string[] names = { "Burke", "Connor", "Frank",
"Everett", "Albert", "George",
"Harris", "David" };
IEnumerable<string> query = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
foreach (string item in query)
Console.WriteLine(item);
}
}
Si fuera a compilar y ejecutar este programa, lo vería como salida:
BURKE
DAVID
FRANK
To understand how language-integrated query works, we need to dissect the
first statement of our program.
IEnumerable<string> query = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
La consulta de variable local se inicializa con una expresión de consulta. Una expresión de consulta funciona en uno o varios orígenes de información aplicando uno o varios operadores de consulta de los operadores de consulta estándar o los operadores específicos del dominio. Esta expresión usa tres de los operadores de consulta estándar: Where, OrderBy y Select.
Visual Basic 9.0 también admite LINQ. Esta es la instrucción anterior escrita en Visual Basic 9.0:
Dim query As IEnumerable(Of String) = From s in names _
Where s.Length = 5 _
Order By s _
Select s.ToUpper()
Las instrucciones de C# y Visual Basic que se muestran aquí usan expresiones de consulta. Al igual que la instrucción foreach , las expresiones de consulta son una abreviatura declarativa práctica sobre el código que puede escribir manualmente. Las instrucciones anteriores son semánticamente idénticas a la siguiente sintaxis explícita que se muestra en C#:
IEnumerable<string> query = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
Esta forma de consulta se denomina consulta basada en métodos . Los argumentos de los operadores Where, OrderBy y Select se denominan expresiones lambda, que son fragmentos de código muy similares a los delegados. Permiten que los operadores de consulta estándar se definan individualmente como métodos y se agrupan mediante notación de puntos. Juntos, estos métodos forman la base de un lenguaje de consulta extensible.
Características de lenguaje compatibles con el proyecto LINQ
LINQ se basa completamente en características de lenguaje de uso general, algunas de las cuales son nuevas en C# 3.0 y Visual Basic 9.0. Cada una de estas características tiene utilidad propia, pero colectivamente estas características proporcionan una manera extensible de definir consultas y API consultables. En esta sección se exploran estas características de lenguaje y cómo contribuyen a un estilo mucho más directo y declarativo de consultas.
Expresiones lambda y árboles de expresión
Muchos operadores de consulta permiten al usuario proporcionar una función que realiza el filtrado, la proyección o la extracción de claves. Las instalaciones de consulta se basan en el concepto de expresiones lambda, que proporcionan a los desarrolladores una manera cómoda de escribir funciones que se pueden pasar como argumentos para la evaluación posterior. Las expresiones lambda son similares a los delegados CLR y deben cumplir una firma de método definida por un tipo delegado. Para ilustrar esto, podemos expandir la instrucción anterior en un formulario equivalente pero más explícito mediante el tipo de delegado Func :
Func<string, bool> filter = s => s.Length == 5;
Func<string, string> extract = s => s;
Func<string, string> project = s => s.ToUpper();
IEnumerable<string> query = names.Where(filter)
.OrderBy(extract)
.Select(project);
Las expresiones lambda son la evolución natural de los métodos anónimos en C# 2.0. Por ejemplo, podríamos haber escrito el ejemplo anterior mediante métodos anónimos como este:
Func<string, bool> filter = delegate (string s) {
return s.Length == 5;
};
Func<string, string> extract = delegate (string s) {
return s;
};
Func<string, string> project = delegate (string s) {
return s.ToUpper();
};
IEnumerable<string> query = names.Where(filter)
.OrderBy(extract)
.Select(project);
En general, el desarrollador puede usar métodos con nombre, métodos anónimos o expresiones lambda con operadores de consulta. Las expresiones lambda tienen la ventaja de proporcionar la sintaxis más directa y compacta para la creación. Lo más importante es que las expresiones lambda se pueden compilar como código o datos, lo que permite que los optimizadores, los traductores y los evaluadores procesen expresiones lambda en tiempo de ejecución.
El espacio de nombres System.Linq.Expressions define un tipo genérico distintivo, Expresión<T>, que indica que se desea un árbol de expresiones para una expresión lambda determinada en lugar de un cuerpo de método basado en IL tradicional. Los árboles de expresión son representaciones de datos eficaces en memoria de expresiones lambda y hacen que la estructura de la expresión sea transparente y explícita.
La determinación de si el compilador emitirá il ejecutable o un árbol de expresión viene determinado por cómo se usa la expresión lambda. Cuando se asigna una expresión lambda a una variable, campo o parámetro cuyo tipo es un delegado, el compilador emite IL idéntico al de un método anónimo. Cuando se asigna una expresión lambda a una variable, campo o parámetro cuyo tipo es Expression<T> para algún tipo delegado T, el compilador emite un árbol de expresiones en su lugar.
Por ejemplo, tenga en cuenta las dos declaraciones de variables siguientes:
Func<int, bool> f = n => n < 5;
Expression<Func<int, bool>> e = n => n < 5;
La variable f es una referencia a un delegado que es ejecutable directamente:
bool isSmall = f(2); // isSmall is now true
La variable e es una referencia a un árbol de expresión que no es ejecutable directamente:
bool isSmall = e(2); // compile error, expressions == data
A diferencia de los delegados, que son código opaco de forma eficaz, podemos interactuar con el árbol de expresiones igual que cualquier otra estructura de datos del programa.
Expression<Func<int, bool>> filter = n => n < 5;
BinaryExpression body = (BinaryExpression)filter.Body;
ParameterExpression left = (ParameterExpression)body.Left;
ConstantExpression right = (ConstantExpression)body.Right;
Console.WriteLine("{0} {1} {2}",
left.Name, body.NodeType, right.Value);
En el ejemplo anterior se descompone el árbol de expresiones en tiempo de ejecución e imprime la cadena siguiente:
n LessThan 5
Esta capacidad para tratar expresiones como datos en tiempo de ejecución es fundamental para habilitar un ecosistema de bibliotecas de terceros que aprovechan las abstracciones de consulta base que forman parte de la plataforma. La implementación de acceso a datos LINQ to SQL aprovecha esta instalación para traducir árboles de expresión a instrucciones T-SQL adecuadas para la evaluación en el almacén.
Métodos de extensión
Las expresiones lambda son una parte importante de la arquitectura de consulta. Los métodos de extensión son otros. Los métodos de extensión combinan la flexibilidad de "escritura de pato" hecha popular en lenguajes dinámicos con la validación en tiempo de compilación y rendimiento de lenguajes de tipo estático. Con los métodos de extensión, terceros pueden aumentar el contrato público de un tipo con nuevos métodos, a la vez que permiten a los autores de tipos individuales proporcionar su propia implementación especializada de esos métodos.
Los métodos de extensión se definen en clases estáticas como métodos estáticos, pero se marcan con el atributo [System.Runtime.CompilerServices.Extension] en los metadatos clR. Se recomienda que los lenguajes proporcionen una sintaxis directa para los métodos de extensión. En C#, los métodos de extensión se indican mediante el modificador que se debe aplicar al primer parámetro del método de extensión. Echemos un vistazo a la definición del operador de consulta más sencillo, Donde:
namespace System.Linq {
using System;
using System.Collections.Generic;
public static class Enumerable {
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, bool> predicate) {
foreach (T item in source)
if (predicate(item))
yield return item;
}
}
}
El tipo del primer parámetro de un método de extensión indica a qué tipo se aplica la extensión. En el ejemplo anterior, el método de extensión Where extiende el tipo IEnumerable<T>. Dado que Where es un método estático, podemos invocarlo directamente como cualquier otro método estático:
IEnumerable<string> query = Enumerable.Where(names,
s => s.Length < 6);
Sin embargo, lo que hace que los métodos de extensión sean únicos es que también se pueden invocar mediante la sintaxis de instancia:
IEnumerable<string> query = names.Where(s => s.Length < 6);
Los métodos de extensión se resuelven en tiempo de compilación en función de qué métodos de extensión están en el ámbito. Cuando se importa un espacio de nombres con una instrucción using en C# o una instrucción Import en Visual Basic, todos los métodos de extensión definidos por clases estáticas de ese espacio de nombres se incorporan al ámbito.
Los operadores de consulta estándar se definen como métodos de extensión en el tipo System.Linq.Enumerable. Al examinar los operadores de consulta estándar, observará que todos pero algunos de ellos se definen en términos de la interfaz IEnumerable<T> . Esto significa que cada origen de información compatible con T> con IEnumerable< obtiene los operadores de consulta estándar simplemente agregando la siguiente instrucción using en C#:
using System.Linq; // makes query operators visible
Los usuarios que quieran reemplazar los operadores de consulta estándar para un tipo específico pueden: definir sus propios métodos con mismo nombre en el tipo específico con firmas compatibles o definir nuevos métodos de extensión con mismo nombre que extienden el tipo específico. Los usuarios que quieran evitar por completo los operadores de consulta estándar simplemente no pueden poner System.Linq en el ámbito y escribir sus propios métodos de extensión para IEnumerable<T>.
Los métodos de extensión tienen la prioridad más baja en términos de resolución y solo se usan si no hay ninguna coincidencia adecuada en el tipo de destino y sus tipos base. Esto permite que los tipos definidos por el usuario proporcionen sus propios operadores de consulta que tienen prioridad sobre los operadores estándar. Por ejemplo, considere la siguiente colección personalizada:
public class MySequence : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
for (int i = 1; i <= 10; i++)
yield return i;
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
public IEnumerable<int> Where(Func<int, bool> filter) {
for (int i = 1; i <= 10; i++)
if (filter(i))
yield return i;
}
}
Dada esta definición de clase, el siguiente programa usará la implementación MySequence.Where , no el método de extensión, ya que los métodos de instancia tienen prioridad sobre los métodos de extensión:
MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
Console.WriteLine(item);
El operador OfType es uno de los pocos operadores de consulta estándar que no extiende un origen de información basado en T> IEnumerable<. Echemos un vistazo al operador de consulta OfType :
public static IEnumerable<T> OfType<T>(this IEnumerable source) {
foreach (object item in source)
if (item is T)
yield return (T)item;
}
OfType acepta no solo orígenes basados en T> IEnumerable<, sino también orígenes escritos en la interfaz IEnumerable no parametrizada que estaba presente en la versión 1.0 de .NET Framework. El operador OfType permite a los usuarios aplicar los operadores de consulta estándar a colecciones clásicas de .NET como esta:
// "classic" cannot be used directly with query operators
IEnumerable classic = new OlderCollectionType();
// "modern" can be used directly with query operators
IEnumerable<object> modern = classic.OfType<object>();
En este ejemplo, la variable modern
produce la misma secuencia de valores que el clásico. Sin embargo, su tipo es compatible con el código T> IEnumerable< moderno, incluidos los operadores de consulta estándar.
El operador OfType también es útil para orígenes de información más recientes, ya que permite filtrar valores de un origen basado en el tipo. Al generar la nueva secuencia, OfType simplemente omite los miembros de la secuencia original que no son compatibles con el argumento type. Considere este programa sencillo que extrae cadenas de una matriz heterogénea:
object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();
Al enumerar la variable justStrings en una instrucción foreach , obtendremos una secuencia de dos cadenas: "Hello" y "World".
Evaluación diferida de consultas
Es posible que los lectores observantes hayan observado que el operador Where estándar se implementa mediante la construcción de rendimiento introducida en C# 2.0. Esta técnica de implementación es común para todos los operadores estándar que devuelven secuencias de valores. El uso de yield tiene una ventaja interesante, que es que la consulta no se evalúa realmente hasta que se itera con una instrucción foreach o mediante el uso manual de los métodos GetEnumerator y MoveNext subyacentes. Esta evaluación diferida permite mantener las consultas como valores basados en T> IEnumerable< que se pueden evaluar varias veces, cada vez que producen resultados potencialmente diferentes.
Para muchas aplicaciones, este es exactamente el comportamiento que se desea. En el caso de las aplicaciones que quieran almacenar en caché los resultados de la evaluación de consultas, se proporcionan dos operadores, ToList y ToArray, que fuerzan la evaluación inmediata de la consulta y devuelven una lista<T> o una matriz que contiene los resultados de la evaluación de la consulta.
Para ver cómo funciona la evaluación de consultas diferida, considere este programa que ejecuta una consulta simple sobre una matriz:
// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };
// declare a variable that represents a query
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');
// evaluate the query
foreach (string item in ayes)
Console.WriteLine(item);
// modify the original information source
names[0] = "Bob";
// evaluate the query again, this time no "Allen"
foreach (string item in ayes)
Console.WriteLine(item);
La consulta se evalúa cada vez que se itera la variable. Para indicar que se necesita una copia almacenada en caché de los resultados, simplemente podemos anexar un operador ToList o ToArray a la consulta de la siguiente manera:
// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };
// declare a variable that represents the result
// of an immediate query evaluation
string[] ayes = names.Where(s => s[0] == 'A').ToArray();
// iterate over the cached query results
foreach (string item in ayes)
Console.WriteLine(item);
// modifying the original source has no effect on ayes
names[0] = "Bob";
// iterate over result again, which still contains "Allen"
foreach (string item in ayes)
Console.WriteLine(item);
Tanto ToArray comoToList fuerzan la evaluación inmediata de consultas. Lo mismo sucede con los operadores de consulta estándar que devuelven valores singleton (por ejemplo: First, ElementAt, Sum, Average, All, Any).
Interfaz IQueryable<T>
Normalmente, se desea el mismo modelo de ejecución diferido para los orígenes de datos que implementan la funcionalidad de consulta mediante árboles de expresión, como LINQ to SQL. Estos orígenes de datos pueden beneficiarse de la implementación de la interfaz IQueryable<T> para la que todos los operadores de consulta requeridos por el patrón LINQ se implementan mediante árboles de expresión. Cada IQueryable<T> tiene una representación de "el código necesario para ejecutar la consulta" en forma de árbol de expresión. Todos los operadores de consulta diferidos devuelven un nuevo T IQueryable<> que aumenta ese árbol de expresiones con una representación de una llamada a ese operador de consulta. Por lo tanto, cuando se convierte en el momento de evaluar la consulta, normalmente porque se enumera IQueryable<T> , el origen de datos puede procesar el árbol de expresiones que representa toda la consulta en un lote. Por ejemplo, una consulta LINQ to SQL complicada obtenida por numerosas llamadas a operadores de consulta puede dar lugar a que solo se envíe una sola consulta SQL a la base de datos.
La ventaja de los implementadores de orígenes de datos de reutilizar esta funcionalidad de aplazamiento mediante la implementación de la interfaz IQueryable<T>
es obvia. Por otro lado, para los clientes que escriben las consultas, es una gran ventaja tener un tipo común para orígenes de información remotos. No solo les permite escribir consultas polimórficas que se pueden usar en diferentes orígenes de datos, sino que también abre la posibilidad de escribir consultas que pasan por dominios.
Inicialización de valores compuestos
Las expresiones lambda y los métodos de extensión proporcionan todo lo que necesitamos para las consultas que simplemente filtran los miembros de una secuencia de valores. La mayoría de las expresiones de consulta también realizan proyección sobre esos miembros, transformando eficazmente los miembros de la secuencia original en miembros cuyo valor y tipo pueden diferir del original. Para admitir la escritura de estas transformaciones, LINQ se basa en una nueva construcción denominada inicializadores de objetos para crear nuevas instancias de tipos estructurados. En el resto de este documento, se supone que se ha definido el siguiente tipo:
public class Person {
string name;
int age;
bool canCode;
public string Name {
get { return name; } set { name = value; }
}
public int Age {
get { return age; } set { age = value; }
}
public bool CanCode {
get { return canCode; } set { canCode = value; }
}
}
Los inicializadores de objetos nos permiten construir fácilmente valores basados en los campos y propiedades públicos de un tipo. Por ejemplo, para crear un nuevo valor de tipo Person, podemos escribir esta instrucción:
Person value = new Person {
Name = "Chris Smith", Age = 31, CanCode = false
};
Semánticamente, esta instrucción es equivalente a la siguiente secuencia de instrucciones:
Person value = new Person();
value.Name = "Chris Smith";
value.Age = 31;
value.CanCode = false;
Los inicializadores de objetos son una característica importante para la consulta integrada en lenguaje, ya que permiten la construcción de nuevos valores estructurados en contextos en los que solo se permiten expresiones (como en expresiones lambda y árboles de expresión). Por ejemplo, considere esta expresión de consulta que crea un nuevo valor person para cada valor de la secuencia de entrada:
IEnumerable<Person> query = names.Select(s => new Person {
Name = s, Age = 21, CanCode = s.Length == 5
});
La sintaxis de inicialización de objetos también es conveniente para inicializar matrices de valores estructurados. Por ejemplo, considere esta variable de matriz que se inicializa mediante inicializadores de objetos individuales:
static Person[] people = {
new Person { Name="Allen Frances", Age=11, CanCode=false },
new Person { Name="Burke Madison", Age=50, CanCode=true },
new Person { Name="Connor Morgan", Age=59, CanCode=false },
new Person { Name="David Charles", Age=33, CanCode=true },
new Person { Name="Everett Frank", Age=16, CanCode=true },
};
Valores y tipos estructurados
El proyecto LINQ admite un estilo de programación centrado en los datos en el que algunos tipos existen principalmente para proporcionar una "forma" estática sobre un valor estructurado en lugar de un objeto completo con el estado y el comportamiento. Tomando esta premisa en su conclusión lógica, a menudo es el caso de que todo el desarrollador se preocupa por es la estructura del valor y la necesidad de un tipo con nombre para esa forma es de poco uso. Esto conduce a la introducción de tipos anónimos que permiten definir nuevas estructuras "insertadas" con su inicialización.
En C#, la sintaxis de los tipos anónimos es similar a la sintaxis de inicialización de objetos, salvo que se omite el nombre del tipo. Por ejemplo, tenga en cuenta las dos instrucciones siguientes:
object v1 = new Person {
Name = "Brian Smith", Age = 31, CanCode = false
};
object v2 = new { // note the omission of type name
Name = "Brian Smith", Age = 31, CanCode = false
};
Las variables v1 y v2 apuntan a un objeto en memoria cuyo tipo CLR tiene tres propiedades públicas Name, Age y CanCode. Las variables difieren en que v2 hace referencia a una instancia de un tipo anónimo. En términos CLR, los tipos anónimos no son diferentes de ningún otro tipo. Lo que hace que los tipos anónimos sean especiales es que no tienen ningún nombre significativo en el lenguaje de programación. La única manera de crear instancias de un tipo anónimo es usar la sintaxis mostrada anteriormente.
Para permitir que las variables hacen referencia a instancias de tipos anónimos pero todavía se beneficien de la escritura estática, C# presenta variables locales con tipo implícito: la palabra clave var se puede usar en lugar del nombre de tipo para las declaraciones de variables locales. Por ejemplo, considere este programa legal de C# 3.0:
var s = "Bob";
var n = 32;
var b = true;
La palabra clave var indica al compilador que infiera el tipo de variable del tipo estático de la expresión utilizada para inicializar la variable. En este ejemplo, los tipos de s, n y b son string, int y bool, respectivamente. Este programa es idéntico al siguiente:
string s = "Bob";
int n = 32;
bool b = true;
La palabra clave var es una comodidad para las variables cuyos tipos tienen nombres significativos, pero es una necesidad para las variables que hacen referencia a instancias de tipos anónimos.
var value = new {
Name = " Brian Smith", Age = 31, CanCode = false
};
En el ejemplo anterior, el valor de variable es de un tipo anónimo cuya definición es equivalente a la siguiente pseudo-C#:
internal class ??? {
string _Name;
int _Age;
bool _CanCode;
public string Name {
get { return _Name; } set { _Name = value; }
}
public int Age{
get { return _Age; } set { _Age = value; }
}
public bool CanCode {
get { return _CanCode; } set { _CanCode = value; }
}
public bool Equals(object obj) { ... }
public bool GetHashCode() { ... }
}
Los tipos anónimos no se pueden compartir entre límites de ensamblado; Sin embargo, el compilador garantiza que hay como máximo un tipo anónimo para una secuencia determinada de pares de nombre y tipo de propiedad dentro de cada ensamblado.
Dado que los tipos anónimos se suelen usar en proyecciones para seleccionar uno o varios miembros de un valor estructurado existente, podemos hacer referencia a campos o propiedades de otro valor en la inicialización de un tipo anónimo. Esto da como resultado que el nuevo tipo anónimo obtenga una propiedad cuyo nombre, tipo y valor se copien de la propiedad o campo al que se hace referencia.
Por ejemplo, considere este ejemplo que crea un nuevo valor estructurado mediante la combinación de propiedades de otros valores:
var bob = new Person { Name = "Bob", Age = 51, CanCode = true };
var jane = new { Age = 29, FirstName = "Jane" };
var couple = new {
Husband = new { bob.Name, bob.Age },
Wife = new { Name = jane.FirstName, jane.Age }
};
int ha = couple.Husband.Age; // ha == 51
string wn = couple.Wife.Name; // wn == "Jane"
La referencia a campos o propiedades que se muestran anteriormente es simplemente una sintaxis cómoda para escribir la siguiente forma más explícita:
var couple = new {
Husband = new { Name = bob.Name, Age = bob.Age },
Wife = new { Name = jane.FirstName, Age = jane.Age }
};
En ambos casos, la variable de pareja obtiene su propia copia de las propiedades Name y Age de bob y jane.
Los tipos anónimos se usan con más frecuencia en la cláusula select de una consulta. Por ejemplo, considere la siguiente consulta:
var query = people.Select(p => new {
p.Name, BadCoder = p.Age == 11
});
foreach (var item in query)
Console.WriteLine("{0} is a {1} coder",
item.Name,
item.BadCoder ? "bad" : "good");
En este ejemplo, pudimos crear una nueva proyección sobre el tipo Person que coincida exactamente con la forma que necesitamos para el código de procesamiento, pero aún nos dio las ventajas de un tipo estático.
Más operadores de consulta estándar
Además de las instalaciones básicas de consulta descritas anteriormente, varios operadores proporcionan formas útiles de manipular secuencias y redactar consultas, lo que proporciona al usuario un alto grado de control sobre el resultado dentro del marco práctico de los operadores de consulta estándar.
Ordenación y agrupación
En general, la evaluación de una consulta da como resultado una secuencia de valores que se producen en algún orden intrínseco en los orígenes de información subyacentes. Para proporcionar a los desarrolladores un control explícito sobre el orden en el que se generan estos valores, los operadores de consulta estándar se definen para controlar el orden. El más básico de estos operadores es el operador OrderBy .
Los operadores OrderBy y OrderByDescending se pueden aplicar a cualquier origen de información y permitir que el usuario proporcione una función de extracción de claves que genere el valor que se usa para ordenar los resultados. OrderBy y OrderByDescending también aceptan una función de comparación opcional que se puede usar para imponer un orden parcial sobre las claves. Echemos un vistazo a un ejemplo básico:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
// unity sort
var s1 = names.OrderBy(s => s);
var s2 = names.OrderByDescending(s => s);
// sort by length
var s3 = names.OrderBy(s => s.Length);
var s4 = names.OrderByDescending(s => s.Length);
Las dos primeras expresiones de consulta generan nuevas secuencias basadas en la ordenación de los miembros del origen en función de la comparación de cadenas. Las dos segundas consultas generan nuevas secuencias basadas en la ordenación de los miembros del origen en función de la longitud de cada cadena.
Para permitir varios criterios de ordenación, OrderBy y OrderByDescendingdevuelven OrderedSequence<T> en lugar de la T IEnumerable<> genérica. Dos operadores solo se definen en OrderedSequence<T>, es decir , ThenBy y ThenByDescending , que aplican un criterio de ordenación adicional (subordinado). ThenBy/ThenByDescending devuelven OrderedSequence<T>, lo que permite aplicar cualquier número de operadores ThenBy/ThenByDescending :
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);
La evaluación de la consulta a la que hace referencia s1 en este ejemplo produciría la siguiente secuencia de valores:
"Burke", "David", "Frank",
"Albert", "Connor", "George", "Harris",
"Everett"
Además de la familia de operadores OrderBy , los operadores de consulta estándar también incluyen un operador Reverse . Inversa simplemente enumera sobre una secuencia y produce los mismos valores en orden inverso. A diferencia de OrderBy, Reverse no considera los valores reales en la determinación del orden, sino que se basa únicamente en el orden en que el origen subyacente genera los valores.
El operador OrderBy impone un criterio de ordenación sobre una secuencia de valores. Los operadores de consulta estándar también incluyen el operador GroupBy , que impone una partición sobre una secuencia de valores basada en una función de extracción de claves. El operador GroupBy devuelve una secuencia de valores de IGrouping , uno para cada valor de clave distinto que se encontró. Un IGrouping es un IEnumerable que contiene además la clave que se usó para extraer su contenido:
public interface IGrouping<K, T> : IEnumerable<T> {
public K Key { get; }
}
La aplicación más sencilla de GroupBy tiene este aspecto:
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
// group by length
var groups = names.GroupBy(s => s.Length);
foreach (IGrouping<int, string> group in groups) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (string value in group)
Console.WriteLine(" {0}", value);
}
Cuando se ejecuta, este programa imprime lo siguiente:
Strings of length 6
Albert
Connor
George
Harris
Strings of length 5
Burke
David
Frank
Strings of length 7
Everett
Una clase Select, GroupBy permite proporcionar una función de proyección que se usa para rellenar los miembros de los grupos.
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
// group by length
var groups = names.GroupBy(s => s.Length, s => s[0]);
foreach (IGrouping<int, char> group in groups) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (char value in group)
Console.WriteLine(" {0}", value);
}
Esta variación imprime lo siguiente:
Strings of length 6
A
C
G
H
Strings of length 5
B
D
F
Strings of length 7
E
Nota En este ejemplo, no es necesario que el tipo proyectado sea el mismo que el origen. En este caso, creamos una agrupación de enteros en caracteres de una secuencia de cadenas.
Operadores de agregación
Se definen varios operadores de consulta estándar para agregar una secuencia de valores en un único valor. El operador de agregación más general es Aggregate, que se define de la siguiente manera:
public static U Aggregate<T, U>(this IEnumerable<T> source,
U seed, Func<U, T, U> func) {
U result = seed;
foreach (T element in source)
result = func(result, element);
return result;
}
El operador Aggregate facilita la realización de un cálculo a través de una secuencia de valores. Aggregate funciona llamando a la expresión lambda una vez para cada miembro de la secuencia subyacente. Cada vez que Aggregate llama a la expresión lambda, pasa el miembro de la secuencia y un valor agregado (el valor inicial es el parámetro de inicialización a Aggregate). El resultado de la expresión lambda reemplaza el valor agregado anterior y Aggregate devuelve el resultado final de la expresión lambda.
Por ejemplo, este programa usa Aggregate para acumular el recuento total de caracteres en una matriz de cadenas:
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
int count = names.Aggregate(0, (c, s) => c + s.Length);
// count == 46
Además del operador Aggregate de uso general, los operadores de consulta estándar también incluyen un operador Count de uso general y cuatro operadores de agregación numéricos (Min, Max, Sum y Average) que simplifican estas operaciones de agregación comunes. Las funciones de agregación numérica funcionan en secuencias de tipos numéricos (por ejemplo, int, double, decimal) o en secuencias de valores arbitrarios siempre que se proporcione una función que proyecta miembros de la secuencia en un tipo numérico.
Este programa ilustra ambas formas del operador Sum que acaba de describir:
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
int total1 = numbers.Sum(); // total1 == 55
int total2 = names.Sum(s => s.Length); // total2 == 46
Nota La segunda instrucción Sum es equivalente al ejemplo anterior mediante Aggregate.
Seleccionar frente a SelectMany
El operador Select requiere que la función de transformación genere un valor para cada valor de la secuencia de origen. Si la función de transformación devuelve un valor que es en sí misma una secuencia, el consumidor debe recorrer manualmente las subcadenas. Por ejemplo, considere este programa que divide las cadenas en tokens mediante el método String.Split existente:
string[] text = { "Albert was here",
"Burke slept late",
"Connor is happy" };
var tokens = text.Select(s => s.Split(' '));
foreach (string[] line in tokens)
foreach (string token in line)
Console.Write("{0}.", token);
Cuando se ejecuta, este programa imprime el texto siguiente:
Albert.was.here.Burke.slept.late.Connor.is.happy.
Idealmente, nos gustaría que nuestra consulta hubiera devuelto una secuencia fusionada de tokens y no exponera la cadena intermedia[] al consumidor. Para ello, usamos el operador SelectMany en lugar del operador Select . El operador SelectMany funciona de forma similar al operador Select . Difiere en que se espera que la función de transformación devuelva una secuencia que, a continuación, se expanda mediante el operador SelectMany . Este es nuestro programa reescrito mediante SelectMany:
string[] text = { "Albert was here",
"Burke slept late",
"Connor is happy" };
var tokens = text.SelectMany(s => s.Split(' '));
foreach (string token in tokens)
Console.Write("{0}.", token);
El uso de SelectMany hace que cada secuencia intermedia se expanda como parte de la evaluación normal.
SelectMany es ideal para combinar dos orígenes de información:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.SelectMany(n =>
people.Where(p => n.Equals(p.Name))
);
En la expresión lambda pasada a SelectMany, la consulta anidada se aplica a un origen diferente, pero tiene en el ámbito
el n
parámetro pasado desde el origen externo. Así , la gente. Donde se llama una vez para cada n, con las secuencias resultantes acopladas por SelectMany para la salida final. El resultado es una secuencia de todas las personas cuyo nombre aparece en la matriz de nombres .
Operadores de combinación
En un programa orientado a objetos, los objetos relacionados entre sí normalmente se vincularán con referencias de objetos que son fáciles de navegar. Normalmente, lo mismo no es cierto para los orígenes de información externos, donde las entradas de datos a menudo no tienen ninguna opción, sino "apuntar" entre sí simbólicamente, con identificadores u otros datos que pueden identificar de forma única la entidad a la que apunta. El concepto de combinaciones hace referencia al funcionamiento de reunir los elementos de una secuencia junto con los elementos con los que "coinciden" desde otra secuencia.
El ejemplo anterior con SelectMany realmente hace exactamente eso, haciendo coincidir cadenas con personas cuyos nombres son esas cadenas. Sin embargo, para este propósito concreto, el enfoque SelectMany no es muy eficaz; recorrerá en bucle todos los elementos de las personas para cada elemento de nombres y cada elemento de nombres. Al reunir toda la información de este escenario( los dos orígenes de información y las "claves" con las que se encuentran coincidentes) en una llamada al método, el operador Join puede realizar un trabajo mucho mejor:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.Join(people, n => n, p => p.Name, (n,p) => p);
Esto es un poco de bocado, pero vea cómo encajan las piezas: se llama al método Join en el origen de datos "externo". El primer argumento es el origen de datos "interno", personas. Los argumentos segundo y tercero son expresiones lambda para extraer claves de los elementos de los orígenes externos e internos, respectivamente. Estas claves son las que usa el método Join para buscar coincidencias con los elementos. Aquí queremos que los nombres coincidan con la propiedad Name de las personas. La expresión lambda final es responsable de generar los elementos de la secuencia resultante: se llama con cada par de elementos coincidentes n y p, y se usa para dar forma al resultado. En este caso, elegimos descartar n y devolver la p. El resultado final es la lista de elementos Person de personas cuyo nombre se encuentra en la lista de nombres.
Un primo más poderoso de Join es el operador GroupJoin . GroupJoin difiere de Join de la forma en que se usa la expresión lambda de forma de resultado: en lugar de invocarse con cada par individual de elementos externos e internos, se llamará solo una vez para cada elemento externo, con una secuencia de todos los elementos internos que coinciden con ese elemento externo. Para hacer ese concreto:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var query = names.GroupJoin(people, n => n, p => p.Name,
(n, matching) =>
new { Name = n, Count = matching.Count() }
);
Esta llamada genera una secuencia de los nombres con los que comenzó con emparejado con el número de personas que tienen ese nombre. Por lo tanto, el operador GroupJoin permite basar los resultados en todo el "conjunto de coincidencias" para un elemento externo.
Sintaxis de las consultas
La instrucción foreach existente en C# proporciona una sintaxis declarativa para la iteración a través de los métodos IEnumerable/IEnumerator de .NET Frameworks. La instrucción foreach es estrictamente opcional, pero ha demostrado ser un mecanismo de lenguaje muy conveniente y popular.
Basándose en este precedente, las expresiones de consulta simplifican las consultas con una sintaxis declarativa para los operadores de consulta más comunes: Where, Join, GroupJoin, Select, SelectMany, GroupBy, OrderBy, ThenBy, OrderByDescending, ThenByDescending y Cast.
Empecemos examinando la consulta sencilla con la que empezamos este documento:
IEnumerable<string> query = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
Con una expresión de consulta, podemos volver a escribir esta instrucción exacta como esta:
IEnumerable<string> query = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
Al igual que la instrucción foreach en C#, las expresiones de consulta son más compactas y fáciles de leer, pero son completamente opcionales. Cada expresión que se puede escribir como una expresión de consulta tiene una sintaxis correspondiente (aunque más detallada) mediante notación de puntos.
Comencemos examinando la estructura básica de una expresión de consulta. Cada expresión de consulta sintáctica de C# comienza con una cláusula from y termina con una cláusula select o group . La cláusula from inicial se puede seguir por cero o más desde, let, where, join y orderby cláusulas. Cada cláusula from es un generador que introduce una variable de rango sobre una secuencia; cada cláusula let proporciona un nombre al resultado de una expresión; y cada cláusula where es un filtro que excluye los elementos del resultado. Cada cláusula join correlaciona un nuevo origen de datos con los resultados de las cláusulas anteriores. Una cláusula orderby especifica una ordenación para el resultado:
query-expression ::= from-clause query-body
query-body ::=
query-body-clause* final-query-clause query-continuation?
query-body-clause ::=
(from-clause
| join-clause
| let-clause
| where-clause
| orderby-clause)
from-clause ::=from itemName in srcExpr
join-clause ::=join itemName in srcExpr on keyExpr equals keyExpr
(into itemName)?
let-clause ::=let itemName = selExpr
where-clause ::= where predExpr
orderby-clause ::= orderby (keyExpr (ascending | descending)?)*
final-query-clause ::=
(select-clause | groupby-clause)
select-clause ::= select selExpr
groupby-clause ::= group selExpr by keyExprquery-continuation ::= intoitemName query-body
Por ejemplo, considere estas dos expresiones de consulta:
var query1 = from p in people
where p.Age > 20
orderby p.Age descending, p.Name
select new {
p.Name, Senior = p.Age > 30, p.CanCode
};
var query2 = from p in people
where p.Age > 20
orderby p.Age descending, p.Name
group new {
p.Name, Senior = p.Age > 30, p.CanCode
} by p.CanCode;
El compilador trata estas expresiones de consulta como si se escribieran con la siguiente notación de puntos explícita:
var query1 = people.Where(p => p.Age > 20)
.OrderByDescending(p => p.Age)
.ThenBy(p => p.Name)
.Select(p => new {
p.Name,
Senior = p.Age > 30,
p.CanCode
});
var query2 = people.Where(p => p.Age > 20)
.OrderByDescending(p => p.Age)
.ThenBy(p => p.Name)
.GroupBy(p => p.CanCode,
p => new {
p.Name,
Senior = p.Age > 30,
p.CanCode
});
Las expresiones de consulta se someten a una traducción mecánica en llamadas de métodos con nombres específicos. La implementación exacta del operador de consulta elegida, por lo tanto, depende tanto del tipo de las variables que se consultan como de los métodos de extensión que están en el ámbito.
Las expresiones de consulta que se muestran hasta ahora solo han usado un generador. Cuando se usa más de un generador, cada generador posterior se evalúa en el contexto de su predecesor. Por ejemplo, considere esta ligera modificación en nuestra consulta:
var query = from s1 in names
where s1.Length == 5
from s2 in names
where s1 == s2
select s1 + " " + s2;
Cuando se ejecuta en esta matriz de entrada:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
obtenemos los siguientes resultados:
Burke Burke
Frank Frank
David David
La expresión de consulta anterior se expande a esta expresión de notación de puntos:
var query = names.Where(s1 => s1.Length == 5)
.SelectMany(s1 => names, (s1,s2) => new {s1,s2})
.Where($1 => $1.s1 == $1.s2)
.Select($1 => $1.s1 + " " + $1.s2);
Nota Esta versión de SelectMany toma una expresión lambda adicional que se usa para generar el resultado en función de los elementos de las secuencias externas e internas. En esta expresión lambda, las dos variables de intervalo se recopilan en un tipo anónimo. El compilador inventa un nombre de variable $1 para indicar ese tipo anónimo en expresiones lambda posteriores.
Un tipo especial de generador es la cláusula join , que introducirá elementos de otro origen que coincidan con los elementos de las cláusulas anteriores según las claves dadas. Una cláusula join puede producir los elementos coincidentes uno por uno, pero si se especifica con una cláusula into , los elementos coincidentes se proporcionarán como un grupo:
var query = from n in names
join p in people on n equals p.Name into matching
select new { Name = n, Count = matching.Count() };
No es sorprendente que esta consulta se expanda directamente en una que hemos visto antes:
var query = names.GroupJoin(people, n => n, p => p.Name,
(n, matching) =>
new { Name = n, Count = matching.Count() }
);
A menudo resulta útil tratar los resultados de una consulta como generador en una consulta posterior. Para admitir esto, las expresiones de consulta usan la palabra clave into para insertar una nueva expresión de consulta después de una cláusula select o group. Esto se denomina continuación de consulta.
La palabra clave into es especialmente útil para el posprocesamiento de los resultados de una cláusula group
by . Por ejemplo, considere este programa:
var query = from item in names
orderby item
group item by item.Length into lengthGroups
orderby lengthGroups.Key descending
select lengthGroups;
foreach (var group in query) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (var val in group)
Console.WriteLine(" {0}", val);
}
Este programa genera lo siguiente:
Strings of length 7
Everett
Strings of length 6
Albert
Connor
George
Harris
Strings of length 5
Burke
David
Frank
En esta sección se ha descrito cómo C# implementa expresiones de consulta. Otros lenguajes pueden optar por admitir operadores de consulta adicionales con sintaxis explícita o no tener expresiones de consulta en absoluto.
Es importante tener en cuenta que la sintaxis de consulta no está conectada de forma automática a los operadores de consulta estándar. Es una característica puramente sintáctica que se aplica a cualquier cosa que cumpla el patrón de consulta mediante la implementación de métodos subyacentes con los nombres y firmas adecuados. Los operadores de consulta estándar descritos anteriormente lo hacen mediante métodos de extensión para aumentar la interfaz T> IEnumerable<. Los desarrolladores pueden aprovechar la sintaxis de consulta en cualquier tipo que deseen, siempre y cuando se aseguren de que se adhieren al patrón de consulta, ya sea mediante la implementación directa de los métodos necesarios o agregandolos como métodos de extensión.
Esta extensibilidad se aprovecha en el propio proyecto LINQ mediante el aprovisionamiento de dos API habilitadas para LINQ, es decir, LINQ to SQL, que implementa el patrón LINQ para el acceso a datos basados en SQL y LINQ to XML que permite consultas LINQ sobre datos XML. Ambos se describen en las secciones siguientes.
LINQ to SQL: Integración de SQL
.NET Language-Integrated Query se puede usar para consultar almacenes de datos relacionales sin salir de la sintaxis ni del entorno en tiempo de compilación del lenguaje de programación local. Esta instalación, con nombre de código LINQ to SQL, aprovecha la integración de la información del esquema SQL en metadatos clR. Esta integración compila definiciones de tabla y vista de SQL en tipos CLR a los que se puede acceder desde cualquier lenguaje.
LINQ to SQL define dos atributos principales, [Table] y [Column], que indican qué tipos y propiedades clR corresponden a datos SQL externos. El atributo [Table] se puede aplicar a una clase y asocia el tipo CLR a una tabla o vista con nombre SQL. El atributo [Column] se puede aplicar a cualquier campo o propiedad y asociar el miembro a una columna SQL con nombre. Ambos atributos se parametrizan para permitir que se conserven los metadatos específicos de SQL. Por ejemplo, considere esta definición de esquema SQL simple:
create table People (
Name nvarchar(32) primary key not null,
Age int not null,
CanCode bit not null
)
create table Orders (
OrderID nvarchar(32) primary key not null,
Customer nvarchar(32) not null,
Amount int
)
El equivalente de CLR tiene este aspecto:
[Table(Name="People")]
public class Person {
[Column(DbType="nvarchar(32) not null", Id=true)]
public string Name;
[Column]
public int Age;
[Column]
public bool CanCode;
}
[Table(Name="Orders")]
public class Order {
[Column(DbType="nvarchar(32) not null", Id=true)]
public string OrderID;
[Column(DbType="nvarchar(32) not null")]
public string Customer;
[Column]
public int? Amount;
}
Nota En este ejemplo, las columnas que aceptan valores NULL se asignan a tipos que aceptan valores NULL en CLR (los tipos que aceptan valores NULL aparecieron por primera vez en la versión 2.0 de .NET Framework) y que para los tipos SQL que no tienen una correspondencia 1:1 con un tipo CLR (por ejemplo, nvarchar, char, text), el tipo SQL original se conserva en los metadatos clR.
Para emitir una consulta en un almacén relacional, la implementación LINQ to SQL del patrón LINQ traduce la consulta de su formulario de árbol de expresiones en una expresión SQL y ADO.NET objeto DbCommand adecuado para la evaluación remota. Por ejemplo, considere esta consulta simple:
// establish a query context over ADO.NET sql connection
DataContext context = new DataContext(
"Initial Catalog=petdb;Integrated Security=sspi");
// grab variables that represent the remote tables that
// correspond to the Person and Order CLR types
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders = context.GetTable<Order>();
// build the query
var query = from c in custs
from o in orders
where o.Customer == c.Name
select new {
c.Name,
o.OrderID,
o.Amount,
c.Age
};
// execute the query
foreach (var item in query)
Console.WriteLine("{0} {1} {2} {3}",
item.Name, item.OrderID,
item.Amount, item.Age);
El tipo DataContext proporciona un traductor ligero que traduce los operadores de consulta estándar a SQL. DataContext usa la ADO.NET IDbConnection existente para acceder al almacén y se puede inicializar con un objeto de conexión ADO.NET establecido o una cadena de conexión que se puede usar para crear una.
El método GetTable proporciona variables compatibles con IEnumerable que se pueden usar en expresiones de consulta para representar la tabla o vista remota. Las llamadas a GetTable no provocan ninguna interacción con la base de datos, sino que representan la posibilidad de interactuar con la tabla remota o la vista mediante expresiones de consulta. En nuestro ejemplo anterior, la consulta no se transmite al almacén hasta que el programa recorre en iteración la expresión de consulta, en este caso mediante la instrucción foreach en C#. Cuando el programa recorre en iteración la consulta por primera vez, la maquinaria DataContext convierte el árbol de expresiones en la siguiente instrucción SQL que se envía al almacén:
SELECT [t0].[Age], [t1].[Amount],
[t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]
Es importante tener en cuenta que al compilar la funcionalidad de consulta directamente en el lenguaje de programación local, los desarrolladores obtienen toda la eficacia del modelo relacional sin tener que hornear estáticamente las relaciones en el tipo CLR. Dicho esto, la asignación completa de objetos o relacionales también puede aprovechar esta funcionalidad de consulta principal para los usuarios que quieran esa funcionalidad. LINQ to SQL proporciona funcionalidad de asignación relacional de objetos con la que el desarrollador puede definir y navegar por las relaciones entre objetos. Puede hacer referencia a Orders como una propiedad de la clase Customer mediante la asignación, de modo que no necesite combinaciones explícitas para vincular las dos juntas. Los archivos de asignación externa permiten separar la asignación del modelo de objetos para obtener funcionalidades de asignación más enriquecidas.
LINQ to XML: Integración XML
.NET Language-Integrated Query for XML (LINQ to XML) permite consultar datos XML mediante operadores de consulta estándar, así como operadores específicos del árbol que proporcionan navegación de tipo XPath a través de descendientes, antecesores y elementos relacionados. Proporciona una representación eficaz en memoria para XML que se integra con la infraestructura de lectura y escritura deSystem.Xml existente y es más fácil de usar que W3C DOM. Hay tres tipos que realizan la mayor parte del trabajo de integración de XML con consultas: XName, XElement y XAttribute.
XName proporciona una manera fácil de usar para tratar con los identificadores calificados del espacio de nombres (QNames) que se usan como nombres de elemento y atributo. XName controla la atomización eficaz de los identificadores de forma transparente y permite usar símbolos o cadenas sin formato siempre que se necesite un QName.
Los elementos y atributos XML se representan mediante XElement y XAttribute respectivamente. XElement y XAttribute admiten la sintaxis de construcción normal, lo que permite a los desarrolladores escribir expresiones XML mediante una sintaxis natural:
var e = new XElement("Person",
new XAttribute("CanCode", true),
new XElement("Name", "Loren David"),
new XElement("Age", 31));
var s = e.ToString();
Esto corresponde al siguiente XML:
<Person CanCode="true">
<Name>Loren David</Name>
<Age>31</Age>
</Person>
Observe que no se necesitaba ningún patrón de fábrica basado en DOM para crear la expresión XML y que la implementación de ToString produjo el XML textual. Los elementos XML también se pueden construir a partir de un xmlReader existente o a partir de un literal de cadena:
var e2 = XElement.Load(xmlReader);
var e1 = XElement.Parse(
@"<Person CanCode='true'>
<Name>Loren David</Name>
<Age>31</Age>
</Person>");
XElement también admite la emisión de XML mediante el tipo XmlWriter existente.
XElement se integra con los operadores de consulta, lo que permite a los desarrolladores escribir consultas en información no XML y generar resultados XML mediante la construcción de XElements en el cuerpo de una cláusula select:
var query = from p in people
where p.CanCode
select new XElement("Person",
new XAttribute("Age", p.Age),
p.Name);
Esta consulta devuelve una secuencia de XElements. Para permitir que XElements se cree a partir del resultado de este tipo de consulta, el constructor XElement permite pasar secuencias de elementos como argumentos directamente:
var x = new XElement("People",
from p in people
where p.CanCode
select
new XElement("Person",
new XAttribute("Age", p.Age),
p.Name));
Esta expresión XML da como resultado el siguiente XML:
<People>
<Person Age="11">Allen Frances</Person>
<Person Age="59">Connor Morgan</Person>
</People>
La instrucción anterior tiene una traducción directa a Visual Basic. Sin embargo, Visual Basic 9.0 también admite el uso de literales XML, que permiten expresar expresiones de consulta mediante una sintaxis XML declarativa directamente desde Visual Basic. El ejemplo anterior se podría construir con la instrucción de Visual Basic:
Dim x = _
<People>
<%= From p In people __
Where p.CanCode _
Select <Person Age=<%= p.Age %>>p.Name</Person> _
%>
</People>
En los ejemplos hasta ahora se ha mostrado cómo construir nuevos valores XML mediante la consulta integrada en lenguaje. Los tipos XElement y XAttribute también simplifican la extracción de información de estructuras XML. XElement proporciona métodos de descriptor de acceso que permiten aplicar expresiones de consulta a los ejes XPath tradicionales. Por ejemplo, la consulta siguiente extrae solo los nombres de XElement mostrados anteriormente:
IEnumerable<string> justNames =
from e in x.Descendants("Person")
select e.Value;
//justNames = ["Allen Frances", "Connor Morgan"]
Para extraer valores estructurados del XML, simplemente usamos una expresión de inicializador de objeto en nuestra cláusula select:
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int)e.Attribute("Age")
};
Tenga en cuenta que tanto XAttribute como XElement admiten conversiones explícitas para extraer el valor de texto como un tipo primitivo. Para tratar los datos que faltan, simplemente podemos convertir a un tipo que acepta valores NULL:
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int?)e.Attribute("Age") ?? 21
};
En este caso, usamos un valor predeterminado de 21 cuando falta el atributo Age .
Visual Basic 9.0 proporciona compatibilidad con lenguaje directo para los métodos de descriptor de acceso Elements, Attribute y Descendants de XElement, lo que permite el acceso a los datos basados en XML mediante una sintaxis más compacta y directa denominada propiedades del eje XML. Podemos usar esta funcionalidad para escribir la instrucción de C# anterior como esta:
Dim persons = _
From e In x...<Person> _
Select new Person { _
.Name = e.Value, _
.Age = IIF(e.@Age, 21) _
}
En Visual Basic, x...<Person> obtiene todos los elementos de la colección Descendants de x con el nombre Person, mientras que la expresión e.@Age busca todos los XAttributes con el nombre Age.
La propiedad Value obtiene el primer atributo de la colección y llama a la propiedad Value en ese atributo.
Resumen
.NET Language-Integrated Query agrega funcionalidades de consulta a CLR y a los lenguajes que se destinan a ella. La instalación de consultas se basa en expresiones lambda y árboles de expresión para permitir que los predicados, las proyecciones y las expresiones de extracción de claves se usen como código ejecutable opaco o como datos transparentes en memoria adecuados para el procesamiento o la traducción de bajada. Los operadores de consulta estándar definidos por el proyecto LINQ funcionan en cualquier origen de información basado en T> de IEnumerable< y se integran con ADO.NET (LINQ to SQL) y System.Xml (LINQ to XML) para permitir que los datos relacionales y XML obtengan las ventajas de la consulta integrada en lenguaje.
Operadores de consulta estándar en un resumen
Operator | Descripción |
---|---|
Where | Operador de restricción basado en la función de predicado |
Select/SelectMany | Operadores de proyección basados en la función selector |
Take/Skip/TakeWhile/SkipWhile | Operadores de creación de particiones basados en la función de posición o predicado |
Join/GroupJoin | Operadores de combinación basados en funciones del selector de claves |
Concat | Concatenation, operador |
OrderBy/ThenBy/OrderByDescending/ThenByDescending | Ordenar operadores de ordenación en orden ascendente o descendente en función del selector de claves y las funciones de comparador opcionales |
Reverse | Operador de ordenación que invierte el orden de una secuencia |
GroupBy | Operador de agrupación basado en funciones de comparador y selector de claves opcionales |
Distinct | Establecer operador que quita duplicados |
Unión/intersección | Establecer operadores que devuelven unión o intersección de conjuntos |
Except | Establecer operador que devuelve la diferencia de conjunto |
AsEnumerable | Operador de conversión a IEnumerable<T> |
ToArray/ToList | Operador de conversión a matriz o Lista<T> |
ToDictionary/ToLookup | Operadores de conversión a Dictionary<K,T> o Lookup<K,T> (varios diccionarios) basados en la función del selector de claves |
OfType/Cast | Operadores de conversión a IEnumerable<T> basados en el filtrado por o conversión al argumento de tipo |
SequenceEqual | Igualdad de operadores que comprueban la igualdad de elementos en pares |
First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault | Operadores de elemento que devuelven el elemento inicial, final o solo basado en la función de predicado opcional |
ElementAt/ElementAtOrDefault | Operadores de elemento que devuelven elementos en función de la posición |
DefaultIfEmpty | Operador de elemento que reemplaza la secuencia vacía por una secuencia singleton con valores predeterminados |
Intervalo | Operador de generación que devuelve números en un intervalo |
Repeat | Operador de generación que devuelve varias repeticiones de un valor determinado |
Vacío | Operador de generación que devuelve una secuencia vacía |
Any/All | Comprobación del cuantificador de la satisfacción existencial o universal de la función de predicado |
Contains | Comprobación del cuantificador para la presencia de un elemento determinado |
Count/LongCount | Operadores agregados que cuentan elementos en función de la función de predicado opcional |
Sum/Min/Max/Average | Operadores agregados basados en funciones de selector opcionales |
Agregado | Operador de agregado que acumula varios valores en función de la función de acumulación y la inicialización opcional |