Está en la página 1de 388

Manual de LINQ

INDICE GENERAL

1. Introducción a LINQ
2. Introducción a LINQ en Visual Basic
2.1. Escribir la Primera Consulta con LINQ
2.2. Operaciones Básicas de Consulta
2.3. Características de Visual Basic que Admiten LINQ
2.4. Relaciones entre Tipos en Operaciones de Consulta
3. Cómo: Crear un Proyecto con LINQ
4. Características de IDE de Visual Studio y Herramientas para LINQ
5. Guía de Programación General con LINQ
5.1. Información General sobre Operadores de Consulta Estándar
5.1.1. Sintaxis de Expresiones de Consulta para Operadores de Consulta Estándar
5.1.2. Clasificación de Operadores de Consulta Estándar por Modo de Ejecución
5.1.3. Ordenar Datos
5.1.4. Operaciones Set
5.1.5. Filtrar Datos
5.1.6. Operaciones Cuantificadotas
5.1.7. Operaciones de Proyección
5.1.8. Realizar Particiones de Datos
5.1.9. Operaciones de Combinación
5.1.10. Agrupar Datos
5.1.11. Operaciones de Generación
5.1.12. Operaciones de Igualdad
5.1.13. Operaciones de Elementos
5.1.14. Convertir Tipos de Datos
5.1.15. Operaciones de Concatenación
5.1.16. Operaciones de Agregación
5.2. Arboles de Expresión en LINQ
5.2.1. Cómo: Usar Arboles de Expresión para Crear Consultas Dinámicas
5.3. Habilitar un Origen de Datos para Realizar Consultas
6. LINQ a Objectos
6.1. LINQ y Cadenas
6.1.1. Cómo: Realizar Recuento de las Repeticiones de una Palabra en una Cadena
6.1.2. Cómo: Buscar Frases que Contengan un Conjunto Especificado de Palabras
6.1.3. Cómo: Buscar Caracteres en una Cadena
6.1.4. Cómo: Combinar Consultas LINQ con Expresiones Regulares
6.1.5. Cómo: Buscar la Diferencia de Conjuntos entre Dos Listas
6.1.6. Cómo: Ordenar o Filtrar Datos de Texto por Palabra o Campo
6.1.7. Cómo: Reordenar los Campos de un Archivo Delimitado
6.1.8. Cómo: Combinar y Comparar Colecciones de Cadenas
6.1.9. Cómo: Rellenar Colecciones de Objetos de Varios Orígenes
6.1.10. Cómo: Dividir un Archivo en Varios Archivos Mediante el Uso de Grupos
6.1.11. Cómo: Combinar Contenido de Archivos No Similares
6.1.12. Cómo: Calcular Valores de Columna en un Archivo de Texto CSV
6.2. LINQ y Reflection
6.2.1. Cómo: Consultar los Metadatos de un Ensamblado con la Función de Reflexión
6.3. LINQ y Directorios de Archivos
6.3.1. Cómo: Buscar Archivos con un Nombre o Atributo Especificados
6.3.2. Cómo: Agrupar Archivos por Extensión
6.3.3. Cómo: Buscar el Número Total de Bytes en un Conjunto de Carpetas
6.3.4. Cómo: Comparar el Contenido de dos Carpetas
6.3.5. Cómo: Buscar el Archivo(s) de Mayor Tamaño en un Arbol de Directorios
6.3.6. Cómo: Buscar Archivos Duplicados en un Arbol de Directorios
6.3.7. Cómo: Consultar el Contenido de los Archivos de una Carpeta
6.4. Cómo: Consultar un Objeto ArrayList con LINQ
7. LINQ a XML
7.1. Introducción LINQ a XML
7.1.1. Información General Acerca de LINQ a XML
7.1.2. Diferencias entre LINQ a XML y DOM

MCT: Luis Dueñas Pag 1 de 388


Manual de LINQ

7.1.3. Diferencias entre LINQ a XML y otras Tecnologías XML


7.2. Guía de Programación LINQ a XML
7.2.1. Información General Acerca de la Programación de LINQ a XML
7.2.1.1. Información General de los Enfoques de Programación LINQ a XML
7.2.1.2. Información General de las Clases LINQ a XML
7.2.1.3. Información General Acerca de la Clase XElement
7.2.1.4. Información General Acerca de la Clase XAttribute
7.2.1.5. Información General Acerca de la Clase XDocument
7.2.1.6. Cómo Crear Ejemplos de LINQ a XML
7.2.2. Crear Arboles XML
7.2.2.1. Construcción Funcional
7.2.2.2. Introducción a los Literales XML en Visual Basic
7.2.2.3. Diferencias entre Agregar y Clonar
7.2.2.4. Analizar XML
7.2.2.4.1. Cómo Analizar una Cadena
7.2.2.4.2. Cómo Cargar XML de un Archivo
7.2.2.4.3. Conservar Espacios en Blanco mientras se Carga o se
Analiza el XML
7.2.2.4.4. Cómo Detectar Errores de Análisis
7.2.2.4.5. Cómo Crear un Arbol de XmlReader
7.2.2.4.6. Cómo Transmitir por Secuencias Fragmentos XML de
XmlReader
7.2.2.5. Cómo Rellenar un Arbol XML Con XmlWriter
7.2.2.6. Cómo Validar con XSD
7.2.2.7. Contenido Válido de Objetos XElement y XDocument
7.2.3. Trabajar con Espacios de Nombres XML
7.2.3.1. Información General Sobre los Espacios de Nombres
7.2.3.2. Espacios de Nombres en Visual Basic
7.2.3.2.1. Cómo Crear un Documento con Espacios de Nombres
7.2.3.2.2. Cómo Controlar Prefijos de Espacios de Nombres
7.2.3.2.3. Trabajar con Espacios de Nombres Globales
7.2.3.3. Cómo Escribir Consultas en XML en Espacios de Nombre
7.2.4. Serializar Arboles XML
7.2.4.1. Preservar los Espacios en Blanco durante la Serialización
7.2.4.2. Serializar con una Declaración XML
7.2.4.3. Serializar en Archivos, TextWriters y XmlWriters
7.2.4.4. Serializar en XmlReader (Invocar XSLT)
7.2.5. Ejes LINQ a XML
7.2.5.1. Información General Acerca de los Ejes de LINQ a XML
7.2.5.2. Cómo Recuperar una Recopilación de Elementos
7.2.5.3. Cómo Recuperar el Valor de un Elemento
7.2.5.4. Cómo Filtrar Nombres de Elemento
7.2.5.5. Cómo Encadenar Llamadas a Métodos de Eje
7.2.5.6. Cómo Recuperar un Unico Elemento Secundario
7.2.5.7. Cómo Recuperar una Colección de Atributos
7.2.5.8. Cómo Recuperar un Atributo en Particular
7.2.5.9. Cómo Recuperar el Valor de un Atributo
7.2.5.10. Ejes Integrados en el Lenguaje en Visual Basic
7.2.6. Consultar Arboles XML
7.2.6.1. Consultas Básicas LINQ a XML
7.2.6.1.1. Cómo Buscar un Elemento con un Atributo Específico
7.2.6.1.2. Cómo Buscar un Elemento con un Elemento Secundario
Específico
7.2.6.1.3. Diferencias entre Realizar Consultas de un XDocument y de
un XElement
7.2.6.1.4. Cómo Encontrar Descendientes con un Nombre de
Elemento Específico
7.2.6.1.5. Cómo Buscar un Descendiente Unico mediante el Método
Descendants
7.2.6.1.6. Cómo Escribir Consultas con Filtrado Complejo

MCT: Luis Dueñas Pag 2 de 388


Manual de LINQ

7.2.6.1.7. Cómo Filtrar por un Elemento Opcional


7.2.6.1.8. Cómo Buscar Todos los Nodos en un Espacio de Nombres
7.2.6.1.9. Cómo Ordenar Elementos
7.2.6.1.10. Cómo Ordenar Elementos en Varias Claves
7.2.6.1.11. Cómo Calcular Valores Intermedios
7.2.6.1.12. Cómo Escribir una Consulta que Busca Elementos
Basándose en el Contexto
7.2.6.1.13. Cómo Depurar Conjuntos de Resultados de Consultas
Vacíos
7.2.6.2. Proyecciones y Transformaciones LINQ a XML
7.2.6.2.1. Cómo Trabajar con Diccionarios usando LINQ a XML
7.2.6.2.2. Cómo Transformar la Forma de un Arbol XML
7.2.6.2.3. Cómo Controlar el Tipo de una Proyección
7.2.6.2.4. Cómo Proyectar un Nuevo Tipo
7.2.6.2.5. Cómo Proyectar un Gráfico de Objetos
7.2.6.2.6. Cómo Proyectar un Tipo Anónimo
7.2.6.2.7. Cómo Generar Archivos de Texto a partir de XML
7.2.6.2.8. Cómo Generar un XML a partir de Archivos CSV
7.2.6.3. Técnicas Avanzadas de Consulta LINQ a XML
7.2.6.3.1. Cómo Crear una Jerarquía con la Agrupación
7.2.6.3.2. Cómo Realizar Consultas de LINQ a XML con Xpath
7.2.6.3.3. Cómo Enumerar Todos los Nodos de un Arbol
7.2.6.3.4. Cómo Rellenar un Arbol XML a partir del Sistema de
Archivos
7.2.6.4. LINQ a XML para usuarios de Xpath
7.2.6.4.1. Comparación de XPath y LINQ a XML
7.2.6.4.2. Cómo Buscar un Elemento Secundario
7.2.6.4.3. Cómo Buscar una Lista de Elementos Secundarios
7.2.6.4.4. Cómo Buscar el Elemento Raíz
7.2.6.4.5. Cómo Buscar Elementos Descendientes
7.2.6.4.6. Cómo Filtrar por Atributo
7.2.6.4.7. Cómo Buscar Elementos Relacionados
7.2.6.4.8. Cómo Buscar Elementos en un Espacio de Nombres
7.2.6.4.9. Cómo Encontrar Elementos Relacionados Anteriores
7.2.6.4.10. Cómo Buscar Descendientes de un Elemento Secundario
7.2.6.4.11. Cómo Buscar una Unión de dos Rutas de Ubicación
7.2.6.4.12. Cómo Buscar Nodos Relacionados
7.2.6.4.13. Cómo Buscar un Atributo del Elemento Primario
7.2.6.4.14. Cómo Buscar Atributos de Elementos Secundarios con un
Nombre Específico
7.2.6.4.15. Cómo Buscar Elementos que Tienen un Atributo
Especifico
7.2.6.4.16. Cómo Buscar Elementos Secundarios en base a la
Posición
7.2.6.4.17. Cómo Buscar el Elemento del Mismo Nivel
Inmediatamente Anterior
7.2.7. Modificar Arboles XML
7.2.7.1. Diferencias Entre la Modificación del Arbol XML en Memoria y la
Construcción Funcional
7.2.7.2. Agregar Elementos, Atributos y Nodos a un Arbol XML
7.2.7.3. Modificar Elementos, Atributos y Nodos en un Arbol XML
7.2.7.4. Quitar Elementos, Atributos y Nodos de un Arbol XML
7.2.7.5. Mantener los Pares Nombre/Valor
7.2.7.6. Cómo Cambiar el Espacio de Nombres de un Arbol XML Completo
7.2.8. Programación Avanzada de LINQ a XML
7.2.8.1. Anotaciones en LINQ a XML
7.2.8.2. Eventos de LINQ a XML
7.2.8.3. Programar con Nodos
7.2.8.4. Cómo Leer y Escribir un Documento Codificado
7.2.8.5. Usar XSLT para Transformar un Arbol XML

MCT: Luis Dueñas Pag 3 de 388


Manual de LINQ

7.2.9. Seguridad de LINQ a XML


8. LINQ a ADO .NET
8.1. Información General de LINQ a ADO .NET
8.2. LINQ a DataSet
8.2.1. Introducción LINQ a DataSet
8.2.1.1. Información General de LINQ a DataSet
8.2.1.2. Cargar Datos en un DataSet
8.2.1.3. Cómo crear un Proyecto LINQ a DataSet en Visual Studio
8.2.2. Guía de Programación LINQ a DataSet
8.2.2.1. Consultas en LINQ a DataSet
8.2.2.2. Consultar DataSets
8.2.2.2.1. Consultas de Tabla Unica
8.2.2.2.2. Consultas Entre Tablas
8.2.2.2.3. Consultar DataSets con Establecimiento de Tipos
8.2.2.3. Comparar DataRows
8.2.2.4. Crear DataTable desde una Consulta
8.2.2.5. Métodos Genéricos Field y SetField
8.2.2.6. Enlace de Datos y LINQ a DataSet
8.2.2.6.1. Crear un Objeto DataView
8.2.2.6.2. Filtrar con DataView
8.2.2.6.3. Ordenar con DataView
8.2.2.6.4. Rendimiento del DataView
8.2.2.6.5. Cómo Enlazar un Objeto DataView al Control DataGridView
de Windows Forms
8.2.2.7. Depurar Consultas LINQ a DataSet
8.2.2.8. Seguridad
8.2.2.9. Ejemplos de LINQ a DataSet
8.2.2.9.1. Ejemplos de Sintaxis de Expresiones de Consulta
8.2.2.9.1.1. Proyección
8.2.2.9.1.2. Restricción
8.2.2.9.1.3. Creación de Particiones
8.2.2.9.1.4. Ordenación
8.2.2.9.1.5. Operadores de Elementos
8.2.2.9.1.6. Operadores de Agregado
8.2.2.9.1.7. Operadores de Combinación
8.2.2.9.2. Ejemplos de Sintaxis de Consulta Basada en Métodos
8.2.2.9.2.1. Proyección
8.2.2.9.2.2. Creación de Particiones
8.2.2.9.2.3. Ordenación
8.2.2.9.2.4. Operadores de Conjuntos
8.2.2.9.2.5. Operadores de Conversión
8.2.2.9.2.6. Operadores de Elementos
8.2.2.9.2.7. Operadores de Agregado
8.2.2.9.2.8. Combinación
8.2.2.9.3. Ejemplos de Operadores Específicos de DataSet
8.3. LINQ a SQL
8.3.1. Introducción LINQ a SQL
8.3.1.1. Qué se Puede Hacer con LINQ a SQL
8.3.1.2. Procedimientos Típicos para Usar LINQ a SQL
8.3.2. Guía de Programación LINQ a SQL
8.3.2.1. Crear el Modelo de Objetos
8.3.2.1.1. Cómo: Generar el Modelo de Objetos en Visual Basic o C#
8.3.2.1.2. Cómo: Generar el Modelo de Objetos como un Archivo
Externo
8.3.2.1.3. Cómo: Generar Código Personalizado Mediante la
Modificación de un Archivo DBML
8.3.2.1.4. Cómo: Validar Archivos de Asignación Externa y DBML
8.3.2.1.5. Cómo: Convertir Entidades en Serializables
8.3.2.1.6. Cómo: Personalizar Clases de Entidad Mediante el Editor
de Código

MCT: Luis Dueñas Pag 4 de 388


Manual de LINQ

8.3.2.1.6.1. Cómo: Especificar Nombres de Base de Datos


8.3.2.1.6.2. Cómo: Representar Tablas como Clases
8.3.2.1.6.3. Cómo: Representar Columnas como Miembros de
Clase
8.3.2.1.6.4. Cómo: Representar Claves Principales
8.3.2.1.6.5. Cómo: Asignar Relaciones de Base de Datos
8.3.2.1.6.6. Cómo: Representar Columnas como Generadas
por la Base de Datos
8.3.2.1.6.7. Cómo: Representar Columnas como Columnas
de Marca de Tiempo o Versión
8.3.2.1.6.8. Cómo: Especificar Tipos de Datos de una Base
de Datos
8.3.2.1.6.9. Cómo: Representar Columnas Calculadas
8.3.2.1.6.10. Cómo: Especificar Campos de Almacenamiento
Privado
8.3.2.1.6.11. Cómo: Representar Columnas que Permitan
Valores Null
8.3.2.1.6.12. Cómo: Asignar Jerarquías de Herencia
8.3.2.1.6.13. Cómo: Especificar la Comprobación de
Conflictos de Simultaneidad
8.3.2.2. Comunicar con la Base de Datos
8.3.2.2.1. Cómo: Conectarse a una Base de Datos
8.3.2.2.2. Cómo: Ejecutar Directamente Comandos SQL
8.3.2.2.3. Cómo: Volver a Usar una Conexión entre un
Comando ADO.NET y DataContext

MCT: Luis Dueñas Pag 5 de 388


Manual de LINQ

Language Integrated Query (LINQ)


Language Integrated Query (LINQ) es un conjunto de características en Visual Studio 2008 que agrega
eficaces capacidades de consulta a la sintaxis de los lenguajes C# y Visual Basic. LINQ incluye patrones
estándar y de fácil aprendizaje para consultar y actualizar datos, y su tecnología se puede extender para
utilizar potencialmente cualquier tipo de almacén de datos. Visual Studio 2008 incluye ensamblados de
proveedores para LINQ que habilitan el uso de LINQ con colecciones de .NET Framework, bases de datos
de SQL Server, conjuntos de datos de ADO.NET y documentos XML.

1. Introducción a LINQ
Language-Integrated Query (LINQ) es una importante innovación en Visual Studio 2008 y .NET
Framework versión 3.5 que elimina la distancia que separa el mundo de los objetos y el mundo de los
datos.

Tradicionalmente, las consultas con datos se expresan como cadenas sencillas, sin comprobación de tipos
en tiempo de compilación ni compatibilidad con IntelliSense. Además, es necesario aprender un lenguaje
de consultas diferente para cada tipo de origen de datos: bases de datos SQL, documentos XML,
servicios web diversos, etc. LINQ convierte una consulta en una construcción de lenguaje de primera
clase en C# y Visual Basic. Las consultas se escriben para colecciones de objetos con establecimiento
inflexible de tipos, utilizando palabras clave del lenguaje y operadores con los que se está familiarizado.
La ilustración siguiente muestra una consulta LINQ parcialmente completada en una base de datos de
SQL Server en C#, con comprobación de tipos completa y compatibilidad con IntelliSense.

En Visual Studio se pueden escribir consultas LINQ en Visual Basic o en C# con bases de datos de SQL
Server, documentos XML, conjuntos de datos ADO.NET y cualquier colección de objetos que admita
IEnumerable o la interfaz genérica IEnumerable<(Of <(T>)>). También se ha previsto la compatibilidad
de LINQ con el marco de entidades ADO.NET, y otros fabricantes se encuentran escribiendo proveedores
LINQ para muchos servicios web y otras implementaciones de base de datos.

Puede utilizar consultas LINQ en proyectos nuevos o junto a consultas que no son LINQ en proyectos
existentes. El único requisito es que el proyecto esté orientado a la versión 3.5 de .NET Framework.

2. Introducción a LINQ en Visual Basic

MCT: Luis Dueñas Pag 6 de 388


Manual de LINQ

Esta sección contiene descripciones generales, ejemplos e información básica que le ayudarán a entender
y utilizar Visual Basic y Language-Integrated Query (LINQ).

2.1. Escribir la Primera Consulta con LINQ


Una consulta es una expresión que recupera datos de un origen de datos. Las consultas se expresan en
un lenguaje de consultas dedicado. A lo largo del tiempo se han ido desarrollando lenguajes diferentes
para los distintos tipos de orígenes de datos, como SQL para las bases de datos relacionales y XQuery
para XML. De esta manera, el desarrollador de aplicaciones debe aprender un nuevo lenguaje de
consultas para cada tipo de origen de datos o formato de datos admitido.

Language-Integrated Query (LINQ) simplifica esta situación al proporcionar un modelo coherente para
trabajar con los datos de varios formatos y orígenes de datos. En una consulta LINQ, siempre se trabaja
con objetos. Se utilizan los mismos modelos de codificación básicos para consultar y transformar los
datos de documentos XML, bases de datos de SQL, conjuntos de datos y entidades de ADO.NET,
colecciones de .NET Framework y cualquier otro formato u origen de datos para el que haya un
proveedor LINQ disponible. En este documento se describen las tres fases para crear y utilizar consultas
LINQ básicas.

Las tres etapas de una operación de consulta


Las operaciones de consulta LINQ se componen de tres acciones:
1. Obtención de uno o varios orígenes de datos.
2. Creación de la consulta.
3. Ejecución de la consulta.
En LINQ, la ejecución y la creación de una consulta son operaciones distintas. Por el simple hecho de
crear una consulta, no se recuperan datos. Este punto se analiza con más detalle más adelante, en este
mismo tema.

En el ejemplo siguiente se muestran las tres partes de una operación de consulta. En el ejemplo se
utiliza una matriz de enteros como origen de datos, cómodo a efectos de demostración. Sin embargo, los
mismos conceptos también se aplican a otros orígenes de datos.
' Data source.
Dim numbers() As Integer = {0, 1, 2, 3, 4, 5, 6}
' Query creation.
Dim evensQuery = From num In numbers Where num Mod 2 = 0 Select num
' Query execution.
For Each number In evensQuery
Console.Write(number & " ")
Next
Resultado:
0246

El origen de datos
Dado que el origen de datos del ejemplo anterior es una matriz, se admite implícitamente la interfaz
genérica IEnumerable<(Of <(T>)>). Es este hecho lo que permite utilizar una matriz como origen de
datos para una consulta LINQ. Los tipos que admiten IEnumerable(Of T) o una interfaz derivada, como la
genérica IQueryable<(Of <(T>)>), se conocen como tipos que se pueden consultar.

Como tipo que se puede consultar implícitamente, la matriz no requiere modificaciones ni un tratamiento
especial para actuar como origen de datos LINQ. Lo mismo sucede con cualquier tipo de colección que

MCT: Luis Dueñas Pag 7 de 388


Manual de LINQ

admita IEnumerable(Of T), incluidas las genéricas List<(Of <(T>)>), Dictionary<(Of <(TKey,
TValue>)>) y otras clases de la biblioteca de clases de .NET Framework.

Si los datos de origen todavía no implementan IEnumerable(Of T), se requiere un proveedor LINQ para
implementar la funcionalidad de los operadores de consulta estándar para ese origen de datos. Por
ejemplo, LINQ a XML controla el trabajo de cargar un documento XML en un tipo XElement que se pueda
consultar, como se muestra en el ejemplo siguiente.
' Create a data source from an XML document.
Dim contacts As XElement = XElement.Load("c:\myContactList.xml")
Con LINQ a SQL, primero se crea una asignación relacional de objetos en tiempo de diseño, ya sea
manualmente o mediante el Diseñador relacional de objetos (Diseñador R/O). Después, se escriben las
consultas en los objetos y, en tiempo de ejecución, LINQ a SQL controla la comunicación con la base de
datos. En el ejemplo siguiente, customers representa una tabla concreta de la base de datos y Table<(Of
<(TEntity>)>) admite la interfaz genérica IQueryable<(Of <(T>)>).
' Create a data source from a SQL table.
Dim db As New DataContext("C:\Northwind\Northwnd.mdf")
Dim customers As Table(Of Customer) = db.GetTable(Of Customer)
La regla básica es simple: un origen de datos LINQ es cualquier objeto que admite la interfaz genérica
IEnumerable<(Of <(T>)>) o una interfaz que herede de ella.

Nota:

También se pueden utilizar tipos como ArrayList, que admite la interfaz no genérica IEnumerable,
como orígenes de datos LINQ.

La consulta
En la consulta se especifica qué información se desea recuperar del origen o de los orígenes de datos.
También tiene la opción de especificar cómo se debería ordenar, agrupar o estructurar esa información
antes de devolverse. Para habilitar la creación de consultas, Visual Basic incorpora nueva sintaxis de
consulta en el lenguaje.

Cuando se ejecuta, la consulta del ejemplo siguiente devuelve todos los números pares de una matriz de
enteros, numbers.
' Data source.
Dim numbers() As Integer = {0, 1, 2, 3, 4, 5, 6}
' Query creation.
Dim evensQuery = From num In numbers Where num Mod 2 = 0 Select num
' Query execution.
For Each number In evensQuery
Console.Write(number & " ")
Next
La expresión de consulta contiene tres cláusulas: From, Where y Select. La función y el propósito
específicos de cada una de las cláusulas de las expresiones de consulta se analiza en Operaciones básicas
de consulta (Visual Basic). Observe que en LINQ una definición de consulta suele almacenarse en una
variable y se ejecuta después. La variable de consulta, como evensQuery en el ejemplo anterior, debe
ser un tipo que se pueda consultar. El tipo de evensQuery es IEnumerable(Of Integer), asignado por el
compilador mediante la inferencia de tipo de variable local.

Es importante recordar que la propia variable de consulta no realiza ninguna acción ni devuelve datos.
Sólo almacena la definición de la consulta. En el ejemplo anterior, es el bucle For Each el que ejecuta la
consulta.

MCT: Luis Dueñas Pag 8 de 388


Manual de LINQ

Ejecución de la consulta
La ejecución de la consulta es proceso independiente de su creación. La creación de la consulta la define,
pero su ejecución la desencadena un mecanismo diferente. Se puede ejecutar un consulta en cuanto esté
definida (ejecución inmediata), o se puede guardar la definición y ejecutar la consulta más tarde
(ejecución diferida).

Ejecución diferida
Una consulta LINQ típica se parece a la del ejemplo anterior, en el que se define evensQuery. En él se
crea la consulta, pero no se ejecuta de inmediato. La definición de la consulta se almacena en la variable
de consulta evensQuery. La consulta se ejecuta más adelante, normalmente mediante un bucle For
Each, que devuelve una secuencia de valores, o aplicando un operador de consulta estándar, como
Count o Max. Este proceso se denomina ejecución diferida.
' Query execution that results in a sequence of values.
For Each number In evensQuery
Console.Write(number & " ")
Next
' Query execution that results in a single value.
Dim evens = evensQuery.Count()
Para una secuencia de valores, se tiene acceso a los datos recuperados mediante la variable de iteración
del bucle For Each (number en el ejemplo anterior). Dado que la variable de consulta, evensQuery,
contiene la definición de la consulta en lugar de los resultados, puede ejecutar una consulta tantas veces
como desee, utilizando la variable de consulta. Por ejemplo, podría tener una base de datos en su
aplicación que sea actualizada continuamente por una aplicación independiente. Después de haber
creado una consulta que recupere los datos de esa base de datos, puede utilizar un bucle For Each para
ejecutar la consulta una y otra vez, recuperando en cada ocasión los datos más recientes.

En el siguiente ejemplo se muestra cómo funciona la ejecución diferida. Una vez definida evensQuery2 y
ejecutada con un bucle For Each, como en los ejemplos anteriores, algunos elementos del origen de
datos numbers cambian. A continuación, un segundo bucle For Each vuelve a ejecutar evensQuery2. Los
resultados son diferentes la segunda vez, porque el bucle For Each ejecuta la consulta otra vez,
utilizando los nuevos valores de numbers.
Dim numberArray() As Integer = {0, 1, 2, 3, 4, 5, 6}
Dim evensQuery2 = From num In numberArray Where num Mod 2 = 0 Select num
Console.WriteLine("Evens in original array:")
For Each number In evensQuery2
Console.Write(" " & number)
Next
Console.WriteLine()
' Change a few array elements.
numberArray(1) = 10
numberArray(4) = 22
numberArray(6) = 8
' Run the same query again.
Console.WriteLine(vbCrLf & "Evens in changed array:")
For Each number In evensQuery2
Console.Write(" " & number)
Next
Console.WriteLine()
Resultado:

MCT: Luis Dueñas Pag 9 de 388


Manual de LINQ

Evens in original array:


0246
Evens in changed array:
0 10 2 22 8
Ejecución inmediata
En la ejecución diferida de consultas, la definición de la consulta se almacena en una variable de consulta
para su posterior ejecución. En la ejecución inmediata, la consulta se ejecuta en el momento de su
definición. La ejecución se activa al aplicar un método que requiere acceso a los elementos individuales
del resultado de la consulta. La ejecución inmediata se fuerza mediante el uso de uno de los operadores
de consulta estándar que devuelven valores únicos. Algunos ejemplos son Count, Max, Average y
First. Estos operadores de consulta estándar ejecutan la consulta en cuanto se aplican para calcular y
devolver un resultado singleton.

La consulta siguiente devuelve un recuento de los números pares de una matriz de enteros. La definición
de la consulta no se guarda y numEvens es un Integer simple.
Dim numEvens=(From num In numbers Where num Mod 2 = 0 Select num).Count()
Se puede conseguir el mismo resultado utilizando el método Aggregate.
Dim numEvensAgg = Aggregate num In numbers Where num Mod 2 = 0 _
Select num Into Count()
También puede forzar la ejecución de una consulta llamando al método ToList o ToArray en una
consulta (ejecución inmediata) o variable de consulta (ejecución diferida), como se muestra en el código
siguiente.
' Immediate execution.
Dim evensList=(From num In numbers Where num Mod 2 = 0 Select num).ToList
' Deferred execution.
Dim evensQuery3 = From num In numbers Where num Mod 2 = 0 Select num
Dim evensArray = evensQuery.ToArray()
En los ejemplos anteriores, evensQuery3 es una variable de consulta, pero evensList es una lista y
evensArray es una matriz.

El uso de ToList o ToArray para forzar la ejecución inmediata es especialmente útil cuando se desea
ejecutar la consulta inmediatamente y almacenar los resultados en memoria caché en un objeto de
colección único.

También puede provocar la ejecución de una consulta mediante el uso de un método IEnumerable,
como GetEnumerator (Método, objeto Collection).

2.2. Operaciones Básicas de Consulta


En este tema se proporciona una breve introducción a las expresiones Language-Integrated Query
(LINQ) en Visual Basic y algunos de los tipos de operaciones que suelen realizarse en las consultas.

Especificar el origen de datos (From)


En una consulta LINQ, el primer paso es especificar el origen de datos que se desea consultar. Por
consiguiente, la cláusula From siempre ocupa el primer lugar en una consulta. Los operadores de
consulta seleccionan y dan forma al resultado basándose en el tipo del origen.
Dim query = From cust In customers...
La cláusula From especifica el origen de datos, customers, y una variable de rango, cust. La variable de
rango es como una variable de iteración de bucle, con la diferencia de que, en una expresión de
consulta, realmente no se produce ninguna iteración. Cuando se ejecuta la consulta, a menudo mediante

MCT: Luis Dueñas Pag 10 de 388


Manual de LINQ

un bucle For Each, la variable de rango actúa como referencia para cada elemento sucesivo de
customers. Dado que el compilador puede deducir el tipo de cust, no tiene que especificarlo
explícitamente.

Filtrar los datos (Where)


Probablemente la operación de consulta más común es aplicar un filtro en forma de expresión booleana.
Así, la consulta devuelve sólo los elementos para los que la expresión es verdadera. La cláusula Where
se utiliza para realizar el filtrado. El filtro especifica qué elementos del origen de datos se incluirán en la
secuencia resultante. En el ejemplo siguiente, sólo se incluyen los clientes que tienen una dirección en
Londres (London).
Dim londonCusts = From cust In customers Where cust.City = "London" ...
Puede utilizar operadores lógicos como And y Or para combinar expresiones de filtro en una cláusula
Where. Por ejemplo, para devolver sólo los clientes de Londres que se llamen Devon, utilice el código
siguiente:
Where cust.City = "London" And cust.Name = "Devon" _
Para devolver los clientes de Londres o París, utilice el código siguiente:
Where cust.City = "London" Or cust.City = "Paris" _
Ordenar los datos (Order By)
A menudo es útil ordenar los datos devueltos según un criterio determinado. La cláusula Order By hará
que se ordenen los elementos de la secuencia devuelta según uno o varios campos que se especifiquen.
Por ejemplo, la consulta siguiente ordena los resultados según la propiedad Name. Dado que Name es
una cadena, los datos devueltos se ordenarán alfabéticamente, de la A a la Z.
Dim londonCusts1 = From cust In customers Where cust.City = "London" _
Order By cust.Name Ascending ...
Para ordenar los resultados en orden inverso, de la Z a la A, utilice la cláusula Order By...Descending.
Cuando no se especifica Ascending ni Descending, se usa Ascending de manera predeterminada.

Seleccionar los datos (Select)


La cláusula Select especifica la forma y el contenido de los elementos devueltos. Por ejemplo, puede
especificar si sus resultados estarán formados por objetos Customer completos, sólo una propiedad
Customer, un subconjunto de propiedades, una combinación de propiedades de varios orígenes de datos
o algún tipo de resultado nuevo basado en un cálculo. Cuando la cláusula Select genera algo distinto de
una copia del elemento de origen, la operación se denomina proyección.

Para recuperar una colección formada por objetos Customer completos, seleccione la variable de rango:
Dim londonCusts2 = From cust In customers Where cust.City = "London" _
Order By cust.Name Ascending Select cust
Si una instancia de Customer es un objeto grande con muchos campos y lo único que desea recuperar es
el nombre, puede seleccionar cust.Name, como se muestra en el ejemplo siguiente. La inferencia de tipo
de variable local reconoce que se cambia el tipo de resultado de una colección de objetos Customer a
una colección de cadenas.
Dim londonCusts3 = From cust In customers Where cust.City = "London" _
Order By cust.Name Ascending Select cust.Name
Para seleccionar varios campos del origen de datos, tiene dos opciones:

En la cláusula Select, especifique los campos que desea incluir en el resultado. El compilador
definirá un tipo anónimo que tiene esos campos como propiedades.

Dado que los elementos devueltos en el ejemplo siguiente son instancias de un tipo anónimo, no
puede hacer referencia al tipo por su nombre en ninguna otra parte del código. El nombre

MCT: Luis Dueñas Pag 11 de 388


Manual de LINQ

designado para el tipo por el compilador contiene caracteres que no son válidos en el código de
Visual Basic normal. En el ejemplo siguiente, los elementos de la colección devuelta por la
consulta en londonCusts4 son todos instancias de un tipo anónimo.
Dim londonCusts4=From cust In customers Where cust.City="London" _
Order By cust.Name Ascending Select Name=cust.Name,Phone=cust.Phone
For Each londonCust In londonCusts4
Console.WriteLine(londonCust.Name & " " & londonCust.Phone)
Next
O bien,

Defina un tipo con nombre que contenga los campos concretos que desea incluir en el resultado
y cree e inicialice instancias del tipo en la cláusula Select. Utilice esta opción sólo si tiene que
utilizar los resultados individuales fuera de la colección en la que se devuelven, o si tiene que
pasarlos como parámetros en llamadas a método. El tipo de londonCusts5 en el ejemplo
siguiente es IEnumerable(Of NamePhone).
Public Class NamePhone
Public Name As String
Public Phone As String
' Additional class elements
End Class
Dim londonCusts5=From cust In customers Where cust.City="London" _
Order By cust.Name Ascending Select New NamePhone With {.Name =
cust.Name,.Phone = cust.Phone}
Combinar los datos (Join y Group Join)
Puede combinar más de un origen de datos en la cláusula From de varias maneras. Por ejemplo, el
código siguiente utiliza dos orígenes de datos y combina implícitamente las propiedades de ambos en el
resultado. La consulta selecciona los estudiantes cuyos apellidos empiezan por vocal.
Dim vowels() As String = {"A", "E", "I", "O", "U"}
Dim vowelNames = From student In students, vowel In vowels Where _
student.Last.IndexOf(vowel) = 0 Select Name = student.First & " " & _
student.Last, Initial = vowel Order By Initial
For Each vName In vowelNames
Console.WriteLine(vName.Initial & ": " & vName.Name)
Next

Nota:

Puede ejecutar este código con la lista de estudiantes creada en Cómo: Crear una lista de elementos.

La palabra clave Join es equivalente a INNER JOIN en SQL. Combina dos colecciones según los valores
de clave coincidentes entre los elementos de las dos colecciones. La consulta devuelve la totalidad o una
parte de los elementos de la colección que tienen valores de clave coincidentes. Por ejemplo, el código
siguiente duplica la acción de la combinación implícita anterior.
Dim vowelNames2 = From student In students Join vowel In vowels _
On student.Last(0) Equals vowel Select Name = student.First & " " & _
student.Last, Initial = vowel Order By Initial
Group Join combina las colecciones en una sola colección jerárquica, igual que LEFT JOIN en SQL.

Agrupar los datos (Group By)

MCT: Luis Dueñas Pag 12 de 388


Manual de LINQ

Puede agregar una cláusula Group By para agrupar los elementos de un resultado de consulta según
uno o más campos de los elementos. Por ejemplo, el código siguiente agrupa los estudiantes por año de
clase.
Dim studentsByYear = From student In students Select student Group By
year = student.Year Into Classes = Group
For Each yearGroup In studentsByYear
Console.WriteLine(vbCrLf & "Year: " & yearGroup.year)
For Each student In yearGroup.Classes
Console.WriteLine(" " & student.Last & ", " & student.First)
Next
Next
Si ejecuta este código utilizando la lista de estudiantes creada en Cómo: Crear una lista de elementos, el
resultado de la instrucción For Each es:
Year: Junior
Tucker, Michael
Garcia, Hugo
Garcia, Debra
Tucker, Lance
Year: Senior
Omelchenko, Svetlana
Osada, Michiko
Fakhouri, Fadi
Feng, Hanying
Adams, Terry
Year: Freshman
Mortensen, Sven
Garcia, Cesar
La variación mostrada en el código siguiente ordena los años de clase y, a continuación, ordena los
estudiantes de cada año por apellido.
Dim studentsByYear2=From student In students Select student Order By
student.Year,student.Last Group By year=student.Year Into Classes=Group

2.3. Características de Visual Basic que Admiten LINQ


El nombre Language-Integrated Query (LINQ) hace referencia a la nueva tecnología incluida en Visual
Basic 2008 que admite sintaxis de consulta y otras nuevas construcciones de lenguaje directamente en el
lenguaje. Con LINQ, no tiene que aprender un nuevo lenguaje para realizar consultas contra un origen de
datos externo. Puede realizar consultas contra datos de bases de datos relacionales, almacenes XML u
objetos utilizando Visual Basic. Esta integración de capacidades de consulta en el lenguaje permite
comprobar, en tiempo de compilación, errores de sintaxis y seguridad de tipos. Esta integración también
le garantiza el conocimiento de los elementos esenciales para escribir consultas complejas y variadas en
Visual Basic 2008.

Las secciones siguientes describen las nuevas construcciones de lenguaje con suficiente detalle como
para permitirle empezar con la lectura de la documentación introductoria, ejemplos de código y
aplicaciones de ejemplo. También puede hacer clic en los vínculos para encontrar explicaciones más
detalladas de cómo las características del lenguaje contribuyen a permitir las consultas integradas en el
lenguaje.

MCT: Luis Dueñas Pag 13 de 388


Manual de LINQ

Expresiones de consulta
Las expresiones de consulta en Visual Basic 2008 se pueden expresar en una sintaxis declarativa similar
a la de SQL o XQuery. En tiempo de compilación, la sintaxis de consulta se convierte en llamadas a
métodos en una implementación del proveedor LINQ de los métodos de extensión de operadores de
consulta estándar. Las aplicaciones controlan qué operadores de consulta estándar están dentro del
ámbito especificando el espacio de nombres adecuado con una instrucción Imports. La sintaxis para una
expresión de consulta en Visual Basic es similar a ésta:
Dim londonCusts = From cust In customers Where cust.City = "London" _
Order By cust.Name Ascending Select cust.Name, cust.Phone
Variables con tipo implícito
En lugar de especificar explícitamente un tipo al declarar e inicializar una variable, puede permitir ahora
al compilador deducir y asignar el tipo, como se muestra en el ejemplo siguiente. Esto se conoce como
inferencia de tipo de variable local.

Nota:

La inferencia de tipo de variable local sólo funciona cuando define una variable local dentro del
cuerpo de un método con Option Infer establecida en On. On es el valor predeterminado para
nuevos proyectos en LINQ.

' The variable number will be typed as an integer.


Dim aNumber = 5
' The variable name will be typed as a String.
Dim aName = "Virginia"

Nota:

En Visual Basic 2005 y versiones anteriores, estos ejemplos se compilan bien, pero el tipo asignado a
aNumber y aName es Object. Por consiguiente, un proyecto existente que se vuelve a compilar en
Visual Basic 2008 con Option Infer establecida en On puede comportarse de manera diferente que
en versiones anteriores del lenguaje.

' Query example.


' If numbers is a one-dimensional array of integers, num will be typed
' as an integer and numQuery will be typed as IEnumerable(Of Integer)--
' basically a collection of integers.
Dim numQuery = From num In numbers Where num Mod 2 = 0 Select num
Las variables declaradas de esta manera presentan un establecimiento inflexible de tipos, al igual que las
variables cuyo tipo se especifica explícitamente. La inferencia de tipo de variable local permite crear tipos
anónimos, que son necesarios para las consultas LINQ, pero también se puede utilizar para cualquier
variable local.

Inicializadores de objeto
Los inicializadores de objeto se utilizan en expresiones de consulta cuando se debe crear un tipo anónimo
para albergar los resultados de una consulta. También se pueden utilizar para inicializar objetos de tipos
con nombre fuera de las consultas. Utilizando un inicializador de objeto, puede inicializar un objeto en
una sola línea sin llamar explícitamente a un constructor. Suponiendo que tiene una clase denominada
Customer con propiedades públicas Name y Phone, además de otras propiedades, un inicializador de
objeto se puede utilizar de esta manera:
Dim aCust As Customer=New Customer With{.Name="Mike",.Phone = "555-0212"}
Tipos anónimos
Los tipos anónimos proporcionan una manera práctica de agrupar temporalmente un conjunto de
propiedades en un elemento que se desea incluir en un resultado de la consulta. Esto permite elegir

MCT: Luis Dueñas Pag 14 de 388


Manual de LINQ

cualquier combinación de campos disponibles en la consulta, en cualquier orden, sin definir un tipo de
datos con nombre para el elemento.

El tipo anónimo lo construye dinámicamente el compilador. El nombre del tipo es asignado por el
compilador, y podría cambiar con cada nueva compilación. Por tanto, el nombre no se puede utilizar
directamente. Los tipos anónimos se inicializan de la manera siguiente:
' Outside a query.
Dim product = New With {.Name = "paperclips", .Price = 1.29}
' Inside a query.
' You can use the existing member names of the selected fields, as was
' shown previously in the Query Expressions section of this topic.
Dim londonCusts1 = From cust In customers Where cust.City = "London" _
Select cust.Name, cust.Phone
' Or you can specify new names for the selected fields.
Dim londonCusts2 = From cust In customers Where cust.City = "London" _
Select CustomerName = cust.Name, CustomerPhone = cust.Phone
Métodos de extensión
Los métodos de extensión permiten agregar métodos a un tipo de datos o a una interfaz desde fuera de
la definición. Esta característica permite agregar nuevos métodos a un tipo existente sin tener que
modificar el tipo. Los operadores de consulta estándar son un conjunto de métodos de extensión que
proporcionan funcionalidad de consulta LINQ para cualquier tipo que implementa IEnumerable<(Of
<(T>)>). Otras extensiones a IEnumerable<(Of <(T>)>) incluyen Count, Union y Intersect.

El siguiente método de extensión agrega un método de impresión a la clase String.


' Import System.Runtime.CompilerServices to use the Extension attribute.
<Extension()> _
Public Sub Print(ByVal str As String)
Console.WriteLine(str)
End Sub
Al método se le llama como a un método de instancia ordinario de String:
Dim greeting As String = "Hello"
greeting.Print()
Expresiones lambda
Una expresión lambda es una función sin nombre que calcula y devuelve un solo valor. A diferencia de
las funciones con nombre, una expresión lambda se puede definir y ejecutar al mismo tiempo. El ejemplo
siguiente muestra 4.
Console.WriteLine((Function(num As Integer) num + 1)(3))
Puede asignar la definición de la expresión lambda a un nombre de variable y, a continuación, utilizar el
nombre para llamar a la función. El ejemplo siguiente también muestra 4.
Dim add1 = Function(num As Integer) num + 1
Console.WriteLine(add1(3))
En LINQ, las expresiones lambda subyacen a muchos de los operadores de consulta estándar. El
compilador crea expresiones lambda para capturar los cálculos definidos en los métodos de consulta
básicos como Where, Select, Order By, Take While, etc.

Por ejemplo, el código siguiente define una consulta que devuelve todos los alumnos de último curso de
una lista de alumnos.
Dim seniorsQuery = From stdnt In students Where stdnt.Year = "Senior" _
Select stdnt

MCT: Luis Dueñas Pag 15 de 388


Manual de LINQ

La definición de la consulta se compila en código similar al del ejemplo siguiente, el cual utiliza dos
expresiones lambda para especificar los argumentos de Where y Select.
Dim seniorsQuery2 = students .Where(Function(st) st.Year = "Senior") _
.Select(Function(s) s)
Cualquiera de las versiones se puede ejecutar mediante un bucle For Each:
For Each senior In seniorsQuery
Console.WriteLine(senior.Last & ", " & senior.First)
Next

2.4. Relaciones entre Tipos en Operaciones de Consulta


Las variables que se usan en las operaciones de consulta de Language-Integrated Query (LINQ) tienen
establecimiento inflexible de tipos y deben ser compatibles entre sí. Los tipos seguros se usan en el
origen de datos, en la propia consulta y en la ejecución de la consulta. La ilustración siguiente identifica
los términos que se utilizan para describir una consulta LINQ.

Partes de una consulta LINQ

El tipo de la variable de rango de la consulta debe ser compatible con el tipo de los elementos del origen
de datos. El tipo de la variable de consulta debe ser compatible con el elemento de secuencia definido en
la cláusula Select. Finalmente, el tipo de los elementos de secuencia también debe ser compatible con el
tipo de la variable de control del bucle que se utiliza en la instrucción For Each que ejecuta la consulta.
Este establecimiento inflexible de tipos facilita la identificación de los errores de tipos en tiempo de
compilación.

Visual Basic 2008 simplifica el establecimiento inflexible de tipos al implementar la inferencia de tipo de
variable local, lo que también se conoce como tipo implícito. Esta característica se utiliza en el ejemplo
anterior y, como observará, se utiliza en los ejemplos y la documentación de LINQ. En Visual Basic, la
inferencia de tipo de variable local se consigue simplemente utilizando una instrucción Dim sin una
cláusula As. En el ejemplo siguiente, city tiene establecimiento inflexible de tipos como cadena.
Dim city = "Seattle"

Nota:

La inferencia de tipo de variable local sólo funciona cuando Option Infer se establece en On.

Sin embargo, aun cuando se utiliza la inferencia de tipo de variable local en una consulta, las mismas
relaciones de tipo se encuentran entre las variables del origen de datos, la variable de consulta y el bucle
de ejecución de la consulta. Es útil conocer los fundamentos de estas relaciones de tipos para escribir
consultas LINQ o trabajar con los ejemplos y ejemplos de código de la documentación.

Consultas que devuelven elementos completos de los datos de origen

MCT: Luis Dueñas Pag 16 de 388


Manual de LINQ

En el ejemplo siguiente se muestra una operación de consulta LINQ que devuelve una secuencia de
elementos seleccionados de los datos de origen. El origen, names, contiene una matriz de cadenas y el
resultado de la consulta es una secuencia que contiene cadenas que empiezan por la letra M.
Dim names = New String() {"John", "Rick", "Maggie", "Mary"}
Dim mNames = From name In names Where name.IndexOf("M") = 0 Select name
For Each nm In mNames
Console.WriteLine(nm)
Next
Es equivalente al código siguiente, pero es mucho más corto y más fácil de escribir. En Visual Basic, se
prefiere la inferencia de tipo de variable local en las consultas.
Dim names2() As String = {"John", "Rick", "Maggie", "Mary"}
Dim mNames2 As IEnumerable(Of String) = From name As String In names _
Where name.IndexOf("M") = 0 Select name
For Each nm As String In mNames
Console.WriteLine(nm)
Next
En los dos ejemplos de código anteriores existen las siguientes relaciones, se determinen los tipos de
forma implícita o explícita.

1. El tipo de los elementos del origen de datos, names, es el tipo de la variable de rango, name, en
la consulta.

2. El tipo de objeto seleccionado, name, determina el tipo de la variable de consulta, mNames. Aquí
name es una cadena, por lo que la variable de consulta es IEnumerable(Of String) en Visual Basic.

3. La consulta definida en mNames se ejecuta en el bucle For Each El bucle recorre en iteración el
resultado de ejecutar la consulta. Dado que mNames, cuando se ejecuta, devuelve una secuencia
de cadenas, la variable de iteración del bucle, nm, también es una cadena.

Consultas que devuelven un campo de los elementos seleccionados


En el ejemplo siguiente se muestra una operación de consulta LINQ a SQL que devuelve una secuencia
que contiene sólo una parte de cada elemento seleccionado del origen de datos. La consulta utiliza una
colección de objetos Customer como origen de datos y proyecta sólo la propiedad Name en el resultado.
Dado que el nombre del cliente es una cadena, la consulta genera una secuencia de cadenas como
resultado.
' Method GetTable returns a table of Customer objects.
Dim customers = db.GetTable(Of Customer)()
Dim custNames = From cust In customers Where cust.City = "London" _
Select cust.Name
For Each custName In custNames
Console.WriteLine(custName)
Next
Las relaciones entre las variables son como las del ejemplo más sencillo.

1. El tipo de los elementos del origen de datos, customers, es el tipo de la variable de rango, cust,
en la consulta. En este ejemplo, ese tipo es Customer.

2. La instrucción Select devuelve la propiedad Name de cada objeto Customer en lugar del objeto
completo. Dado que Name es una cadena, la variable de consulta, custNames, será de nuevo
IEnumerable(Of String), no Customer.

MCT: Luis Dueñas Pag 17 de 388


Manual de LINQ

3. Dado que custNames representa una secuencia de cadenas, la variable de iteración del bucle For
Each, custName, debe ser una cadena.

Sin la inferencia de tipo de variable local, el ejemplo anterior sería más difícil de escribir y de entender,
como demuestra el ejemplo siguiente.
' Method GetTable returns a table of Customer objects.
Dim customers As Table(Of Customer) = db.GetTable(Of Customer)()
Dim custNames As IEnumerable(Of String) = From cust As Customer In
customers Where cust.City = "London" Select cust.Name
For Each custName As String In custNames
Console.WriteLine(custName)
Next
Consultas que requieren tipos anónimos
En el ejemplo siguiente se muestra una situación más compleja. En el ejemplo anterior, no era apropiado
especificar explícitamente los tipos de todas las variables. En este ejemplo, no se puede. En lugar de
seleccionar los elementos Customer completos del origen de datos o un campo único de cada elemento,
la cláusula Select de esta consulta devuelve dos propiedades del objeto Customer original: Name y City.
Como respuesta a la cláusula Select, el compilador define un tipo anónimo que contiene esas dos
propiedades. El resultado de ejecutar nameCityQuery en el bucle For Each es una colección de instancias
del nuevo tipo anónimo. Dado que el tipo anónimo no tiene ningún nombre utilizable, no puede
especificar explícitamente el tipo de nameCityQuery o custInfo. Es decir, con un tipo anónimo, no hay
ningún nombre de tipo que se pueda utilizar en lugar de String en IEnumerable(Of String).
' Method GetTable returns a table of Customer objects.
Dim customers = db.GetTable(Of Customer)()
Dim nameCityQuery = From cust In customers Where cust.City = "London" _
Select cust.Name, cust.City
For Each custInfo In nameCityQuery
Console.WriteLine(custInfo.Name)
Next
Aunque no es posible especificar los tipos de todas las variables en el ejemplo anterior, las relaciones
siguen siendo las mismas.

1. El tipo de los elementos del origen de datos es de nuevo el tipo de la variable de rango de la
consulta. En este ejemplo, cust es una instancia de Customer.

2. Dado que la instrucción Select genera un tipo anónimo, el tipo de la variable de consulta,
nameCityQuery, debe declararse implícitamente como un tipo anónimo. Un tipo anónimo no tiene
un nombre utilizable y, por consiguiente, no se puede especificar explícitamente.

3. El tipo de la variable de iteración del bucle For Each es el tipo anónimo creado en el paso 2.
Dado que no tiene un nombre utilizable, el tipo de la variable de iteración del bucle se debe
determinar implícitamente.

3. Cómo: Crear un Proyecto con LINQ


Los nuevos proyectos de Visual C# y Visual Basic orientados a .NET Framework versión 3.5 incluyen los
espacios de nombres y las referencias necesarios para la funcionalidad básica de LINQ. Simplemente
cree un nuevo proyecto y podrá empezar a escribir consultas LINQ en colecciones de objetos. Visual
Basic proporciona además una referencia y un espacio de nombres importado para la funcionalidad de
LINQ a XML. En Visual C# se deben agregar manualmente.

MCT: Luis Dueñas Pag 18 de 388


Manual de LINQ

Para utilizar LINQ a XML o LINQ a DataSet en cualquier lenguaje, deberá agregar manualmente los
espacios de nombres y las referencias como se describe en las secciones siguientes.

Si está actualizando un proyecto creado con una versión anterior de Visual Studio, es posible que tenga
que proporcionar éstas u otras referencias relacionadas con LINQ manualmente y configurar el proyecto,
también manualmente, de manera que esté orientado a la versión 3.5 de .NET Framework.

Nota:

Si realiza la compilación desde un símbolo del sistema, debe hacer referencia manualmente a las DLL
relacionadas con LINQ ubicadas en unidad:\Archivos de programa\Reference
Assemblies\Microsoft\Framework\v3.5.

Procedimientos para agregar espacios de nombres y referencias de LINQ

Para orientar el proyecto a la versión 3.5 de .NET Framework

1. En Visual Studio, abra un proyecto de Visual Basic o C# creado en Visual Studio 2005 y siga las
indicaciones para convertirlo en proyecto de Visual Studio 2008.

2. Si es un proyecto de C#, haga clic en el menú Proyecto y, a continuación, haga clic en


Propiedades.

a. En la página de propiedades Aplicación, seleccione .NET Framework 3.5 en la lista


desplegable Versión de .NET Framework de destino.

3. Si es un proyecto de Visual Basic, haga clic en el menú Proyecto y, a continuación, haga clic en
Propiedades.

En la página de propiedades Compilación, haga clic en Opciones de compilación avanzadas y,


a continuación, seleccione .NET Framework 3.5 en la lista desplegable Versión de .NET
Framework de destino (todas las configuraciones).

Para habilitar la funcionalidad básica de LINQ

1. En un proyecto de Visual Basic o C#, haga clic en el menú Proyecto y, a continuación, haga clic
en Agregar referencia.

2. En el cuadro de diálogo Agregar referencia, haga clic en la ficha .NET, desplácese hasta el
archivo System.Core.dll y, a continuación, haga clic en él. Haga clic en Aceptar.

3. Agregue una directiva using o una instrucción Imports para System.Linq a su proyecto o
archivo de código fuente.

Para habilitar la funcionalidad avanzada de LINQ con árboles de expresión

Si ya tiene una referencia a System.Core.dll, agregue una directiva using o una instrucción
Imports para System.Linq.Expressions.

Para usar LINQ a XML

MCT: Luis Dueñas Pag 19 de 388


Manual de LINQ

1. Si es necesario, siga los pasos antes descritos para agregar una referencia a System.Core.dll y
una directiva using o una instrucción Imports para System.Linq.

2. Agregue una referencia a System.Xml.Linq.

3. Agregue una directiva using o una instrucción Imports para System.Xml.Linq.

Nota:

De forma predeterminada, esta funcionalidad se proporciona para proyectos de Visual Basic.

Para usar LINQ a SQL

1. Si es necesario, siga los pasos antes descritos para agregar una referencia a System.Core.dll y
una directiva using o una instrucción Imports para System.Linq.

2. Agregue una referencia a System.Data.Linq.

3. Agregue una directiva using o una instrucción Imports para System.Data.Linq o uno de los
otros espacios de nombres de System.Data.Linq, en función de los requisitos de su proyecto.

Para usar LINQ a DataSet

1. Si es necesario, siga los pasos antes descritos para agregar una referencia a System.Core.dll y
una directiva using o una instrucción Imports para System.Linq.

2. Agregue una referencia a System.Data.DataSetExtensions.dll para la funcionalidad de LINQ a


DataSet. Agregue una referencia a System.Data.dll, si no existe.

3. Agregue una directiva using o una instrucción Imports para System.Data y, opcionalmente,
para System.Data.Common o System.Data.SqlClient, en función de cómo se conecte a la base de
datos.

4. Características de IDE de Visual Studio y Herramientas


para LINQ
El entorno de desarrollo integrado (IDE) de Visual Studio 2008 proporciona las características siguientes
que admiten el desarrollo de aplicaciones LINQ:

Diseñador relacional de objetos


Diseñador relacional de objetos es una herramienta de diseño visual que se puede utilizar en aplicaciones
LINQ a SQL para generar clases en C# o Visual Basic que representen los datos relacionales de una base
de datos subyacente.

Herramienta de línea de comandos SQLMetal


SQLMetal es una herramienta de línea de comandos que se puede utilizar en procesos de compilación
para generar clases a partir de bases de datos existentes para su uso en aplicaciones LINQ a SQL.

Editores de código que reconocen LINQ

MCT: Luis Dueñas Pag 20 de 388


Manual de LINQ

Tanto el editor de código de C# como el de Visual Basic admiten LINQ ampliamente, con la nueva
funcionalidad IntelliSense y de formato.

Compatibilidad del depurador de Visual Studio


El depurador de Visual Studio permite depurar expresiones de consulta.

5. Guía de Programación General con LINQ


Esta sección contiene información sobre cómo programar con LINQ, e incluye los operadores de consulta
estándar, el uso de árboles de expresión en LINQ y los proveedores LINQ personalizados.

5.1. Información General sobre Operadores de Consulta


Estándar
Los operadores de consulta estándar son los métodos que forman el modelo de Language-Integrated
Query (LINQ). La mayoría de estos métodos funciona en secuencias, donde una secuencia es un objeto
cuyo tipo implementa la interfaz IEnumerable<(Of <(T>)>)o la interfaz IQueryable<(Of <(T>)>). Los
operadores de consulta estándar proporcionan capacidades de consulta que incluyen filtrado, proyección,
agregación, ordenación y otras.

Hay dos conjuntos de operadores de consulta estándar de LINQ, uno que funciona sobre objetos de tipo
IEnumerable<(Of <(T>)>) y otro que funciona sobre objetos de tipo IQueryable<(Of <(T>)>). Los
métodos que constituyen cada conjunto son miembros estáticos de las clases Enumerable y Queryable,
respectivamente. Se definen como métodos de extensión del tipo sobre el que operan. Esto significa que
se pueden llamar utilizando sintaxis del método estático o sintaxis del método de instancia.

Además, varios métodos de operador de consulta estándar funcionan con tipos distintos de los que se
basan en IEnumerable<(Of <(T>)>) o IQueryable<(Of <(T>)>). El tipo Enumerable define dos de esos
métodos, que operan sobre objetos de tipo IEnumerable. Estos métodos, Cast<(Of
<(TResult>)>)(IEnumerable) y OfType<(Of <(TResult>)>)(IEnumerable), permiten que una colección
no parametrizada, o no genérica, pueda ser consultada en el modelo de LINQ. Esto lo consiguen creando
una colección de objetos con establecimiento inflexible de tipos. La clase Queryable define dos métodos
similares, Cast<(Of <(TResult>)>)(IQueryable) y OfType<(Of <(TResult>)>)(IQueryable), que operan
sobre objetos de tipo Queryable.

Los operadores de consulta estándar difieren en el momento de su ejecución, dependiendo de si


devuelven un valor singleton o una secuencia de valores. Los métodos que devuelven un valor singleton
(por ejemplo, Average y Sum) se ejecutan inmediatamente. Los métodos que devuelven una secuencia
retrasan la ejecución de la consulta y devuelven un objeto enumerable.

En el caso de los métodos que operan sobre colecciones en memoria, es decir, aquellos métodos que
extienden IEnumerable<(Of <(T>)>), el objeto enumerable devuelto captura los argumentos que se
pasaron al método. Cuando se enumera ese objeto, se emplea la lógica del operador de consulta y se
devuelven los resultados de la consulta.

En contraste, los métodos que extienden IQueryable<(Of <(T>)>) no implementan cualquier


comportamiento de consulta, sino que generan un árbol de expresión que representa la consulta que se
va a realizar. El procesamiento de la consulta es administrado por el objeto IQueryable<(Of <(T>)>) del
origen.

MCT: Luis Dueñas Pag 21 de 388


Manual de LINQ

Las llamadas a métodos de consulta se pueden encadenar juntas en una sola consulta, lo cual permite
hacer consultas arbitrariamente complejas.

El ejemplo de código siguiente muestra cómo se pueden utilizar los operadores de consulta estándar
para obtener información sobre una secuencia.
Dim sentence As String = "the quick brown fox jumps over the lazy dog"
' Split the string into individual words to create a collection.
Dim words As String() = sentence.Split(" "c)
Dim query = From word In words Group word.ToUpper() By word.Length Into
gr = Group Order By Length Select Length, GroupedWords = gr
Dim output As New System.Text.StringBuilder
For Each obj In query
output.AppendLine(String.Format("Words of length {0}:", obj.Length))
For Each word As String In obj.GroupedWords
output.AppendLine(word)
Next
Next
'Display the output
MsgBox(output.ToString())
' This code example produces the following output:
' Words of length 3:
' THE
' FOX
' THE
' DOG
' Words of length 4:
' OVER
' LAZY
' Words of length 5:
' QUICK
' BROWN
' JUMPS
Sintaxis de las expresiones de consulta
Algunos de los operadores de consulta estándar utilizados con mayor frecuencia tienen una sintaxis de
palabras clave dedicadas del lenguaje C# y Visual Basic que permite llamarlos como parte de una
expresión de consulta.

Extender los operadores de consulta estándar


Puede aumentar el conjunto de operadores de consulta estándar creando métodos específicos del
dominio que sean adecuados para su tecnología o dominio de destino. También puede reemplazar los
operadores de consulta estándar con sus propias implementaciones que proporcionen servicios
adicionales como evaluación remota, traducción de consultas y optimización.

5.1.1. Sintaxis de las Expresiones de Consulta para


Operadores de Consulta Estándar
Algunos de los operadores de consulta estándar más frecuentemente utilizados poseen una sintaxis de
palabras clave dedicadas del lenguaje C# y Visual Basic que les permite ser llamados como parte de una
expresión de consulta. Una expresión de consulta constituye una forma diferente de expresar una

MCT: Luis Dueñas Pag 22 de 388


Manual de LINQ

consulta, más legible que su equivalente basado en método. Las cláusulas de las expresiones de consulta
se traducen en llamadas a los métodos de consulta en tiempo de compilación.

Tabla de sintaxis de las expresiones de consulta


La tabla siguiente enumera los operadores de consulta estándar que poseen cláusulas de expresiones de
consulta equivalentes. Los lenguajes de programación Visual Basic y C# no proporcionan sintaxis
dedicada de expresiones de consulta para los mismos métodos. La tabla muestra la sintaxis para ambos
lenguajes.

Sintaxis de las
Sintaxis de las expresiones de
expresiones de consulta de
Método consulta de C# Visual Basic

All<(Of <(TSource>)>) No aplicable Aggregate … In …


Into All(…).

Any No aplicable Agrégate … In …


Into Any().

Average No aplicable Agrégate … In …


Into Average().

Cast<(Of <(TResult>)>) Utilice una From … As ….


variable de rango
con asignación
explícita de tipo,
por ejemplo:
from int i in
numbers.

Count No aplicable Aggregate … In …


Into Count().

Distinct<(Of <(TSource>)>)(IEnumerable<(Of No aplicable Distinct .


<(Tsource>)>))

GroupBy group … by Group … By …


O bien Into … .
group … by … into
….

GroupJoin<(Of <(TOuter, TInner, TKey, join … in … on … Group Join … In


TResult>)>)(IEnumerable<(Of <(TOuter>)>), equals … into … . … On ….
IEnumerable<(Of <(TInner>)>), Func<(Of <(TOuter,
TKey>)>), Func<(Of <(TInner, TKey>)>), Func<(Of
<(TOuter, IEnumerable<(Of <(TInner>)>), TResult>)>))

Join<(Of <(TOuter, TInner, TKey, join … in … on … From x In …, y In


TResult>)>)(IEnumerable<(Of <(TOuter>)>), equals … . … Where x.a =
IEnumerable<(Of <(TInner>)>), Func<(Of <(TOuter, b.a
TKey>)>), Func<(Of <(TInner, TKey>)>), Func<(Of O bien
<(TOuter, TInner, TResult>)>)) Join … [As …]In …
On … .

LongCount No aplicable Aggregate … In …


Into LongCount().

Max No aplicable Aggregate … In …


Into Max().

MCT: Luis Dueñas Pag 23 de 388


Manual de LINQ

Min No aplicable Aggregate … In …


Into Min().

OrderBy<(Of <(TSource, TKey>)>)(IEnumerable<(Of orderby. Order By.


<(Tsource>)>), Func<(Of <(TSource, TKey>)>))

OrderByDescending<(Of <(TSource, orderby … Order By …


TKey>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of descending. Descending.
<(Tsource, TKey>)>))

Select select. Select.

SelectMany Varias cláusulas Varias cláusulas


from. From.

Skip<(Of <(TSource>)>) No aplicable Skip.

SkipWhile No aplicable Skip While.

Sum No aplicable Aggregate … In …


Into Sum().

Take<(Of <(TSource>)>) No aplicable Take.

TakeWhile No aplicable Take While.

ThenBy<(Of <(TSource, orderby …, … . Order By …, … .


TKey>)>)(IOrderedEnumerable<(Of <(TSource>)>),
Func<(Of <(TSource, TKey>)>))

ThenByDescending<(Of <(TSource, orderby …, … Order By …, …


TKey>)>)(IOrderedEnumerable<(Of <(TSource>)>), descending. Descending.
Func<(Of <(TSource, TKey>)>))

Where where. Where.

5.1.2. Clasificación de Operadores de Consulta Estándar


por Modo de Ejecución
LINQ a Objects implementa los métodos de operadores de consulta estándar mediante dos formas
posibles de ejecución: inmediata o aplazada. Los operadores de consulta que utilizan la ejecución
aplazada pueden dividirse a su vez en dos categorías: con o sin transmisión por secuencias. Saber cómo
se ejecutan los distintos operadores de consulta puede resultar útil para entender los resultados
obtenidos a partir de una consulta determinada. Esto se cumple especialmente si el origen de datos
cambia o si genera una consulta sobre otra. En este tema se clasifican los operadores de consulta
estándar según su modo de ejecución.

Modos de ejecución
Inmediata
Ejecución inmediata significa que el origen de datos se lee y la operación se realiza en el punto del
código donde se declara la consulta. Todos los operadores de consulta estándar que devuelven un
resultado único no enumerable se ejecutan de forma inmediata.

Aplazada

MCT: Luis Dueñas Pag 24 de 388


Manual de LINQ

Ejecución aplazada significa que la operación no se realiza en el punto del código donde se declara la
consulta. La operación se realiza solamente cuando se enumera la variable de consulta, por ejemplo,
cuando se utiliza una instrucción foreach (For Each en Visual Basic). Esto significa que los resultados de
la ejecución de la consulta dependen del contenido del origen de datos cuando se ejecuta la consulta, no
cuando ésta se define. Si la variable de consulta se enumera varias veces, pueden obtenerse resultados
distintos en cada ocasión. Casi todos los operadores de consulta estándar cuyo tipo de valor devuelto es
IEnumerable<(Of <(T>)>) o IOrderedEnumerable<(Of <(TElement>)>) se ejecutan de forma aplazada.

Los operadores de consulta que utilizan la ejecución aplazada pueden clasificarse a su vez en operadores
con o sin transmisión por secuencias.

Con transmisión por secuencias


Los operadores con transmisión por secuencias no tienen que leer todos los datos de origen para
proporcionar los elementos de resultado. En el momento de la ejecución, un operador de este tipo actúa
en cada elemento de origen cuando se lee y proporciona el elemento si es pertinente. Un operador con
transmisión por secuencias continúa leyendo elementos de origen hasta que puede generarse un
elemento de resultado. Esto significa que puede leerse más de un elemento de origen para generar un
elemento de resultado.

Sin transmisión por secuencias


Los operadores sin transmisión por secuencias deben leer todos los datos de origen para poder
proporcionar un elemento de resultado. Operaciones como la ordenación o la agrupación pertenecen a
esta categoría. En el momento de la ejecución, los operadores de consulta sin transmisión por secuencias
leen todos los datos de origen, los colocan en una estructura de datos, realizan la operación y
proporcionan los elementos resultantes.

Tabla de clasificación
En la tabla siguiente se clasifica cada método de operador de consulta estándar según su método de
ejecución.

Nota:

Si un operador aparece en dos columnas, dos secuencias de entrada intervienen en la operación y


cada una de ellas se evalúa de forma distinta. En estos casos, la primera secuencia de la lista de
parámetros siempre se evalúa de forma aplazada con transmisión por secuencias.

Ejecución
aplazada Ejecución
con aplazada sin
transmisión transmisión
Operador de consulta Ejecución por por
estándar Tipo de valor devuelto inmediata secuencias secuencias

Aggregate TSource X

All<(Of <(TSource>)>) Boolean X

Any Boolean X

AsEnumerable<(Of IEnumerable<(Of X
<(TSource>)>) <(T>)>)

Average Valor numérico único X

MCT: Luis Dueñas Pag 25 de 388


Manual de LINQ

Cast<(Of <(TResult>)>) IEnumerable<(Of X


<(T>)>)

Concat<(Of IEnumerable<(Of X
<(TSource>)>) <(T>)>)

Contains Boolean X

Count Int32 X

DefaultIfEmpty IEnumerable<(Of X
<(T>)>)

Distinct IEnumerable<(Of X
<(T>)>)

ElementAt<(Of TSource X
<(TSource>)>)

ElementAtOrDefault<(Of TSource X
<(TSource>)>)

Empty<(Of IEnumerable<(Of X
<(TResult>)>) <(T>)>)

Except IEnumerable<(Of X X
<(T>)>)

First TSource X

FirstOrDefault TSource X

GroupBy IEnumerable<(Of X
<(T>)>)

GroupJoin IEnumerable<(Of X X
<(T>)>)

Intersect IEnumerable<(Of X X
<(T>)>)

Join IEnumerable<(Of X X
<(T>)>)

Last TSource X

LastOrDefault TSource X

LongCount Int64 X

Max Valor numérico único, X


TSource o TResult

Min Valor numérico único, X


TSource o TResult

OfType<(Of IEnumerable<(Of X
<(TResult>)>) <(T>)>)

MCT: Luis Dueñas Pag 26 de 388


Manual de LINQ

OrderBy IOrderedEnumerable<(Of X
<(TElement>)>)

OrderByDescending IOrderedEnumerable<(Of X
<(TElement>)>)

Range IEnumerable<(Of X
<(T>)>)

Repeat<(Of IEnumerable<(Of X
<(TResult>)>) <(T>)>)

Reverse<(Of IEnumerable<(Of X
<(TSource>)>) <(T>)>)

Select IEnumerable<(Of X
<(T>)>)

SelectMany IEnumerable<(Of X
<(T>)>)

SequenceEqual Boolean X

Single TSource X

SingleOrDefault TSource X

Skip<(Of IEnumerable<(Of X
<(TSource>)>) <(T>)>)

SkipWhile IEnumerable<(Of X
<(T>)>)

Sum Valor numérico único X

Take<(Of IEnumerable<(Of X
<(TSource>)>) <(T>)>)

TakeWhile IEnumerable<(Of X
<(T>)>)

ThenBy IOrderedEnumerable<(Of X
<(TElement>)>)

ThenByDescending IOrderedEnumerable<(Of X
<(TElement>)>)

ToArray<(Of Matriz de TSource X


<(TSource>)>)

ToDictionary Dictionary<(Of <(TKey, X


TValue>)>)

ToList<(Of IList<(Of <(T>)>) X


<(TSource>)>)

ToLookup ILookup<(Of <(TKey, X


TElement>)>)

MCT: Luis Dueñas Pag 27 de 388


Manual de LINQ

Union IEnumerable<(Of X
<(T>)>)

Where IEnumerable<(Of X
<(T>)>)

5.1.3. Ordenar Datos


Una operación de ordenación ordena los elementos de una secuencia según uno o varios atributos. El
primer criterio de ordenación realiza una ordenación principal de los elementos. Al especificar un
segundo criterio de ordenación, se pueden ordenar los elementos dentro de cada grupo de ordenación
principal.

La ilustración siguiente muestra los resultados de una operación de ordenación alfabética en una
secuencia de caracteres.

Los métodos de operador de consulta estándar que ordenan los datos se enumeran en la sección
siguiente.

Métodos

Sintaxis de
Sintaxis de las
las expresiones
expresiones de consulta
Nombre del de consulta de Visual
método Description de C# Basic Más información

OrderBy Ordena los orderby Order By Enumerable..::.OrderBy


valores de Queryable..::.OrderBy
forma
ascendente.

OrderByDescending Ordena los orderby … Order By … Enumerable..::.OrderByDescending


valores de descending Descending Queryable..::.OrderByDescending
forma
descendente.

ThenBy Realiza una orderby …, Order By …, Enumerable..::.ThenBy


operación de … … Queryable..::.ThenBy
ordenación
secundaria
de forma
ascendente.

ThenByDescending Realiza una orderby …, Order By …, Enumerable..::.ThenByDescending


operación de … … Queryable..::.ThenByDescending
ordenación descending Descending
secundaria
de forma
descendente.

MCT: Luis Dueñas Pag 28 de 388


Manual de LINQ

Reverse Invierte el No es No es Enumerable..::.Reverse<(Of


orden de los aplicable aplicable <(TSource>)>)
elementos Queryable..::.Reverse<(Of
de una <(TSource>)>)
colección.

Ejemplos de sintaxis de expresiones de consulta


Ejemplos de ordenación principal
Ordenación principal ascendente
En el ejemplo siguiente se muestra cómo utilizar la cláusula orderby (Order By en Visual Basic) en una
consulta LINQ para ordenar las cadenas de una matriz por longitud de cadena, en orden ascendente.
Dim words() As String = {"the", "quick", "brown", "fox", "jumps"}
Dim sortQuery = From word In words Order By word.Length Select word
Dim sb As New System.Text.StringBuilder()
For Each str As String In sortQuery
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
the
fox
quick
brown
jumps
Ordenación principal descendente
En el ejemplo siguiente se muestra cómo utilizar la cláusula orderby descending (Order By
Descending en Visual Basic) en una consulta LINQ para ordenar las cadenas por su primera letra, en
orden descendente.
Dim words() As String = {"the", "quick", "brown", "fox", "jumps"}
Dim sortQuery = From word In words Order By word.Substring(0, 1)
Descending Select word
Dim sb As New System.Text.StringBuilder()
For Each str As String In sortQuery
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
the
quick
jumps
fox
brown
Ejemplos de ordenación secundaria
Ordenación secundaria ascendente
En el ejemplo siguiente se muestra cómo utilizar la cláusula orderby (Order By en Visual Basic) en una
consulta LINQ para realizar una ordenación principal y secundaria de las cadenas de una matriz. Las
cadenas se ordenan primero por longitud y después por la primera letra de la cadena, en ambos casos en
orden ascendente.

MCT: Luis Dueñas Pag 29 de 388


Manual de LINQ

Dim words() As String = {"the", "quick", "brown", "fox", "jumps"}


Dim sortQuery = From word In words Order By word.Length,
word.Substring(0, 1) Select word
Dim sb As New System.Text.StringBuilder()
For Each str As String In sortQuery
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
fox
the
brown
jumps
quick
Ordenación secundaria descendente
En el ejemplo siguiente se muestra cómo utilizar la cláusula orderby descending (Order By
Descending en Visual Basic) en una consulta LINQ para realizar una ordenación principal, en orden
ascendente, y una ordenación secundaria, en orden descendente. Las cadenas se ordenan primero por
longitud y después por la primera letra de la cadena.
Dim words() As String = {"the", "quick", "brown", "fox", "jumps"}
Dim sortQuery = From word In words Order By word.Length,
word.Substring(0, 1) Descending Select word
Dim sb As New System.Text.StringBuilder()
For Each str As String In sortQuery
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
fox
the
quick
jumps
brown

5.1.4. Operaciones Set


Las operaciones de conjuntos (Set) de LINQ son operaciones de consulta que generan un conjunto de
resultados que se basa en la presencia o ausencia de elementos equivalentes dentro de las mismas u
otras colecciones (o conjuntos).

Los métodos de operador de consulta estándar que realizan operaciones de conjuntos se enumeran en la
sección siguiente.

Métodos

Sintaxis de las Sintaxis de las


Nombre expresiones expresiones de
del de consulta de consulta de
método Description C# Visual Basic Más información

MCT: Luis Dueñas Pag 30 de 388


Manual de LINQ

Distinct Quita los valores No es aplicable Distinct Enumerable..::.Distinct


duplicados de una Queryable..::.Distinct
colección.

Except Devuelve la No es aplicable No es aplicable Enumerable..::.Except


diferencia de Queryable..::.Except
conjuntos, es decir,
los elementos de
una colección que no
aparecen en una
segunda colección.

Intersect Devuelve la No es aplicable No es aplicable Enumerable..::.Intersect


intersección de Queryable..::.Intersect
conjuntos, es decir,
los elementos que
aparecen en cada
una de dos
colecciones.

Unión Devuelve la unión de No es aplicable No es aplicable Enumerable..::.Union


conjuntos, es decir, Queryable..::.Union
los elementos únicos
que aparecen en
cualquiera de las dos
colecciones.

Comparación de operaciones de conjuntos


Distinct
La ilustración siguiente muestra el comportamiento del método Enumerable..::.Distinct en una secuencia
de caracteres. La secuencia devuelta contiene los elementos únicos de la secuencia de entrada.

Except
La ilustración siguiente muestra el comportamiento de Enumerable..::.Except. La secuencia devuelta
contiene sólo los elementos de la primera secuencia de entrada que no están en la segunda secuencia de
entrada.

Intersect
La ilustración siguiente muestra el comportamiento de Enumerable..::.Intersect. La secuencia devuelta
contiene los elementos que son comunes a las dos secuencias de entrada.

Unión
La ilustración siguiente muestra una operación de unión de dos secuencias de caracteres. La secuencia
devuelta contiene los elementos únicos de las dos secuencias de entrada.

MCT: Luis Dueñas Pag 31 de 388


Manual de LINQ

Ejemplo de sintaxis de expresiones de consulta


En el ejemplo siguiente se utiliza la cláusula Distinct (disponible sólo en Visual Basic) en una consulta
LINQ para devolver los números únicos de una lista de enteros.
Dim classGrades As New System.Collections.Generic.List(Of Integer)(New
Integer() {63, 68, 71, 75, 68, 92, 75})
Dim distinctQuery = From grade In classGrades Select grade Distinct
Dim sb As New System.Text.StringBuilder("The distinct grades are: ")
For Each number As Integer In distinctQuery
sb.Append(number & " ")
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
The distinct grades are: 63 68 71 75 92

5.1.5. Filtrar Datos


Filtrado es la operación de restringir el conjunto de resultados de manera que sólo contenga los
elementos que satisfacen una condición especificada. También se conoce como selección.

La ilustración siguiente muestra los resultados de filtrar una secuencia de caracteres. El predicado de la
operación de filtrado especifica que el carácter debe ser 'A'.

Los métodos de operador de consulta estándar que realizan la selección se enumeran en la sección
siguiente.

Métodos

Sintaxis de las Sintaxis de las


Nombre expresiones expresiones de
del de consulta de consulta de
método Description C# Visual Basic Más información

OfType Selecciona No es aplicable No es aplicable Enumerable..::.OfType<(Of


valores en función <(TResult>)>)
de si pueden o no Queryable..::.OfType<(Of
convertirse a un <(TResult>)>)
tipo especificado.

Where Selecciona where Where Enumerable..::.Where


valores basados Queryable..::.Where
en una función de
predicado.

MCT: Luis Dueñas Pag 32 de 388


Manual de LINQ

Ejemplo de sintaxis de expresiones de consulta


En el ejemplo siguiente se utiliza la cláusula where en C# o la cláusula Where en Visual Basic para
filtrar en una matriz las cadenas que tienen una longitud concreta.
Dim words() As String = {"the", "quick", "brown", "fox", "jumps"}
Dim query = From word In words Where word.Length = 3 Select word
Dim sb As New System.Text.StringBuilder()
For Each str As String In query
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
the
fox

5.1.6. Operaciones Cuantificadoras


Las operaciones cuantificadoras devuelven un valor Boolean que indica si algunos o todos los elementos
de una secuencia satisfacen una condición.

La ilustración siguiente muestra dos operaciones cuantificadoras diferentes en dos secuencias de origen
diferentes. La primera operación pregunta si uno o varios de los elementos son el carácter 'A', y el
resultado es true. La segunda operación pregunta si todos los elementos son el carácter 'A', y el
resultado es true.

Los métodos de operador de consulta estándar que realizan operaciones cuantificadoras se enumeran en
la sección siguiente.

Métodos

Sintaxis de las
Nombre Sintaxis de las expresiones de
del expresiones de consulta de
método Description consulta de C# Visual Basic Más información

Todo Determina si todos No es aplicable Aggregate … In Enumerable..::.All<(Of


los elementos de … Into All(…) <(TSource>)>)
una secuencia Queryable..::.All<(Of
satisfacen una <(TSource>)>)
condición.

Any Determina si No es aplicable Aggregate … In Enumerable..::.Any


alguno de los … Into Any() Queryable..::.Any
elementos de una
secuencia satisface
una condición.

MCT: Luis Dueñas Pag 33 de 388


Manual de LINQ

Contains Determina si una No es aplicable No es aplicable Enumerable..::.Contains


secuencia contiene Queryable..::.Contains
un elemento
especificado.

Ejemplos de sintaxis de expresiones de consulta


En estos ejemplos se utiliza la cláusula Aggregate en Visual Basic como parte de la condición de filtrado
de una consulta LINQ.

En el ejemplo siguiente se utiliza la cláusula Aggregate y el método de extensión All<(Of


<(TSource>)>) para devolver de una colección las personas cuyos animales domésticos tienen más de
una edad especificada.
Class Person
Private _name As String
Private _pets As Pet()
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Public Property Pets() As Pet()
Get
Return _pets
End Get
Set(ByVal value As Pet())
_pets = value
End Set
End Property
End Class
Class Pet
Private _name As String
Private _age As Integer
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Public Property Age() As Integer
Get
Return _age
End Get
Set(ByVal value As Integer)
_age = value
End Set
End Property

MCT: Luis Dueñas Pag 34 de 388


Manual de LINQ

End Class
Sub All()
Dim barley As New Pet With {.Name = "Barley", .Age = 4}
Dim boots As New Pet With {.Name = "Boots", .Age = 1}
Dim whiskers As New Pet With {.Name = "Whiskers", .Age = 6}
Dim bluemoon As New Pet With {.Name = "Blue Moon", .Age = 9}
Dim daisy As New Pet With {.Name = "Daisy", .Age = 3}
Dim charlotte As New Person With {.Name = "Charlotte", .Pets = _
New Pet() {barley, boots}}
Dim arlene As New Person With {.Name = "Arlene", .Pets = _
New Pet() {whiskers}}
Dim rui As New Person With {.Name = "Rui", .Pets = _
New Pet() {bluemoon, daisy}}
' Create the list of Person objects that will be queried.
Dim people As New System.Collections.Generic.List(Of Person)
(New Person() {charlotte, arlene, rui})
Dim query = From pers In people _
Where (Aggregate pt In pers.Pets Into All(pt.Age > 2)) _
Select pers.Name
Dim sb As New System.Text.StringBuilder()
For Each name As String In query
sb.AppendLine(name)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
' Arlene
' Rui
End Sub
En el ejemplo siguiente se utiliza la cláusula Aggregate y el método de extensión Any para devolver de
una colección las personas que tienen por lo menos un animal doméstico que es mayor que una edad
especificada.
Class Person
Private _name As String
Private _pets As Pet()
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Public Property Pets() As Pet()
Get
Return _pets
End Get
Set(ByVal value As Pet())
_pets = value
End Set

MCT: Luis Dueñas Pag 35 de 388


Manual de LINQ

End Property
End Class
Class Pet
Private _name As String
Private _age As Integer
Public Property Name() As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Public Property Age() As Integer
Get
Return _age
End Get
Set(ByVal value As Integer)
_age = value
End Set
End Property
End Class
Sub Any()
Dim barley As New Pet With {.Name = "Barley", .Age = 4}
Dim boots As New Pet With {.Name = "Boots", .Age = 1}
Dim whiskers As New Pet With {.Name = "Whiskers", .Age = 6}
Dim bluemoon As New Pet With {.Name = "Blue Moon", .Age = 9}
Dim daisy As New Pet With {.Name = "Daisy", .Age = 3}
Dim charlotte As New Person With {.Name = "Charlotte", .Pets = _
New Pet() {barley, boots}}
Dim arlene As New Person With {.Name = "Arlene", .Pets = _
New Pet() {whiskers}}
Dim rui As New Person With {.Name = "Rui", .Pets = _
New Pet() {bluemoon, daisy}}
' Create the list of Person objects that will be queried.
Dim people As New System.Collections.Generic.List(Of Person)
(New Person() {charlotte, arlene, rui})
Dim query = From pers In people Where (Aggregate pt In pers.Pets Into
Any(pt.Age > 7)) Select pers.Name
Dim sb As New System.Text.StringBuilder()
For Each name As String In query
sb.AppendLine(name)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
' Rui
End Sub

MCT: Luis Dueñas Pag 36 de 388


Manual de LINQ

5.1.7. Operaciones de Proyección


El término proyección se refiere a la operación de transformar un objeto en un nueva forma que, a
menudo, consta sólo de aquellas propiedades que se utilizarán posteriormente. Utilizando la proyección,
puede construir un nuevo tipo generado a partir de cada objeto. Puede proyectar una propiedad y
realizar una función matemática sobre ella. También puede proyectar el objeto original sin cambiarlo.

Los métodos de operador de consulta estándar que realizan proyección se enumeran en la siguiente
sección.

Métodos

Sintaxis de
Sintaxis de las
las expresiones
Nombre expresiones de consulta
del de consulta de Visual
método Descripción de C# Basic Más información

Select Proyecta valores select Select Enumerable..::.Select


basados en una Queryable..::.Select
función de
transformación.

SelectMany Proyecta secuencias Utilice varias Utilice varias Enumerable..::.SelectMany


de valores basados cláusulas cláusulas Queryable..::.SelectMany
en una función de from From
transformación y, a
continuación, los
condensa en una
sola secuencia.

Ejemplos de sintaxis de expresiones de consulta


Select
El ejemplo siguiente utiliza la cláusula select en C# o la cláusula Select en Visual Basic para proyectar
la primera letra de cada cadena de una lista de cadenas.
Dim words As New List(Of String)(New String(){"an","apple","a","day"})
Dim query = From word In words Select word.Substring(0, 1)
Dim sb As New System.Text.StringBuilder()
For Each letter As String In query
sb.AppendLine(letter)
Next
' Display the output.
MsgBox(sb.ToString())
' This code produces the following output:
' a
' a
' a
' d
SelectMany
El ejemplo siguiente utiliza varias cláusulas from en C# o cláusulas From en Visual Basic para proyectar
cada palabra de cada cadena de una lista de cadenas.
Dim phrases As New List(Of String)(New String(){"an apple a day","the
quick brown fox"})
Dim query = From phrase In phrases From word In phrase.Split(" "c) _

MCT: Luis Dueñas Pag 37 de 388


Manual de LINQ

Select word
Dim sb As New System.Text.StringBuilder()
For Each str As String In query
sb.AppendLine(str)
Next
' Display the output.
MsgBox(sb.ToString())
' This code produces the following output:
' an
' apple
' a
' day
' the
' quick
' brown
' fox
Comparación entre Select y SelectMany
El trabajo de Select() y SelectMany() es generar un valor de resultado (o valores) a partir de los
valores de origen. Select() genera un valor de resultado para cada valor de origen. El resultado total es,
por tanto, una colección que tiene el mismo número de elementos que la colección de origen. En
contraste, SelectMany() genera un resultado total único que contiene subcolecciones concatenadas
procedentes de cada valor de origen. La función de transformación que se pasa como un argumento a
SelectMany() debe devolver una secuencia enumerable de valores para cada valor de origen. Estas
secuencias enumerables se concatenan entonces mediante SelectMany() para crear una sola secuencia
mayor.

Las dos ilustraciones siguientes muestran la diferencia conceptual entre las acciones de estos dos
métodos. En cada caso, suponga que la función de selector (transformación) selecciona la matriz de
flores de cada valor de origen.

Esta ilustración describe cómo Select() devuelve una colección que tiene el mismo número de
elementos que la colección de origen.

Esta ilustración describe cómo SelectMany() concatena la secuencia intermedia de matrices en un valor
de resultado final que contiene cada valor de cada matriz intermedia.

MCT: Luis Dueñas Pag 38 de 388


Manual de LINQ

Ejemplo de código
En el siguiente ejemplo se compara el comportamiento de Select() y SelectMany(). El código crea un
"ramo" de flores tomando los primeros dos elementos de cada lista de nombres de flores de la colección
de origen. En este ejemplo, el "valor único" que la función de transformación Select<(Of <(TSource,
TResult>)>)(IEnumerable<(Of <(TSource>)>), Func<(Of <(TSource, TResult>)>)) utiliza es en sí
mismo una colección de valores. Esto requiere el bucle foreach (For Each en Visual Basic) adicional
para enumerar cada cadena de cada subsecuencia.
Class Bouquet
Public Flowers As List(Of String)
End Class
Sub SelectVsSelectMany()
Dim bouquets As New List(Of Bouquet)(New Bouquet() { _
New Bouquet With {.Flowers = New List(Of String)(New String()
{"sunflower", "daisy", "daffodil", "larkspur"})}, _
New Bouquet With {.Flowers = New List(Of String)(New String()
{"tulip", "rose", "orchid"})}, _
New Bouquet With {.Flowers = New List(Of String)(New String()
{"gladiolis", "lily", "snapdragon", "aster", "protea"})}, _
New Bouquet With {.Flowers = New List(Of String)(New String()
{"larkspur", "lilac", "iris", "dahlia"})}})
Dim output As New System.Text.StringBuilder
' Select()
Dim query1 = bouquets.Select(Function(b) b.Flowers)
output.AppendLine("Using Select():")
For Each flowerList In query1
For Each str As String In flowerList
output.AppendLine(str)
Next
Next
' SelectMany()
Dim query2 = bouquets.SelectMany(Function(b) b.Flowers)
output.AppendLine(vbCrLf & "Using SelectMany():")
For Each str As String In query2
output.AppendLine(str)
Next
MsgBox(output.ToString())
End Sub

MCT: Luis Dueñas Pag 39 de 388


Manual de LINQ

5.1.8. Realizar Particiones de Datos


Partición en LINQ es la operación de dividir una secuencia de entrada en dos secciones, sin reorganizar
los elementos, y devolver después una de las secciones.

La ilustración siguiente muestra los resultados de tres operaciones de partición diferentes en una
secuencia de caracteres. La primera operación devuelve los tres primeros elementos de la secuencia. La
segunda operación omite los tres primeros elementos y devuelve los restantes. La tercera operación
omite los dos primeros elementos de la secuencia y devuelve los tres siguientes.

Los métodos de operador de consulta estándar que realizan particiones de las secuencias se enumeran
en la sección siguiente.

Operadores

Sintaxis de
las Sintaxis de las
Nombre expresiones expresiones
de de consulta de consulta de
operador Description de C# Visual Basic Más información

Saltar Omite los No es aplicable Skip Enumerable..::.Skip<(Of


elementos hasta <(TSource>)>)
una determinada Queryable..::.Skip<(Of
posición de una <(TSource>)>)
secuencia.

SkipWhile Omite los No es aplicable Skip While Enumerable..::.SkipWhile


elementos según Queryable..::.SkipWhile
una función de
predicado hasta
que un elemento
no satisface la
condición.

Take Admite los No es aplicable Take Enumerable..::.Take<(Of


elementos hasta <(TSource>)>)
una determinada Queryable..::.Take<(Of
posición de una <(TSource>)>)
secuencia.

TakeWhile Admite los No es aplicable Take While Enumerable..::.TakeWhile


elementos según Queryable..::.TakeWhile
una función de
predicado hasta
que un elemento
no satisface la
condición.

MCT: Luis Dueñas Pag 40 de 388


Manual de LINQ

Ejemplos de sintaxis de expresiones de consulta


Saltar
En el ejemplo de código siguiente se utiliza la cláusula Skip en Visual Basic para omitir las cuatro
primeras cadenas de una matriz de cadenas antes de devolver las cadenas restantes de la matriz.
Dim words() As String = New String(){"an","apple","a","day","keeps",
"the","doctor","away"}
Dim query = From word In words Skip 4
Dim sb As New System.Text.StringBuilder()
For Each str As String In query
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
' keeps
' the
' doctor
' away
SkipWhile
En el ejemplo de código siguiente se utiliza la cláusula Skip While en Visual Basic para omitir las
cadenas de una matriz mientras la primera letra de la cadena sea una "a". Se devuelven las cadenas
restantes de la matriz.
Dim words() As String = New String() {"an", "apple", "a", "day", "keeps",
"the", "doctor", "away"}
Dim query = From word In words Skip While word.Substring(0, 1) = "a"
Dim sb As New System.Text.StringBuilder()
For Each str As String In query
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
' day
' keeps
' the
' doctor
' away
Take
En el ejemplo de código siguiente se utiliza la cláusula Take en Visual Basic para devolver las dos
primeras cadenas de una matriz de cadenas.
Dim words() As String = New String() {"an", "apple", "a", "day", "keeps",
"the", "doctor", "away"}
Dim query = From word In words Take 2
Dim sb As New System.Text.StringBuilder()
For Each str As String In query
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())

MCT: Luis Dueñas Pag 41 de 388


Manual de LINQ

' This code produces the following output:


' an
' apple
TakeWhile
En el ejemplo de código siguiente se utiliza la cláusula Take While en Visual Basic para devolver las
cadenas de una matriz mientras la longitud de la cadena sea de cinco o menor de cinco.
Dim words() As String = New String() {"an", "apple", "a", "day", "keeps",
"the", "doctor", "away"}
Dim query = From word In words Take While word.Length < 6
Dim sb As New System.Text.StringBuilder()
For Each str As String In query
sb.AppendLine(str)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
' an
' apple
' a
' day
' keeps
' the

5.1.9. Operaciones de Combinación


Una combinación de dos orígenes de datos es la asociación de los objetos de un origen de datos con los
objetos que comparten un atributo común de otro origen de datos.

La combinación es una operación importante en las consultas orientadas a orígenes de datos cuyas
relaciones mutuas no se pueden seguir directamente. En la programación orientada a objetos, podría ser
una correlación no modelada entre objetos, como la dirección inversa de una relación unidireccional. Una
relación unidireccional es, por ejemplo, la que existe cuando una clase Customer tiene una propiedad de
tipo City pero la clase City no tiene ninguna propiedad que sea una colección de objetos Customer. Si
tiene una lista de objetos City y desea buscar todos los clientes de cada ciudad, podría utilizar una
operación de combinación para encontrarlos.

Los métodos de combinación proporcionados en el marco de trabajo LINQ son Join y GroupJoin. Estos
métodos realizan combinaciones de igualdad, es decir, combinaciones que buscan las coincidencias entre
dos orígenes de datos según la igualdad de sus claves. (Para la comparación, Transact-SQL admite
operadores de combinación que no sean el de igualdad, "equals"; por ejemplo, el operador "menor
que".) En términos de bases de datos relacionales, Join implementa una combinación interna, un tipo de
combinación en la que se devuelven sólo los objetos que tienen una coincidencia en el otro conjunto de
datos. El método GroupJoin no tiene ningún equivalente directo para las bases de datos relacionales,
pero implementa un supraconjunto de combinaciones internas y combinaciones externas izquierdas. Una
combinación externa izquierda es una combinación que devuelve cada uno de los elementos del primer
origen de datos (izquierda), aun cuando no tiene ningún elemento correlacionado en el otro origen de
datos.

La ilustración siguiente muestra una vista conceptual de dos conjuntos y los elementos de esos conjuntos
que están incluidos en una combinación interna o una combinación externa izquierda.

MCT: Luis Dueñas Pag 42 de 388


Manual de LINQ

Métodos

Sintaxis de
las Sintaxis de las
Nombre expresiones expresiones
del de consulta de consulta de
método Description de C# Visual Basic Más información

Join Combina dos join … in … on From x In …, y Enumerable..::.Join


secuencias según … equals … In … Where x.a Queryable..::.Join
las funciones del = b.a
selector de claves y O bien,
extrae pares de Join … [As …]In
valores. … On …

GroupJoin Combina dos join … in … on Group Join … In Enumerable..::.GroupJoin


secuencias según … equals … … On … Queryable..::.GroupJoin
las funciones del into …
selector de claves y
agrupa las
coincidencias
resultantes para
cada elemento.

5.1.10. Agrupar Datos


Agrupar es la operación de colocar los datos en grupos de manera que los elementos de cada grupo
compartan un atributo común.

La ilustración siguiente muestra los resultados de agrupar una secuencia de caracteres. La clave de cada
grupo es el carácter.

Los métodos de operador de consulta estándar que agrupan los elementos de datos se enumeran en la
sección siguiente.

Métodos

Nombre Description Sintaxis de Sintaxis de las Más información

MCT: Luis Dueñas Pag 43 de 388


Manual de LINQ

del las expresiones


método expresiones de consulta
de consulta de Visual
de C# Basic

GroupBy Agrupa los elementos group … by Group … By … Enumerable..::.GroupBy


que comparten un O bien, Into … Queryable..::.GroupBy
atributo común. Cada group … by …
grupo se representa into …
mediante un objeto
IGrouping<(Of
<(TKey,
TElement>)>).

ToLookup Inserta elementos en No es No es aplicable Enumerable..::.ToLookup


un objeto Lookup<(Of aplicable
<(TKey,
TElement>)>)
(diccionario uno a
varios) según una
función del selector
de claves.

Ejemplo de sintaxis de expresiones de consulta


En el ejemplo de código siguiente se utiliza la cláusula group by en C# o la cláusula Group By en Visual
Basic para agrupar los enteros en una lista en función de si son pares o impares.
Dim numbers As New System.Collections.Generic.List(Of Integer) _
(New Integer() {35, 44, 200, 84, 3987, 4, 199, 329, 446, 208})
Dim query = From number In numbers _
Group By Remainder = (number Mod 2) Into Group
Dim sb As New System.Text.StringBuilder()
For Each group In query
sb.AppendLine(If(group.Remainder = 0, vbCrLf & "Even numbers:",
vbCrLf & "Odd numbers:"))
For Each num In group.Group
sb.AppendLine(num)
Next
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
' Odd numbers:
' 35
' 3987
' 199
' 329
' Even numbers:
' 44
' 200
' 84
' 4
' 446
' 208

MCT: Luis Dueñas Pag 44 de 388


Manual de LINQ

5.1.11. Operaciones de Generación


Generación hace referencia al proceso de crear una nueva secuencia de valores.

Los métodos de operador de consulta estándar que realizan la generación se enumeran en la sección
siguiente.

Métodos

Sintaxis de
Sintaxis de las
las expresiones
expresiones de consulta
Nombre del de consulta de Visual
método Description de C# Basic Más información

DefaultIfEmpty Reemplaza una No es No es Enumerable..::.DefaultIfEmpty


colección vacía aplicable aplicable Queryable..::.DefaultIfEmpty
con una
colección
singleton con
valores
predeterminados.

Vacío Devuelve una No es No es Enumerable..::.Empty<(Of


colección vacía. aplicable aplicable <(TResult>)>)

Range Genera una No es No es Enumerable..::.Range


colección que aplicable aplicable
contiene una
secuencia de
números.

Repeat Genera una No es No es Enumerable..::.Repeat<(Of


colección que aplicable aplicable <(TResult>)>)
contiene un valor
repetido.

5.1.12. Operaciones de Igualdad


Dos secuencias cuyos elementos correspondientes son iguales y tienen el mismo número de elementos
se consideran iguales.

Métodos

Sintaxis de
Sintaxis de las
las expresiones
expresiones de consulta
Nombre del de consulta de Visual
método Description de C# Basic Más información

SequenceEqual Determina si No es No es Enumerable..::.SequenceEqual


dos aplicable aplicable Queryable..::.SequenceEqual
secuencias
son iguales
mediante la
comparación
de los
elementos par

MCT: Luis Dueñas Pag 45 de 388


Manual de LINQ

a par.

5.1.13. Operaciones de Elementos


Las operaciones de elementos devuelven un único elemento específico de una secuencia.

Los métodos de operador de consulta estándar que realizan operaciones de elementos se enumeran en la
sección siguiente.

Métodos

Sintaxis de
Sintaxis de las
las expresiones
expresiones de consulta
Nombre del de consulta de Visual
método Description de C# Basic Más información

ElementAt Devuelve el No es No es Enumerable..::.ElementAt


elemento aplicable aplicable <(Of <(TSource>)>)
situado en un Queryable..::.ElementAt
índice <(Of <(TSource>)>)
especificado de
una colección.

ElementAtOrDefault Devuelve el No es No es Enumerable..::.ElementAtOrDefault


elemento aplicable aplicable <(Of <(TSource>)>)
situado en un Queryable..::.ElementAtOrDefault
índice <(Of <(TSource>)>)
especificado en
una colección o
un valor
predeterminado
si el índice está
fuera del
intervalo.

Primero Devuelve el No es No es Enumerable..::.First


primer aplicable aplicable Queryable..::.First
elemento de
una colección o
el primer
elemento que
satisface una
condición.

FirstOrDefault Devuelve el No es No es Enumerable..::.FirstOrDefault


primer aplicable aplicable Queryable..::.FirstOrDefault
elemento de Queryable..::.FirstOrDefault<(Of
una colección o <(TSource>)>)(IQueryable<(Of
el primer <(TSource>)>))
elemento que
satisface una
condición.
Devuelve un
valor
predeterminado
si no existe tal
elemento.

Último Devuelve el No es No es Enumerable..::.Last

MCT: Luis Dueñas Pag 46 de 388


Manual de LINQ

último aplicable aplicable Queryable..::.Last


elemento de
una colección o
el último
elemento que
satisface una
condición.

LastOrDefault Devuelve el No es No es Enumerable..::.LastOrDefault


último aplicable aplicable Queryable..::.LastOrDefault
elemento de
una colección o
el último
elemento que
satisface una
condición.
Devuelve un
valor
predeterminado
si no existe tal
elemento.

Single Devuelve el No es No es Enumerable..::.Single


único elemento aplicable aplicable Queryable..::.Single
de una
colección o el
único elemento
que satisface
una condición.

SingleOrDefault Devuelve el No es No es Enumerable..::.SingleOrDefault


único elemento aplicable aplicable Queryable..::.SingleOrDefault
de una
colección o el
único elemento
que satisface
una condición.
Devuelve un
valor
predeterminado
si no existe tal
elemento o si la
colección no
contiene
exactamente
un elemento.

5.1.14. Convertir Tipos de Datos


Los métodos de conversión cambian el tipo de los objetos de entrada.

Las operaciones de conversión en las consultas LINQ son útiles en varios sentidos. A continuación se
ofrecen algunos ejemplos:

Se puede utilizar el método Enumerable..::.AsEnumerable<(Of <(TSource>)>) para ocultar la


implementación personalizada de un tipo de un operador de consulta estándar.

El método Enumerable..::.OfType<(Of <(TResult>)>) se puede utilizar para habilitar las


colecciones no parametrizadas para las consultas LINQ.

MCT: Luis Dueñas Pag 47 de 388


Manual de LINQ

Los métodos Enumerable..::.ToArray<(Of <(TSource>)>), Enumerable..::.ToDictionary,


Enumerable..::.ToList<(Of <(TSource>)>) y Enumerable..::.ToLookup se pueden utilizar para
forzar la ejecución inmediata de la consulta, en lugar de diferirla hasta su enumeración.

Métodos
En la tabla siguiente se enumeran los métodos de operador de consulta estándar que realizan
conversiones de tipos de datos.

Los métodos de conversión de esta tabla cuyo nombre comienza por "As" cambian el tipo estático de la
colección de origen pero no lo enumeran. Los métodos cuyos nombres empiezan por "To" enumeran la
colección de origen y colocan los elementos en el tipo de colección correspondiente.

Sintaxis de
Sintaxis de las
las expresiones
expresiones de consulta
Nombre del de consulta de Visual
método Description de C# Basic Más información

AsEnumerable Devuelve la No es No es Enumerable..::.AsEnumerable


entrada tipificada aplicable aplicable <(Of <(TSource>)>)
como
IEnumerable<(Of
<(T>)>).

AsQueryable Convierte una No es No es Queryable..::.AsQueryable


interfaz aplicable aplicable
IEnumerable
(genérica) en una
interfaz
IQueryable
(genérica).

Conversión de Convierte los Utilice una From … As … Enumerable..::.Cast<(Of


tipos explícita elementos de una variable de <(TResult>)>)
colección a un rango con Queryable..::.Cast<(Of
tipo especificado. tipo <(TResult>)>)
explícito.
Por ejemplo:
from string
str in words

OfType Filtra valores en No es No es Enumerable..::.OfType<(Of


función de si aplicable aplicable <(TResult>)>)
pueden o no Queryable..::.OfType<(Of
convertirse a un <(TResult>)>)
tipo especificado.

ToArray Convierte una No es No es Enumerable..::.ToArray<(Of


colección en una aplicable aplicable <(TSource>)>)
matriz. Este
método fuerza la
ejecución de la
consulta.

ToDictionary Coloca los No es No es Enumerable..::.ToDictionary


elementos en aplicable aplicable
Dictionary<(Of
<(TKey,
TValue>)>)
según una

MCT: Luis Dueñas Pag 48 de 388


Manual de LINQ

función del
selector de
claves. Este
método fuerza la
ejecución de la
consulta.

ToList Convierte una No es No es Enumerable..::.ToList<(Of


colección a aplicable aplicable <(TSource>)>)
List<(Of
<(T>)>). Este
método fuerza la
ejecución de la
consulta.

ToLookup Coloca los No es No es Enumerable..::.ToLookup


elementos en un aplicable aplicable
objeto
Lookup<(Of
<(TKey,
TElement>)>)
(diccionario uno a
varios) según una
función del
selector de
claves. Este
método fuerza la
ejecución de la
consulta.

Ejemplo de sintaxis de expresiones de consulta


En el ejemplo de código siguiente se utiliza una variable de rango con tipo explícito en C# o la cláusula
From As en Visual Basic para convertir un tipo a un subtipo antes de tener acceso a un miembro que
sólo está disponible en el subtipo.
Class Plant
Public Name As String
End Class
Class CarnivorousPlant
Inherits Plant
Public TrapType As String
End Class
Sub Cast()
Dim plants() As Plant = {New CarnivorousPlant With {.Name = "Venus
Fly Trap", .TrapType = "Snap Trap"}, New CarnivorousPlant With {.Name =
"Pitcher Plant", .TrapType = "Pitfall Trap"}, New CarnivorousPlant With
{.Name = "Sundew", .TrapType = "Flypaper Trap"}, New CarnivorousPlant
With {.Name = "Waterwheel Plant", .TrapType = "Snap Trap"}}
Dim query = From plant As CarnivorousPlant In plants _
Where plant.TrapType = "Snap Trap" Select plant
Dim sb As New System.Text.StringBuilder()
For Each plant In query
sb.AppendLine(plant.Name)
Next
' Display the results.
MsgBox(sb.ToString())
' This code produces the following output:
' Venus Fly Trap

MCT: Luis Dueñas Pag 49 de 388


Manual de LINQ

' Waterwheel Plant


End Sub

5.1.15. Operaciones de Concatenación


Concatenación hace referencia a la operación de anexar una secuencia a otra.

La ilustración siguiente muestra una operación de concatenación de dos secuencias de caracteres.

Los métodos de operador de consulta estándar que realizan la concatenación se enumeran en la sección
siguiente.

Métodos

Sintaxis de las
Nombre Sintaxis de las expresiones de
del expresiones de consulta de
método Description consulta de C# Visual Basic Más información

Concat Concatena dos No es aplicable No es aplicable Enumerable..::.Concat<(Of


secuencias para <(TSource>)>)
formar una Queryable..::.Concat<(Of
secuencia. <(TSource>)>)

5.1.16. Operaciones de Agregación


Una operación de agregación calcula un valor único a partir de una colección de valores. Un ejemplo de
operación de agregación sería calcular la temperatura diaria promedio a partir de los valores de
temperatura diarios de un mes completo.

La ilustración siguiente muestra los resultados de dos operaciones de agregación diferentes con una
secuencia de números. La primera operación suma los números. La segunda operación devuelve el valor
máximo de la secuencia.

Los métodos de operador de consulta estándar que realizan operaciones de agregación se enumeran en
la sección siguiente.

Métodos

Nombre Sintaxis de Sintaxis de


del las las
método Description expresiones expresiones Más información

MCT: Luis Dueñas Pag 50 de 388


Manual de LINQ

de consulta de consulta
de C# de Visual
Basic

Agregado Realiza una No es No es aplicable Enumerable..::.Aggregate


operación de aplicable Queryable..::.Aggregate
agregación
personalizada con
los valores de una
colección.

Average Calcula el valor No es Aggregate … Enumerable..::.Average


promedio de una aplicable In … Into Queryable..::.Average
colección de valores. Average()

Count Cuenta los No es Aggregate … Enumerable..::.Count


elementos de una aplicable In … Into Queryable..::.Count
colección y, Count()
opcionalmente, sólo
aquéllos que
satisfacen una
función de
predicado.

LongCount Cuenta los No es Aggregate … Enumerable..::.LongCount


elementos de una aplicable In … Into Queryable..::.LongCount
colección grande y, LongCount()
opcionalmente, sólo
aquéllos que
satisfacen una
función de
predicado.

Max Determina el valor No es Aggregate … Enumerable..::.Max


máximo de una aplicable In … Into Queryable..::.Max
colección. Max()

Min Determina el valor No es Aggregate … Enumerable..::.Min


mínimo de una aplicable In … Into Min() Queryable..::.Min
colección.

Sum Calcula la suma de No es Aggregate … Enumerable..::.Sum


los valores de una aplicable In … Into Queryable..::.Sum
colección. Sum()

Ejemplos de sintaxis de expresiones de consulta


Average
En el ejemplo de código siguiente se utiliza la cláusula Aggregate Into Average en Visual Basic para
calcular la temperatura promedio en una matriz de números que representan temperaturas.
Dim temperatures() As Double = {72.0, 81.5, 69.3, 88.6, 80.0, 68.5}
Dim avg = Aggregate temp In temperatures Into Average()
' Display the result.
MsgBox(avg)
' This code produces the following output:
' 76.65
Count
En el ejemplo de código siguiente se utiliza la cláusula Aggregate Into Count en Visual Basic para
contar el número de valores de una matriz que son mayores o iguales que 80.
Dim temperatures() As Double = {72.0, 81.5, 69.3, 88.6, 80.0, 68.5}

MCT: Luis Dueñas Pag 51 de 388


Manual de LINQ

Dim highTemps As Integer = Aggregate temp In temperatures Into Count(temp


>= 80)
' Display the result.
MsgBox(highTemps)
' This code produces the following output:
' 3
LongCount
En el ejemplo de código siguiente se utiliza la cláusula Aggregate Into LongCount en Visual Basic para
contar el número de valores de una matriz.
Dim temperatures() As Double = {72.0, 81.5, 69.3, 88.6, 80.0, 68.5}
Dim numTemps As Long = Aggregate temp In temperatures Into LongCount()
' Display the result.
MsgBox(numTemps)
' This code produces the following output:
' 6
Max
En el ejemplo de código siguiente se utiliza la cláusula Aggregate Into Max en Visual Basic para
calcular la temperatura máxima en una matriz de números que representan temperaturas.
Dim temperatures() As Double = {72.0, 81.5, 69.3, 88.6, 80.0, 68.5}
Dim maxTemp = Aggregate temp In temperatures Into Max()
' Display the result.
MsgBox(maxTemp)
' This code produces the following output:
' 88.6
Min
En el ejemplo de código siguiente se utiliza la cláusula Aggregate Into Min en Visual Basic para calcular
la temperatura mínima en una matriz de números que representan temperaturas.
Dim temperatures() As Double = {72.0, 81.5, 69.3, 88.6, 80.0, 68.5}
Dim minTemp = Aggregate temp In temperatures Into Min()
' Display the result.
MsgBox(minTemp)
' This code produces the following output:
' 68.5
Sum
En el ejemplo de código siguiente se utiliza la cláusula Aggregate Into Sum en Visual Basic para
calcular el importe total de gastos en una matriz de valores que representan gastos.
Dim expenses() As Double = {560.0, 300.0, 1080.5, 29.95, 64.75, 200.0}
Dim totalExpense = Aggregate expense In expenses Into Sum()
' Display the result.
MsgBox(totalExpense)
' This code produces the following output:
' 2235.2

5.2. Arboles de Expresión en LINQ


En LINQ, los árboles de expresión se utilizan para representar consultas estructuradas para orígenes de
datos que implementan IQueryable<(Of <(T>)>). Por ejemplo, el proveedor LINQ a SQL implementa la
interfaz IQueryable<(Of <(T>)>) para realizar consultas en almacenes de datos relacionales. Los
compiladores de Visual Basic y C# compilan las consultas destinadas a esos orígenes de datos en código

MCT: Luis Dueñas Pag 52 de 388


Manual de LINQ

que genera un árbol de expresión en tiempo de ejecución. El proveedor de la consulta puede entonces
recorrer la estructura de datos del árbol de expresión y traducirla en un lenguaje de consulta apropiado
para el origen de datos.

Los árboles de expresión también se utilizan en LINQ para representar expresiones lambda asignadas a
variables de tipo Expression<(Of <(TDelegate>)>).

Los árboles de expresión también se pueden utilizar para crear consultas dinámicas de LINQ. También
podría utilizar árboles de expresión para crear un proveedor de LINQ.

5.2.1. Cómo: Usar Arboles de Expresión para Crear


Consultas Dinámicas
En este tema se describe cómo utilizar árboles de expresión para crear consultas dinámicas de LINQ. Las
consultas dinámicas son útiles cuando no se conocen los detalles de la consulta en tiempo de
compilación. Por ejemplo, una aplicación podría proporcionar una interfaz de usuario que permite al
usuario final especificar uno o más predicados para filtrar los datos. Para poder usar LINQ en las
consultas, este tipo de aplicación debe utilizar árboles de expresión para crear la consulta de LINQ en
tiempo de ejecución.

Ejemplo
El ejemplo siguiente muestra cómo utilizar árboles de expresión para construir una consulta contra un
origen de datos IQueryable y, a continuación, ejecutar dicha consulta. El código genera un árbol de
expresión para representar la consulta siguiente:

Consulta en C#

companies.Where(company => (company.ToLower() == "coho winery" || company.Length >


16)).OrderBy(company => company)

Consulta en Visual Basic

companies.Where(Function(company) company.ToLower() = "coho winery" OrElse company.Length >


16).OrderBy(Function(company) company)

Los métodos de generador incluidos en el espacio de nombres System.Linq.Expressions se utilizan para


crear árboles de expresión que representan las expresiones que forman la consulta completa. Las
expresiones que representan llamadas a métodos de operador de consulta estándar hacen referencia a
las implementaciones de Queryable de estos métodos. El árbol de expresión final se pasa a la
implementación CreateQuery<(Of <(TElement>)>)(Expression) del proveedor del origen de datos
IQueryable para crear una consulta ejecutable de tipo IQueryable. Los resultados se obtienen
enumerando esa variable de consulta.
Dim companies() As String = {"Consolidated Messenger", "Alpine Ski
House", "Southridge Video", "City Power & Light", "Coho Winery", "Wide
World Importers", "Graphic Design Institute", "Adventure Works",
"Humongous Insurance", "Woodgrove Bank", "Margie's Travel", "Northwind
Traders", "Blue Yonder Airlines", "Trey Research", "The Phone Company",
"Wingtip Toys", "Lucerne Publishing", "Fourth Coffee"}
' The IQueryable data to query.
Dim queryableData As IQueryable(Of String) = companies.AsQueryable()

MCT: Luis Dueñas Pag 53 de 388


Manual de LINQ

' Compose the expression tree that represents the parameter to the
predicate.
Dim pe As ParameterExpression = Expression.Parameter(GetType(String),
"company")
' ***** Where(Function(company) company.ToLower() = "coho winery" OrElse
company.Length > 16) *****
' Create an expression tree that represents the expression:
company.ToLower() = "coho winery".
Dim left As Expression = Expression.Call(pe,
GetType(String).GetMethod("ToLower", System.Type.EmptyTypes))
Dim right As Expression = Expression.Constant("coho winery")
Dim e1 As Expression = Expression.Equal(left, right)
' Create an expression tree that represents the expression:
company.Length > 16.
left = Expression.Property(pe, GetType(String).GetProperty("Length"))
right = Expression.Constant(16, GetType(Integer))
Dim e2 As Expression = Expression.GreaterThan(left, right)
' Combine the expressions to create an expression tree that represents
the
' expression: company.ToLower() = "coho winery" OrElse company.Length >
16).
Dim predicateBody As Expression = Expression.OrElse(e1, e2)
' Create an expression tree that represents the expression:
' queryableData.Where(Function(company) company.ToLower() = "coho winery"
OrElse company.Length > 16)
Dim whereCallExpression As MethodCallExpression = Expression.Call( _
GetType(Queryable), "Where", _
New Type(){queryableData.ElementType},queryableData.Expression, _
Expression.Lambda(Of Func(Of String, Boolean))(predicateBody,
New ParameterExpression() {pe}))
' ***** End Where *****
' ***** OrderBy(Function(company) company) *****
' Create an expression tree that represents the expression:
' whereCallExpression.OrderBy(Function(company) company)
Dim orderByCallExpression As MethodCallExpression = Expression.Call( _
GetType(Queryable), "OrderBy", _
New Type() {queryableData.ElementType,
queryableData.ElementType}, whereCallExpression, _
Expression.Lambda(Of Func(Of String, String))(pe, New
ParameterExpression() {pe}))
' ***** End OrderBy *****
' Create an executable query from the expression tree.
Dim results As IQueryable(Of String) =
queryableData.Provider.CreateQuery(Of String)(orderByCallExpression)
' Enumerate the results.
For Each company As String In results
Console.WriteLine(company)
Next
' This code produces the following output:
' Blue Yonder Airlines

MCT: Luis Dueñas Pag 54 de 388


Manual de LINQ

' City Power & Light


' Coho Winery
' Consolidated Messenger
' Graphic Design Institute
' Humongous Insurance
' Lucerne Publishing
' Northwind Traders
' The Phone Company
' Wide World Importers
Este código utiliza un número fijo de expresiones en el predicado que se pasa al método
Queryable.Where. Sin embargo, puede escribir una aplicación que combina un número variable de
expresiones de predicado que depende de la información proporcionada por el usuario. También puede
variar los operadores de consulta estándar a los que se llaman en la consulta, según la información
proporcionada por el usuario.

5.3. Habilitar un Origen de Datos para Realizar Consultas


Existen varias maneras de extender LINQ para permitir consultar cualquier origen de datos en el modelo
de LINQ. El origen de datos podría ser una estructura de datos, un servicio web, un sistema de archivos
o una base de datos, por nombrar algunos. El modelo de LINQ facilita a los clientes las consultas a un
origen de datos para el que las consultas de LINQ están habilitadas, ya que la sintaxis y el modelo de la
consulta no cambian. Las maneras en las que LINQ se puede extender a estos orígenes de datos son las
siguientes:

Implementar la interfaz IEnumerable<(Of <(T>)>) en un tipo para habilitar las consultas de


LINQ a Objects para ese tipo.

Crear métodos de operador de consulta estándar como Where y Select que extienden un tipo
para habilitar las consultas personalizadas de LINQ para ese tipo.

Crear un proveedor para el origen de datos que implementa la interfaz IQueryable<(Of


<(T>)>). Un proveedor que implementa esta interfaz recibe consultas de LINQ en forma de
árboles de expresión, que puede ejecutar de una manera personalizada, por ejemplo,
remotamente.

Crear un proveedor para el origen de datos que aproveche una tecnología de LINQ existente.
Este tipo de proveedor permitiría no sólo las consultas, sino también las operaciones de
inserción, actualización y eliminación, así como la asignación para tipos definidos por el usuario.

En este tema se analizan estas opciones.

Cómo habilitar las consultas LINQ de un origen de datos


Datos en memoria
Hay dos maneras de habilitar las consultas de LINQ para datos en memoria. Si los datos son de un tipo
que implementa IEnumerable<(Of <(T>)>), puede consultar los datos utilizando LINQ a Objects. Si no
tiene sentido habilitar la enumeración de su tipo implementando la interfaz IEnumerable<(Of <(T>)>),
puede definir métodos de operador de consulta estándar de LINQ en ese tipo o crear métodos de
operador de consulta estándar de LINQ que extiendan el tipo. Las implementaciones personalizadas de
los operadores de consulta estándar deberían utilizar ejecución diferida para devolver los resultados.

MCT: Luis Dueñas Pag 55 de 388


Manual de LINQ

Datos remotos
La mejor opción para permitir las consultas de LINQ para un origen de datos remoto consiste en
implementar la interfaz IQueryable<(Of <(T>)>). Sin embargo, esto es diferente de extender un
proveedor como LINQ a SQL para un origen de datos. En Visual Studio 2008, no existen modelos de
proveedor para extender tecnologías de LINQ existentes, como LINQ a SQL, a otros tipos de origen de
datos.

Proveedores LINQ de IQueryable


Los proveedores de LINQ que implementan IQueryable<(Of <(T>)>) pueden variar ampliamente en su
complejidad. En esta sección se analizan los diferentes niveles de complejidad.

Un proveedor de IQueryable menos complejo podría comunicar con un solo método de un servicio web,
como es el caso del proveedor que se crea en el tema Tutorial: Crear un proveedor LINQ IQueryable.
Este tipo de proveedor es muy específico, ya que espera información específica en las consultas que
administra. Posee un sistema de tipos cerrado, y expone quizá un único tipo de resultado. La mayor
parte de la ejecución de la consulta ocurre localmente, por ejemplo, utilizando las implementaciones
Enumerable de los operadores de consulta estándar. Un proveedor menos complejo podría examinar sólo
una expresión de llamada a método en el árbol de expresión que representa la consulta, y dejaría que la
lógica restante de la consulta se procesara en otra parte.

Un proveedor IQueryable de complejidad media podría destinarse a un origen de datos que tuviera un
lenguaje de consulta parcialmente expresivo. Si se destina a un servicio web, podría comunicarse con
más de un método del servicio web y seleccionar el método que va a llamar en función de la pregunta
que plantea la consulta. Un proveedor de complejidad media tendría un sistema de tipos más amplio que
un proveedor simple, pero todavía sería un sistema de tipos fijo. Por ejemplo, el proveedor podría
exponer tipos que tienen relaciones uno a varios y que se pueden recorrer, pero no proporcionaría
tecnología de asignación para tipos definidos por el usuario.

Un proveedor IQueryable complejo, tal como el proveedor LINQ a SQL, podría traducir consultas de
LINQ completas a un lenguaje de consulta más expresivo, como SQL. Un proveedor complejo es más
general que un proveedor menos complejo, ya que puede manejar una variedad más amplia de
preguntas en la consulta. También posee un sistema de tipos abierto y, por tanto, debe contener una
amplia infraestructura para utilizar tipos definidos por el usuario. El desarrollo de un proveedor complejo
requiere un esfuerzo significativo.

6. LINQ a Objectos
El término "LINQ a Objectos" hace referencia al uso de consultas LINQ con cualquier colección
IEnumerable o IEnumerable<(Of <(T>)>) directamente, sin utilizar ninguna API o proveedor LINQ
intermedio, como LINQ a SQL o LINQ a XML. Puede utilizar LINQ para consultar cualquier colección
enumerable, como List<(Of <(T>)>), Array o Dictionary<(Of <(TKey, TValue>)>). La colección puede
estar definida por el usuario o ser devuelta por una API de .NET Framework.

Básicamente, LINQ a Objects representa una nueva forma de ver las colecciones. De la manera
convencional, es necesario escribir bucles foreach complejos que especifican cómo recuperar los datos
de una colección. Según LINQ, se escribe código declarativo que describe lo que se desea recuperar.

Además, las consultas LINQ ofrecen tres ventajas principales respecto a los bucles foreach tradicionales:

1. Son más concisas y legibles, sobre todo al filtrar varias condiciones.

MCT: Luis Dueñas Pag 56 de 388


Manual de LINQ

2. Proporcionan funcionalidad eficaz de filtrado, ordenación y agrupación con código de aplicación


mínimo.

3. Se pueden trasladar a otros orígenes de datos con pocas o ningunas modificaciones.

En general, cuanto más compleja sea la operación que se deba realizar con los datos, observará un
número mayor de ventajas al utilizar LINQ en lugar de las técnicas de iteración convencionales.

El propósito de esta sección es mostrar el planteamiento de LINQ con unos cuantos ejemplos específicos.
No pretende ser exhaustiva.

6.1. LINQ y Cadenas


LINQ se puede usar para consultar y transformar cadenas y colecciones de cadenas. Puede ser
especialmente útil con datos semiestructurados en archivos de texto. Las consultas LINQ se pueden
combinar con las funciones de cadena y expresiones regulares tradicionales. Por ejemplo, puede utilizar
el método Split o Split para crear una matriz de cadenas que después puede consultar o modificar
mediante LINQ. Puede utilizar el método IsMatch en la cláusula where de una consulta LINQ. Asimismo,
puede utilizar LINQ para consultar o modificar los resultados MatchCollection devueltos por una
expresión regular.

También puede utilizar las técnicas descritas en esta sección para transformar datos de texto
semiestructurados en XML.

Los ejemplos de esta sección se clasifican en dos categorías:

Consultar un bloque de texto


Puede consultar, analizar y modificar bloques de texto dividiéndolos en una matriz de cadenas menores
que se puede consultar mediante el uso del método Split. Puede dividir el texto de origen en palabras,
frases, párrafos, páginas o según otro criterio y, a continuación, realizar las divisiones adicionales que
requiera la consulta.

Cómo: Realizar un recuento de las repeticiones de una palabra en una cadena


Muestra cómo utilizar LINQ para realizar consultas de texto simples.

Cómo: Buscar frases que contengan un conjunto especificado de palabras


Muestra cómo dividir archivos de texto según límites arbitrarios y cómo realizar consultas en
cada una de las partes.

Cómo: Buscar caracteres en una cadena


Muestra que una cadena es un tipo que se puede consultar.

Cómo: Combinar consultas LINQ con expresiones regulares


Muestra cómo utilizar expresiones regulares en consultas LINQ para realizar operaciones de
coincidencia de modelos complejas en resultados de consulta filtrados.

Consultar datos semiestructurados en formato de texto


Muchos tipos de archivos de texto se componen de una serie de líneas, a menudo de formato similar,
como los archivos delimitados por tabuladores o por comas o líneas de longitud fija. Después de leer este
tipo de archivo de texto en la memoria, puede utilizar LINQ para consultar o modificar las líneas. Las
consultas de LINQ también simplifican la tarea de combinar los datos procedentes de varios orígenes.

MCT: Luis Dueñas Pag 57 de 388


Manual de LINQ

Cómo: Buscar la diferencia de conjuntos entre dos listas


Muestra cómo encontrar todas las cadenas que se encuentran en una lista pero no en otra.

Cómo: Ordenar o filtrar datos de texto por palabra o campo


Muestra cómo ordenar las líneas de texto por cualquier palabra o campo.

Cómo: Reordenar los campos de un archivo delimitado


Muestra cómo reordenar los campos de una línea en un archivo .csv.

Cómo: Combinar y comparar colecciones de cadenas


Muestra cómo combinar listas de cadenas de varias maneras.

Cómo: Rellenar colecciones de objetos de varios orígenes


Muestra cómo crear colecciones de objetos utilizando varios archivos de texto como orígenes de
datos.

Cómo: Combinar contenido de archivos no similares


Muestra cómo combinar cadenas de dos listas en una cadena única utilizando una clave
coincidente.

Cómo: Dividir un archivo en varios archivos mediante el uso de grupos (LINQ)


Muestra cómo crear nuevos archivos utilizando un solo archivo como origen de datos.

Cómo: Calcular valores de columna en un archivo de texto CSV (LINQ)


Muestra cómo realizar cálculos matemáticos en datos de texto de archivos .csv.

6.1.1. Cómo: Realizar un Recuento de las Repeticiones de


una Palabra en una Cadena
En este ejemplo se muestra cómo utilizar una consulta LINQ para contar el número de veces que aparece
una palabra especificada en una cadena. Observe que, para realizar el recuento, primero se llama al
método Split para crear una matriz de palabras. La ejecución del método Split afecta al rendimiento. Si la
única operación que se va a realizar con la cadena es contar las palabras, debería considerar el uso del
método Matches o IndexOf en su lugar. Sin embargo, si el rendimiento no es un problema crítico o si ya
ha dividido la frase para realizar otros tipos de consultas con ella, sería sensato utilizar LINQ para contar
las palabras o las frases también.

Ejemplo
Class CountWords
Shared Sub Main()
Dim text As String = "Historically, the world of data and the
world of objects" & _
" have not been well integrated. Programmers work in C#
or Visual Basic" & _
" and also in SQL or XQuery. On the one side are
concepts such as classes," & _
" objects, fields, inheritance, and .NET Framework
APIs. On the other side" & _
" are tables, columns, rows, nodes, and separate
languages for dealing with" & _

MCT: Luis Dueñas Pag 58 de 388


Manual de LINQ

" them. Data types often require translation between


the two worlds; there are" & _
" different standard functions. Because the object
world has no notion of query, a" & _
" query can only be represented as a string without
compile-time type checking or" & _
" IntelliSense support in the IDE. Transferring data
from SQL tables or XML trees to" & _
" objects in memory is often tedious and error-prone."
Dim searchTerm As String = "data"
' Convert the string into an array of words.
Dim dataSource As String() = text.Split(New Char() {" ", ",",
".", ";", ":"}, StringSplitOptions.RemoveEmptyEntries)
' Create and execute the query. It executes immediately
' because a singleton value is produced.
' Use ToLower to match "data" and "Data"
Dim matchQuery = From word In dataSource Where
word.ToLowerInvariant() = searchTerm.ToLowerInvariant() Select word
' Count the matches.
Dim count As Integer = matchQuery.Count()
Console.WriteLine(count & " occurrence(s) of the search term """ & _
searchTerm & """ were found.")
' Keep console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
End Class
' Output:
' 3 occurrence(s) of the search term "data" were found.

6.1.2. Cómo: Buscar Frases que Contengan un Conjunto


Especificado de Palabras
En este ejemplo se muestra cómo buscar las frases de un archivo de texto que contienen coincidencias
para cada palabra de un conjunto de palabras especificado. Aunque la matriz de términos de búsqueda
está incluida en el código en este ejemplo, también se puede llenar dinámicamente en tiempo de
ejecución. En este ejemplo, la consulta devuelve las frases que contienen las palabras "Historically",
"data" e "integrated".

Ejemplo
Class FindSentences
Shared Sub Main()
Dim text As String = "Historically, the world of data and the
world of objects " & _
"have not been well integrated. Programmers work in C# or Visual
Basic " & _
"and also in SQL or XQuery. On the one side are concepts such as
classes, " & _

MCT: Luis Dueñas Pag 59 de 388


Manual de LINQ

"objects, fields, inheritance, and .NET Framework APIs. On the


other side " & _
"are tables, columns, rows, nodes, and separate languages for
dealing with " & _
"them. Data types often require translation between the two
worlds; there are " & _
"different standard functions. Because the object world has no
notion of query, a " & _
"query can only be represented as a string without compile-time
type checking or " & _
"IntelliSense support in the IDE. Transferring data from SQL
tables or XML trees to " & _
"objects in memory is often tedious and error-prone."
' Split the text block into an array of sentences.
Dim sentences As String() = text.Split(New Char() {".", "?", "!"})
' Define the search terms. This list could also be dynamically
populated at runtime
Dim wordsToMatch As String() = {"Historically", "data",
"integrated"}
' Find sentences that contain all the terms in the wordsToMatch array
' Note that the number of terms to match is not specified at compile time
Dim sentenceQuery = From sentence In sentences _
Let w = sentence.Split(New Char() {" ", ",", ".", ";", ":"}, _
StringSplitOptions.RemoveEmptyEntries) Where _
w.Distinct().Intersect(wordsToMatch).Count = wordsToMatch.Count() _
Select sentence
' Execute the query
For Each str As String In sentenceQuery
Console.WriteLine(str)
Next
' Keep console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
End Class
' Output:
' Historically, the world of data and the world of objects have not been
well integrated
En primer lugar, la consulta divide el texto en frases y después divide las frases en una matriz de
cadenas que contienen cada palabra. Para cada una de estas matrices, el método Distinct quita todas las
palabras repetidas y, a continuación, la consulta realiza una operación Intersect en la matriz de palabras
y la matriz wordstoMatch. Si el recuento de la intersección es igual al recuento de la matriz
wordsToMatch, se encontraron todas las palabras y se devuelve la frase original.

En la llamada a Split, los signos de puntuación se utilizan como separadores para quitarlos de la cadena.
De lo contrario, podría obtener una cadena "Historically," que no coincidiría con "Historically" en la matriz
wordsToMatch. Puede que sea necesario utilizar separadores adicionales, dependiendo de los tipos de
puntuación que contenga el texto de origen.

MCT: Luis Dueñas Pag 60 de 388


Manual de LINQ

6.1.3. Cómo: Buscar Caracteres en una Cadena


Dado que la clase String implementa la interfaz genérica IEnumerable<(Of <(T>)>), cualquier cadena se
puede consultar como una secuencia de caracteres. Sin embargo, éste no es ningún uso común de LINQ.
Para operaciones de coincidencia de modelos complejas, utilice la clase Regex.

Ejemplo
En el ejemplo siguiente se consulta una cadena para determinar el número de dígitos numéricos que
contiene. Observe que la consulta se "reutiliza" después de que se ejecuta la primera vez. Esto es posible
porque la propia consulta no almacena ningún resultado real.
Class QueryAString
Shared Sub Main()
' A string is an IEnumerable data source.
Dim aString As String = "ABCDE99F-J74-12-89A"
' Select only those characters that are numbers
Dim stringQuery = From ch In aString Where Char.IsDigit(ch) _
Select ch
' Execute the query
For Each c As Char In stringQuery
Console.Write(c & " ")
Next
' Call the Count method on the existing query.
Dim count As Integer = stringQuery.Count()
Console.WriteLine(System.Environment.NewLine & "Count = " & count)
' Select all characters before the first '-'
Dim stringQuery2 = aString.TakeWhile(Function(c) c <> "-")
' Execute the second query
For Each ch In stringQuery2
Console.Write(ch)
Next
Console.WriteLine(System.Environment.NewLine & "Press any key to exit")
Console.ReadKey()
End Sub
End Class
' Output:
' 9 9 7 4 1 2 8 9
' Count = 8
' ABCDE99F

6.1.4. Cómo: Combinar Consultas LINQ con Expresiones


Regulares
En este ejemplo se muestra cómo utilizar la clase Regex para crear una expresión regular para
coincidencias más complejas en cadenas de texto. La consulta LINQ hace que sea sencillo aplicar un filtro
exactamente a los archivos en los que se desea buscar con la expresión regular y dar forma los
resultados.

Ejemplo
Class LinqRegExVB

MCT: Luis Dueñas Pag 61 de 388


Manual de LINQ

Shared Sub Main()


' Root folder to query, along with all subfolders.
' Modify this path as necessary.
Dim startFolder As String = "C:\program files\Microsoft Visual
Studio 9.0\"
' Take a snapshot of the file system.
Dim fileList As IEnumerable(Of System.IO.FileInfo) =
GetFiles(startFolder)
' Create a regular expression to find all things "Visual".
Dim searchTerm As System.Text.RegularExpressions.Regex = _
New System.Text.RegularExpressions.Regex("Visual
(Basic|C#|C\+\+|J#|SourceSafe|Studio)")
' Search the contents of each .htm file.
' Remove the where clause to find even more matches!
' This query produces a list of files where a match
' was found, and a list of the matches in that file.
' Note: Explicit typing of "Match" in select clause.
' This is required because MatchCollection is not a
' generic IEnumerable collection.
Dim queryMatchingFiles = From afile In fileList Where
afile.Extension = ".htm" Let fileText =
System.IO.File.ReadAllText(afile.FullName) Let matches =
searchTerm.Matches(fileText) Where
(searchTerm.Matches(fileText).Count > 0) Select Name =
afile.FullName, Matches = From match As
System.Text.RegularExpressions.Match In matches Select
match.Value
' Execute the query.
Console.WriteLine("The term " & searchTerm.ToString() & " was found in:")
For Each fileMatches In queryMatchingFiles
' Trim the path a bit, then write
' the file name in which a match was found.
Dim s = fileMatches.Name.Substring(startFolder.Length - 1)
Console.WriteLine(s)
' For this file, write out all the matching strings
For Each match In fileMatches.Matches
Console.WriteLine(" " + match)
Next
Next
' Keep the console window open in debug mode
Console.WriteLine("Press any key to exit")
Console.ReadKey()
End Sub
' Function to retrieve a list of files. Note that this is a copy
' of the file information.
Shared Function GetFiles(ByVal root As String) As IEnumerable(Of
System.IO.FileInfo)
Return From file In My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
Select New System.IO.FileInfo(file)

MCT: Luis Dueñas Pag 62 de 388


Manual de LINQ

End Function
End Class
Observe que también se puede consultar el objeto MatchCollection que se devuelve tras una búsqueda
RegEx. En este ejemplo, sólo se genera el valor de cada coincidencia en los resultados. Sin embargo,
también es posible utilizar LINQ para realizar toda clase de operaciones de filtrado, ordenación y
agrupación en esa colección. Dado que MatchCollection es una colección IEnumerable no genérica, es
necesario declarar explícitamente el tipo de la variable de rango en la consulta.

6.1.5. Cómo: Buscar la Diferencia de Conjuntos entre Dos


Listas
En este ejemplo se muestra cómo utilizar LINQ para comparar dos listas de cadenas y generar las líneas
que estén en names1.txt pero no en names2.txt.

Para crear los archivos de datos

Copie names1.txt y names2.txt en la carpeta de la solución.


Ejemplo
Class CompareLists
Shared Sub Main()
' Create the IEnumerable data sources.
Dim names1 As String() =
System.IO.File.ReadAllLines("../../../names1.txt")
Dim names2 As String() =
System.IO.File.ReadAllLines("../../../names2.txt")
' Create the query. Note that method syntax must be used here.
Dim differenceQuery = names1.Except(names2)
Console.WriteLine("The following lines are in names1.txt but not
names2.txt")
' Execute the query.
For Each name As String In differenceQuery
Console.WriteLine(name)
Next
' Keep console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
End Class
Algunos tipos de operaciones de consulta en C# y Visual Basic, como Except, Distinct, Union y
Concat<(Of <(TSource>)>), sólo se pueden expresar con sintaxis basada en método.

6.1.6. Cómo: Ordenar o Filtrar Datos de Texto por Palabra o


Campo
En el ejemplo siguiente se muestra cómo ordenar líneas de texto estructurado, como valores separados
por comas, según un campo cualquiera de la línea. El campo se puede especificar dinámicamente en
tiempo de ejecución. Supongamos que los campos de scores.csv representan el número de identificador
de un estudiante, seguido de una serie de cuatro puntuaciones de exámenes.

MCT: Luis Dueñas Pag 63 de 388


Manual de LINQ

Para crear un archivo que contenga datos

Copie los datos de scores.csv del tema Cómo: Combinar contenido de archivos no similares
(LINQ) y guárdelos en la carpeta de la solución.

Ejemplo
Class SortLines
Shared Sub Main()
Dim scores As String() =
System.IO.File.ReadAllLines("../../../scores.csv")
' Change this to any value from 0 to 4
Dim sortField As Integer = 1
Console.WriteLine("Sorted highest to lowest by field " & sortField)
' Demonstrates how to return query from a method.
' The query is executed here.
For Each str As String In SortQuery(scores, sortField)
Console.WriteLine(str)
Next
' Keep console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Shared Function SortQuery(ByVal source As IEnumerable(Of String),
ByVal num As Integer) As IEnumerable(Of String)
Dim scoreQuery = From line In source Let fields = line.Split(New
Char() {","}) Order By fields(num) Descending Select line
Return scoreQuery
End Function
End Class

6.1.7. Cómo: Reordenar los Campos de un Archivo


Delimitado
Un archivo de valores separados por comas (CSV) es un archivo de texto que se utiliza a menudo para
almacenar datos de hoja de cálculo u otros datos tabulares que se representan en filas y columnas. Al
usar el método Split para separar los campos, es muy fácil consultar y manipular los archivos CSV con
LINQ. De hecho, la misma técnica se puede utilizar para reordenar partes de cualquier línea de texto
estructurada; no se limita a los archivos CSV.

Supongamos que las tres columnas del ejemplo siguiente representan el "apellido", "nombre" e
"identificador" de los estudiantes. Los campos están ordenados alfabéticamente por apellido de
estudiante. La consulta genera una nueva secuencia en la que la columna de identificador aparece
primero, seguida de una segunda columna que combina el nombre del estudiante y su apellido. Las
líneas se reordenan según el campo de identificador. Los resultados se guardan en un nuevo archivo sin
que se modifiquen los datos originales.

Para crear el archivo de datos

MCT: Luis Dueñas Pag 64 de 388


Manual de LINQ

Cree un nuevo proyecto de Visual C# o Visual Basic y copie las líneas siguientes en un archivo
de texto sin formato que se denomine spreadsheet1.csv. Guarde el archivo en la carpeta de la
solución.
Adams,Terry,120
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Cesar,114
Garcia,Debra,115
Garcia,Hugo,118
Mortensen,Sven,113
O'Donnell,Claire,112
Omelchenko,Svetlana,111
Tucker,Lance,119
Tucker,Michael,122
Zabokritski,Eugene,121
Ejemplo
Class CSVFiles
Shared Sub Main()
' Create the IEnumerable data source.
Dim lines As String() =
System.IO.File.ReadAllLines("../../../spreadsheet1.csv")
' Execute the query. Put field 2 first, then
' reverse and combine fields 0 and 1 from the old field
Dim lineQuery = From line In lines Let x = line.Split(New Char()
{","}) Order By x(2) Select x(2) & ", " & (x(1) & " " & x(0))
' Execute the query and write out the new file. Note that WriteAllLines
' takes a string array, so ToArray is called on the query.
System.IO.File.WriteAllLines("../../../spreadsheet2.csv",
lineQuery.ToArray())
' Keep console window open in debug mode.
Console.WriteLine("Spreadsheet2.csv written to disk.Press key to exit")
Console.ReadKey()
End Sub
End Class

6.1.8. Cómo: Combinar y Comparar Colecciones de


Cadenas
En este ejemplo se muestra cómo combinar archivos que contienen líneas de texto y, a continuación,
ordenar los resultados. En concreto, muestra cómo realizar una concatenación simple, una unión y una
intersección en los dos conjuntos de líneas de texto.

Para configurar el proyecto y los archivos de texto

1. Copie estos nombres en un archivo de texto que se denomine names1.txt y guárdelo en la


carpeta de la solución:
Bankov, Peter
Holm, Michael
Garcia, Hugo

MCT: Luis Dueñas Pag 65 de 388


Manual de LINQ

Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra
2. Copie estos nombres en un archivo de texto que se denomine names2.txt y guárdelo en la
carpeta de la solución. Observe que los dos archivos tienen algunos nombres en común.
Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi
Ejemplo
Class ConcatenateStrings
Shared Sub Main()
' Create the IEnumerable data sources.
Dim fileA As String() =
System.IO.File.ReadAllLines("../../../names1.txt")
Dim fileB As String() =
System.IO.File.ReadAllLines("../../../names2.txt")
' Simple concatenation and sort.
Dim concatQuery = fileA.Concat(fileB).OrderBy(Function(name) name)
' Pass the query variable to another function for execution
OutputQueryResults(concatQuery, "Simple concatenation and sort.
Duplicates are preserved:")
' New query. Concatenate files and remove duplicates
Dim uniqueNamesQuery = fileA.Union(fileB).OrderBy(Function(name) name)
OutputQueryResults(uniqueNamesQuery, "Union removes duplicate names:")
' New query. Find the names that occur in both files.
Dim commonNamesQuery = fileA.Intersect(fileB)
OutputQueryResults(commonNamesQuery, "Merge based on intersect: ")
' New query in three steps for better readability
' First filter each list separately
Dim nameToSearch As String = "Garcia"
Dim mergeQueryA As IEnumerable(Of String) = From name In fileA _
Let n = name.Split(New Char() {","}) _
Where n(0) = nameToSearch _
Select name
Dim mergeQueryB = From name In fileB _
Let n = name.Split(New Char() {","}) _
Where n(0) = nameToSearch _
Select name
' Create a new query to concatenate and sort results. Duplicates

MCT: Luis Dueñas Pag 66 de 388


Manual de LINQ

' are removed in Union. Note that none of the queries actually
' executed until the call to OutputQueryResults.
Dim mergeSortQuery=mergeQueryA.Union(mergeQueryB).OrderBy
(Function(str) str)
' Now execute mergeSortQuery
OutputQueryResults(mergeSortQuery, "Concat based on partial name
match """ & nameToSearch & """ from each list:")
' Keep console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Shared Sub OutputQueryResults(ByVal query As IEnumerable(Of String),
ByVal message As String)
Console.WriteLine(System.Environment.NewLine & message)
For Each item As String In query
Console.WriteLine(item)
Next
Console.WriteLine(query.Count & " total names in list")
End Sub
End Class

6.1.9. Cómo: Rellenar Colecciones de Objetos de Varios


Orígenes
En este ejemplo se muestra cómo combinar datos de distintos tipos de orígenes en una secuencia de
tipos nuevos. En los ejemplos de código siguientes se combinan cadenas con matrices de enteros. Sin
embargo, el mismo principio se aplica a cualquier par de orígenes de datos, incluida cualquier
combinación de objetos en memoria (como resultados de consultas LINQ a SQL, conjuntos de datos
ADO.NET y documentos XML).

Nota:

No intente combinar datos en memoria o datos del sistema de archivos con datos que todavía estén
en una base de datos. Este tipo de combinaciones entre dominios puede generar resultados sin
definir, dadas las distintas maneras en las que se pueden definir las operaciones de combinación para
las consultas de base de datos y otros tipos de orígenes. Existe además el riesgo de que este tipo de
operación provoque una excepción de memoria insuficiente si la cantidad de datos de la base de
datos es lo suficientemente grande. Para combinar datos de una base de datos con datos en
memoria, llame primero a ToList o ToArray en la consulta de base de datos y, a continuación,
realice la combinación con la colección devuelta.

Para crear el archivo de datos

Cree un nuevo proyecto de Visual C# o Visual Basic y copie los archivos names.csv y scores.csv
en la carpeta de la solución.

Ejemplo
En el ejemplo siguiente se muestra cómo utilizar un tipo con nombre Student para almacenar los datos
combinados de dos colecciones de cadenas en memoria que simulan datos de hoja de cálculo en formato
.csv. La primera colección de cadenas representa los nombres e identificadores de los estudiantes y la

MCT: Luis Dueñas Pag 67 de 388


Manual de LINQ

segunda colección representa el identificador de estudiante (en la primera columna) y cuatro


puntuaciones de examen.
Class Student
Public FirstName As String
Public LastName As String
Public ID As Integer
Public ExamScores As List(Of Integer)
End Class
Class PopulateCollections
Shared Sub Main()
' Join content from spreadsheets into a list of Student objectss.
' names.csv contains the student name
' plus an ID number. scores.csv contains the ID and a
' set of four test scores. The following query joins
' the scores to the student names by using ID as a
' matching key, and then projects the results into a new type.
Dim names As String() =
System.IO.File.ReadAllLines("../../../names.csv")
Dim scores As String() =
System.IO.File.ReadAllLines("../../../scores.csv")
' Name: Last[0], First[1], ID[2], Grade Level[3]
' Omelchenko, Svetlana, 111, 2
' Score: StudentID[0], Exam1[1] Exam2[2], Exam3[3], Exam4[4]
' 111, 97, 92, 81, 60
' This query joins two dissimilar spreadsheets based on common ID value.
' Multiple from clauses are used instead of a join clause
' in order to store results of id.Split.
' Note the dynamic creation of a list of ints for the
' TestScores member. We skip 1 because the first string
' in the array is the student ID, not an exam score.
Dim scoreQuery1 = From name In names Let n = name.Split(New
Char() {","}) From id In scores Let s = id.Split(New Char()
{","}) Where n(2) = s(0) Select New Student() With {.FirstName =
n(0),.LastName = n(1),.ID = n(2),.ExamScores=(From scoreAsText In
s Skip 1 Select Convert.ToInt32(scoreAsText)).ToList()}
' Optional. Store the query results for faster access
' in future queries. May be useful with very large data files.
Dim students As List(Of Student) = scoreQuery1.ToList()
' Display the list contents
' and perform a further calculation
For Each s In students
Console.WriteLine("The average score of " & s.FirstName & " " & _
s.LastName & " is " & s.ExamScores.Average())
Next
' Keep console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
End Class

MCT: Luis Dueñas Pag 68 de 388


Manual de LINQ

Los orígenes de datos de estos ejemplos se inicializan con inicializadores de objeto. La consulta utiliza
una cláusula join para hallar las correspondencias entre los nombres y las puntuaciones. Como clave
externa, se usa ID. Sin embargo, en un origen el identificador (ID) es una cadena y en el otro es un
entero. Dado que join requiere una comparación de igualdad, primero es necesario extraer el
identificador de la cadena y luego convertirlo a entero. Esto se logra en las dos cláusulas let. El
identificador temporal x de la primera cláusula let almacena una matriz de tres cadenas obtenida
mediante la división de la cadena original en cada espacio. El identificador n de la segunda cláusula let
almacena el resultado de convertir la subcadena del identificador en un entero. En la cláusula select, se
usa un inicializador de objeto para crear instancias de cada nuevo objeto Student utilizando los datos de
los dos orígenes.

Si no es necesario almacenar los resultados de la consulta, los tipos anónimos pueden ser más
adecuados que los tipos con nombre. Los tipos con nombre son necesarios si los resultados de la
consulta se pasan fuera del método en el que ésta se ejecuta.

6.1.10. Cómo: Dividir un Archivo en Varios Archivos


Mediante el Uso de Grupos
En este ejemplo se muestra una forma de combinar el contenido de dos archivos y crear después un
conjunto de archivos nuevos que organicen los datos de manera distinta.

Para crear los archivos de datos

1. Copie estos nombres en un archivo de texto que se denomine names1.txt y guárdelo en la


carpeta de la solución:
Bankov, Peter
Holm, Michael
Garcia, Hugo
Potra, Cristina
Noriega, Fabricio
Aw, Kam Foo
Beebe, Ann
Toyoshima, Tim
Guy, Wey Yuan
Garcia, Debra
2. Copie estos nombres en un archivo de texto que se denomine names2.txt y guárdelo en la
carpeta de la solución. Observe que los dos archivos tienen algunos nombres en común.
Liu, Jinghao
Bankov, Peter
Holm, Michael
Garcia, Hugo
Beebe, Ann
Gilchrist, Beth
Myrcha, Jacek
Giakoumakis, Leo
McLin, Nkenge
El Yassir, Mehdi
Ejemplo

MCT: Luis Dueñas Pag 69 de 388


Manual de LINQ

Class SplitWithGroups
Shared Sub Main()
Dim fileA As String() =
System.IO.File.ReadAllLines("../../../names1.txt")
Dim fileB As String() =
System.IO.File.ReadAllLines("../../../names2.txt")
' Concatenate and remove duplicate names based on
Dim mergeQuery As IEnumerable(Of String) = fileA.Union(fileB)
' Group the names by the first letter in the last name
Dim groupQuery = From name In mergeQuery Let n = name.Split(New
Char() {","}) Order By n(0) Group By groupKey = n(0)(0) Into
groupName = Group
' Create a new file for each group that was created
' Note that nested foreach loops are required to access
' individual items with each group.
For Each gGroup In groupQuery
Dim fileName As String = "..'..'..'testFile_" &
gGroup.groupKey & ".txt"
Dim sw As New System.IO.StreamWriter(fileName)
Console.WriteLine(gGroup.groupKey)
For Each item In gGroup.groupName
Console.WriteLine(" " & item.name)
sw.WriteLine(item.name)
Next
sw.Close()
Next
' Keep console window open in debug mode.
Console.WriteLine("Files have been written. Press any key to exit.")
Console.ReadKey()
End Sub
End Class
El programa escribe un archivo independiente para cada grupo en la misma carpeta que los archivos de
datos.

6.1.11. Cómo: Combinar Contenido de Archivos No


Similares
En este ejemplo se muestra cómo combinar datos de dos archivos de valores separados por comas que
comparten un valor común que se utiliza como clave coincidente. Esta técnica puede ser útil si es
necesario combinar en un nuevo archivo los datos de dos hojas de cálculo o los datos de una hoja de
cálculo y de un archivo en otro formato. También puede modificar el ejemplo para que funcione con
cualquier tipo de texto estructurado.

Para crear los archivos de datos

1. Copie estas líneas en un archivo denominado scores.csv y guárdelo en la misma carpeta que el
archivo de proyecto. Este archivo representa datos de hoja de cálculo. La columna 1 es el
identificador del estudiante y las columnas de la 2 a la 5 son las puntuaciones de exámenes.
111, 97, 92, 81, 60

MCT: Luis Dueñas Pag 70 de 388


Manual de LINQ

112, 75, 84, 91, 39


113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91
2. Copie estas líneas en un archivo denominado names.csv y guárdelo en la misma carpeta que el
archivo de proyecto. Este archivo representa una hoja de cálculo que contiene el apellido del
estudiante, su nombre y su identificador.
Omelchenko,Svetlana,111
O'Donnell,Claire,112
Mortensen,Sven,113
Garcia,Cesar,114
Garcia,Debra,115
Fakhouri,Fadi,116
Feng,Hanying,117
Garcia,Hugo,118
Tucker,Lance,119
Adams,Terry,120
Zabokritski,Eugene,121
Tucker,Michael,122
Ejemplo
Class JoinStrings
Shared Sub Main()
' Join content from spreadsheet files that contain
' related information. names.csv contains the student name
' plus an ID number. scores.csv contains the ID and a
' set of four test scores. The following query joins
' the scores to the student names by using ID as a
' matching key.
Dim names As String() =
System.IO.File.ReadAllLines("../../../names.csv")
Dim scores As String() =
System.IO.File.ReadAllLines("../../../scores.csv")
' Name: Last[0], First[1], ID[2], Grade Level[3]
' Omelchenko, Svetlana, 111, 2
' Score: StudentID[0], Exam1[1] Exam2[2], Exam3[3], Exam4[4]
' 111, 97, 92, 81, 60
' This query joins two dissimilar spreadsheets based on common ID value.
' Multiple from clauses are used instead of a join clause
' in order to store results of id.Split.
Dim scoreQuery1 = From name In names Let n = name.Split(New
Char() {","}) From id In scores Let n2 = id.Split(New Char()
{","}) Where n(2) = n2(0) Select n(0) & "," & n(1) & "," & n2(0)
& "," & n2(1) & "," & n2(2) & "," & n2(3)

MCT: Luis Dueñas Pag 71 de 388


Manual de LINQ

' Pass a query variable to a Sub and execute it there.


' The query itself is unchanged.
OutputQueryResults(scoreQuery1, "Merge two spreadsheets:")
' Keep console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
Shared Sub OutputQueryResults(ByVal query As IEnumerable(Of String),
ByVal message As String)
Console.WriteLine(System.Environment.NewLine & message)
For Each item As String In query
Console.WriteLine(item)
Next
Console.WriteLine(query.Count & " total names in list")
End Sub
End Class

6.1.12. Cómo: Calcular Valores de Columna en un Archivo


de Texto CSV
Este ejemplo muestra cómo realizar cálculos de totales, como Suma, Media, Min y Max, sobre las
columnas de un archivo .csv. Los principios del ejemplo que se muestran aquí se pueden aplicar a otros
tipos de texto estructurado.

Para crear el archivo de origen

Copie las líneas siguientes en un archivo denominado scores.csv y guárdelo en su carpeta de


solución. Suponga que la primera columna representa un identificador de alumno, y las
columnas subsiguientes representan las calificaciones de cuatro exámenes.
111, 97, 92, 81, 60
112, 75, 84, 91, 39
113, 88, 94, 65, 91
114, 97, 89, 85, 82
115, 35, 72, 91, 70
116, 99, 86, 90, 94
117, 93, 92, 80, 87
118, 92, 90, 83, 78
119, 68, 79, 88, 92
120, 99, 82, 81, 79
121, 96, 85, 91, 60
122, 94, 92, 91, 91
Ejemplo
Class SumColumns
Public Shared Sub Main()
Dim lines As String() =
System.IO.File.ReadAllLines("../../../scores.csv")
' Specifies the column to compute
' This value could be passed in at runtime.
Dim exam = 3

MCT: Luis Dueñas Pag 72 de 388


Manual de LINQ

' Spreadsheet format:


' Student ID Exam#1 Exam#2 Exam#3 Exam#4
' 111, 97, 92, 81, 60
' one is added to skip over the first column
' which holds the student ID.
SumColumn(lines, exam + 1)
Console.WriteLine()
MultiColumns(lines)
' Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit...")
Console.ReadKey()
End Sub
Shared Sub SumColumn(ByVal lines As IEnumerable(Of String), ByVal col
As Integer)
' This query performs two steps:
' split the string into a string array
' convert the specified element to
' integer and select it.
Dim columnQuery = From line In lines Let x = line.Split(",") _
Select Convert.ToInt32(x(col))
' Execute and cache the results for performance.
' Only needed with very large files.
Dim results = columnQuery.ToList()
' Perform aggregate calculations
' on the column specified by col.
Dim avgScore = Aggregate score In results Into Average(score)
Dim minScore = Aggregate score In results Into Min(score)
Dim maxScore = Aggregate score In results Into Max(score)
Console.WriteLine("Single Column Query:")
Console.WriteLine("Exam #{0}: Average:{1:##.##} High Score:{2}
Low Score:{3}", col, avgScore, maxScore, minScore)
End Sub
Shared Sub MultiColumns(ByVal lines As IEnumerable(Of String))
Console.WriteLine("Multi Column Query:")
' Create the query. It will produce nested sequences.
' multiColQuery performs these steps:
' 1) convert the string to a string array
' 2) skip over the "Student ID" column and take the rest
' 3) convert each field to an int and select that
' entire sequence as one row in the results.
Dim multiColQuery = From line In lines Let fields =
line.Split(",") Select From str In fields Skip 1 Select
Convert.ToInt32(str)
Dim results = multiColQuery.ToList()
' Find out how many columns we have.
Dim columnCount = results(0).Count()
' Perform aggregate calculations on each column.
' One loop for each score column in scores.
' We can use a for loop because we have already
' executed the multiColQuery in the call to ToList.

MCT: Luis Dueñas Pag 73 de 388


Manual de LINQ

For j As Integer = 0 To columnCount - 1


Dim column = j
Dim res2 = From row In results Select row.ElementAt(column)
' Perform aggregate calculations
' on the column specified by col.
Dim avgScore = Aggregate score In res2 Into Average(score)
Dim minScore = Aggregate score In res2 Into Min(score)
Dim maxScore = Aggregate score In res2 Into Max(score)
' Add 1 to column numbers because exams in this course start with #1
Console.WriteLine("Exam #{0} Average: {1:##.##} High Score:
{2} Low Score: {3}", column + 1, avgScore, maxScore, minScore)
Next
End Sub
End Class
Si su archivo es un archivo de valores separados por tabuladores, simplemente cambie el argumento del
método Split por \t.

6.2. LINQ y Reflection


Las API de reflexión de la biblioteca de clases de .NET Framework se pueden utilizar para examinar los
metadatos de un ensamblado de .NET y crear colecciones de tipos, miembros de tipos, parámetros, etc.
que están en ese ensamblado. Dado que estas colecciones admiten la interfaz genérica IEnumerable,
se pueden consultar utilizando LINQ.

6.2.1. Cómo: Consultar los Metadatos de un Ensamblado


con la Función de Reflexión
En el ejemplo siguiente se muestra cómo se puede usar LINQ con la reflexión para recuperar metadatos
concretos sobre métodos que coinciden con un criterio de búsqueda especificado. En este caso, la
consulta buscará los nombres de todos los métodos del ensamblado que devuelven tipos enumerables,
como matrices.

Ejemplo
Imports System.Reflection
Imports System.IO
Imports System.Linq
Module Module1
Sub Main()
Dim file As String = "C:\Program Files\Reference
Assemblies\Microsoft\Framework\v3.5\System.Core.dll"
Dim asmbly As Assembly = Assembly.LoadFrom(file)
Dim pubTypesQuery = From type In asmbly.GetTypes() Where
type.IsPublic From method In type.GetMethods() _
Where method.ReturnType.IsArray = True _
Let name = method.ToString() Let typeName = type.ToString() _
Group name By typeName Into methodNames = Group
Console.WriteLine("Getting ready to iterate")
For Each item In pubTypesQuery
Console.WriteLine(item.methodNames)

MCT: Luis Dueñas Pag 74 de 388


Manual de LINQ

For Each type In item.methodNames


Console.WriteLine(" " & type)
Next
Next
Console.ReadKey()
End Sub
End Module
En el ejemplo se utiliza el método GetTypes para devolver una matriz de tipos del ensamblado
especificado. El filtro where se aplica para que sólo se devuelvan los tipos públicos. Para cada tipo
público, se genera una subconsulta mediante la matriz MethodInfo que se devuelve de la llamada a
GetMethods. Estos resultados se filtran para devolver sólo los métodos cuyo tipo de valor devuelto sea
una matriz o, si no, un tipo que implemente IEnumerable<(Of <(T>)>). Finalmente, los resultados se
agrupan utilizando el nombre de tipo como clave.

6.3. LINQ y Directorios de Archivos


Muchas operaciones del sistema de archivos son esencialmente consultas y, por lo tanto, son adecuadas
para LINQ.

Nota:

Si desea realizar consultas mediante programación en el contenido de varios tipos de archivos y


documentos, piense en utilizar Windows Desktop Search Engine. Aunque actualmente no se puede
consultar con LINQ, proporciona un servicio de indización eficaz que administra eficazmente las
dificultades del sistema de archivos.

Observe que las consultas de esta sección no son destructivas. No se utilizan para cambiar el contenido
de los archivos o carpetas originales. Se aplica la regla de que las consultas no deberían producir efectos
adversos. Por lo general, el código (incluidas las consultas que realizan operaciones de creación,
actualización o eliminación) que se utiliza para modificar los datos de origen se debería mantener
apartado del código que se usa simplemente para consultar los datos.

6.3.1. Cómo: Buscar Archivos con un Nombre o Atributo


Especificados
En este ejemplo se muestra cómo buscar todos los archivos que tienen una extensión especificada (por
ejemplo, ".txt") en un árbol de directorios especificado. También se muestra cómo devolver el archivo
más reciente o más antiguo del árbol según la hora de creación.

Ejemplo
Module FindFileByExtension
Sub Main()
' Change the drive\path if necessary
Dim root As String = "C:\Program Files\Microsoft Visual Studio 9.0"
'Take a snapshot of the folder contents
Dim fileList = GetFiles(root)
' This query will produce the full path for all .txt files
' under the specified folder including subfolders.
' It orders the list according to the file name.
Dim fileQuery = From file In fileList Where file.Extension =

MCT: Luis Dueñas Pag 75 de 388


Manual de LINQ

".txt" Order By file.Name Select file


For Each file In fileQuery
Console.WriteLine(file.FullName)
Next
' Create and execute a new query by using
' the previous query as a starting point.
' fileQuery is not executed again until the
' call to Last
Dim fileQuery2=From file In fileQuery Order By file.CreationTime
Select file.Name, file.CreationTime
' Execute the query
Dim newestFile = fileQuery2.Last
Console.WriteLine("\r\nThe newest .txt file is {0}. Creation
time: {1}", newestFile.Name, newestFile.CreationTime)
' Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
' Function to retrieve a list of files. Note that this is a copy
' of the file information.
Function GetFiles(ByVal root As String) As
System.Collections.Generic.IEnumerable(Of System.IO.FileInfo)
Return From file In My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
Select New System.IO.FileInfo(file)
End Function
End Module

6.3.2. Cómo: Agrupar Archivos por Extensión


En este ejemplo se muestra cómo se puede usar LINQ para realizar operaciones avanzadas de
agrupación y ordenación en listas de archivos o carpetas. También muestra cómo desplazarse por los
resultados página a página en la ventana de la consola utilizando los métodos Skip<(Of <(TSource>)>)
y Take<(Of <(TSource>)>).

Ejemplo
La consulta siguiente muestra cómo agrupar el contenido de un árbol de directorios especificado por la
extensión de archivo.
Module GroupByExtension
Public Sub Main()
' Root folder to query, along with all subfolders.
Dim startFolder As String = "C:\program files\Microsoft Visual
Studio 9.0\VB\"
' Used in WriteLine() to skip over startfolder in output lines.
Dim rootLength As Integer = startFolder.Length
' Take a snapshot of the file system.
Dim fileList As IEnumerable(Of System.IO.FileInfo) =
GetFiles(startFolder)
' Create the query.
Dim queryGroupByExt = From file In fileList Group By

MCT: Luis Dueñas Pag 76 de 388


Manual de LINQ

file.Extension.ToLower() Into fileGroup = Group Order By ToLower


Select fileGroup
' Execute the query. By storing the result we can
' page the display with good performance.
Dim groupByExtList = queryGroupByExt.ToList()
' Display one group at a time. If the number of
' entries is greater than the number of lines
' in the console window, then page the output.
Dim trimLength = startFolder.Length
PageOutput(groupByExtList, trimLength)
End Sub
' Function to retrieve a list of files. Note that this is a copy
' of the file information.
Function GetFiles(ByVal root As String) As IEnumerable(Of
System.IO.FileInfo)
Return From file In My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
Select New System.IO.FileInfo(file)
End Function
' Pages console diplay for large query results. No more than one
' group per page. This sub specifically works with group queries of
' FileInfo objects but can be modified for any type.
Sub PageOutput(ByVal groupQuery, ByVal charsToSkip)
' "3" = 1 line for extension key + 1 for "Press any key" + 1 for
' input cursor.
Dim numLines As Integer = Console.WindowHeight - 3
' Flag to indicate whether there are more results to diplay
Dim goAgain As Boolean = True
For Each fg As IEnumerable(Of System.IO.FileInfo) In groupQuery
' Start a new extension at the top of a page.
Dim currentLine As Integer = 0
Do While (currentLine < fg.Count())
Console.Clear()
Console.WriteLine(fg(0).Extension)
' Get the next page of results
' No more than one filename per page
Dim resultPage = From file In fg Skip currentLine Take numLines
' Execute the query. Trim the display output.
For Each line In resultPage
Console.WriteLine(vbTab & line.FullName.Substring(charsToSkip))
Next
' Advance the current position
currentLine = numLines + currentLine
' Give the user a chance to break out of the loop
Console.WriteLine("Press any key for next page or the
'End' key to exit.")
Dim key As ConsoleKey = Console.ReadKey().Key
If key = ConsoleKey.End Then
goAgain = False
Exit For

MCT: Luis Dueñas Pag 77 de 388


Manual de LINQ

End If
Loop
Next
End Sub
End Module
El resultado de este programa puede ser largo, dependiendo de los detalles del sistema de archivos local
y del valor en que se haya establecido startFolder. Para habilitar la presentación de todos los resultados,
este ejemplo muestra cómo recorrer los resultados página a página. Las mismas técnicas se pueden
aplicar a aplicaciones Windows y web. Observe que, dado que el código página los elementos de un
grupo, se requiere un bucle foreach anidado. Existe lógica adicional para calcular la posición actual en la
lista y para permitir al usuario detener la paginación y salir del programa. En este caso determinado, la
consulta de paginación se ejecuta contra los resultados de la consulta original almacenados en memoria
caché. En otros contextos, como LINQ a SQL, no se requiere tal almacenamiento en caché.

6.3.3. Cómo: Buscar el Número Total de Bytes en un


Conjunto de Carpetas
En este ejemplo se muestra cómo recuperar el número total de bytes utilizados por todos los archivos en
una carpeta especificada y todas sus subcarpetas.

Ejemplo
El método Sum agrega los valores de todos los elementos seleccionados en la cláusula select. Es fácil
modificar esta consulta para recuperar el archivo mayor o menor del árbol de directorios especificado
mediante una llamada al método Min o Max en lugar de Sum.
Module QueryTotalBytes
Sub Main()
' Change the drive\path if necessary.
Dim root As String = "C:\Program Files\Microsoft Visual Studio 9.0\VB"
'Take a snapshot of the folder contents.
' This method assumes that the application has discovery permissions
' for all folders under the specified path.
Dim fileList = My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*")
Dim fileQuery = From file In fileList Select GetFileLength(file)
' Force execution and cache the results to avoid multiple trips
' to the file system.
Dim fileLengths = fileQuery.ToArray()
' Find the largest file
Dim maxSize = Aggregate aFile In fileLengths Into Max()
' Find the total number of bytes
Dim totalBytes = Aggregate aFile In fileLengths Into Sum()
Console.WriteLine("The largest file is " & maxSize & " bytes")
Console.WriteLine("There are " & totalBytes & " total bytes in " & _
fileList.Count & " files under " & root)
' Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
' This method is used to catch the possible exception

MCT: Luis Dueñas Pag 78 de 388


Manual de LINQ

' that can be raised when accessing the FileInfo.Length property.


Function GetFileLength(ByVal filename As String) As Long
Dim retval As Long
Try
Dim fi As New System.IO.FileInfo(filename)
retval = fi.Length
Catch ex As System.IO.FileNotFoundException
' If a file is no longer present,
' just return zero bytes.
retval = 0
End Try
Return retval
End Function
End Module
Si sólo es necesario contar el número de bytes de un árbol de directorios especificado, puede hacerlo de
forma más eficaz sin crear una consulta LINQ, sin el esfuerzo adicional de tener que crear la colección de
listas como origen de datos. La utilidad de LINQ aumenta cuanto más compleja es la consulta, o cuando
es necesario ejecutar varias consultas en el mismo origen de datos.

La consulta llama a un método independiente para obtener la longitud del archivo. La razón es utilizar la
posible excepción que se producirá si el archivo se eliminó en otro subproceso después de haber creado
el objeto FileInfo en la llamada a GetFiles. Aunque ya se haya creado el objeto FileInfo, la excepción se
puede producir igualmente si un objeto FileInfo intenta actualizar su propiedad Length con la longitud
más actualizada la primera vez que se tenga acceso a la propiedad. Al incluir esta operación en un
bloque try-catch fuera de la consulta, el código sigue la regla de evitar usar en las consultas operaciones
que pueden tener efectos adversos. Por lo general, se deben tomar precauciones al utilizar las
excepciones, para asegurarse de que una aplicación no queda en un estado desconocido.

6.3.4. Cómo: Comparar el Contenido de dos Carpetas


En este ejemplo se muestran tres maneras de comparar dos listas de archivos:

Consultar un valor booleano que especifica si las dos listas de archivos son idénticas.

Consultar la intersección para recuperar los archivos que están en ambas carpetas.

Consultar la diferencia de conjuntos para recuperar los archivos que están en una carpeta pero
no en la otra.

Nota:

Las técnicas mostradas aquí pueden adaptarse para comparar secuencias de objetos de cualquier
tipo.

La clase FileComparer que se incluye aquí muestra cómo utilizar una clase comparadora personalizada
junto con los operadores de consulta estándar. La clase no está pensada para el uso en escenarios
reales. Simplemente utiliza el nombre y la longitud en bytes de cada archivo para determinar si el
contenido de cada carpeta es idéntico o no. En un escenario real, debería modificar este comparador
para realizar una comprobación de igualdad más rigurosa.

Ejemplo

MCT: Luis Dueñas Pag 79 de 388


Manual de LINQ

Module CompareDirs
Public Sub Main()
' Create two identical or different temporary folders
' on a local drive and add files to them.
' Then set these file paths accordingly.
Dim pathA As String = "C:\TestDir"
Dim pathB As String = "C:\TestDir2"
' Take a snapshot of the file system.
Dim list1 = GetFiles(pathA)
Dim list2 = GetFiles(pathB)
' Create the FileCompare object we'll use in each query
Dim myFileCompare As New FileCompare
' This query determines whether the two folders contain
' identical file lists, based on the custom file comparer
' that is defined in the FileCompare class.
' The query executes immediately because it returns a bool.
Dim areIdentical As Boolean = list1.SequenceEqual(list2, myFileCompare)
If areIdentical = True Then
Console.WriteLine("The two folders are the same.")
Else
Console.WriteLine("The two folders are not the same.")
End If
' Find common files in both folders. It produces a sequence and
' doesn't execute until the foreach statement.
Dim queryCommonFiles = list1.Intersect(list2, myFileCompare)
If queryCommonFiles.Count() > 0 Then
Console.WriteLine("The following files are in both folders:")
For Each fi As System.IO.FileInfo In queryCommonFiles
Console.WriteLine(fi.FullName)
Next
Else
Console.WriteLine("There are no common files in the two folders.")
End If
' Find the set difference between the two folders.
' For this example we only check one way.
Dim queryDirAOnly = list1.Except(list2, myFileCompare)
Console.WriteLine("The following files are in dirA but not dirB:")
For Each fi As System.IO.FileInfo In queryDirAOnly
Console.WriteLine(fi.FullName)
Next
' Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
' This implementation defines a very simple comparison
' between two FileInfo objects. It only compares the name
' of the files being compared and their length in bytes.
Public Class FileCompare
Implements System.Collections.Generic.IEqualityComparer(Of
System.IO.FileInfo)

MCT: Luis Dueñas Pag 80 de 388


Manual de LINQ

Public Function Equals1(ByVal x As System.IO.FileInfo, ByVal y As


System.IO.FileInfo) As Boolean Implements
System.Collections.Generic.IEqualityComparer(Of
System.IO.FileInfo).Equals
If (x.Name = y.Name) And (x.Length = y.Length) Then
Return True
Else
Return False
End If
End Function
' Return a hash that reflects the comparison criteria. According to the
' rules for IEqualityComparer(Of T), if Equals is true, then the hash
' codes must also be equal. Because equality as defined here is a
' simple value equality, not reference identity, it is possible that
' two or more objects will produce the same hash code.
Public Function GetHashCode1(ByVal fi As System.IO.FileInfo) _
As Integer Implements
System.Collections.Generic.IEqualityComparer(Of
System.IO.FileInfo).GetHashCode
Dim s As String = fi.Name & fi.Length
Return s.GetHashCode()
End Function
End Class
' Function to retrieve a list of files. Note that this is a copy
' of the file information.
Function GetFiles(ByVal root As String) As IEnumerable(Of
System.IO.FileInfo)
Return From file In My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
Select New System.IO.FileInfo(file)
End Function
End Module

6.3.5. Cómo: Buscar el Archivo(s) de Mayor Tamaño en un


Arbol de Directorios
En este ejemplo se muestran cinco consultas relacionadas con el tamaño de archivo en bytes:

Cómo recuperar el tamaño en bytes del archivo más grande.

Cómo recuperar el tamaño en bytes del archivo más pequeño.

Cómo recuperar el archivo mayor o menor del objeto FileInfo de una o más carpetas bajo una
carpeta raíz especificada.

Cómo recuperar una secuencia como los 10 archivos mayores.

Cómo ordenar los archivos por grupos según su tamaño en bytes, omitiendo los archivos cuyo
tamaño sea menor que el especificado.

MCT: Luis Dueñas Pag 81 de 388


Manual de LINQ

Ejemplo
El ejemplo siguiente contiene cinco consultas independientes que muestran cómo consultar y agrupar
archivos, dependiendo de su tamaño en bytes. Puede modificar fácilmente estos ejemplos para basar la
consulta en alguna otra propiedad del objeto FileInfo.
Module QueryBySize
Sub Main()
' Change the drive\path if necessary
Dim root As String = "C:\Program Files\Microsoft Visual Studio 9.0"
Dim fileList = GetFiles(root)
' Return the size of the largest file
Dim maxSize = Aggregate aFile In fileList Into Max(GetFileLength(aFile))
'Dim maxSize = fileLengths.Max
Console.WriteLine("The length of the largest file under {0} is {1}", _
root, maxSize)
' Return the FileInfo object of the largest file
' by sorting and selecting from the beginning of the list
Dim filesByLengDesc = From file In fileList Let filelength =
GetFileLength(file) Where filelength > 0 Order By filelength
Descending Select file
Dim longestFile = filesByLengDesc.First
Console.WriteLine("The largest file under {0} is {1} with a
length of {2} bytes", root, longestFile.FullName, longestFile.Length)
Dim smallestFile = filesByLengDesc.Last
Console.WriteLine("The smallest file under {0} is {1} with a
length of {2} bytes", root, smallestFile.FullName, smallestFile.Length)
' Return the FileInfos for the 10 largest files
' Based on a previous query, but nothing is executed
' until the For Each statement below.
Dim tenLargest = From file In filesByLengDesc Take 10
Console.WriteLine("The 10 largest files under {0} are:", root)
For Each fi As System.IO.FileInfo In tenLargest
Console.WriteLine("{0}: {1} bytes", fi.FullName, fi.Length)
Next
' Group files according to their size,
' leaving out the ones under 200K
Dim sizeGroups = From file As System.IO.FileInfo In
GetFiles(root) Where file.Length > 0 Let groupLength =
file.Length / 100000 Group file By groupLength Into fileGroup =
Group Where groupLength >= 2 Order By groupLength Descending
For Each group In sizeGroups
Console.WriteLine(group.groupLength + "00000")
For Each item As System.IO.FileInfo In group.fileGroup
Console.WriteLine(" {0}: {1}", item.Name, item.Length)
Next
Next
' Keep the console window open in debug mode
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
' This method is used to catch the possible exception

MCT: Luis Dueñas Pag 82 de 388


Manual de LINQ

' that can be raised when accessing the FileInfo.Length property.


' In this particular case, it is safe to ignore the exception.
Function GetFileLength(ByVal fi As System.IO.FileInfo) As Long
Dim retval As Long
Try
retval = fi.Length
Catch ex As FileNotFoundException
' If a file is no longer present,
' just return zero bytes.
retval = 0
End Try
Return retval
End Function
' Function to retrieve a list of files. Note that this is a copy
' of the file information.
Function GetFiles(ByVal root As String) As
System.Collections.Generic.IEnumerable(Of System.IO.FileInfo)
Return From file In My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
Select New System.IO.FileInfo(file)
End Function
End Module
Para devolver uno o más objetos FileInfo completos, en primer lugar la consulta debe examinarlos
individualmente en el origen de datos y después ordenarlos según el valor de su propiedad Length. A
continuación, puede devolver el objeto mayor o la secuencia de objetos mayores. Utilice First para
devolver el primer elemento de una lista. Utilice Take<(Of <(TSource>)>) para devolver los primeros n
elementos. Especifique un orden descendente para colocar los elementos menores al principio de la lista.

La consulta llama a un método independiente para obtener el tamaño de archivo en bytes con el fin de
utilizar la posible excepción que se producirá si se eliminó un archivo en otro subproceso desde que se
creó el objeto FileInfo en la llamada a GetFiles. Aunque ya se haya creado el objeto FileInfo, la excepción
se puede producir igualmente si un objeto FileInfo intenta actualizar su propiedad Length con el tamaño
en bytes más actualizado la primera vez que se tenga acceso a la propiedad. Al incluir esta operación en
un bloque try-catch fuera de la consulta, seguimos la regla de evitar usar en las consultas operaciones
que pueden tener efectos adversos. Por lo general, debemos tener cuidado al utilizar las excepciones,
para asegurarnos de que no dejamos una aplicación en un estado desconocido.

6.3.6. Cómo: Buscar Archivos Duplicados en un Arbol de


Directorios
A veces, en varias carpetas puede haber archivos que tienen el mismo nombre. Por ejemplo, bajo la
carpeta de instalación de Visual Studio, hay varias carpetas con un archivo readme.htm. En este ejemplo
se muestra cómo buscar nombres de archivo duplicados bajo una carpeta raíz especificada. En el
segundo ejemplo se muestra cómo buscar archivos cuyo tamaño y fecha de creación también coinciden.

Ejemplo
Module QueryDuplicateFileNames
Public Sub Main()
Dim path As String = "C:\Program Files\Microsoft Visual Studio

MCT: Luis Dueñas Pag 83 de 388


Manual de LINQ

9.0\Common7"
'QueryDuplicates1(path)
' Uncomment to run this query instead
QueryDuplicates2(path)
End Sub
Sub QueryDuplicates1(ByVal root As String)
Dim duplicates = From aFile In GetFiles(root) Order By aFile.Name
Group aFile By aFile.Name Into newGroup = Group Where
newGroup.Count() >= 2 Select newGroup
' Page the display so that the results can be read.
Dim trimLength = root.Length
PageOutput(duplicates, trimLength)
End Sub
Sub QueryDuplicates2(ByVal root As String)
' This time a composite key is used. This sub finds all files
' that have been copied into multiple subfolders.
Dim duplicates = From aFile In GetFiles(root) Order By aFile.Name
Group aFile By aFile.Name, aFile.CreationTime, aFile.Length Into
newGroup = Group Where newGroup.Count() >= 2 Select newGroup
' Page the display so that the results can be read.
Dim trimLength = root.Length
PageOutput(duplicates, trimLength)
End Sub
' Pages console diplay for large query results. No more than one
group per page.
' This sub specifically works with group queries of FileInfo objects
' but can be modified for any type.
Sub PageOutput(ByVal groupQuery, ByVal charsToSkip)
' "3" = 1 line for extension key + 1 for "Press any key" + 1 for
' input cursor.
Dim numLines As Integer = Console.WindowHeight - 3
' Flag to indicate whether there are more results to diplay
Dim goAgain As Boolean = True
For Each fg As IEnumerable(Of System.IO.FileInfo) In groupQuery
' Start a new extension at the top of a page.
Dim currentLine As Integer = 0
Do While (currentLine < fg.Count())
Console.Clear()
' Get the next page of results
' No more than one filename per page
Dim resultPage=From file In fg Skip currentLine Take numLines
' Execute the query. Trim the paths in the output.
For Each line In resultPage
Console.WriteLine(vbTab &
line.FullName.Substring(charsToSkip))
Next
' Advance the current position
currentLine = numLines + currentLine
' Give the user a chance to break out of the loop
Console.WriteLine("Press any key for next page or the

MCT: Luis Dueñas Pag 84 de 388


Manual de LINQ

'End' key to exit.")


Dim key As ConsoleKey = Console.ReadKey().Key
If key = ConsoleKey.End Then
goAgain = False
Exit For
End If
Loop
Next
End Sub
' Function to retrieve a list of files. Note that this is a copy
' of the file information.
Function GetFiles(ByVal root As String) As
System.Collections.Generic.IEnumerable(Of System.IO.FileInfo)
Return From file In My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
Select New System.IO.FileInfo(file)
End Function
End Module
La primera consulta utiliza una clave simple para determinar una coincidencia; se buscan archivos que
tienen el mismo nombre pero cuyo contenido podría ser diferente. La segunda consulta usa una clave
compuesta para buscar las coincidencias de tres propiedades del objeto FileInfo. Lo más probable es que
esta consulta encuentre archivos que tienen el mismo nombre y contenido similar o idéntico.

6.3.7. Cómo: Consultar el Contenido de los Archivos de


una Carpeta
En este ejemplo se muestra cómo realizar consultas en todos los archivos de un árbol de directorios
especificado, abrir cada archivo e inspeccionar su contenido. Este tipo de técnica se podría utilizar para
crear índices o índices inversos del contenido de un árbol de directorios. En este ejemplo se realiza una
búsqueda de cadena simple. Sin embargo, se pueden realizar tipos de coincidencia de modelos más
complejos con una expresión regular.

Ejemplo
Module QueryContents
Public Sub Main()
' Modify this path as necessary.
Dim startFolder = "c:\program files\Microsoft Visual Studio
9.0\VB\"
' Take a snapshot of the file system.
Dim fileList = GetFiles(startFolder)
Dim searchTerm = "Visual Studio"
' Search the contents of each file.
' A regular expression created with the RegEx class
' could be used instead of the Contains method.
Dim queryMatchingFiles = From file In fileList Where
file.Extension = ".htm" Let fileText = GetFileText(file.FullName)
Where fileText.Contains(searchTerm) Select file.FullName
Console.WriteLine("The term " & searchTerm & " was found in:")
' Execute the query.

MCT: Luis Dueñas Pag 85 de 388


Manual de LINQ

For Each filename In queryMatchingFiles


Console.WriteLine(filename)
Next
' Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit")
Console.ReadKey()
End Sub
' Read the contents of the file. This is done in a separate
' function in order to handle potential file system errors.
Function GetFileText(ByVal name As String) As String
' If the file has been deleted, the right thing
' to do in this case is return an empty string.
Dim fileContents = String.Empty
' If the file has been deleted since we took
' the snapshot, ignore it and return the empty string.
If System.IO.File.Exists(name) Then
fileContents = System.IO.File.ReadAllText(name)
End If
Return fileContents
End Function
' Function to retrieve a list of files. Note that this is a copy
' of the file information.
Function GetFiles(ByVal root As String) As
System.Collections.Generic.IEnumerable(Of System.IO.FileInfo)
Return From file In My.Computer.FileSystem.GetFiles _
(root, FileIO.SearchOption.SearchAllSubDirectories, "*.*") _
Select New System.IO.FileInfo(file)
End Function
End Module

6.4. Cómo: Consultar un Objeto ArrayList con LINQ


Al utilizar LINQ para consultar colecciones IEnumerable no genéricas, como ArrayList, debe declarar
explícitamente el tipo de la variable de rango para reflejar el tipo específico de los objetos de la
colección. Por ejemplo, si tiene una estructura ArrayList de objetos Student, su cláusula from (C#) o
From (Cláusula, Visual Basic) debería ser similar a la siguiente:
Dim query = From student As Student In arrList...
Al especificar el tipo de la variable de rango, convierte cada elemento de ArrayList en Student.

Utilizar una variable de rango con un tipo declarado explícitamente en una expresión de consulta es
equivalente a llamar al método Cast<(Of <(TResult>)>). Cast<(Of <(TResult>)>) inicia una excepción
si no se puede realizar la conversión de tipos especificada. Cast<(Of <(TResult>)>) y OfType<(Of
<(TResult>)>) son los dos métodos de operador de consulta estándar que actúan en tipos IEnumerable
no genéricos.

Ejemplo
En el ejemplo siguiente se muestra una consulta simple sobre ArrayList. Observe que en este ejemplo se
utilizan inicializadores de objeto cuando el código llama al método Add, pero no es obligatorio.
Imports System.Collections
Imports System.Linq

MCT: Luis Dueñas Pag 86 de 388


Manual de LINQ

Module Module1
Public Class Student
Public FirstName As String
Public LastName As String
Public Scores As Integer()
End Class
Sub Main()
Dim student1 As New Student With {.FirstName = "Svetlana", _
.LastName = "Omelchenko",.Scores = New Integer() {98,92,81,60}}
Dim student2 As New Student With {.FirstName = "Claire", _
.LastName = "O'Donnell",.Scores = New Integer() {75, 84, 91, 39}}
Dim student3 As New Student With {.FirstName = "Cesar", _
.LastName = "Garcia",.Scores = New Integer() {97, 89, 85, 82}}
Dim student4 As New Student With {.FirstName = "Sven", _
.LastName = "Mortensen",.Scores = New Integer() {88, 94, 65, 91}}
Dim arrList As New ArrayList()
arrList.Add(student1)
arrList.Add(student2)
arrList.Add(student3)
arrList.Add(student4)
' Use an explicit type for non-generic collections
Dim query = From student As Student In arrList Where
student.Scores(0) > 95 Select student
For Each student As Student In query
Console.WriteLine(student.LastName & ": " & student.Scores(0))
Next
' Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.")
Console.ReadKey()
End Sub
End Module
' Output:
' Omelchenko: 98
' Garcia: 97

7. LINQ a XML
LINQ a XML proporciona una interfaz de programación XML en memoria que aprovecha las características
de .NET Language-Integrated Query (LINQ) Framework. LINQ a XML utiliza las características más
recientes del lenguaje .NET Framework y es comparable a una actualizada y rediseñada interfaz de
programación XML para el Modelo de objetos de documento (DOM).

La familia de tecnologías de LINQ proporciona un completo entorno de creación de consultas para objetos
(LINQ), bases de datos relacionales (LINQ a SQL) y XML (LINQ a XML).

7.1. Introducción LINQ a XML


En los siguientes temas se presenta LINQ a XML.

7.1.1. Información General Acerca de LINQ a XML

MCT: Luis Dueñas Pag 87 de 388


Manual de LINQ

XML se ha adoptado ampliamente como un modo de formatear datos en diversos contextos. Puede
encontrar XML en la Web, en archivos de configuración, en archivos de Microsoft Office Word y en bases
de datos.

LINQ a XML es un método actualizado y rediseñado para la programación con XML. Proporciona
capacidades de modificación de documento en memoria de Document Object Model (DOM), y es
compatible con expresiones de consulta LINQ. Aunque estas expresiones de consulta difieren
sintácticamente de XPath, proporcionan una funcionalidad similar con un establecimiento inflexible de
tipos superior.

Desarrolladores de LINQ a XML


LINQ a XML se dirige a diversos desarrolladores. Para el desarrollador medio que sólo desea completar
una tarea, LINQ a XML simplifica el código XML al proporcionar una experiencia de consulta similar a
SQL. Con un poco de estudio, los programadores pueden aprender a escribir consultas sucintas y
eficaces.

Los desarrolladores profesionales pueden usar LINQ a XML para aumentar considerablemente su
productividad. Con LINQ a XML, pueden escribir menos código, que a su vez resulte más expresivo,
compacto y eficaz. Pueden usar expresiones de consulta de distintos dominios de datos
simultáneamente.

¿Qué es LINQ a XML?


LINQ a XML es una interfaz de programación XML en memoria y habilitada para LINQ que permite
trabajar con XML desde los lenguajes de programación de .NET Framework.

Se parece a Document Object Model (DOM) en lo que respecta a la inserción del documento XML en la
memoria. Puede consultar y modificar el documento; una vez modificado, puede guardarlo en un archivo
o serializarlo y enviarlo a través de una conexión. Sin embargo, LINQ a XML difiere de DOM: proporciona
un nuevo modelo de objetos más ligero con el que se trabaja más fácilmente y que aprovecha las
mejoras de lenguaje de Visual C# 2008.

La ventaja más importante de LINQ a XML radica en su integración con Language-Integrated Query
(LINQ). Esta integración permite escribir consultas en el documento XML en memoria para recuperar
colecciones de elementos y atributos. Las capacidades de consulta de LINQ a XML son comparables en
cuanto a funcionalidad (aunque no en sintaxis) a XPath y XQuery. La integración de LINQ en Visual C#
2008 proporciona una escritura más rápida, comprobación en tiempo de compilación y una
compatibilidad mejorada con el depurador.

La capacidad de usar los resultados de la consulta como parámetros en constructores de objetos


XElement y XAttribute habilita un método eficaz para crear árboles XML. Este método, denominado
construcción funcional, permite que los desarrolladores transformen fácilmente árboles XML de una
forma a otra.

La capacidad LINQ de LINQ a XML permite ejecutar consultas en XML. Por ejemplo, es posible que tenga
un pedido de compra XML típico, tal como se describe en Archivo XML de muestra: pedido de compra
típico (LINQ a XML). Mediante LINQ a XML, podría ejecutar la siguiente consulta para obtener el valor del
atributo de número de pieza relativo a cada elemento del pedido de compra:
Dim partNos = From item In purchaseOrder...<Item> Select item.@PartNumber
A modo de ejemplo, imagine que necesita una lista, ordenada por números de pieza, de los elementos
con un valor superior a 100 $. Para obtener esta información, podría ejecutar la siguiente consulta:

MCT: Luis Dueñas Pag 88 de 388


Manual de LINQ

Dim partNos = From item In purchaseOrder...<Item> _


Where (item.<Quantity>.Value * item.<USPrice>.Value) > 100 _
Order By item.<PartNumber>.Value Select item
Tal vez sea tan significativo como las capacidades LINQ de LINQ a XML el hecho de que LINQ a XML
proporciona una interfaz de programación XML mejorada. Mediante LINQ a XML, puede hacer todo lo que
espera de la programación con XML, incluido lo siguiente:
Cargar XML a partir de archivos o secuencias.
Serializar XML a archivos o secuencias.
Crear árboles XML desde cero mediante la construcción funcional.
Realizar consultas de XML con ejes de tipo XPath.
Manipular el árbol XML en memoria con métodos como Add, Remove, ReplaceWith y SetValue.
Validar árboles XML mediante XSD.
Usar una combinación de estas características para transformar las formas de los árboles XML.
Crear árboles XML
La facilidad con la que puede crear árboles XML es especialmente significativa. Por ejemplo, para crear
un árbol XML pequeño, puede escribir código C# de la siguiente manera:
Dim contacts = _
<Contacts>
<Contact>
<Name>Patrick Hines</Name>
<Phone Type="Home">206-555-0144</Phone>
<Phone Type="Work">425-555-0145</Phone>
<Address>
<Street1>123 Main St</Street1>
<City>Mercer Island</City>
<State>WA</State>
<Postal>68042</Postal>
</Address>
</Contact>
</Contacts>
El compilador de Visual Basic traduce los literales XML a llamadas a métodos en LINQ a XML.

7.1.2. Diferencias entre LINQ a XML y DOM


Esta sección describe algunas diferencias fundamentales entre LINQ a XML y la API de programación XML
predominante actual, el modelo de objetos de documento W3C.

Nuevas formas de crear árboles XML


En DOM W3C, los árboles XML se crean de abajo arriba; es decir, se crea un documento, se crean
elementos y, a continuación, se agregan los elementos al documento.

Por ejemplo, a continuación se muestra una forma típica de crear un árbol XML usando la
implementación de DOM de Microsoft, XmlDocument:
Dim doc As XmlDocument = New XmlDocument()
Dim name As XmlElement = doc.CreateElement("Name")
name.InnerText = "Patrick Hines"
Dim phone1 As XmlElement = doc.CreateElement("Phone")
phone1.SetAttribute("Type", "Home")
phone1.InnerText = "206-555-0144"
Dim phone2 As XmlElement = doc.CreateElement("Phone")

MCT: Luis Dueñas Pag 89 de 388


Manual de LINQ

phone2.SetAttribute("Type", "Work")
phone2.InnerText = "425-555-0145"
Dim street1 As XmlElement = doc.CreateElement("Street1")
street1.InnerText = "123 Main St"
Dim city As XmlElement = doc.CreateElement("City")
city.InnerText = "Mercer Island"
Dim state As XmlElement = doc.CreateElement("State")
state.InnerText = "WA"
Dim postal As XmlElement = doc.CreateElement("Postal")
postal.InnerText = "68042"
Dim address As XmlElement = doc.CreateElement("Address")
address.AppendChild(street1)
address.AppendChild(city)
address.AppendChild(state)
address.AppendChild(postal)
Dim contact As XmlElement = doc.CreateElement("Contact")
contact.AppendChild(name)
contact.AppendChild(phone1)
contact.AppendChild(phone2)
contact.AppendChild(address)
Dim contacts As XmlElement = doc.CreateElement("Contacts")
contacts.AppendChild(contact)
doc.AppendChild(contacts)
Console.WriteLine(doc.OuterXml)
Este estilo de codificación no proporciona mucha información visual acerca de la estructura del árbol
XML. LINQ a XML admite este enfoque para crear un árbol XML, pero también admite un enfoque
alternativo, la construcción funcional. La construcción funcional usa los constructores XElement y
XAttribute para crear un árbol XML.

A continuación se muestra cómo se crea el mismo árbol XML usando la construcción funcional LINQ a
XML:
Dim contacts = _
<Contacts>
<Contact>
<Name>Patrick Hines</Name>
<Phone Type="Home">206-555-0144</Phone>
<Phone Type="Work">425-555-0145</Phone>
<Address>
<Street1>123 Main St</Street1>
<City>Mercer Island</City>
<State>WA</State>
<Postal>68042</Postal>
</Address>
</Contact>
</Contacts>
El compilador de Visual Basic traduce los literales XML a llamadas a métodos en LINQ a XML.

Si tiene un documento XML y desea crear un árbol XML a partir de él, puede abrir el documento XML en
un editor, copiar el código XML en el portapapeles, abrir un módulo de Visual Basic en Visual Studio y
pegar el código XML directamente en el editor de código de Visual Basic.

MCT: Luis Dueñas Pag 90 de 388


Manual de LINQ

Trabajar directamente con elementos XML


La programación con XML suele centrarse en elementos XML y quizás en los atributos. En LINQ a XML, se
puede trabajar directamente con atributos y elementos XML. Por ejemplo, puede realizar lo siguiente:

Crear elementos XML sin usar un objeto de documento. Esto simplifica la programación cuando
se tiene que trabajar con fragmentos de árboles XML.

Cargar objetos T:System.Xml.Linq.XElement directamente de un archivo XML.

Serializar objetos T:System.Xml.Linq.XElement a un archivo o una secuencia.

Compare esto con modelo DOM del consorcio W3C, en el que el documento XML se usa como contenedor
lógico para el árbol XML. En DOM, los nodos XML, incluyendo elementos y atributos, se deben crear en el
contexto de un documento XML. A continuación se muestra un fragmento del código para crear un
nombre de elemento en DOM:
Dim doc As XmlDocument = New XmlDocument()
Dim name As XmlElement = doc.CreateElement("Name")
name.InnerText = "Patrick Hines"
doc.AppendChild(name)
Si desea usar un elemento en varios documentos, debe importar los nodos en los documentos. Se trata
de una complejidad innecesaria que LINQ a XML evita.

Cuando se utiliza LINQ a XML, se usa la clase XDocument solamente si se desea agregar un comentario o
una instrucción de procesamiento en el nivel de raíz del documento.

Control simplificado de nombres y espacios de nombre


Controlar nombres, espacios de nombre y prefijos de espacios de nombre suele ser una parte compleja
de la programación XML. LINQ a XML simplifica los nombres y espacios de nombre eliminando el
requisito de tratar los prefijos de espacios de nombre completamente. Si lo desea, puede controlar los
prefijos de espacios de nombre. Pero si decide no controlar explícitamente los prefijos de espacios de
nombre, durante la serialización LINQ a XML asignará prefijos de espacios de nombre si son necesarios o
serializará usando espacios de nombre predeterminados. Si se usan espacios de nombre
predeterminados, no habrá prefijos de espacios de nombre en el documento resultante.

LINQ a XML también elimina el requisito de que el usuario comprenda qué es NameTable y cómo se
usa.

Otro problema con DOM es que no permite cambiar el nombre de un nodo. En su lugar, se tiene que
crear un nuevo nodo y copiar en él todos los nodos secundarios, perdiéndose así la identidad del nodo
original. LINQ a XML evita este problema permitiendo establecer la propiedad XName en un nodo.

Compatibilidad con el método estático para cargar XML


LINQ a XML permite cargar XML usando métodos estáticos en lugar de métodos de instancia. Esto
simplifica la carga y el análisis.

Eliminación de la compatibilidad con construcciones DTD


LINQ a XML simplifica aún más la programación XML quitando la compatibilidad con entidades y
referencias a entidades. La administración de entidades es compleja y su uso es muy poco común. La
eliminación de la compatibilidad aumenta el rendimiento y simplifica la interfaz de programación. Cuando
se rellena un árbol de LINQ a XML, se expanden todas las entidades DTD.

MCT: Luis Dueñas Pag 91 de 388


Manual de LINQ

Compatibilidad con fragmentos


LINQ a XML no proporciona un equivalente para la clase XmlDocumentFragment. No obstante, es
bastante común que el concepto XmlDocumentFragment se pueda controlar mediante el resultado de
una consulta que se escribe como IEnumerable<(Of <(T>)>) de XNode o IEnumerable<(Of <(T>)>) de
XElement.

Compatibilidad con XPathNavigator


LINQ a XML proporciona compatibilidad con XPathNavigator mediante los métodos de extensión del
espacio de nombres System.Xml.XPath.

Compatibilidad con espacios en blanco y sangría


LINQ a XML trata los espacios en blanco de forma más sencilla que DOM.

Un caso muy común es aquel en el que se leen datos XML con sangría, se crea un árbol XML en memoria
sin ningún nodo de texto con espacios en blanco (es decir, sin preservar los espacios en blanco), se
realizan ciertas operaciones sobre el XML y éste se guarda con sangría. Cuando se serializa el código XML
con formato, sólo se preservan en el árbol XML los espacios en blanco más significativos. Éste es el
comportamiento predeterminado para LINQ a XML.

Otro escenario muy común es aquel en el que se lee y se modifica código XML en el que se ha aplicado
sangría de forma intencionada. Es posible que no desee realizar ningún tipo de modificación en esta
sangría. En LINQ a XML, puede llevarlo a cabo si preserva los espacios en blanco cuando cargue o analice
el código XML y deshabilite el formato al serializar el código XML.

LINQ a XML almacena un espacio en blanco como un nodo XText, en lugar de tener un tipo de nodo
Whitespace especializado, al igual que DOM.

Compatibilidad con anotaciones


Los elementos de LINQ a XML admiten un conjunto extensible de anotaciones. Esto puede resultar útil
para realizar un seguimiento de información diversa de un elemento, como la información de esquema, si
un elemento está ligado a una interfaz de usuario o cualquier otro tipo de información específica de una
aplicación.

Compatibilidad con la información de esquema


LINQ a XML proporciona compatibilidad con la validación XSD mediante métodos de extensión en el
espacio de nombres System.Xml.Schema. Puede validar que un árbol XML sea compatible con un XSD.
Puede rellenar el árbol XML con el conjunto de información posterior a la validación del esquema (PSVI).

7.1.3. Diferencias entre LINQ a XML y otras Tecnologías


XML
Este tema compara LINQ a XML con las siguientes tecnologías XML: XmlReader, XSLT, MSXML y XmlLite.
Esta información puede ayudarle a decidir la tecnología que utilizará.

Diferencias entre LINQ a XML y XmlReader


XmlReader es un analizador rápido, de sólo avance y sin almacenamiento en caché.

LINQ a XML se implementa sobre XmlReader y ambos están estrechamente integrados. No obstante,
también puede usar XmlReader de forma independiente.

MCT: Luis Dueñas Pag 92 de 388


Manual de LINQ

Aunque se superponen, LINQ a XML y XmlReader tienen diferentes escenarios de uso.

Por ejemplo, supongamos que está creando un servicio web que analizará cientos de de documentos XML
por segundo y los documentos tienen la misma estructura, por lo que sólo tiene que escribir una
implementación de código para analizar el XML. En ese caso, probablemente deseará usar XmlReader de
forma independiente.

Por el contrario, si está creando un sistema que analiza varios documentos XML más pequeños y cada
uno es diferente, quizás desee aprovechar las mejoras de productividad que LINQ a XML proporciona.

Diferencias entre LINQ a XML y XSLT


Tanto LINQ a XML como XSLT proporcionan amplias capacidades de transformación de documentos XML.
XSLT es un enfoque declarativo basado en reglas. Los programadores avanzados de XSLT escriben XSLT
en un estilo de programación funcional que resalta un enfoque sin estado, factorizando funciones puras
que se implementan sin efectos secundarios. Este enfoque basado en reglas o funcional no es familiar
para muchos desarrolladores y puede requerir bastante trabajo para su dominio.

XSLT puede ser un sistema muy productivo que produce aplicaciones de alto rendimiento. Algunas
grandes compañías web utilizan XSLT como forma de generar HTML a partir de XML extraído de varios
almacenes de datos. El motor XSLT administrado compila código XSLT a CLR y tiene un rendimiento
incluso mejor en algunos escenarios que el motor XSLT nativo.

No obstante, XSLT no aprovecha el conocimiento de C# y Visual Basic que muchos desarrolladores


tienen. Requiere que los desarrolladores escriban código en un lenguaje de programación complejo y
diferente. Usar dos sistemas de desarrollo diferentes y no integrados como C# (o Visual Basic) y XSLT
tiene como resultado sistemas de software que son más difíciles de desarrollar y mantener.

Cuando se dominen las expresiones de consulta LINQ a XML, las transformaciones de LINQ a XML son
una tecnología eficaz y fácil de usar. Básicamente, se forman documentos XML utilizando construcciones
funcionales, extrayendo datos de varios orígenes, construyendo objetos XElement dinámicamente y
ensamblando el conjunto en un nuevo árbol XML. La transformación puede generar un documento
completamente nuevo. Construir transformaciones en LINQ a XML es relativamente sencillo e intuitivo y
el código resultante es legible. Esto reduce los costos de desarrollo y mantenimiento.

LINQ a XML no se ha diseñado para sustituir a XSLT. XSLT sigue siendo la herramienta preferida de
transformaciones XML complicadas y basadas en documentos, especialmente si la estructura del
documento no está bien definida.

XSLT tiene la ventaja de ser un estándar de W3C. Si tiene el requisito de usar solamente tecnologías que
son estándares, XSLT puede ser más adecuado.

XSLT es XML y, por lo tanto, se puede manipular mediante programación.

Diferencias entre LINQ a XML y MSXML


MSXML es la tecnología basada en COM utilizada para procesar XML y que se incluye con Microsoft
Windows. MSXML ofrece una implementación nativa de DOM, que es compatible con XPath y XSLT.
También contiene el analizador basado en eventos y sin almacenamiento en caché SAX2.

MSXML tiene un buen rendimiento, es seguro de forma predeterminada en la mayoría de casos y se


puede tener acceso a él en Internet Explorer para realizar procesamiento XML en el cliente en

MCT: Luis Dueñas Pag 93 de 388


Manual de LINQ

aplicaciones de estilo AJAX. MSXML se puede usar en cualquier lenguaje de programación que admita
COM, incluyendo C++, JavaScript y Visual Basic 6.0.

No se recomienda el uso de MSXML en código administrado basado en Common Language Runtime


(CLR).

Diferencias entre LINQ a XML y XmlLite


XmlLite es un analizador de extracción sin almacenamiento en caché, de sólo avance. Los desarrolladores
utilizan principalmente XmlLite con C++. No se recomienda a los desarrolladores usar XmlLite con código
administrado.

La principal ventaja de XmlLite es que es un analizador XML rápido y ligero que es seguro en la mayoría
de casos. Su área de exposición a amenazas es muy pequeña. Si debe analizar documentos que no son
de confianza y desea protegerse de ataques tales como denegación de servicio o exposición de datos,
XmlLite puede ser una buena opción.

XmlLite no se integra con Language-Integrated Query (LINQ). No proporciona al programador mejoras


de productividad que sean la fuerza motivadora detrás de LINQ.

7.2. Guía de Programación LINQ a XML


En esta sección se ofrece información conceptual y paso a paso acerca de la programación con LINQ a
XML.

A quiénes va destinada esta documentación


Esta documentación se dirige a aquellos desarrolladores que ya conocen C# y algunos aspectos básicos
de .NET Framework.

El objetivo de esta documentación es facilitar el uso de LINQ a XML a todo tipo de desarrolladores. LINQ
a XML consigue que la programación XML sea más sencilla. No necesita ser un experto desarrollador para
utilizarla.

LINQ a XML se basa principalmente en clases genéricas. Por tanto, es muy importante que comprenda
cómo se utilizan las clases genéricas. Más aún, resultaría de utilidad que estuviera familiarizado con los
delegados que se declaran como tipos parametrizados.

7.2.1. Información General Acerca de la Programación de


LINQ a XML
Estos temas proporcionan información general de alto nivel acerca de las clases LINQ a XML, así como
información detallada de tres de las clases más importantes.

7.2.1.1. Información General de los Enfoques de


Programación de LINQ a XML
Existen varios tipos de aplicaciones XML:

Algunas aplicaciones toman los documentos XML de origen y crean nuevos documentos
XML que tienen una forma diferente que los documentos de origen.

MCT: Luis Dueñas Pag 94 de 388


Manual de LINQ

Algunas aplicaciones toman documentos XML de origen y crean documentos de resultado


de una forma totalmente diferente, como archivos HTML o de texto CSV.

Algunas aplicaciones toman documentos XML de origen e insertan registros en una base de
datos.

Algunas aplicaciones toman datos de otro origen de datos, como una base de datos y
crean documentos XML a partir de él.

Estos no son todos los tipos de aplicaciones XML, pero son un conjunto representativo de los tipos de
funcionalidad que un programador XML debe implementar.

Con todos esos tipos de aplicaciones un desarrollador puede tomar dos enfoques opuestos:

Construcción funcional usando un enfoque declarativo.

Modificación del árbol XML en memoria usando código de procedimiento.

LINQ a XML admite ambos enfoques.

Cuando se utiliza el enfoque funcional, la idea fundamental es escribir transformaciones que toman los
documentos de origen y general documentos de resultados completamente nuevos con la forma que se
desee.

Cuando se modifica un árbol XML, la idea fundamental es escribir código que atraviese y se desplace por
los nodos en un árbol XML en memoria, insertando, eliminando y modificando nodos según sea
necesario.

Puede usar LINQ a XML con cualquier enfoque. Se usan las mismas clases y, en algunos casos, los
mismos métodos. No obstante, la estructura y los objetivos de los dos enfoques son muy diferentes.

Los dos enfoques suelen tener diferentes perfiles de rendimiento. En situaciones diferentes, uno u otro
enfoque tendrá a menudo un rendimiento mejor y usará más o menos memoria.

Asimismo, uno u otro enfoque será más fácil de escribir y proporcionará código más fácil de mantener.

7.2.1.2. Información General de las Clases LINQ a XML


En este tema se proporciona una lista de las clases de LINQ a XML en el espacio de nombres
System.Xml.Linq y proporciona una breve explicación de cada una.

Clases de LINQ a XML


Clase XAttribute
XAttribute representa un atributo XML.

Clase XCData
XCData representa un nodo de texto CDATA.

Clase XComment
XComment representa un comentario XML.

Clase XContainer

MCT: Luis Dueñas Pag 95 de 388


Manual de LINQ

XContainer es una clase base abstracta para todos los nodos que pueden tener nodos secundarios. Las
siguientes clases se derivan de la clase XContainer:
XElement
XDocument
Clase XDeclaration
XDeclaration representa una declaración XML. Una declaración XML se utiliza para declarar la versión de
XML y la codificación de un documento. Asimismo, una declaración XML especifica si el documento XML
es independiente.

Clase XDocument
XDocument representa un documento XML.

Clase XDocumentType
XDocumentType representa una definición de tipo del documento (DTD) XML.

Clase XElement
XElement representa un elemento XML.

Clase XName
XName representa nombres de elementos (XElement) y atributos (XAttribute).

LINQ a XML se ha diseñado para hacer que los nombres XML sean tan sencillos como sea posible. Debido
a su complejidad, los nombres XML a menudo se consideran un tema avanzado en XML. Puede
argumentarse que la complejidad no proviene de los espacios de nombres, que los desarrolladores usan
regularmente en la programación, sino de los prefijos de los espacios de nombres. Los prefijos de los
espacios de nombres pueden ser útiles para reducir las pulsaciones de teclas necesarias cuando se
especifica código XML o hacer que el código XML sea más fácil de leer. No obstante, a menudo los
prefijos son un acceso directo al espacio de nombres XML completo y no son necesarios en la mayoría de
los casos. LINQ a XML simplifica los nombres XML resolviendo todos los prefijos a su espacio de nombres
XML correspondiente. Los prefijos están disponibles, si son necesarios, a través del método
GetPrefixOfNamespace.

Si es necesario es posible controlar los prefijos de espacios de nombres. En algunas circunstancias, si


trabaja con otros sistemas XML, como XSLT o XAML, debe controlar los prefijos de espacio de nombres.
Por ejemplo, si tiene una expresión XPath que utiliza los prefijos del espacio de nombre que está
incrustada en una hoja de estilos XSLT, deberá asegurarse de que el documento XML esté serializado con
prefijos de espacio de nombres que coinciden con los que se utilizan en la expresión XPath.

Clase XNamespace
XNamespace representa un espacio de nombres para un XElement o XAttribute. Los espacios de nombres
son un componente de un XName.

Clase XNode
XNode es una clase abstracta que representa los nodos de un árbol XML. Las siguientes clases se derivan
de la clase XNode:
XText
XContainer
XComment
XProcessingInstruction
XDocumentType
Clase XNodeDocumentOrderComparer

MCT: Luis Dueñas Pag 96 de 388


Manual de LINQ

XNodeDocumentOrderComparer proporciona la funcionalidad para comparar el orden de documentos de


los nodos.

Clase XNodeEqualityComparer
XNodeEqualityComparer proporciona la funcionalidad para comparar la igualdad del valor de los nodos.

Clase XObject
XObject es una clase base abstracta de XNode y XAttribute. Proporciona funcionalidad de evento y
anotación.

Clase XObjectChange
XObjectChange especifica el tipo de evento cuando se produce para un XObject.

Clase XObjectChangeEventArgs
XObjectChangeEventArgs proporciona datos para los eventos Changing y Changed.

Clase XProcessingInstruction
XProcessingInstruction representa una instrucción de procesamiento de XML. Una instrucción de
procesamiento comunica información a una aplicación que procesa el XML.

Clase XText
XText representa un nodo de texto. En la mayoría de casos no tiene que usar esta clase. Esta clase se
utiliza principalmente para el contenido mixto.

7.2.1.3. Información General Acerca de la Clase XElement


La clase XElement es una de las clases fundamentales de LINQ a XML. Representa a un elemento XML.
Puede utilizar esta clase para crear elementos; cambiar el contenido del elemento; agregar, modificar o
eliminar elementos secundarios; agregar atributos a un elemento; o serializar el contenido de un
elemento en forma de texto. También puede operar con otras clases de System.Xml, como son
XmlReader, XmlWriter y XslCompiledTransform.

Funcionalidad de XElement
Este tema describe la funcionalidad que ofrece la clase XElement.

Construir árboles XML


Es posible construir árboles XML de diferentes formas, entre las que se incluyen las siguientes:

Puede construir un árbol XML mediante código.

Puede analizar XML a partir de diferentes orígenes, incluyendo un TextReader, archivos de texto
o direcciones Web (URL).

También puede utilizar un XmlReader para rellenar el árbol.

Si dispone de un módulo que pueda escribir contenidos en un XmlWriter, puede utilizar el


método CreateWriter para crear un sistema de escritura, para pasar éste al módulo y para
utilizar después el contenido que se haya escrito en XmlWriter para rellenar el árbol XML.

No obstante, la forma más habitual de crear un árbol XML es la siguiente:


Dim contacts As XElement = _
<Contacts>

MCT: Luis Dueñas Pag 97 de 388


Manual de LINQ

<Contact>
<Name>Patrick Hines</Name>
<Phone>206-555-0144</Phone>
<Address>
<Street1>123 Main St</Street1>
<City>Mercer Island</City>
<State>WA</State>
<Postal>68042</Postal>
</Address>
</Contact>
</Contacts>
Otra técnica utilizada con frecuencia para crear un árbol XML implica el uso de los resultados de una
consulta LINQ para rellenar el árbol XML, tal y como se muestra en el ejemplo siguiente:
Dim srcTree As XElement = _
<Root>
<Element>1</Element>
<Element>2</Element>
<Element>3</Element>
<Element>4</Element>
<Element>5</Element>
</Root>
Dim xmlTree As XElement = _
<Root>
<Child>1</Child>
<Child>2</Child>
<%= From el In srcTree.Elements() _
Where el.Value > 2 _
Select el %>
</Root>
Console.WriteLine(xmlTree)
Este ejemplo genera el siguiente resultado:
Xml
<Root>
<Child>1</Child>
<Child>2</Child>
<Element>3</Element>
<Element>4</Element>
<Element>5</Element>
</Root>
Serializar árboles XML
Puede serializar un árbol XML en un File, un TextWriter o en un XmlWriter.

Recuperar datos XML mediante los métodos de los ejes


Puede utilizar los métodos de los ejes para recuperar atributos, elementos secundarios, elementos
descendientes y elementos antecesores. Las consultas LINQ utilizan los métodos de los ejes y
proporcionan numerosos mecanismos, flexibles y potentes, para recorrer y procesar árboles XML.

Consultar árboles XML


Puede escribir consultas LINQ que extraigan datos de un árbol XML.

MCT: Luis Dueñas Pag 98 de 388


Manual de LINQ

Modificar árboles XML


Puede modificar un elemento de diferentes maneras, incluyendo el cambiar su contenido o sus atributos.
También puede eliminar un elemento dependiente de un elemento primario.

7.2.1.4. Información General Acerca de la Clase XAttribute


Los atributos son pares de nombre y valor asociados a un elemento. La clase XAttribute representa los
atributos XML.

Información general
Trabajar con atributos en LINQ a XML es similar a trabajar con elementos. Sus constructores son
similares. Los métodos que usa para recuperar colecciones de ellos también son similares. Una expresión
de consulta LINQ de una colección de atributos se parece mucho a una expresión de consulta LINQ de
una colección de elementos.

Se conserva el orden en el que se agregaron los atributos a un elemento. Es decir, cuando procese una
iteración en los atributos, los verá en el mismo orden en el que se agregaron.

El constructor XAttribute
El siguiente constructor de la clase XAttribute es el que usará normalmente:

Constructor Descripción

XAttribute(XName name, Crea un objeto XAttribute. El argumento name especifica el nombre


object content) del atributo; content especifica el contenido del atributo.

Crear un elemento con un atributo


El siguiente código muestra la tarea habitual de crear un elemento que contiene un atributo:

Con Visual Basic, puede usar los literales XML:


Dim phone As XElement = <Phone Type="Home">555-555-5555</Phone>
Console.WriteLine(phone)
Este ejemplo genera el siguiente resultado:
Xml
<Phone Type="Home">555-555-5555</Phone>
Construcción funcional de los atributos
Puede construir objetos XAttribute de modo similar a la construcción de los objetos XElement, tal como
se indica a continuación:
Dim c As XElement = _
<Customers>
<Customer>
<Name>John Doe</Name>
<PhoneNumbers>
<Phone type="home">555-555-5555</Phone>
<Phone type="work">666-666-6666</Phone>
</PhoneNumbers>
</Customer>
</Customers>
Console.WriteLine(c)
Este ejemplo genera el siguiente resultado:
Xml
<Customers>

MCT: Luis Dueñas Pag 99 de 388


Manual de LINQ

<Customer>
<Name>John Doe</Name>
<PhoneNumbers>
<Phone type="home">555-555-5555</Phone>
<Phone type="work">666-666-6666</Phone>
</PhoneNumbers>
</Customer>
</Customers>
Los atributos no son nodos
Existen algunas diferencias entre los atributos y los elementos. Los objetos XAttribute no son nodos en el
árbol XML. Son pares de nombre y valor asociados a un elemento XML. A diferencia de Document Object
Model (DOM), esto refleja de manera más precisa la estructura del código XML. Aunque los objetos
XAttribute no son realmente nodos en el árbol XML, trabajar con objetos XAttribute es muy similar a
trabajar con objetos XElement.

Esta distinción sólo resulta de importancia para los desarrolladores que escriban código que trabaje con
árboles XML en el nivel de nodo. A muchos desarrolladores no les afecta esta distinción.

7.2.1.5. Información General Acerca de la Clase XDocument


En este tema se presenta la clase XDocument.

Información general acerca de la clase XDocument


La clase XDocument contiene la información necesaria para un documento XML válido. Incluye una
declaración XML, instrucciones de procesamiento y comentarios.

Tenga en cuenta que sólo debe crear objetos XDocument si necesita la funcionalidad específica que
proporciona la clase XDocument. En muchas circunstancias, puede trabajar directamente con XElement.
Trabajar directamente con XElement constituye un modelo de programación más simple.

XDocument se deriva de XContainer. Por lo tanto, puede contener nodos secundarios. Sin embargo, los
objetos XDocument sólo pueden tener un nodo XElement secundario. Esto refleja el estándar XML: sólo
puede haber un elemento raíz en un documento XML.

Componentes de XDocument
Un objeto XDocument puede contener los siguientes elementos:

Un objeto XDeclaration. XDeclaration permite especificar las partes pertinentes de una


declaración XML: la versión XML, la codificación del documento y si el documento XML es
independiente.

Un objeto XElement. Es el nodo raíz del documento XML.

Cualquier número de objetos XProcessingInstruction. Una instrucción de procesamiento


comunica información a una aplicación que procesa el XML.

Cualquier número de objetos XComment. Los comentarios serán del mismo nivel que el
elemento raíz. El objeto XComment no puede ser el primer argumento de la lista, ya que no es
válido que el documento XML empiece con un comentario.

Un elemento XDocumentType para el DTD.

MCT: Luis Dueñas Pag 100 de 388


Manual de LINQ

Al serializar un objeto XDocument, aunque XDocument.Declaration sea null, el resultado generará


una declaración XML siempre y cuando el redactor haya establecido
Writer.Settings.OmitXmlDeclaration en false (valor predeterminado).

De manera predeterminada, LINQ a XML establece la versión en "1.0" y la codificación en "utf-8".

Usar XElement sin XDocument


Tal como se indicó anteriormente, la clase XElement es la clase principal de la interfaz de programación
LINQ a XML. En muchos casos, la aplicación no requerirá la creación de un documento. Mediante la clase
XElement, puede crear un árbol XML, agregarle otros árboles XML, modificar el árbol XML o guardarlo.

Usar XDocument
Para construir un objeto XDocument, use la construcción funcional, al igual que cuando se construyen
objetos XElement.

El código siguiente crea un objeto XDocument y sus objetos contenidos asociados.


Dim doc As XDocument = <?xml version="1.0" encoding="utf-8"?>
<!--This is a comment.-->
<?xml-stylesheet href='mystyle.css' title='Compact' type='text/css'?>
<Pubs>
<Book>
<Title>Artifacts of Roman Civilization</Title>
<Author>Moreno, Jordao</Author>
</Book>
<Book>
<Title>Midieval Tools and Implements</Title>
<Author>Gazit, Inbar</Author>
</Book>
</Pubs>
<!--This is another comment.-->
doc.Save("test.xml")

7.2.1.6. Cómo Crear Ejemplos de LINQ a XML


Los diferentes fragmentos de código y ejemplos de esta documentación utilizan clases y tipos de
diferentes espacios de nombres. Cuando se compila código C#, se deben proporcionar las directivas
using adecuadas. Cuando se compila código de Visual Basic, se deben proporcionar instrucciones
Imports adecuadas.

Ejemplo
El siguiente código contiene las instrucciones Imports que los ejemplos de Visual Basic requieren para
crearse y ejecutarse. No todas las instrucciones Imports son necesarias para cada ejemplo.
Imports System
Imports System.Diagnostics
Imports System.Collections
Imports System.Collections.Generic
Imports System.Collections.Specialized
Imports System.Text
Imports System.Linq
Imports System.Xml

MCT: Luis Dueñas Pag 101 de 388


Manual de LINQ

Imports System.Xml.Linq
Imports System.Xml.Schema
Imports System.Xml.XPath
Imports System.Xml.Xsl
Imports System.IO
Imports System.Threading
Imports System.Reflection
Imports System.IO.Packaging

7.2.2. Crear Arboles XML


Una de las tareas XML más habituales es la construcción de un árbol XML. Esta sección describe varias
formas de crearlos.

7.2.2.1. Construcción Funcional


LINQ a XML proporciona una forma eficaz de crear elementos XML denominados construcción funcional.
La construcción funcional es la capacidad de crear un árbol XML en una sola instrucción.

Hay varias características fundamentales de la interfaz de programación de LINQ a XML que permiten la
construcción funcional:

El constructor XElement toma varios tipos de argumentos para el contenido. Por ejemplo, puede
pasar otro objeto XElement, que se convierte en un elemento secundario. Puede pasar un objeto
XAttribute, que se convierte en un atributo del elemento. O bien puede pasar cualquier otro tipo
de objeto, que se convierte en una cadena y en el contenido de texto del elemento.

El constructor XElement toma una matriz de params del tipo Object, de forma que puede pasar
cualquier número de objetos al constructor. Esto permite crear un elemento que tiene un
contenido complejo.

Si un objeto implementa IEnumerable<(Of <(T>)>), se enumera la recopilación del objeto y se


agregan todos los elementos de la recopilación. Si la recopilación contiene objetos XElement o
XAttribute, cada elemento de la recopilación se agrega por separado. Esto es importante porque
permite pasar los resultados de una consulta de LINQ al constructor.

Estas características permiten escribir código para crear un árbol XML. A continuación se muestra un
ejemplo:
Dim srcTree As XElement = _
<Root>
<Element>1</Element>
<Element>2</Element>
<Element>3</Element>
<Element>4</Element>
<Element>5</Element>
</Root>
Dim xmlTree As XElement = _
<Root>
<Child>1</Child>
<Child>2</Child>
<%= From el In srcTree.Elements() _

MCT: Luis Dueñas Pag 102 de 388


Manual de LINQ

Where CInt(el) > 2 _


Select el %>
</Root>
Console.WriteLine(xmlTree)

7.2.2.2. Introducción a los Literales XML en Visual Basic


En esta sección encontrará información acerca de cómo crear árboles XML en Visual Basic.

Crear árboles XML


El siguiente ejemplo muestra cómo crear un XElement, en este caso, contacts:
Dim contacts As XElement = _
<Contacts>
<Contact>
<Name>Patrick Hines</Name>
<Phone>206-555-0144</Phone>
<Address>
<Street1>123 Main St</Street1>
<City>Mercer Island</City>
<State>WA</State>
<Postal>68042</Postal>
</Address>
</Contact>
</Contacts>
Crear un XElement con contenido simple
Puede crear un XElement que incluya un contenido simple, tal y como se detalla a continuación:

Dim n as XElement = <Customer>Adventure Works</Customer>


Console.WriteLine(n)
Este ejemplo genera el siguiente resultado:
Xml
<Customer>Adventure Works</Customer>
Crear un elemento vacío
Puede crear un XElement vacío, tal y como se indica a continuación:
Dim n As XElement = <Customer/>
Console.WriteLine(n)
Este ejemplo genera el siguiente resultado:
Xml
<Customer />
Utilizar expresiones incrustadas
Una característica importante de los literales XML es que admiten el uso de expresiones incrustadas. Las
expresiones incrustadas le permiten evaluar una expresión e incorporar los resultados de la expresión en
un árbol XML. Si el resultado de evaluar la expresión es de tipo XElement, se agregará un elemento al
árbol. Si el resultado de evaluar la expresión es de tipo XAttribute, se agregará un atributo al árbol.
Puede agregar elementos y atributos al árbol sólo en aquellos lugares donde sea válido.

Es importante reseñar que en una expresión incrustada sólo puede aparecer una expresión única. No es
posible incrustar varias instrucciones. Si una expresión se extiende más allá de una única línea, deberá
utilizar el carácter de continuación de línea.

MCT: Luis Dueñas Pag 103 de 388


Manual de LINQ

Si utiliza una expresión incrustada para agregar nodos (incluyendo elementos) y atributos ya existentes
a nuevo árbol XML y los nodos existentes ya tienen elementos primarios, los nodos se clonarán. Esos
nodos clonados nuevos se agregan al nuevo árbol XML. Si los nodos ya existentes no tienen elementos
primarios, los nodos simplemente se agregan al nuevo árbol XML. El último ejemplo de este tema
muestra este comportamiento.

El siguiente ejemplo utiliza una expresión incrustada para agregar un elemento al árbol:
Dim xmlTree1 As XElement = _
<Root>
<Child>Contents</Child>
</Root>
Dim xmlTree2 As XElement = _
<Root>
<%= xmlTree1.<Child> %>
</Root>
Console.WriteLine(xmlTree2)
Utilizar expresiones incrustadas para el contenido
Puede utilizar una expresión incrustada para proporcionar el contenido de un elemento:
Dim str As String
str = "Some content"
Dim root As XElement = <Root><%= str %></Root>
Console.WriteLine(root)
Este ejemplo genera el siguiente resultado:
Xml
<Root>Some content</Root>
Usar una consulta de LINQ en una expresión incrustada
Puede utilizar los resultados proporcionados por una consulta LINQ para el contenido de un elemento:
Dim arr As Integer() = {1, 2, 3}
Dim n As XElement = _
<Root>
<%= From i In arr Select <Child><%= i %></Child> %>
</Root>
Console.WriteLine(n)
Utilizar expresiones incrustadas para los nombres de los nodos
También puede utilizar expresiones incrustadas para calcular nombres de atributos, valores de atributos,
nombres de elementos y valores de elementos:
Dim eleName As String = "ele"
Dim attName As String = "att"
Dim attValue As String = "aValue"
Dim eleValue As String = "eValue"
Dim n As XElement = _
<Root <%= attName %>=<%= attValue %>>
<<%= eleName %>>
<%= eleValue %>
</>
</Root>
Console.WriteLine(n)
Este ejemplo genera el siguiente resultado:
Xml
<Root att="aValue">

MCT: Luis Dueñas Pag 104 de 388


Manual de LINQ

<ele>eValue</ele>
</Root>
Diferencias entre agregar y clonar
Como ya se mencionó anteriormente, si utiliza una expresión incrustada para agregar nodos (incluyendo
elementos) y atributos ya existentes a nuevo árbol XML y los nodos existentes ya tienen elementos
primarios, los nodos se clonarán y esos nuevos nodos clonados se agregarán al nuevo árbol XML. Si los
nodos ya existentes no tienen elementos primarios, simplemente se agregan al nuevo árbol XML.
' Create a tree with a child element
Dim xmlTree1 As XElement = _
<Root>
<Child1>1</Child1>
</Root>
' Create an element that is not parented
Dim child2 As XElement = <Child2>2</Child2>
' Create a tree and add Child1 and Child2 to it
Dim xmlTree2 As XElement = _
<Root>
<%= xmlTree1.<Child1>(0) %>
<%= child2 %>
</Root>
' Compare Child1 identity.
Console.WriteLine("Child1 was {0}", _
IIf(xmlTree1.Element("Child1") Is xmlTree2.Element("Child1"), _
"attached", "cloned"))
' Compare Child2 identity.
Console.WriteLine("Child2 was {0}", _
IIf(child2 Is xmlTree2.Element("Child2"), _
"attached", "cloned"))

7.2.2.3. Diferencias entre Agregar y Clonar


Cuando se agregan objetos XNode (incluyendo el objeto XElement) o XAttribute a un árbol nuevo, si el
contenido nuevo no tiene un elemento primario, los objetos simplemente se agregan al árbol XML. Si el
contenido nuevo ya tiene un elemento primario y forma parte de otro árbol XML, el nuevo contenido se
clonará y ese clon se agregará al árbol XML.

Ejemplo
El siguiente código demuestra qué ocurre si agrega un elemento que tiene elemento primario a un árbol
y qué ocurre si agrega un elemento que no tenga elemento primario a un árbol.
' Create a tree with a child element
Dim xmlTree1 As XElement = _
<Root>
<Child1>1</Child1>
</Root>
' Create an element that is not parented
Dim child2 As XElement = <Child2>2</Child2>
' Create a tree and add Child1 and Child2 to it
Dim xmlTree2 As XElement = _
<Root>
<%= xmlTree1.<Child1>(0) %>

MCT: Luis Dueñas Pag 105 de 388


Manual de LINQ

<%= child2 %>


</Root>
' compare Child1 identity
Console.WriteLine("Child1 was {0}", _
IIf(xmlTree1.Element("Child1") Is xmlTree2.Element("Child1"), _
"attached", "cloned"))
' compare Child2 identity
Console.WriteLine("Child2 was {0}", _
IIf(child2 Is xmlTree2.Element("Child2"), _
"attached", "cloned"))

7.2.2.4. Analizar XML


Los temas de esta sección describen cómo analizar documentos XML.

7.2.2.4.1. Cómo Analizar una Cadena


Este tema muestra cómo analizar una cadena para crear un árbol XML en C# y en Visual Basic.

Ejemplo
Puede analizar una cadena en Visual Basic de forma similar. Sin embargo, resulta más eficaz usar
literales XML, tal como se muestra en el código siguiente, ya que dichos literales no sufren las mismas
pérdidas de rendimiento que se producen al analizar XML de una cadena.

Mediante los literales XML, puede copiar y pegar XML en el programa de Visual Basic.

Nota:

Analizar texto o cargar un documento XML de un archivo de texto es menos eficaz que la
construcción funcional. Si inicializa un árbol XML a partir de código, el uso de la construcción
funcional consume menos tiempo de procesador que el análisis de texto.

Dim contacts as XElement = _


<Contacts>
<Contact>
<Name>Patrick Hines</Name>
<Phone Type="home">206-555-0144</Phone>
<Phone Type="work">425-555-0145</Phone>
<Address>
<Street1>123 Main St</Street1>
<City>Mercer Island</City>
<State>WA</State>
<Postal>68042</Postal>
</Address>
<NetWorth>10</NetWorth>
</Contact>
<Contact>
<Name>Gretchen Rivas</Name>
<Phone Type="mobile">206-555-0163</Phone>
<Address>
<Street1>123 Main St</Street1>
<City>Mercer Island</City>

MCT: Luis Dueñas Pag 106 de 388


Manual de LINQ

<State>WA</State>
<Postal>68042</Postal>
</Address>
<NetWorth>11</NetWorth>
</Contact>
</Contacts>

7.2.2.4.2. Cómo Cargar XML de un Archivo


En este tema se muestra cómo cargar XML de una dirección URI usando el método XElement..::.Load.

Ejemplo
El siguiente ejemplo muestra cómo cargar un documento XML desde un archivo. El siguiente ejemplo
carga books.xml y produce el árbol XML en la consola.
Dim booksFromFile As XElement = XElement.Load("books.xml")
Console.WriteLine(booksFromFile)

7.2.2.4.3. Conservar Espacios en Blanco mientras se Carga


o se Analiza el XML
En este tema se describe cómo controlar el comportamiento de los espacios en blanco de LINQ a XML.

Un caso muy común es aquel en el que se leen datos XML con sangría, se crea un árbol XML en memoria
sin ningún nodo de texto con espacios en blanco (es decir, sin preservar los espacios en blanco), se
realizan ciertas operaciones sobre el XML y éste se guarda con sangría. Si se serializa el código XML con
formato, sólo se preservan en el árbol XML aquellos espacios en blanco más significativos. Éste es el
comportamiento predeterminado para LINQ a XML.

Otro caso muy común es aquel en el que se lee y se modifica código XML en el que se ha aplicado
sangría de forma intencionada. Es posible que no desee modificar esta sangría de ninguna forma. En
LINQ a XML, puede conseguirlo si preserva los espacios en blanco a la hora de cargar o analizar el código
XML y si deshabilita el formato cuando serializa el código XML.

En este tema se describe el comportamiento de espacios en blanco de métodos que rellenan los árboles
XML.

Comportamiento de métodos que rellenan árboles XML


Los siguientes métodos de las clases XElement y XDocument rellenan un árbol XML. Puede rellenar un
árbol XML desde un archivo, un TextReader, un XmlReader o una cadena:
XElement..::.Load
XElement..::.Parse
XDocument..::.Load
XDocument..::.Parse
Si el método no toma LoadOptions como argumento, el método no conservará espacios en blanco
insignificantes.

En la mayoría de casos, si el método toma LoadOptions como argumento, opcionalmente se pueden


conservar los espacios en blanco insignificantes como nodos de texto en el árbol XML. No obstante, si el
método carga XML desde XmlReader, XmlReader determina si los espacios en blanco se conservan.
Establecer PreserveWhitespace no tendrá ningún efecto.

MCT: Luis Dueñas Pag 107 de 388


Manual de LINQ

Con estos métodos, si se conservan los espacios en blanco, se inserta un espacio en blanco insignificante
en el árbol XML como nodos XText. Si no se conservan los espacios en blanco, entonces no se insertan
los nodos de texto.

Puede crear un árbol XML usando XmlWriter. Los nodos escritos en XmlWriter se rellenan en el árbol. No
obstante, al crear un árbol XML usando este método se conservan todos los nodos, independientemente
de si el nodo es un espacio en blanco o insignificante.

7.2.2.4.4. Cómo Detectar Errores de Análisis


En este tema se describe cómo detectar XML no válido o mal formado.

LINQ a XML se implementa sobre XmlReader. Si se pasa XML no válido o mal formado a LINQ a XML, la
clase XmlReader subyacente iniciará una excepción. Los diferentes métodos que analizan XML, como
XElement..::.Parse no detectan la excepción; la excepción se propaga de forma que puede detectarla.

Tenga en cuenta que no puede obtener errores de análisis si utiliza literales XML. El compilador de Visual
Basic detectará errores de XML no válido o mal formado.

Ejemplo
El siguiente código intenta analizar XML no válido:
Try
Dim contacts As XElement = XElement.Parse("<Contacts>" & vbCrLf & _
" <Contact>" & vbCrLf & _
" <Name>Jim Wilson</Name>" & vbCrLf & _
" </Contact>" & vbCrLf & _
"</Contcts>")
Console.WriteLine(contacts)
Catch e As System.Xml.XmlException
Console.WriteLine(e.Message)
End Try
Cuando ejecuta este código, devuelve la siguiente excepción:
The 'Contacts' start tag on line 1 does not match the end tag of
'Contcts'. Line 5, position 13.

7.2.2.4.5. Cómo Crear un Arbol de XmlReader


En este tema se muestra cómo crear un árbol XML directamente de XmlReader. Para crear un XElement
de XmlReader, debe colocar el XmlReader en un nodo de elemento. XmlReader omitirá los comentarios y
las instrucciones de procesamiento, pero si XmlReader se coloca en un nodo de texto, se producirá un
error. Para evitar tales errores, coloque siempre XmlReader en un elemento ante de crear un árbol XML
de XmlReader.

Ejemplo
El siguiente código crea un objeto T:System.Xml.XmlReader y lee nodos hasta que encuentra el primer
nodo de elemento. A continuación, carga el objeto XElement.
Dim r As XmlReader = XmlReader.Create("books.xml")
Do While r.NodeType <> XmlNodeType.Element
r.Read()
Loop
Dim e As XElement = XElement.Load(r)

MCT: Luis Dueñas Pag 108 de 388


Manual de LINQ

Console.WriteLine(e)

7.2.2.4.6. Cómo Transmitir por Secuencias Fragmentos


XML de XmlReader
Cuando tiene que procesar archivos XML de gran tamaño quizás no es factible cargar la totalidad del
árbol XML en memoria. En este tema se muestra cómo transmitir por secuencias fragmentos usando
XmlReader.

Una de las formas más efectivas de usar XmlReader para leer objetos XElement es escribir un método de
eje personalizado propio. Un método de eje suele devolver una recopilación como IEnumerable<(Of
<(T>)>) de XElement, tal y como se muestra en el ejemplo de este tema. En el método de eje
personalizado, tras crear el fragmento XML llamando al método ReadFrom, devuelva la recopilación
usando yield return. Esto proporciona semántica de ejecución aplazada al método de eje personalizado.

Cuando crea un árbol XML de un objeto XmlReader, XmlReader debe estar posicionado en un elemento.
El método ReadFrom no vuelve hasta que ha leído la etiqueta de cierre del elemento.

Si desea crear un árbol parcial, puede crear una instancia de un XmlReader, colocar el lector en el nodo
que desea convertir a un árbol XElement y después crear el objeto XElement.

Ejemplo
Este ejemplo crea un método de eje personalizado. Puede consultarlo usando una consulta de LINQ. El
método de eje personalizado, StreamRootChildDoc, es un método que está específicamente diseñado
para leer un documento que tiene un elemento Child que se repite.

Nota:

En el ejemplo de código siguiente se utiliza la construcción yield return de C#. Puesto que no hay
ninguna característica equivalente en Visual Basic 2008, este ejemplo sólo se proporciona en C#.

static IEnumerable<XElement> StreamRootChildDoc(StringReader


stringReader)
{
using (XmlReader reader = XmlReader.Create(stringReader))
{
reader.MoveToContent();
// Parse the file and display each of the nodes.
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
if (reader.Name == "Child") {
XElement el = XElement.ReadFrom(reader) as XElement;
if (el != null)
yield return el;
}
break;
}
}

MCT: Luis Dueñas Pag 109 de 388


Manual de LINQ

}
}
static void Main(string[] args)
{
string markup = @"<Root>
<Child Key=""01"">
<GrandChild>aaa</GrandChild>
</Child>
<Child Key=""02"">
<GrandChild>bbb</GrandChild>
</Child>
<Child Key=""03"">
<GrandChild>ccc</GrandChild>
</Child>
</Root>";
IEnumerable<string> grandChildData =
from el in StreamRootChildDoc(new StringReader(markup))
where (int)el.Attribute("Key") > 1
select (string)el.Element("GrandChild");
foreach (string str in grandChildData) {
Console.WriteLine(str);
}
}
Este ejemplo genera el siguiente resultado:
bbb
ccc
En este ejemplo el documento de origen es muy pequeño. No obstante, aunque hubiera millones de
elementos Child, este ejemplo seguiría teniendo una superficie de memoria pequeña.

7.2.2.5. Cómo Rellenar un Arbol XML Con XmlWriter


Una forma de rellenar un árbol XML consiste en utilizar CreateWriter para crear XmlWriter y después
escribir en XmlWriter. El árbol XML se rellena con todos los nodos que se escriben en XmlWriter.

Normalmente se utiliza este método cuando se utiliza LINQ a XML con otra clase que espera escribir en
un XmlWriter, como XslCompiledTransform.

Ejemplo
Un uso posible para CreateWriter es la invocación de una transformación XSLT. Este ejemplo crea un
árbol XML, crea un XmlReader del árbol XML, crea un nuevo documento y después crea un XmlWriter
para escribir en el nuevo documento. A continuación invoca la transformación XSLT, pasando XmlReader
y XmlWriter. Después de que se complete correctamente la transformación, se rellenará el nuevo árbol
XML con los resultados de la transformación.
Dim xslMarkup As XDocument = _
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
version='1.0'>
<xsl:template match='/Parent'>
<Root>
<C1>

MCT: Luis Dueñas Pag 110 de 388


Manual de LINQ

<xsl:value-of select='Child1'/>
</C1>
<C2>
<xsl:value-of select='Child2'/>
</C2>
</Root>
</xsl:template>
</xsl:stylesheet>
Dim xmlTree As XDocument = _
<?xml version='1.0'?>
<Parent>
<Child1>Child1 data</Child1>
<Child2>Child2 data</Child2>
</Parent>
Dim newTree As XDocument = New XDocument()
Using writer As XmlWriter = newTree.CreateWriter()
' Load the style sheet.
Dim xslt As XslCompiledTransform = New XslCompiledTransform()
xslt.Load(xslMarkup.CreateReader())
' Execute the transformation and output the results to a writer.
xslt.Transform(xmlTree.CreateReader(), writer)
End Using
Console.WriteLine(newTree)

7.2.2.6. Cómo Validar con XSD


El espacio de nombres System.Xml.Schema contiene métodos de extensión que hacen que sea fácil
validar un árbol XML contra un archivo de lenguaje de definición de esquemas XML (XSD). Para obtener
más información, vea la documentación del método Validate.

Ejemplo
En el ejemplo siguiente se crea un XmlSchemaSet, a continuación, se validan dos objetos XDocument
con el esquema establecido. Uno de los documentos es válido, el otro no.
Dim errors As Boolean = False
Private Sub XSDErrors(ByVal o As Object, ByVal e As ValidationEventArgs)
Console.WriteLine("{0}", e.Message)
errors = True
End Sub
Sub Main()
Dim xsdMarkup As XElement = _
<xsd:schema xmlns:xsd='http://www.w3.org/2001/XMLSchema'>
<xsd:element name='Root'>
<xsd:complexType>
<xsd:sequence>
<xsd:element name='Child1' minOccurs='1' maxOccurs='1'/>
<xsd:element name='Child2' minOccurs='1' maxOccurs='1'/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>

MCT: Luis Dueñas Pag 111 de 388


Manual de LINQ

Dim schemas As XmlSchemaSet = New XmlSchemaSet()


schemas.Add("", xsdMarkup.CreateReader)
Dim doc1 As XDocument = _
<?xml version='1.0'?>
<Root>
<Child1>content1</Child1>
<Child2>content1</Child2>
</Root>
Dim doc2 As XDocument = _
<?xml version='1.0'?>
<Root>
<Child1>content1</Child1>
<Child3>content1</Child3>
</Root>
Console.WriteLine("Validating doc1")
errors = False
doc1.Validate(schemas, AddressOf XSDErrors)
Console.WriteLine("doc1 {0}", IIf(errors = True, "did not validate",
"validated"))
Console.WriteLine()
Console.WriteLine("Validating doc2")
errors = False
doc2.Validate(schemas, AddressOf XSDErrors)
Console.WriteLine("doc2 {0}", IIf(errors = True, "did not validate",
"validated"))
End Sub
Este ejemplo genera el siguiente resultado:
Validating doc1
doc1 validated
Validating doc2
The element 'Root' has invalid child element 'Child3'. List of possible
elements expected: 'Child2'.
doc2 did not validate
El siguiente ejemplo valida que el documento XML de Archivo XML de muestra: clientes y pedidos (LINQ
a XML) es válido de acuerdo con el esquema de Archivo XSD de muestra: clientes y pedidos. A
continuación modifica el documento XML de origen. Cambia el atributo CustomerID del primer cliente.
Tras el cambio, los pedidos harán referencia a un cliente que no existe, de forma que el documento XML
ya no se validará.
Dim errors As Boolean = False
Private Sub XSDErrors(ByVal o As Object, ByVal e As ValidationEventArgs)
Console.WriteLine("{0}", e.Message)
errors = True
End Sub
Sub Main()
Dim schemas As XmlSchemaSet = New XmlSchemaSet()
schemas.Add("", "CustomersOrders.xsd")
Console.WriteLine("Attempting to validate")
Dim custOrdDoc As XDocument = XDocument.Load("CustomersOrders.xml")
errors = False
custOrdDoc.Validate(schemas, AddressOf XSDErrors)

MCT: Luis Dueñas Pag 112 de 388


Manual de LINQ

Console.WriteLine("custOrdDoc {0}", IIf(errors, "did not validate",


"validated"))
Console.WriteLine()
' modify the source document so that it will not validate.
custOrdDoc.<Root>.<Orders>.<Order>.<CustomerID>(0).Value = "AAAAA"
Console.WriteLine("Attempting to validate after modification")
errors = False
custOrdDoc.Validate(schemas, AddressOf XSDErrors)
Console.WriteLine("custOrdDoc {0}", IIf(errors, "did not validate",
"validated"))
End Sub
Este ejemplo genera el siguiente resultado:
Attempting to validate
custOrdDoc validated
Attempting to validate after modification
The key sequence 'AAAAA' in Keyref fails to refer to some key.
custOrdDoc did not validate

7.2.2.7. Contenido Válido de Objetos XElement y


XDocument
Este tema detalla los argumentos válidos que se pueden pasar a los constructores y los métodos que se
usan para agregar contenido a elementos y documentos.

Contenido válido
Puesto que las consultas a menudo se evalúan como IEnumerable<(Of <(T>)>) de XElement o
IEnumerable<(Of <(T>)>) de XAttribute, resulta conveniente pasar los resultados de una consulta como
contenido a los métodos y constructores que use para rellenar árboles XML.

Al agregar contenido simple, se pueden pasar varios tipos a este método. Entre los tipos válidos se
incluyen los siguientes:
String
Double
Single
Decimal
Boolean
DateTime
TimeSpan
DateTimeOffset
Cualquier tipo que implemente ToString()()().
Cualquier tipo que implemente IEnumerable<(Of <(T>)>).
Al agregar contenido complejo, se pueden pasar varios tipos a este método:
XObject
XNode
XAttribute
Cualquier tipo que implemente IEnumerable<(Of <(T>)>).
Si un objeto implementa IEnumerable<(Of <(T>)>), se enumera la colección del objeto y se agregan
todos los elementos de la colección. Si la colección contiene objetos XNode o XAttribute, cada elemento

MCT: Luis Dueñas Pag 113 de 388


Manual de LINQ

de la colección se agrega por separado. Si la colección contiene texto (u objetos convertidos a texto), el
texto de la colección se concatena y se agrega como un nodo de texto.

Si el contenido es null, no se agrega nada. Al pasar una colección, se permite que los elementos de la
colección sean null. Un elemento null de la colección no tiene ningún efecto en el árbol.

Un atributo agregado debe tener un nombre único en el elemento contenedor.

Cuando se agregan objetos XNode o XAttribute, si el contenido nuevo no tiene un elemento primario, los
objetos simplemente se agregan al árbol XML. Si el contenido nuevo ya tiene un elemento primario y
forma parte de otro árbol XML, el nuevo contenido se clonará y dicho clon se agregará al árbol XML.

Contenido válido para documentos


Los atributos y el contenido simple no se pueden agregar a un documento.

No existen muchos escenarios que requieran la creación de un objeto XDocument. En su lugar,


normalmente puede crear los árboles XML con un nodo raíz XElement. A menos que exista un requisito
específico de crear un documento (por ejemplo, porque deba crear instrucciones y comentarios de
procesamiento en el nivel superior, o bien deba admitir tipos de documento), a menudo resulta más
conveniente usar XElement como nodo raíz.

El contenido válido para un documento incluye lo siguiente:


Cero o un objeto XDocumentType. Los tipos de documento deben ir antes del elemento.
Cero o un elemento.
Cero o más comentarios.
Cero o más instrucciones de procesamiento.
Cero o más nodos de texto que contengan sólo espacios en blanco.
Constructores y funciones que permiten agregar contenido
Los métodos siguientes permiten agregar contenido secundario a un objeto XElement o a un objeto
XDocument:

Método Descripción

XElement Construye un objeto XElement.

XDocument Construye un objeto XDocument.

Add Agrega al final del contenido secundario del objeto XElement o del objeto
XDocument.

AddAfterSelf Agrega contenido después de XNode.

AddBeforeSelf Agrega contenido antes de XNode.

AddFirst Agrega contenido al comienzo del contenido secundario del objeto XContainer.

ReplaceAll Reemplaza todo el contenido (atributos y nodos secundarios) de un objeto


XElement.

ReplaceAttributes Reemplaza los atributos de un objeto XElement.

ReplaceNodes Reemplaza los nodos secundarios por contenido nuevo.

ReplaceWith Reemplaza un nodo por contenido nuevo.

MCT: Luis Dueñas Pag 114 de 388


Manual de LINQ

7.2.3. Trabajar con Espacios de Nombres XML


Los temas de esta sección describen cómo admite LINQ a XML los espacios de nombres.

7.2.3.1. Información General Sobre los Espacios de


Nombres
Este tema presenta los espacios de nombres, la clase XName y la clase XNamespace.

Nombres XML
Los nombres XML a menudo son una fuente de complejidad en la programación XML. Un nombre XML se
compone de un espacio de nombres XML (también denominado URI de espacio de nombres XML) y un
nombre local. Un espacio de nombres XML responde a un objetivo similar al del espacio de nombres de
los programas basados en .NET Framework. Permite certificar de manera exclusiva los nombres de los
elementos y los atributos. Evita conflictos de nombres entre varias partes de un documento XML. Una
vez que haya declarado un espacio de nombres XML, puede seleccionar un nombre local que sólo debe
ser único en dicho espacio de nombres.

Otro aspecto de los nombres XML hace referencia a los prefijos de espacios de nombres XML. Los prefijos
XML generan la mayor parte de la complejidad de los nombres XML. Estos prefijos permiten crear un
acceso directo de un espacio de nombres XML, lo que hace que el documento XML sea más conciso y
comprensible. Sin embargo, los prefijos XML dependen de su contexto para tener un significado, lo que
aumenta la complejidad. Por ejemplo, el prefijo XML aw podría asociarse a un espacio de nombres XML
en una parte del árbol XML y a un espacio de nombres XML diferente en una parte distinta de dicho
árbol.

Una de las ventajas de usar LINQ a XML con C# radica en que simplifica los nombres XML al quitar el
requisito de que los desarrolladores usen prefijos XML. Cuando LINQ a XML carga o analiza un
documento XML, cada prefijo XML se resuelve en su espacio de nombres XML correspondiente. Después,
cuando trabaje con un documento que usa espacios de nombres, la mayoría de las veces tendrá acceso a
dichos espacios de nombres mediante el URI de éstos, y no mediante prefijo. Cuando los desarrolladores
trabajan con nombres XML en LINQ a XML, siempre lo hacen con un nombre XML completo (es decir, un
espacio de nombres XML y un nombre local). Sin embargo, si es necesario, LINQ a XML permite trabajar
con prefijos de espacios de nombres y controlarlos.

Si usa LINQ a XML con literales XML y Visual Basic, debe usar los prefijos para trabajar con documentos
en espacios de nombres.

En LINQ a XML, la clase que representa los nombres XML es XName. Los nombres XML aparecen
frecuentemente en la API LINQ a XML, y cuando se requiera un nombre XML, encontrará un parámetro
XName. No obstante, apenas se trabaja directamente con un objeto XName. XName contiene una
conversión implícita de cadena.

7.2.3.2. Espacios de Nombres en Visual Basic


En los temas de esta sección se describe cómo trabajar con espacios de nombres en Visual Basic.

MCT: Luis Dueñas Pag 115 de 388


Manual de LINQ

7.2.3.2.1. Cómo Crear un Documento con Espacios de


Nombres
En este tema se muestra cómo crear un documento con espacios de nombres.

Cuando se utilizan literales XML en Visual Basic, los usuarios pueden definir un espacio de nombres XML
predeterminado que sea global. Este espacio de nombres será el predeterminado tanto para los literales
XML como para las propiedades XML. Es posible definir el espacio de nombres XML predeterminado tanto
a nivel de proyecto como a nivel de archivo. En caso de definirlo a nivel de archivo, éste invalidará al
espacio de nombres predeterminado que se definió a nivel del proyecto.

También puede definir otros espacios de nombres y especificar para ellos los prefijos de espacio de
nombres.

Mediante la palabra clave Imports podrá definir espacios de nombres predeterminados y espacios de
nombres con un prefijo.

Observe que el espacio de nombres XML predeterminado sólo puede aplicarse a los elementos, no a los
atributos. De forma predeterminada, los atributos nunca se encuentran en un espacio de nombres. No
obstante, puede utilizar un prefijo de espacio de nombres para colocar un atributo en un espacio de
nombres.

Ejemplo
Este ejemplo crea un documento que contiene un espacio de nombres.
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = _
<aw:Root>
<aw:Child aw:Att="attvalue"/>
</aw:Root>
Console.WriteLine(root)
End Sub
End Module
Este ejemplo crea un documento que contiene dos espacios de nombres, uno de los cuales es el
predeterminado.
Imports <xmlns="http://www.adventure-works.com">
Imports <xmlns:fc="www.fourthcoffee.com">
Module Module1
Sub Main()
Dim root As XElement = _
<Root>
<Child Att="attvalue"/>
<fc:Child2>child2 content</fc:Child2>
</Root>
Console.WriteLine(root)
End Sub
End Module
El ejemplo siguiente crea un documento que contiene varios espacios de nombres, todos ellos con
prefijos de espacio de nombres.

MCT: Luis Dueñas Pag 116 de 388


Manual de LINQ

Cuando se serializa un árbol XML, LINQ a XML genera atributos que declaran los espacios de nombres a
medida que se necesitan, de forma que cada elemento se encuentre en el espacio de nombres
designado.
Imports <xmlns:aw="http://www.adventure-works.com">
Imports <xmlns:fc="www.fourthcoffee.com">
Module Module1
Sub Main()
Dim root As XElement = _
<aw:Root>
<fc:Child>
<aw:DifferentChild>other content</aw:DifferentChild>
</fc:Child>
<aw:Child2>c2 content</aw:Child2>
<fc:Child3>c3 content</fc:Child3>
</aw:Root>
Console.WriteLine(root)
End Sub
End Module

7.2.3.2.2. Cómo Controlar Prefijos de Espacios de Nombres


En este tema se describe cómo puede controlar prefijos de espacios de nombres.

Ejemplo
Descripción
Este ejemplo declara dos espacios de nombre. Especifica que el espacio de nombres
http://www.adventure-works.com tiene el prefijo aw y que el espacio de nombres www.fourthcoffee.com
tiene el prefijo fc.
Código
Imports <xmlns:aw="http://www.adventure-works.com">
Imports <xmlns:fc="www.fourthcoffee.com">
Module Module1
Sub Main()
Dim root As XElement = _
<aw:Root>
<fc:Child>
<aw:DifferentChild>other content</aw:DifferentChild>
</fc:Child>
<aw:Child2>c2 content</aw:Child2>
<fc:Child3>c3 content</fc:Child3>
</aw:Root>
Console.WriteLine(root)
End Sub
End Module

7.2.3.2.3. Trabajar con Espacios de Nombres Globales


Una de las características clave de los literales XML de Visual Basic 9.0 es la capacidad de declarar
espacios de nombre XML usando la instrucción Imports. Mediante esta función puede declarar un

MCT: Luis Dueñas Pag 117 de 388


Manual de LINQ

espacio de nombres XML que usa un prefijo o bien puede declarar un espacio de nombres XML
predeterminado.

Esta capacidad es útil en dos situaciones. En primer lugar, los espacios de nombres declarados en los
literales XML no se mantienen en expresiones incrustadas. La declaración de espacios de nombres
globales reduce la cantidad de trabajo que tiene que realizar para usar expresiones incrustadas con
espacios de nombres. En segundo lugar, debe declarar espacios de nombres globales para usar espacios
de nombres con propiedades XML.

Puede declarar espacios de nombres globales en el nivel del proyecto. También puede declarar espacios
de nombres globales en el nivel del módulo, lo que invalida los espacios de nombres globales del nivel
del proyecto. Finalmente, puede invalidad los espacios de nombres globales en un literal XML.

Cuando se utilizan literales XML o propiedades XML que están en espacios de nombres que se declaran
globalmente se puede ver el nombre expandido de las propiedades o los literales desplazándose por
encima de ellos en Visual Studio. Verá el nombre expandido en una información sobre herramientas.

Puede obtener un objeto XNamespace que se corresponde con un espacio de nombres global usando el
método GetXmlNamespace.

Ejemplos de espacios de nombres globales


El siguiente ejemplo declara un espacio de nombres global predeterminado usando la instrucción
Imports y después usa un literal XML para inicializar un objeto XElement en ese espacio de nombres:
Imports <xmlns="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = <Root/>
Console.WriteLine(root)
End Sub
End Module
El siguiente ejemplo declara un espacio de nombres global con un prefijo y después usa un literal XML
para inicializar un elemento:
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = <aw:Root/>
Console.WriteLine(root)
End Sub
End Module
Espacios de nombres globales y expresiones incrustadas
Los espacios de nombres que se declaran en los literales XML no se mantienen en expresiones
incrustadas. El siguiente ejemplo declara un espacio de nombres predeterminado. A continuación usa
una expresión incrustada para el elemento Child.
Dim root As XElement = _
<Root xmlns="http://www.adventure-works.com">
<%= <Child/> %>
</Root>
Console.WriteLine(root)
Como puede ver, el XML resultante incluye una declaración de un espacio de nombres predeterminado de
forma que el elemento Child no esté en ningún espacio de nombres.

MCT: Luis Dueñas Pag 118 de 388


Manual de LINQ

Puede volver a declarar el espacio de nombres en la expresión incrustada de ka siguiente manera:


Dim root As XElement = _
<Root xmlns="http://www.adventure-works.com">
<%= <Child xmlns="http://www.adventure-works.com"/> %>
</Root>
Console.WriteLine(root)
No obstante, es más complicado de usar.

Un mejor enfoque es declarar un espacio de nombres predeterminado global. A continuación puede usar
los literales XML sin declarar espacios de nombres. El XML resultante estará en el espacio de nombres
predeterminado declarado globalmente.
Imports <xmlns="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = <Root>
<%= <Child/> %>
</Root>
Console.WriteLine(root)
End Sub
End Module
Usar espacios de nombres con propiedades XML
Si está trabajando con un árbol XML que está en un espacio de nombres y utiliza propiedades XML, debe
usar un espacio de nombres global para que las propiedades XML también estén en el espacio de
nombres correcto. En el ejemplo siguiente se declara un árbol XML en un espacio de nombres. Después
se muestra el número de elementos Child.
Dim root As XElement = _
<Root xmlns="http://www.adventure-works.com">
<Child>content</Child>
</Root>
Console.WriteLine(root.<Child>.Count())
Este ejemplo indica que no hay elementos Child. Genera el siguiente resultado:
0
No obstante, si declara un espacio de nombres global predeterminado, el literal XML y la propiedad XML
estarán en el espacio de nombres global predeterminado:
Imports <xmlns="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = _
<Root>
<Child>content</Child>
</Root>
Console.WriteLine(root.<Child>.Count())
End Sub
End Module
Este ejemplo indica que hay un elemento Child. Genera el siguiente resultado:
1
Si declara un espacio de nombres global que no tenga un prefijo, puede usar el prefijo para literales XML
y propiedades XML:
Imports <xmlns:aw="http://www.adventure-works.com">

MCT: Luis Dueñas Pag 119 de 388


Manual de LINQ

Module Module1
Sub Main()
Dim root As XElement = _
<aw:Root>
<aw:Child>content</aw:Child>
</aw:Root>
Console.WriteLine(root.<aw:Child>.Count())
End Sub
End Module
XNamespace y espacios de nombres globales
Puede obtener un objeto XNamespace usando el método GetXmlNamespace:
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = <aw:Root/>
Dim xn As XNamespace = GetXmlNamespace(aw)
Console.WriteLine(xn)
End Sub
End Module
Este ejemplo genera el siguiente resultado:
http://www.adventure-works.com

7.2.3.3. Cómo Escribir Consultas en XML en Espacios de


Nombre
Para escribir una consulta en XML que esté en un espacio de nombre, debe usar objetos XName en al
consulta que tengan el espacio de nombres correcto.

Para C#, el enfoque más común consiste en inicializar un XNamespace usando una cadena que contiene
la dirección URI y después usar la sobrecarga del operador de suma para combinar el espacio de
nombres con el nombre local.

Cuando se utiliza Visual Basic, el enfoque más común consiste en definir un espacio de nombres global y
después utilizar literales XML y propiedades XML que usen el espacio de nombres global. Puede definir un
espacio de nombres predeterminado global, en cuyo caso los elementos de los literales XML estarán en el
espacio de nombres de forma predeterminada. De forma alternativa puede definir un espacio de nombres
global con un prefijo y después usar el prefijo según se requiera en los literales XML y en las propiedades
XML. Al igual que con otras formas de XML, los atributos no están nunca en ningún espacio de nombres
de forma predeterminada.

Ejemplo
En el ejemplo siguiente se crea un árbol XML que está en un espacio de nombres predeterminado. A
continuación recupera una recopilación de elementos.
Imports <xmlns="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = _
<Root>
<Child>1</Child>

MCT: Luis Dueñas Pag 120 de 388


Manual de LINQ

<Child>2</Child>
<Child>3</Child>
<AnotherChild>4</AnotherChild>
<AnotherChild>5</AnotherChild>
<AnotherChild>6</AnotherChild>
</Root>
Dim c1 As IEnumerable(Of XElement) = _
From el In root.<Child> _
Select el
For Each el As XElement In c1
Console.WriteLine(el.Value)
Next
End Sub
End Module
Este ejemplo genera el siguiente resultado:
1
2
3
Si está escribiendo consultas en C# en un árbol XML que usa un espacio de nombres con un prefijo, el
enfoque que utilice al escribir la consulta es idéntico al enfoque que usará cuando consulte XML que esté
en un espacio de nombres predeterminado. Aunque puede controlar prefijos durante la serialización y
aunque puede determinar la dirección URI del espacio de nombres con un prefijo y puede determinar el
prefijo de un espacio de nombres, para la mayor parte los prefijos de espacio de nombre están
atenuados en la interfaz de programación de LINQ a XML.

No obstante, cuando utilice literales XML y escriba consultas en Visual Basic en un árbol XML que utilice
un espacio de nombres con un prefijo, el enfoque que utiliza es diferente. Normalmente se importa el
espacio de nombres con un prefijo utilizando la instrucción Imports. A continuación se utiliza el prefijo en
los nombres del elemento y del atributo y también se utiliza el prefijo cuando se utilizan las propiedades
XML que tienen acceso al árbol XML.

En el ejemplo siguiente se crea un árbol XML que está en un espacio de nombres con un prefijo. A
continuación recupera una recopilación de elementos.
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = _
<aw:Root>
<aw:Child>1</aw:Child>
<aw:Child>2</aw:Child>
<aw:Child>3</aw:Child>
<aw:AnotherChild>4</aw:AnotherChild>
<aw:AnotherChild>5</aw:AnotherChild>
<aw:AnotherChild>6</aw:AnotherChild>
</aw:Root>
Dim c1 As IEnumerable(Of XElement) = _
From el In root.<aw:Child> _
Select el
For Each el As XElement In c1
Console.WriteLine(CInt(el))

MCT: Luis Dueñas Pag 121 de 388


Manual de LINQ

Next
End Sub
End Module
Este ejemplo genera el siguiente resultado:
1
2
3

7.2.4. Serializar Arboles XML


Serializar un árbol XML significa generar XML a partir de un árbol XML. Puede serializarlo en un archivo,
en una implementación concreta de la clase TextWriter o en una implementación concreta de un
XmlWriter.

Puede controlar diversos aspectos del proceso de serialización. Por ejemplo, puede decidir si desea
agregar una sangría al XML serializado y si desea escribe una declaración XML.

7.2.4.1. Preservar los Espacios en Blanco durante la


Serialización
En este tema se describe cómo controlar los espacios en blanco a la hora de serializar un árbol XML.

Un caso muy común es aquel en el que se leen datos XML con sangría, se crea un árbol XML en memoria
sin ningún nodo de texto con espacios en blanco (es decir, sin preservar los espacios en blanco), se
realizan ciertas operaciones sobre el XML y éste se guarda con sangría. Si se serializa el XML con
formato, sólo se preservan en el XML aquellos espacios en blanco más significativos. Éste es el
comportamiento predeterminado en LINQ a XML.

Otro caso muy común es aquel en el que se lee y se modifica XML en el que ya se ha aplicado sangría
intencionadamente. Es posible que no desee modificar esta sangría de ninguna forma. En LINQ a XML,
puede conseguirlo si preserva los espacios en blanco a la hora de cargar o analizar el XML y si deshabilita
el formato cuando serialice el XML.

Tratamiento de los espacios en blanco en los métodos que serializan árboles XML
Los siguientes métodos de las clases XElement y XDocument serializan un árbol XML. Es posible serializar
un árbol XML en un archivo, un TextReader o un XmlReader. El método ToString serializa en una
cadena..
XElement..::.Save
XDocument..::.Save
XElement..::.ToString
XDocument..::.ToString
Si el método no recibe un SaveOptions como argumento, entonces el método realizará un formato
(sangría) del XML serializado. En ese caso, se descartarán todos los espacios en blanco del árbol XML que
no sean significativos.

Si el método recibe un SaveOptions como argumento, opcionalmente el método podrá no dar formato
(sangría) al XML serializado. En ese caso, se preservarán todos los espacios en blanco del árbol XML.

MCT: Luis Dueñas Pag 122 de 388


Manual de LINQ

7.2.4.2. Serializar con una Declaración XML


En este tema se describe cómo controlar si la serialización genera una declaración XML.

Tenga en cuenta que la serialización en un File o TextWriter genera una declaración XML. Cuando
serializa en XmlWriter, la configuración del escritor (especificada en un objeto XmlWriterSettings)
determina si se genera una declaración XML.

Generación de declaración XML


Si está serializando en un objeto diferente de XmlWriter los siguientes métodos generan una declaración
XML:
XElement..::.Save
XDocument..::.Save
Si está serializando en una cadena mediante el método ToString, el XML resultante no incluirá una
declaración XML.

Serializar con una declaración XML


En el ejemplo siguiente se crea un XElement, se guarda el documento en un archivo y, a continuación, se
imprime el archivo en la consola:
Dim root As XElement = <Root>
<Child>child content</Child>
</Root>
root.Save("Root.xml")
Dim str As String = File.ReadAllText("Root.xml")
Console.WriteLine(str)
Serializar sin una declaración XML
En el siguiente ejemplo se muestra cómo guardar un XElement en un XmlWriter.
Dim sb As StringBuilder = New StringBuilder()
Dim xws As XmlWriterSettings = New XmlWriterSettings()
xws.OmitXmlDeclaration = True
Using xw As XmlWriter = XmlWriter.Create(sb, xws)
Dim root = <Root>
<Child>child content</Child>
</Root>
root.Save(xw)
End Using
Console.WriteLine(sb.ToString())

7.2.4.3. Serializar en Archivos, TextWriters y XmlWriters


Puede serializar árboles XML en File, TextWriter o XmlWriter.

Puede serializar cualquier componente XML, entre los que se incluyen XDocument y XElement, en una
cadena con el método ToString.

Si desea suprimir el formato al serializar en una cadena, puede usar el método XNode..::.ToString.

Serializar en URI, TextWriter o XmlWriter


El comportamiento predeterminado al serializar en un archivo consiste en formatear el documento XML
resultante (aplicarle una sangría). Cuando aplica una sangría, en el árbol XML no se conservan los

MCT: Luis Dueñas Pag 123 de 388


Manual de LINQ

espacios en blanco más insignificantes. Para serializar con formato, use una de las sobrecargas de los
métodos siguientes que no toman SaveOptions como argumento:
XDocument..::.Save
XElement..::.Save
Si desea que la opción no aplique una sangría y conserve los espacios en blanco más insignificantes del
árbol XML, use una de las sobrecargas de los métodos siguientes que toman SaveOptions como
argumento:
XDocument..::.Save
XElement..::.Save

7.2.4.4. Serializar en XmlReader (Invocar XSLT)


Cuando use las capacidades de interoperabilidad System.Xml de LINQ a XML, puede utilizar
CreateReader para crear un objeto XmlReader. El módulo que lee desde el objeto XmlReader lee los
nodos desde el árbol XML y los procesa.

Invocar una transformación XSLT


Un uso posible para este método es la invocación de una transformación XSLT. Puede crear un árbol
XML, crear un objeto XmlReader desde el árbol XML, crear un nuevo documento y crear un objeto
XmlWriter que escribirá en el nuevo documento. A continuación, puede invocar la transformación XSLT,
pasando XmlReader y XmlWriter. Después de que se complete correctamente la transformación, se
rellenará el nuevo árbol XML con los resultados de la transformación.
Dim xslMarkup As XDocument = _
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
version='1.0'>
<xsl:template match='/Parent'>
<Root>
<C1>
<xsl:value-of select='Child1'/>
</C1>
<C2>
<xsl:value-of select='Child2'/>
</C2>
</Root>
</xsl:template>
</xsl:stylesheet>
Dim xmlTree As XDocument = _
<?xml version='1.0'?>
<Parent>
<Child1>Child1 data</Child1>
<Child2>Child2 data</Child2>
</Parent>
Dim newTree As XDocument = New XDocument()
Using writer As XmlWriter = newTree.CreateWriter()
' Load the style sheet.
Dim xslt As XslCompiledTransform = New XslCompiledTransform()
xslt.Load(xslMarkup.CreateReader())
' Execute the transformation and output the results to a writer.
xslt.Transform(xmlTree.CreateReader(), writer)

MCT: Luis Dueñas Pag 124 de 388


Manual de LINQ

End Using
Console.WriteLine(newTree)

7.2.5. Ejes LINQ a XML


Tras crear un árbol XML o haber cargado un documento XML en un árbol XML, puede realizar consultas
sobre él para encontrar elementos y atributos, así como para recuperar sus valores.

Antes de poder escribir consultas, debe entender los ejes LINQ a XML. Hay dos clases de métodos de eje.
En primer lugar, hay métodos a los que se llama en un solo objeto XElement, un objeto XDocument o un
objeto XNode. Estos métodos funcionan en un objeto único y devuelven una colección de objetos
XElement, XAttribute o XNode. En segundo lugar, existen métodos de extensión que funcionan en
colecciones y devuelven colecciones. Los métodos de extensión enumeran la colección de origen, llaman
al método de eje adecuado en cada elemento de la colección y concatenan los resultados.

7.2.5.1. Información General Acerca de los Ejes de LINQ a


XML
Tras crear un árbol XML o haber cargado un documento XML en un árbol XML, puede realizar consultas
sobre él para encontrar elementos y atributos, así como para recuperar sus valores. Mediante los
métodos de eje, recuperará colecciones. Algunos de los ejes son métodos de las clases XElement y
XDocument, que devuelven colecciones IEnumerable<(Of <(T>)>). Algunos de los métodos de los ejes
son métodos de extensión de la clase Extensions. Los ejes que se han implementado como métodos de
extensión trabajan colecciones y devuelven colecciones.

Tal y como se describe en Información general acerca de la clase XElement, un objeto XElement
representa un único nodo de elemento. El contenido de un elemento puede ser complejo (a veces
conocido como contenido estructurado) o puede ser un elemento simple. Un elemento simple puede
estar vacío o contener un valor. Si el nodo incluye contenido estructurado, puede utilizar diferentes
métodos del eje para recuperar enumeraciones con los elementos descendientes. Los métodos de ejes
que se utilizan con más frecuencia son Elements y Descendants.

Además de los métodos de ejes, que devuelven colecciones, existen dos métodos más que utilizará con
regularidad en consultas LINQ a XML. El método Element devuelve un único XElement. El método
Attribute devuelve un único XAttribute.

Por diversas razones, las consultas de LINQ ofrecen el mecanismo más potente para examinar árboles,
extraer datos de ellos y para transformarlos. Las consultas de LINQ trabajan sobre objetos que
implementan IEnumerable<(Of <(T>)>), y los ejes LINQ a XML devuelven colecciones IEnumerable<(Of
<(T>)>) de XElement y IEnumerable<(Of <(T>)>) de XAttribute. Para poder realizar las consultas,
necesitará estas colecciones.

A parte de los métodos de ejes que permiten recuperar colecciones de elementos y atributos, existen
métodos de ejes que le permiten iterar por un árbol con gran detalle. Por ejemplo, en vez de tratar
directamente con los elementos y los atributos, podría trabajar con los nodos del árbol. Los nodos tienen
un nivel de granularidad mayor que los elementos y atributos. Si trabaja con nodos, podrá examinar
comentarios XML, nodos de texto, instrucciones de procesamiento, etc. Esta funcionalidad resultaría
interesante, por ejemplo, para una persona que estuviese escribiendo un procesador de textos y quisiera
guardar los documentos en formato XML. No obstante, la mayoría de programadores en XML se centran
más en los elementos, los atributos y sus valores.

MCT: Luis Dueñas Pag 125 de 388


Manual de LINQ

Métodos para recuperar una colección de elementos


A continuación encontrará un resumen de los métodos de la clase XElement (o de sus clases base) a los
que podrá pasar un XElement para devolver una colección de elementos.

Método Descripción

XNode..::.Ancestors Devuelve un IEnumerable<(Of <(T>)>) de XElement de los


antecesores de este elemento. Una sobrecarga devuelve un
IEnumerable<(Of <(T>)>) de XElement de los antecesores que
tengan el XName especificado.

XContainer..::.Descendants Devuelve un IEnumerable<(Of <(T>)>) de XElement de los


descendientes de este elemento. Una sobrecarga devuelve un
IEnumerable<(Of <(T>)>) de XElement de los descendientes
que tengan el XName especificado.

XContainer..::.Elements Devuelve un IEnumerable<(Of <(T>)>) de XElement de los


elementos secundarios dependientes de este elemento. Una
sobrecarga devuelve un IEnumerable<(Of <(T>)>) de XElement
de los elementos secundarios que tengan el XName
especificado.

XNode..::.ElementsAfterSelf Devuelve un IEnumerable<(Of <(T>)>) de XElement de los


elementos que se encuentran después de este elemento. Una
sobrecarga devuelve un IEnumerable<(Of <(T>)>) de XElement
de los elementos que se encuentren después de este elemento y
que tengan el XName especificado.

XNode..::.ElementsBeforeSelf Devuelve un IEnumerable<(Of <(T>)>) de XElement de los


elementos que se encuentran antes que este elemento. Una
sobrecarga devuelve un IEnumerable<(Of <(T>)>) de XElement
de los elementos que se encuentren antes que este elemento y
que tengan el XName especificado.

XElement..::.AncestorsAndSelf Devuelve un IEnumerable<(Of <(T>)>) de XElement de este


elemento y de sus antecesores. Una sobrecarga devuelve un
IEnumerable<(Of <(T>)>) de XElement de los elementos que
tengan el XName especificado.

XElement..::.DescendantsAndSelf Devuelve un IEnumerable<(Of <(T>)>) de XElement de este


elemento y de sus descendientes. Una sobrecarga devuelve un
IEnumerable<(Of <(T>)>) de XElement de los elementos que
tengan el XName especificado.

Método para recuperar un único elemento


El siguiente método recupera un único elemento secundario a partir del objeto XElement.

Método Descripción

XContainer..::.Element Devuelve el objeto del primer elemento XElement secundario que tenga el
XName especificado.

Método para recuperar una colección de atributos


El siguiente método recupera atributos a partir de un objeto XElement.

Método Descripción

XElement..::.Attributes Devuelve un IEnumerable<(Of <(T>)>) de XAttribute de todos los


atributos.

Método para recuperar un único atributo


El siguiente método recupera un único atributo a partir de un objeto XElement.

MCT: Luis Dueñas Pag 126 de 388


Manual de LINQ

Método Descripción

XElement..::.Attribute Devuelve el XAttribute que tenga el XName especificado.

7.2.5.2. Cómo Recuperar una Recopilación de Elementos


En este tema se demuestra el método Elements. Este método recupera una recopilación de elementos
secundarios de un elemento.

Ejemplo
Este ejemplo recorre en iteración los elementos secundarios del elemento purchaseOrder.
Dim po As XElement = XElement.Load("PurchaseOrder.xml")
Dim childElements As IEnumerable(Of XElement)
childElements = _
From el In po.Elements() _
Select el
For Each el As XElement In childElements
Console.WriteLine("Name: " & el.Name.ToString())
Next

7.2.5.3. Cómo Recuperar el Valor de un Elemento


Este tema muestra cómo obtener el valor de los elementos. Existen dos formas principales de hacerlo.
Una forma consiste en convertir un elemento XElement o un atributo XAttribute al tipo deseado. El
operador de conversión explícita convierte el contenido del elemento o del atributo al tipo especificado y
lo asigna a la variable. Como alternativa, se puede usar la propiedad XElement..::.Value o la propiedad
XAttribute..::.Value.

Sin embargo, con C#, la conversión suele ser el mejor método. Si convierte el elemento o el atributo a
un tipo que admite valores NULL, resulta más fácil escribir el código al recuperar el valor de un elemento
(o atributo) que podría existir o no. El último ejemplo de este tema muestra este comportamiento. No
obstante, no puede establecer el contenido de un elemento mediante la conversión del mismo modo que
con la propiedad XElement..::.Value.

Con Visual Basic, el mejor método consiste en usar la propiedad XElement..::.Value.

Ejemplo
Para recuperar el valor de un elemento, basta con convertir el objeto XElement al tipo deseado. Siempre
puede convertir un elemento a una cadena, como se indica a continuación:
Dim e As XElement = <StringElement>abcde</StringElement>
Console.WriteLine(e)
Console.WriteLine("Value of e:" & e.Value)
También puede convertir elementos a tipos diferentes a una cadena. Por ejemplo, si tiene un elemento
que contiene un entero, puede convertirlo a int, tal como se muestra en el código siguiente:
Dim e As XElement = <Age>44</Age>
Console.WriteLine(e)
Console.WriteLine("Value of e:" & CInt(e))
LINQ a XML proporciona operadores de conversión explícita para los tipos de datos siguientes: string,
bool, bool?, int, int?, uint, uint?, long, long?, ulong, ulong?, float, float?, double, double?,
decimal, decimal?, DateTime, DateTime?, TimeSpan, TimeSpan?, GUID y GUID?.

MCT: Luis Dueñas Pag 127 de 388


Manual de LINQ

LINQ a XML proporciona los mismos operadores de conversión para los objetos XAttribute.

Puede usar la propiedad Value para recuperar el contenido de un elemento:


Dim e As XElement = <StringElement>abcde</StringElement>
Console.WriteLine(e)
Console.WriteLine("Value of e:" & e.Value)
A veces, intenta recuperar el valor de un elemento aunque no esté seguro de que exista. En tal caso, al
asignar el elemento convertido a un tipo que admita valores NULL (ya sea string o uno de los tipos que
admiten valores NULL en .NET Framework), si el elemento no existe, la variable asignada sólo se
establece en null (Nothing en Visual Basic). El código siguiente demuestra que cuando el elemento
podría existir o no, resulta más fácil usar la conversión que la propiedad Value.
Dim root As XElement = <Root>
<Child1>child 1 content</Child1>
<Child2>2</Child2>
</Root>
' The following assignments show why it is easier to use
' casting when the element might or might not exist.
Dim c1 As String = CStr(root.Element("Child1"))
Console.WriteLine("c1:{0}", IIf(c1 Is Nothing, "element does not exist",
c1))
Dim c2 As Nullable(Of Integer) = CType(root.Element("Child2"),
Nullable(Of Integer))
Console.WriteLine("c2:{0}", IIf(Not (c2.HasValue), "element does not
exist", c2.ToString()))
Dim c3 As String = CStr(root.Element("Child3"))
Console.WriteLine("c3:{0}", IIf(c3 Is Nothing, "element does not exist",
c3))
Dim c4 As Nullable(Of Integer) = CType(root.Element("Child4"),
Nullable(Of Integer))
Console.WriteLine("c4:{0}", IIf(Not (c4.HasValue), "element does not
exist", c4.ToString()))
Console.WriteLine()
' The following assignments show the required code when using
' the Value property when the attribute might or might not exist.
' Notice that this is more difficult than the casting approach.
Dim e1 As XElement = root.Element("Child1")
Dim v1 As String
If (e1 Is Nothing) Then
v1 = Nothing
Else
v1 = e1.Value
End If
Console.WriteLine("v1:{0}", IIf(v1 Is Nothing, "element does not exist",
v1))
Dim e2 As XElement = root.Element("Child2")
Dim v2 As Nullable(Of Integer)
If (e2 Is Nothing) Then
v2 = Nothing
Else
v2 = e2.Value

MCT: Luis Dueñas Pag 128 de 388


Manual de LINQ

End If
Console.WriteLine("v2:{0}", IIf(Not (v2.HasValue), "element does not
exist", v2))
Dim e3 As XElement = root.Element("Child3")
Dim v3 As String
If (e3 Is Nothing) Then
v3 = Nothing
Else
v3 = e3.Value
End If
Console.WriteLine("v3:{0}", IIf(v3 Is Nothing, "element does not exist",
v3))
Dim e4 As XElement = root.Element("Child4")
Dim v4 As Nullable(Of Integer)
If (e4 Is Nothing) Then
v4 = Nothing
Else
v4 = e4.Value
End If
Console.WriteLine("v4:{0}", IIf(Not (v4.HasValue), "element does not
exist", v4))
Por lo general, puede escribir un código más simple al usar la conversión para recuperar el contenido de
los elementos y los atributos.

7.2.5.4. Cómo Filtrar Nombres de Elemento


Al llamar a uno de los métodos que devuelven IEnumerable<(Of <(T>)>) de XElement, puede filtrar por
nombre de elemento.

Ejemplo
Este ejemplo muestra cómo recuperar una colección de descendientes. Sin embargo, éstos se filtran, y la
colección sólo contiene descendientes con el nombre especificado.
Dim po As XElement = XElement.Load("PurchaseOrder.xml")
Dim items As IEnumerable(Of XElement) = _
From el In po...<ProductName> _
Select el
For Each prdName As XElement In items
Console.WriteLine(prdName.Name.ToString & ":" & prdName.Value)
Next
Los otros métodos que devuelven IEnumerable<(Of <(T>)>) de colecciones XElement siguen el mismo
patrón. Sus firmas son similares a Elements y Descendants. A continuación, se incluye una lista completa
de los métodos que tienen firmas similares:
Ancestors
Descendants
Elements
ElementsAfterSelf
ElementsBeforeSelf
AncestorsAndSelf
DescendantsAndSelf

MCT: Luis Dueñas Pag 129 de 388


Manual de LINQ

El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim po As XElement = XElement.Load("PurchaseOrderInNamespace.xml")
Dim items As IEnumerable(Of XElement) = _
From el In po...<aw:ProductName> _
Select el
For Each prdName As XElement In items
Console.WriteLine(prdName.Name.ToString & ":" & prdName.Value)
Next
End Sub
End Module

7.2.5.5. Cómo Encadenar Llamadas a Métodos de Eje


Una forma habitual consiste en llamar al método de eje y luego llamar a uno de los ejes de método de
extensión.

Hay dos ejes con el nombre Elements que devuelven una colección de elementos: el método
XContainer..::.Elements y el método Extensions..::.Elements. Puede combinar estos dos ejes para
encontrar todos los elementos de un nombre especificado en una profundidad determinada del árbol.

Ejemplo
Este ejemplo usa los dos ejes descritos anteriormente para encontrar todos los elementos Name de todos
los elementos Address de todos los elementos PurchaseOrder.
Dim purchaseOrders As XElement = XElement.Load("PurchaseOrders.xml")
Dim names As IEnumerable(Of XElement) = _
From el In purchaseOrders.<PurchaseOrder>.<Address>.<Name> _
Select el
For Each e As XElement In names
Console.WriteLine(e)
Next
Esto funciona porque una de las implementaciones del eje Elements es un método de extensión en
IEnumerable<(Of <(T>)>) de XContainer. XElement se deriva de XContainer, de modo que puede llamar
al método Extensions..::.Elements según los resultados de una llamada al método
XContainer..::.Elements.

A veces, desea recuperar todos los elementos de una profundidad determinada cuando es posible que
intervengan (o no) antecesores. Por ejemplo, en el siguiente documento, podría recuperar todos los
elementos ConfigParameter que son secundarios del elemento Customer, pero no el elemento
ConfigParameter que es un secundario del elemento Root.
Xml
<Root>
<ConfigParameter>RootConfigParameter</ConfigParameter>
<Customer>
<Name>Frank</Name>
<Config>
<ConfigParameter>FirstConfigParameter</ConfigParameter>

MCT: Luis Dueñas Pag 130 de 388


Manual de LINQ

</Config>
</Customer>
<Customer>
<Name>Bob</Name>
<!--This customer doesn't have a Config element-->
</Customer>
<Customer>
<Name>Bill</Name>
<Config>
<ConfigParameter>SecondConfigParameter</ConfigParameter>
</Config>
</Customer>
</Root>
Para ello, puede usar el eje Extensions..::.Elements de la siguiente manera:
Dim root As XElement = XElement.Load("Irregular.xml")
Dim configParameters As IEnumerable(Of XElement) = _
root.<Customer>.<Config>.<ConfigParameter>
For Each cp As XElement In configParameters
Console.WriteLine(cp)
Next
El siguiente ejemplo muestra la misma técnica para XML que se encuentre en un espacio de nombres.
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim purchaseOrders As XElement =
XElement.Load("PurchaseOrdersInNamespace.xml")
Dim names As IEnumerable(Of XElement) = From el In
purchaseOrders.<aw:PurchaseOrder>.<aw:Address>.<aw:Name> _
Select el
For Each e As XElement In names
Console.WriteLine(e)
Next
End Sub
End Module

7.2.5.6. Cómo Recuperar un Unico Elemento Secundario


En este tema se explica cómo recuperar un único elemento secundario, dado el nombre del elemento
secundario. Si conoce el nombre del elemento secundario y que sólo hay un elemento que tiene ese
nombre, puede resultar cómodo recuperar solamente un elemento, en lugar de una recopilación.

El método Element devuelve el primer XElement secundario con el XName especificado.

Si desea recuperar un único elemento secundario de Visual Basic, un enfoque común consiste en utilizar
la propiedad XML y después recuperar el primer elemento mediante una notación de indizador de matriz.

Ejemplo
En el siguiente ejemplo se muestra el uso del método Element. Este ejemplo toma un árbol XML con el
nombre po y busca el primer elemento con el nombre Comment.

MCT: Luis Dueñas Pag 131 de 388


Manual de LINQ

El ejemplo de Visual Basic muestra el uso de la notación del indizador de matriz para recuperar un
elemento único.
Dim po As XElement = XElement.Load("PurchaseOrder.xml")
Dim e As XElement = po.<DeliveryNotes>(0)
Console.WriteLine(e)
El siguiente ejemplo muestra el mismo código sobre un XML que se encuentra en un espacio de nombres
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim po As XElement = XElement.Load("PurchaseOrderInNamespace.xml")
Dim e As XElement = po.<aw:DeliveryNotes>(0)
Console.WriteLine(e)
End Sub
End Module

7.2.5.7. Cómo Recuperar una Colección de Atributos


En este tema se presenta el método Attributes. Este método recupera los atributos de un elemento.

Ejemplo
En el ejemplo siguiente se muestra cómo procesar una iteración en la colección de atributos de un
elemento.
Dim val = _
<Value ID="1243" Type="int" ConvertableTo="double">100</Value>
Dim listOfAttributes As IEnumerable(Of XAttribute) = _
From att In val.Attributes() Select att
For Each att As XAttribute In listOfAttributes
Console.WriteLine(att)
Next

7.2.5.8. Cómo Recuperar un Atributo en Particular


Este tema explica cómo recuperar un atributo concreto de un elemento, proporcionando el nombre del
atributo. Esto puede resultar útil para escribir expresiones de consulta mediante las cuales encontrar un
elemento que contiene un atributo en particular.

El método Attribute de la clase XElement devuelve el XAttribute con el nombre especificado.

Ejemplo
El siguiente ejemplo utiliza el método Attribute.
Dim cust As XElement = <PhoneNumbers>
<Phone type="home">555-555-5555</Phone>
<Phone type="work">555-555-6666</Phone>
</PhoneNumbers>
Dim elList = From el In cust...<Phone>
For Each e As XElement In elList
Console.WriteLine(e.@type)
Next
Este ejemplo encuentra todos los descendientes en el árbol cuyo nombre es Phone y, a continuación,
encuentra aquel atributo cuyo nombre es type.

MCT: Luis Dueñas Pag 132 de 388


Manual de LINQ

Si desea recuperar el valor del atributo, puede convertirlo, de la misma forma que lo haría con los
objetos XElement. En el siguiente ejemplo se muestra cómo hacerlo.
Dim cust As XElement = <PhoneNumbers>
<Phone type="home">555-555-5555</Phone>
<Phone type="work">555-555-6666</Phone>
</PhoneNumbers>
Dim elList As IEnumerable(Of XElement) = From el In cust...<Phone> _
Select el
For Each el As XElement In elList
Console.WriteLine(el.@type)
Next
LINQ a XML proporciona operadores de conversión explícita a string, bool, bool?, int, int?, uint,
uint?, long, long?, ulong, ulong?, float, float?, double, double?, decimal, decimal?, DateTime,
DateTime?, TimeSpan, TimeSpan?, GUID y GUID? para la clase XAttribute.

El siguiente ejemplo muestra el mismo código para un atributo que se encuentre en un espacio de
nombres.
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim cust As XElement = _
<aw:PhoneNumbers>
<aw:Phone aw:type="home">555-555-5555</aw:Phone>
<aw:Phone aw:type="work">555-555-6666</aw:Phone>
</aw:PhoneNumbers>
Dim elList As IEnumerable(Of XElement) = _
From el In cust...<aw:Phone> _
Select el
For Each el As XElement In elList
Console.WriteLine(el.@aw:type)
Next
End Sub
End Module

7.2.5.9. Cómo Recuperar el Valor de un Atributo


Este tema muestra cómo obtener el valor de los atributos. Hay dos formas principales: puede convertir
un elemento XAttribute al tipo deseado; el operador de conversión explícita convierte el contenido del
elemento o del atributo al tipo especificado. Como alternativa, puede usar la propiedad Value. Sin
embargo, la conversión suele ser el mejor método. Si convierte el atributo a un tipo que admite valores
NULL, resulta más fácil escribir el código al recuperar el valor de un atributo que podría existir o no.

Ejemplo
Para recuperar el valor de un atributo, basta con convertir el objeto XAttribute al tipo deseado.

En Visual Basic, puede usar la propiedad del atributo integrado para recuperar el valor de un atributo.
Dim root As XElement = <Root Attr="abcde"/>
Console.WriteLine(root)
Dim str As String = root.@Attr
Console.WriteLine(str)

MCT: Luis Dueñas Pag 133 de 388


Manual de LINQ

En Visual Basic, puede usar la propiedad del atributo integrado para establecer el valor de un atributo.
Asimismo, si usa la propiedad del atributo integrado para establecer el valor de un atributo que no
existe, se creará el atributo.
Dim root As XElement = <Root Att1="content"/>
root.@Att1 = "new content"
root.@Att2 = "new attribute"
Console.WriteLine(root)
El siguiente ejemplo muestra cómo recuperar el valor de un atributo, en el que el atributo se encuentra
en un espacio de nombres.
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = <aw:Root aw:Attr="abcde"/>
Dim str As String = root.@aw:Attr
Console.WriteLine(str)
End Sub
End Module

7.2.5.10. Ejes Integrados en el Lenguaje en Visual Basic


En esta sección se describen características integradas directamente en el lenguaje Visual Basic para
facilitar el acceso a XML. Muchos de los ejemplos de la documentación de LINQ a XML usan estos ejes de
Visual Basic integrados.

En esta sección

Tema Descripción

Propiedades de eje Proporciona el acceso a los elementos secundarios de uno de los siguientes:
secundario XML un objeto XElement, un objeto XDocument, una colección de objetos
XElement o una colección de objetos XDocument. Este eje es equivalente al
eje Elements.

Propiedad de eje Proporciona acceso a los descendientes de: un objeto XElement, un objeto
descendiente XML XDocument o una recopilación de objetos XElement. Este eje es equivalente al
eje Descendants.

Propiedad Axis Proporciona acceso a un atributo de un objeto XElement. Este eje es en líneas
para atributo XML generales equivalente al eje Attribute. Este eje difiere del eje Attribute en que
devuelve el valor del atributo, no un objeto XAttribute.

Propiedad de Proporciona acceso a los elementos individuales de una recopilación.


indizador de
extensión

7.2.6. Consultar Arboles XML


En esta sección se ofrecen ejemplos de consultas de LINQ a XML.

Una vez que haya creado una instancia del árbol XML, escribir consultas será la manera más eficaz de
extraer datos de dicho árbol. Asimismo, la consulta, combinada con la construcción funcional, permite
generar un documento XML nuevo que tiene una forma diferente del documento original.

MCT: Luis Dueñas Pag 134 de 388


Manual de LINQ

7.2.6.1. Consultas Básicas LINQ a XML


En esta sección se ofrecen ejemplos de consultas básicas de LINQ a XML.

7.2.6.1.1. Cómo Buscar un Elemento con un Atributo


Específico
En este tema se muestra cómo buscar un elemento que tiene un atributo con un valor específico.

Ejemplo
El ejemplo muestra cómo buscar el elemento Address que tiene un atributo Type con un valor de
"Billing".
Dim root As XElement = XElement.Load("PurchaseOrder.xml")
Dim address As IEnumerable(Of XElement) = From el In root.<Address> _
Where el.@Type = "Billing" Select el
For Each el As XElement In address
Console.WriteLine(el)
Next
Tenga en cuenta que la versión de Visual Basic de este ejemplo utiliza la Propiedades de eje secundario
XML, la Propiedad Axis para atributo XML y la Propiedad Value de XML.

El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns:aw='http://www.adventure-works.com'>
Module Module1
Sub Main()
Dim root As XElement = Element.Load("PurchaseOrderInNamespace.xml")
Dim address As IEnumerable(Of XElement) = From el In root.<aw:Address> _
Where el.@aw:Type = "Billing" Select el
For Each el As XElement In address
Console.WriteLine(el)
Next
End Sub
End Module

7.2.6.1.2. Cómo Buscar un Elemento con un Elemento


Secundario Específico
Este tema muestra cómo encontrar un elemento en particular que tenga un elemento secundario con un
valor específico.

Ejemplo
El ejemplo busca el elemento Test que tenga un elemento secundario CommandLine con el valor de
"Examp2.EXE".
Dim root As XElement = XElement.Load("TestConfig.xml")
Dim tests As IEnumerable(Of XElement) = From el In root.<Test> _
Where el.<CommandLine>.Value = "Examp2.EXE" Select el
For Each el as XElement In tests
Console.WriteLine(el.@TestId)

MCT: Luis Dueñas Pag 135 de 388


Manual de LINQ

Next
Tenga en cuenta que la versión de Visual Basic de este ejemplo utiliza la Propiedades de eje secundario
XML, la Propiedad Axis para atributo XML y la Propiedad Value de XML.

El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns='http://www.adatum.com'>
Module Module1
Sub Main()
Dim root As XElement = XElement.Load("TestConfigInNamespace.xml")
Dim tests As IEnumerable(Of XElement) = From el In root.<Test> _
Where el.<CommandLine>.Value = "Examp2.EXE" Select el
For Each el As XElement In tests
Console.WriteLine(el.@TestId)
Next
End Sub
End Module

7.2.6.1.3. Diferencias entre Realizar Consultas de un


XDocument y de un XElement
Cuando carga un documento mediante XDocument..::.Load, observará que es necesario escribir las
consultas de forma ligeramente diferente a como lo haría en caso de cargar el documento mediante
XElement..::.Load.

Comparación entre XDocument.Load y XElement.Load


Cuando carga un documento XML en un XElement mediante XElement..::.Load, el XElement situado en la
raíz del árbol XML contiene al elemento raíz del documento que se ha cargado. Sin embargo, cuando
carga el mismo documento XML en un XDocument mediante XDocument..::.Load, la raíz del árbol es un
nodo XDocument el elemento raíz del documento cargado es el nodo secundario permitido XElement de
XDocument. Los ejes de LINQ a XML funcionan en relación al nodo raíz.

Este primer ejemplo carga un árbol XML utilizando Load. A continuación, consulta los elementos
secundarios de la raíz del árbol.
' Create a simple document and write it to a file
File.WriteAllText("Test.xml", "<Root>" + Environment.NewLine + _
" <Child1>1</Child1>" + Environment.NewLine + _
" <Child2>2</Child2>" + Environment.NewLine + _
" <Child3>3</Child3>" + Environment.NewLine + _
"</Root>")
Console.WriteLine("Querying tree loaded with XElement.Load")
Console.WriteLine("----")
Dim doc As XElement = XElement.Load("Test.xml")
Dim childList As IEnumerable(Of XElement) = From el In doc.Elements() _
Select el
For Each e As XElement In childList
Console.WriteLine(e)
Next

MCT: Luis Dueñas Pag 136 de 388


Manual de LINQ

El siguiente ejemplo es igual al anterior, con la diferencia de que el árbol XML se carga en un XDocument
en vez de un XElement.
' Create a simple document and write it to a file
File.WriteAllText("Test.xml", "<Root>" + Environment.NewLine + _
" <Child1>1</Child1>" + Environment.NewLine + _
" <Child2>2</Child2>" + Environment.NewLine + _
" <Child3>3</Child3>" + Environment.NewLine + _
"</Root>")
Console.WriteLine("Querying tree loaded with XDocument.Load")
Console.WriteLine("----")
Dim doc As XDocument = XDocument.Load("Test.xml")
Dim childList As IEnumerable(Of XElement) = From el In doc.Elements() _
Select el
For Each e As XElement In childList
Console.WriteLine(e)
Next
Observe que la misma consulta devolvió el nodo Root en vez de los tres nodos secundarios.

Una forma de afrontar este asunto es utilizar la propiedad Root antes de obtener acceso a los métodos
de los ejes, tal y como se indica a continuación:
' Create a simple document and write it to a file
File.WriteAllText("Test.xml", "<Root>" + Environment.NewLine + _
" <Child1>1</Child1>" + Environment.NewLine + _
" <Child2>2</Child2>" + Environment.NewLine + _
" <Child3>3</Child3>" + Environment.NewLine + _
"</Root>")
Console.WriteLine("Querying tree loaded with XDocument.Load")
Console.WriteLine("----")
Dim doc As XDocument = XDocument.Load("Test.xml")
Dim childList As IEnumerable(Of XElement) = From el In
doc.Root.Elements() Select el
For Each e As XElement In childList
Console.WriteLine(e)
Next
Ahora, esta consulta genera el mismo resultado que la consulta del árbol cuya raíz comienza en
XElement.

7.2.6.1.4. Cómo Encontrar Descendientes con un Nombre


de Elemento Específico
A veces, desea encontrar todos los descendientes con un nombre determinado. Podría escribir código
para procesar una iteración en todos los descendientes, pero es más fácil usar el eje Descendants.

Ejemplo
El ejemplo siguiente muestra cómo encontrar descendientes según el nombre de elemento.
Dim root As XElement = _
<root>
<para>
<r>

MCT: Luis Dueñas Pag 137 de 388


Manual de LINQ

<t>Some text </t>


</r>
<n>
<r>
<t>that is broken up into </t>
</r>
</n>
<n>
<r>
<t>multiple segments.</t>
</r>
</n>
</para>
</root>
Dim textSegs As IEnumerable(Of String) = From seg In root...<t> _
Select seg.Value
Dim str As String = textSegs.Aggregate(New StringBuilder, _
Function(sb, i) sb.Append(i), Function(sb) sb.ToString)
Console.WriteLine(str)
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns='http://www.adatum.com'>
Module Module1
Sub Main()
Dim root As XElement = _
<root>
<para>
<r>
<t>Some text </t>
</r>
<n>
<r>
<t>that is broken up into </t>
</r>
</n>
<n>
<r>
<t>multiple segments.</t>
</r>
</n>
</para>
</root>
Dim textSegs As IEnumerable(Of String) = From seg In root...<t> _
Select seg.Value
Dim str As String = textSegs.Aggregate(New StringBuilder, _
Function(sb, i) sb.Append(i), Function(sb) sb.ToString)
Console.WriteLine(str)
End Sub
End Module

MCT: Luis Dueñas Pag 138 de 388


Manual de LINQ

7.2.6.1.5. Cómo Buscar un Descendiente Unico mediante el


Método Descendants
Puede utilizar el método de eje Descendants para escribir rápidamente código para buscar un solo
elemento cuyo nombre es único. Esta técnica es especialmente útil si desea buscar un descendiente
particular con un nombre específico. Puede escribir el código para desplazarse al elemento deseado, pero
a menudo resulta más rápido escribir el código usando el eje Descendants.

Ejemplo
Este ejemplo utiliza el operador de consulta estándar First.
Dim root As XElement = _
<Root>
<Child1>
<GrandChild1>GC1 Value</GrandChild1>
</Child1>
<Child2>
<GrandChild2>GC2 Value</GrandChild2>
</Child2>
<Child3>
<GrandChild3>GC3 Value</GrandChild3>
</Child3>
<Child4>
<GrandChild4>GC4 Value</GrandChild4>
</Child4>
</Root>
Dim grandChild3 As String = (From el In root...<GrandChild3> _
Select el).First()
Console.WriteLine(grandChild3)
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns:aw='http://www.adventure-works.com'>
Module Module1
Sub Main()
Dim root As XElement = _
<aw:Root>
<aw:Child1>
<aw:GrandChild1>GC1 Value</aw:GrandChild1>
</aw:Child1>
<aw:Child2>
<aw:GrandChild2>GC2 Value</aw:GrandChild2>
</aw:Child2>
<aw:Child3>
<aw:GrandChild3>GC3 Value</aw:GrandChild3>
</aw:Child3>
<aw:Child4>
<aw:GrandChild4>GC4 Value</aw:GrandChild4>
</aw:Child4>
</aw:Root>

MCT: Luis Dueñas Pag 139 de 388


Manual de LINQ

Dim grandChild3 As String = (From el In root...<aw:GrandChild3> _


Select el).First()
Console.WriteLine(grandChild3)
End Sub
End Module

7.2.6.1.6. Cómo Escribir Consultas con Filtrado Complejo


Es posible que desee escribir consultas LINQ a XML con filtros complejos. Por ejemplo, quizás debe
buscar todos los elementos que tienen un elemento secundario con un valor y un nombre específicos. En
este tema se proporciona un ejemplo de escritura de una consulta con un filtrado complejo.

Ejemplo
En este ejemplo se muestra cómo buscar todos los elementos PurchaseOrder que tienen un elemento
Address secundario que tiene un atributo Type igual a "Shipping" (envío) y un elemento State secundario
igual a "NY". Utiliza una consulta anidada en la cláusula Where y el operador Any devuelve true si la
colección tiene elementos en ella.
Dim root As XElement = XElement.Load("PurchaseOrders.xml")
Dim purchaseOrders As IEnumerable(Of XElement) = From el In
root.<PurchaseOrder> Where (From add In el.<Address> Where add.@Type =
"Shipping" And add.<State>.Value = "NY" Select add).Any()Select el
For Each el As XElement In purchaseOrders
Console.WriteLine(el.@PurchaseOrderNumber)
Next
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns:aw='http://www.adventure-works.com'>
Module Module1
Sub Main()
Dim root As XElement = XElement.Load("PurchaseOrdersInNamespace.xml")
Dim purchaseOrders As IEnumerable(Of XElement) = From el In
root.<aw:PurchaseOrder> Where (From add In el.<aw:Address> Where
add.@aw:Type = "Shipping" And add.<aw:State>.Value = "NY" Select
add) .Any() Select el
For Each el As XElement In purchaseOrders
Console.WriteLine(el.@aw:PurchaseOrderNumber)
Next
End Sub
End Module

7.2.6.1.7. Cómo Filtrar por un Elemento Opcional


En ocasiones, deseará filtrar por un elemento dado a pesar de que no está seguro de si existe o no en el
documento XML. La consulta debería ejecutarse de forma que si el elemento en particular no tiene
ningún elemento secundario, no se produzca una excepción de referencia nula al filtrar por él. En el
ejemplo siguiente, el elemento Child5 no tiene ningún elemento secundario Type, pero aún así, la
consulta se ejecuta correctamente.

Ejemplo
Este ejemplo utiliza el método de extensión Elements.

MCT: Luis Dueñas Pag 140 de 388


Manual de LINQ

Dim root As XElement = _


<Root>
<Child1>
<Text>Child One Text</Text>
<Type Value="Yes"/>
</Child1>
<Child2>
<Text>Child Two Text</Text>
<Type Value="Yes"/>
</Child2>
<Child3>
<Text>Child Three Text</Text>
<Type Value="No"/>
</Child3>
<Child4>
<Text>Child Four Text</Text>
<Type Value="Yes"/>
</Child4>
<Child5>
<Text>Child Five Text</Text>
</Child5>
</Root>
Dim cList As IEnumerable(Of String) = From typeElement In
root.Elements().<Type> Where typeElement.@Value = "Yes" Select
typeElement.Parent.<Text>.Value
Dim str As String
For Each str In cList
Console.WriteLine(str)
Next
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns='http://www.adatum.com'>
Module Module1
Sub Main()
Dim root As XElement = _
<Root>
<Child1>
<Text>Child One Text</Text>
<Type Value="Yes"/>
</Child1>
<Child2>
<Text>Child Two Text</Text>
<Type Value="Yes"/>
</Child2>
<Child3>
<Text>Child Three Text</Text>
<Type Value="No"/>
</Child3>
<Child4>
<Text>Child Four Text</Text>

MCT: Luis Dueñas Pag 141 de 388


Manual de LINQ

<Type Value="Yes"/>
</Child4>
<Child5>
<Text>Child Five Text</Text>
</Child5>
</Root>
Dim cList As IEnumerable(Of String) = From typeElement In
root.Elements().<Type> Where typeElement.@Value = "Yes" Select
typeElement.Parent.<Text>.Value
Dim str As String
For Each str In cList
Console.WriteLine(str)
Next
End Sub
End Module

7.2.6.1.8. Cómo Buscar Todos los Nodos en un Espacio de


Nombres
Puede filtrar en el espacio de nombres de cada elemento o atributo para buscar todos los nodos de ese
espacio de nombres particular.

Ejemplo
En el ejemplo siguiente se crea un árbol XML con dos espacios de nombres. A continuación, recorre en
iteración los árboles y muestra los nombres de todos los elementos y atributos de esos espacios de
nombres.
Imports <xmlns:aw="http://www.adventure-works.com">
Imports <xmlns:fc="www.fourthcoffee.com">
Module Module1
Sub Main()
Dim xmlTree As XElement = _
<aw:Root>
<fc:Child1>abc</fc:Child1>
<fc:Child2>def</fc:Child2>
<aw:Child3>ghi</aw:Child3>
<fc:Child4>
<fc:GrandChild1>jkl</fc:GrandChild1>
<aw:GrandChild2>mno</aw:GrandChild2>
</fc:Child4>
</aw:Root>
Console.WriteLine("Nodes in the http://www.adventure-works.com
namespace")
Dim awElements As IEnumerable(Of XElement) = From el In
xmlTree.Descendants() Where (el.Name.Namespace =
GetXmlNamespace(aw)) Select el
For Each el As XElement In awElements
Console.WriteLine(el.Name.ToString())
Next
End Sub

MCT: Luis Dueñas Pag 142 de 388


Manual de LINQ

End Module
El archivo XML al que tiene acceso la siguiente consulta contiene pedidos de compra en dos espacios de
nombres diferentes. La consulta crea un nuevo árbol con solo los elementos de uno de los espacios de
nombres.
Imports <xmlns:aw="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim cpo As XDocument = XDocument.Load("ConsolidatedPurchaseOrders.xml")
Dim newTree As XElement = _
<Root>
<%= From el In cpo.Root.Elements() _
Where el.Name.Namespace = GetXmlNamespace(aw) _
Select el %>
</Root>
Console.WriteLine(newTree)
End Sub
End Module

7.2.6.1.9. Cómo Ordenar Elementos


Este ejemplo muestra cómo escribir una consulta que ordene sus resultados.

Ejemplo
Dim root As XElement = XElement.Load("Data.xml")
Dim prices As IEnumerable(Of Decimal) = From el In root.<Data> Let price
= Convert.ToDecimal(el.<Price>.Value) Order By (price) Select price
For Each el As Decimal In prices
Console.WriteLine(el)
Next
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns='http://www.adatum.com'>
Module Module1
Sub Main()
Dim root As XElement = XElement.Load("DataInNamespace.xml")
Dim prices As IEnumerable(Of Decimal) = From el In root.<Data>
Let price = Convert.ToDecimal(el.<Price>.Value) Order By (price)
Select price
For Each el As Decimal In prices
Console.WriteLine(el)
Next
End Sub
End Module

7.2.6.1.10. Cómo Ordenar Elementos en Varias Claves


En este tema se describe cómo ordenar en varias claves.

Ejemplo

MCT: Luis Dueñas Pag 143 de 388


Manual de LINQ

En este ejemplo los resultados se ofrecen primero por código postal de envío y después por fecha de
ordenación.
Dim co As XElement = XElement.Load("CustomersOrders.xml")
Dim result = _
From c In co.<Orders>.<Order> _
Order By c.<ShipInfo>.<ShipPostalCode>.Value,
Convert.ToDateTime(c.<OrderDate>.Value) _
Select New With { _
.CustomerID = c.<CustomerID>.Value, _
.EmployeeID = c.<EmployeeID>.Value, _
.ShipPostalCode = c.<ShipInfo>.<ShipPostalCode>.Value, _
.OrderDate = Convert.ToDateTime(c.<OrderDate>.Value) _
}
For Each r In result
Console.WriteLine("CustomerID:{0} EmployeeID:{1} ShipPostalCode:{2}
OrderDate:{3:d}", r.CustomerID, r.EmployeeID, r.ShipPostalCode,
r.OrderDate)
Next
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns='http://www.adventure-works.com'>
Module Module1
Sub Main()
Dim co As XElement = XElement.Load("CustomersOrdersInNamespace.xml")
Dim result = _
From c In co.<Orders>.<Order> _
Order By c.<ShipInfo>.<ShipPostalCode>.Value,
Convert.ToDateTime(c.<OrderDate>.Value) _
Select New With { _
.CustomerID = c.<CustomerID>.Value, _
.EmployeeID = c.<EmployeeID>.Value, _
.ShipPostalCode = c.<ShipInfo>.<ShipPostalCode>.Value, _
.OrderDate = Convert.ToDateTime(c.<OrderDate>.Value)}
For Each r In result
Console.WriteLine("CustomerID:{0} EmployeeID:{1}
ShipPostalCode:{2} OrderDate:{3:d}", _
r.CustomerID, r.EmployeeID, r.ShipPostalCode, r.OrderDate)
Next
End Sub
End Module

7.2.6.1.11. Cómo Calcular Valores Intermedios


Este ejemplo muestra cómo calcular valores intermedios que se pueden usar para ordenar, filtrar y
seleccionar.

Ejemplo
El siguiente ejemplo utiliza la cláusula Let.
Dim root As XElement = XElement.Load("Data.xml")
Dim extensions As IEnumerable(Of Decimal) = From el In root.<Data> _

MCT: Luis Dueñas Pag 144 de 388


Manual de LINQ

Let extension = CDec(el.<Quantity>.Value) * CDec(el.<Price>.Value) _


Where extension > 25 Order By extension Select extension
For Each ex As Decimal In extensions
Console.WriteLine(ex)
Next
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns="http://www.adatum.com">
Module Module1
Sub Main()
Dim root As XElement = XElement.Load("DataInNamespace.xml")
Dim extensions As IEnumerable(Of Decimal) = From el In
root.<Data> Let extension = CDec(el.<Quantity>.Value) *
CDec(el.<Price>.Value) Where extension > 25 Order By extension _
Select extension
For Each ex As Decimal In extensions
Console.WriteLine(ex)
Next
End Sub
End Module

7.2.6.1.12. Cómo Escribir una Consulta que Busca


Elementos Basándose en el Contexto
Es posible que alguna vez tenga que escribir una consulta que seleccione elementos basándose en su
contexto. Quizás desea filtrar basándose en elementos de mismo nivel precedentes o siguientes. Quizás
desea filtrar basándose e elementos secundarios o elementos antecesores.

Puede hacerlo escribiendo una consulta y utilizando los resultados de la consulta en la cláusula where.
Si primero tiene que realizar una prueba con valores NULL y después probar el valor, resulta más
cómodo realizar la consulta en una cláusula let y después utilizar los resultados en la cláusula where.

Ejemplo
El siguiente ejemplo selecciona todos los elementos p que van seguidos inmediatamente por un elemento
ul.
Dim doc As XElement = _
<Root>
<p id='1'/>
<ul>abc</ul>
<Child>
<p id='2'/>
<notul/>
<p id='3'/>
<ul>def</ul>
<p id='4'/>
</Child>
<Child>
<p id='5'/>
<notul/>

MCT: Luis Dueñas Pag 145 de 388


Manual de LINQ

<p id='6'/>
<ul>abc</ul>
<p id='7'/>
</Child>
</Root>
Dim items As IEnumerable(Of XElement) = From e In doc...<p> _
Let z = e.ElementsAfterSelf().FirstOrDefault() _
Where z IsNot Nothing AndAlso z.Name.LocalName = "ul" Select e
For Each e As XElement In items
Console.WriteLine("id = {0}", e.@<id>)
Next
El siguiente ejemplo muestra la misma consulta sobre un XML que se encuentra en un espacio de
nombres.
Imports <xmlns='http://www.adatum.com'>
Module Module1
Sub Main()
Dim doc As XElement = _
<Root>
<p id='1'/>
<ul>abc</ul>
<Child>
<p id='2'/>
<notul/>
<p id='3'/>
<ul>def</ul>
<p id='4'/>
</Child>
<Child>
<p id='5'/>
<notul/>
<p id='6'/>
<ul>abc</ul>
<p id='7'/>
</Child>
</Root>
Dim items As IEnumerable(Of XElement) = From e In doc...<p> _
Let z = e.ElementsAfterSelf().FirstOrDefault() _
Where z IsNot Nothing AndAlso z.Name =
GetXmlNamespace().GetName("ul") Select e
For Each e As XElement In items
Console.WriteLine("id = {0}", e.@<id>)
Next
End Sub
End Module

7.2.6.1.13. Cómo Depurar Conjuntos de Resultados de


Consultas Vacíos

MCT: Luis Dueñas Pag 146 de 388


Manual de LINQ

Uno de los problemas más habituales al consultar árboles XML es que si el árbol XML tiene un espacio de
nombres predeterminado, el desarrollador a veces escribe la consulta como si el código XML no estuviera
en un espacio de nombres.

El primer conjunto de ejemplos de este tema muestra una forma habitual de cargar XML en un espacio
de nombres predeterminado con una consulta incorrecta.

El segundo conjunto de ejemplos muestra las correcciones necesarias para que pueda consultar el código
XML en un espacio de nombres.

Ejemplo
Este ejemplo muestra la creación de XML en un espacio de nombres, y una consulta que devuelve un
conjunto de resultados vacío.
Dim root As XElement = _
<Root xmlns='http://www.adventure-works.com'>
<Child>1</Child>
<Child>2</Child>
<Child>3</Child>
<AnotherChild>4</AnotherChild>
<AnotherChild>5</AnotherChild>
<AnotherChild>6</AnotherChild>
</Root>
Dim c1 As IEnumerable(Of XElement) = From el In root.<Child> Select el
Console.WriteLine("Result set follows:")
For Each el As XElement In c1
Console.WriteLine(el.Value)
Next
Console.WriteLine("End of result set")
Este ejemplo muestra la creación de XML en un espacio de nombres, y una consulta que se codifica
correctamente.

La solución al usar C# consiste en declarar e inicializar un objeto XNamespace, y usarlo cuando se


especifiquen objetos XName. En este caso, el argumento para el método Elements es un objeto XName.

La solución de Visual Basic consiste en declarar e inicializar un espacio de nombres predeterminado


global. Esto coloca todas las propiedades XML en el espacio de nombres predeterminado. No se requieren
otras modificaciones en el ejemplo para que funcione correctamente.
Imports <xmlns="http://www.adventure-works.com">
Module Module1
Sub Main()
Dim root As XElement = _
<Root xmlns='http://www.adventure-works.com'>
<Child>1</Child>
<Child>2</Child>
<Child>3</Child>
<AnotherChild>4</AnotherChild>
<AnotherChild>5</AnotherChild>
<AnotherChild>6</AnotherChild>
</Root>
Dim c1 As IEnumerable(Of XElement) = From el In root.<Child> Select el
Console.WriteLine("Result set follows:")

MCT: Luis Dueñas Pag 147 de 388


Manual de LINQ

For Each el As XElement In c1


Console.WriteLine(CInt(el))
Next
Console.WriteLine("End of result set")
End Sub
End Module

7.2.6.2. Proyecciones y Transformaciones LINQ a XML


Esta sección proporciona ejemplos de proyecciones y transformaciones LINQ a XML.

7.2.6.2.1. Cómo Trabajar con Diccionarios usando LINQ a


XML
A menudo resulta cómodo convertir variedades de estructuras de datos a XML y de XML a otras
estructuras de datos. Este tema muestra una implementación específica de este enfoque general
convirtiendo un Dictionary<(Of <(TKey, TValue>)>) a XML y de XML.

Ejemplo
La versión de C# de este ejemplo usa una forma de construcción funcional en la que una consulta
proyecta nuevos objetos XElement y la recopilación resultante se pasa como argumento al constructor
del objeto raíz XElement.

La versión de Visual Basic de este ejemplo usa literales de XML y una consulta en una expresión
incrustada. La consulta proyecta nuevos objetos XElement, que se convierten en el contenido nuevo para
el objeto Root XElement.
Dim dict As Dictionary(Of String, String) = New Dictionary(Of String,
String)()
dict.Add("Child1", "Value1")
dict.Add("Child2", "Value2")
dict.Add("Child3", "Value3")
dict.Add("Child4", "Value4")
Dim root As XElement = _
<Root>
<%= From keyValue In dict _
Select New XElement(keyValue.Key, keyValue.Value) %>
</Root>
Console.WriteLine(root)
El siguiente código genera un diccionario de XML.
Dim root As XElement = _
<Root>
<Child1>Value1</Child1>
<Child2>Value2</Child2>
<Child3>Value3</Child3>
<Child4>Value4</Child4>
</Root>
Dim dict As Dictionary(Of String,String)=New Dictionary(Of String,String)
For Each el As XElement In root.Elements
dict.Add(el.Name.LocalName, el.Value)

MCT: Luis Dueñas Pag 148 de 388


Manual de LINQ

Next
For Each str As String In dict.Keys
Console.WriteLine("{0}:{1}", str, dict(str))
Next

7.2.6.2.2. Cómo Transformar la Forma de un Arbol XML


La forma de un elemento XML hace referencia a sus nombres de elemento, nombres de atributo y las
características de su jerarquía.

A veces, deberá cambiar la forma de un elemento XML. Por ejemplo, es posible que deba enviar un
documento XML a otro sistema que requiere nombres de elemento y atributo diferentes. Podría revisar el
documento y eliminar y cambiar el nombre de los elementos necesarios, pero el uso de la construcción
funcional proporciona un código más legible y fácil de mantener.

El primer ejemplo cambia la organización del documento XML. Mueve los elementos complejos de una
ubicación del árbol a otra.

El segundo ejemplo de este tema crea un documento XML con una forma diferente a la del documento de
origen. Cambia el uso de mayúsculas y minúsculas de los nombres de elemento, cambia el nombre de
algunos elementos y deja algunos de los elementos del árbol de origen fuera del árbol transformado.

Ejemplo
El código siguiente cambia la forma de un archivo XML con las expresiones de consulta incrustadas.

El documento XML de origen de este ejemplo contiene un elemento Customers en el elemento Root que
contiene todos los clientes. También contiene un elemento Orders en el elemento Root que contiene
todos los pedidos. Este ejemplo crea un nuevo árbol XML en el que los pedidos de cada cliente se
incluyen en un elemento Orders dentro del elemento Customer. El documento original también contiene
un elemento CustomerID en el elemento Order; este elemento se quitará del documento con la forma
cambiada.
Dim co As XElement = XElement.Load("CustomersOrders.xml")
Dim newCustOrd = _
<Root>
<%= From cust In co.<Customers>.<Customer> _
Select _
<Customer>
<%= cust.Attributes() %>
<%= cust.Elements() %>
<Orders>
<%= From ord In co.<Orders>.<Order> _
Where ord.<CustomerID>.Value = cust.@CustomerID _
Select _
<Order>
<%= ord.Attributes() %>
<%= ord.<EmployeeID> %>
<%= ord.<OrderDate> %>
<%= ord.<RequiredDate> %>
<%= ord.<ShipInfo> %>
</Order> _
%>

MCT: Luis Dueñas Pag 149 de 388


Manual de LINQ

</Orders>
</Customer> _
%>
</Root>
Console.WriteLine(newCustOrd)
Este ejemplo cambia el nombre de algunos elementos y convierte algunos atributos en elementos.

El código llama a ConvertAddress, que devuelve una lista de objetos XElement. El argumento del método
es una consulta que determina el elemento complejo Address en el que el atributo Type tiene un valor
"Shipping".
Function ConvertAddress(ByVal add As XElement) As IEnumerable(Of
XElement)
Dim fragment = New List(Of XElement)
fragment.Add(<NAME><%= add.<Name>.Value %></NAME>)
fragment.Add(<STREET><%= add.<Street>.Value %></STREET>)
fragment.Add(<CITY><%= add.<City>.Value %></CITY>)
fragment.Add(<ST><%= add.<State>.Value %></ST>)
fragment.Add(<POSTALCODE><%= add.<Zip>.Value %></POSTALCODE>)
fragment.Add(<COUNTRY><%= add.<Country>.Value %></COUNTRY>)
Return fragment
End Function
Sub Main()
Dim po As XElement = XElement.Load("PurchaseOrder.xml")
Dim newPo As XElement = _
<PO>
<ID><%= po.@PurchaseOrderNumber %></ID>
<DATE><%= CDate(po.@OrderDate) %></DATE>
<%= ConvertAddress((From el In po.<Address> _
Where el.@Type = "Shipping" Select el).First())%>
</PO>
Console.WriteLine(newPo)
End Sub

7.2.6.2.3. Cómo Controlar el Tipo de una Proyección


La proyección es el proceso de tomar un conjunto de datos, filtrarlo, cambiar su forma e incluso cambiar
su tipo. La mayoría de las expresiones de consulta realizan proyecciones. La mayor parte de las
expresiones de consulta de esta sección se evalúan como IEnumerable<(Of <(T>)>) de XElement,
aunque puede controlar el tipo de proyección para crear colecciones de otros tipos. En este tema se
explica cómo hacerlo.

Ejemplo
En el ejemplo siguiente, se define un nuevo tipo, Customer. La expresión de consulta crea una instancia
de nuevos objetos Customer en la cláusula Select. Esto hace que el tipo de la expresión de consulta sea
IEnumerable<(Of <(T>)>) de Customer.
Public Class Customer
Private customerIDValue As String
Public Property CustomerID() As String
Get
Return customerIDValue

MCT: Luis Dueñas Pag 150 de 388


Manual de LINQ

End Get
Set(ByVal value As String)
customerIDValue = value
End Set
End Property
Private companyNameValue As String
Public Property CompanyName() As String
Get
Return companyNameValue
End Get
Set(ByVal value As String)
companyNameValue = value
End Set
End Property
Private contactNameValue As String
Public Property ContactName() As String
Get
Return contactNameValue
End Get
Set(ByVal value As String)
contactNameValue = value
End Set
End Property
Public Sub New(ByVal customerID As String, _
ByVal companyName As String, _
ByVal contactName As String)
CustomerIDValue = customerID
CompanyNameValue = companyName
ContactNameValue = contactName
End Sub
Public Overrides Function ToString() As String
Return String.Format("{0}:{1}:{2}", Me.CustomerID,
Me.CompanyName, Me.ContactName)
End Function
End Class
Sub Main()
Dim custOrd As XElement = XElement.Load("CustomersOrders.xml")
Dim custList As IEnumerable(Of Customer) = _
From el In custOrd.<Customers>.<Customer> _
Select New Customer(el.@<CustomerID>, el.<CompanyName>.Value, _
el.<ContactName>.Value)
For Each cust In custList
Console.WriteLine(cust)
Next
End Sub

7.2.6.2.4. Cómo Proyectar un Nuevo Tipo


Otros ejemplos de esta sección han mostrado consultados que devuelven resultados como
IEnumerable<(Of <(T>)>) de XElement, IEnumerable<(Of <(T>)>) de string y IEnumerable<(Of

MCT: Luis Dueñas Pag 151 de 388


Manual de LINQ

<(T>)>) de int. Se trata de tipos de resultados comunes, pero no son adecuados para cada caso. En
muchos casos querrá que sus consultas devuelvan un IEnumerable<(Of <(T>)>) de algún otro tipo.

Ejemplo
En este ejemplo se muestra como crear instancias de objetos en la cláusula select. El código primero
define una nueva clase con un constructor y después modifica la instrucción select para que la expresión
sea una nueva instancia de la nueva clase.
Public Class NameQty
Public name As String
Public qty As Integer
Public Sub New(ByVal n As String, ByVal q As Integer)
name = n
qty = q
End Sub
End Class
Public Class Program
Shared Sub Main()
Dim po As XElement = XElement.Load("PurchaseOrder.xml")
Dim nqList As IEnumerable(Of NameQty) = From n In po...<Item> _
Select New NameQty(n.<ProductName>.Value, CInt(n.<Quantity>.Value))
For Each n As NameQty In nqList
Console.WriteLine(n.name & ":" & n.qty)
Next
End Sub
End Class

7.2.6.2.5. Cómo Proyectar un Gráfico de Objetos


En este tema se ilustra cómo proyectar o rellenar un gráfico de objetos de XML.

Ejemplo
El siguiente código rellena un gráfico de objetos con las clases Address, PurchaseOrder y
PurchaseOrderItem.
Class Address
Public Enum AddressUse
Shipping
Billing
End Enum
Private addressTypeVal As AddressUse
Private nameVal As String
Private streetVal As String
Private cityVal As String
Private stateVal As String
Private zipVal As String
Private countryVal As String
Public Property AddressType() As AddressUse
Get
Return addressTypeVal
End Get
Set(ByVal value As AddressUse)

MCT: Luis Dueñas Pag 152 de 388


Manual de LINQ

addressTypeVal = value
End Set
End Property
Public Property Name() As String
Get
Return nameVal
End Get
Set(ByVal value As String)
nameVal = value
End Set
End Property
Public Property Street() As String
Get
Return streetVal
End Get
Set(ByVal value As String)
streetVal = value
End Set
End Property
Public Property City() As String
Get
Return cityVal
End Get
Set(ByVal value As String)
cityVal = value
End Set
End Property
Public Property State() As String
Get
Return stateVal
End Get
Set(ByVal value As String)
stateVal = value
End Set
End Property
Public Property Zip() As String
Get
Return zipVal
End Get
Set(ByVal value As String)
zipVal = value
End Set
End Property
Public Property Country() As String
Get
Return countryVal
End Get
Set(ByVal value As String)
countryVal = value
End Set

MCT: Luis Dueñas Pag 153 de 388


Manual de LINQ

End Property
Public Overrides Function ToString() As String
Dim sb As StringBuilder = New StringBuilder()
sb.Append(String.Format("Type: {0}" + vbNewLine, _
IIf(AddressType = AddressUse.Shipping, "Shipping", "Billing")))
sb.Append(String.Format("Name: {0}" + vbNewLine, Name))
sb.Append(String.Format("Street: {0}" + vbNewLine, Street))
sb.Append(String.Format("City: {0}" + vbNewLine, City))
sb.Append(String.Format("State: {0}" + vbNewLine, State))
sb.Append(String.Format("Zip: {0}" + vbNewLine, Zip))
sb.Append(String.Format("Country: {0}" + vbNewLine, Country))
Return sb.ToString()
End Function
End Class
Class PurchaseOrderItem
Private partNumberVal As String
Private productNameVal As String
Private quantityVal As Integer
Private usPriceVal As Decimal
Private commentVal As String
Private shipDateVal As DateTime
Public Property PartNumber() As String
Get
Return partNumberVal
End Get
Set(ByVal value As String)
partNumberVal = value
End Set
End Property
Public Property ProductName() As String
Get
Return productNameVal
End Get
Set(ByVal value As String)
productNameVal = value
End Set
End Property
Public Property Quantity() As Integer
Get
Return quantityVal
End Get
Set(ByVal value As Integer)
quantityVal = value
End Set
End Property
Public Property USPrice() As Decimal
Get
Return usPriceVal
End Get
Set(ByVal value As Decimal)

MCT: Luis Dueñas Pag 154 de 388


Manual de LINQ

usPriceVal = value
End Set
End Property
Public Property Comment() As String
Get
Return commentVal
End Get
Set(ByVal value As String)
commentVal = value
End Set
End Property
Public Property ShipDate() As DateTime
Get
Return shipDateVal
End Get
Set(ByVal value As DateTime)
shipDateVal = value
End Set
End Property
Public Overrides Function ToString() As String
Dim sb As StringBuilder = New StringBuilder()
sb.Append(String.Format("PartNumber: {0}" + vbNewLine,
PartNumber))
sb.Append(String.Format("ProductName: {0}" + vbNewLine,
ProductName))
sb.Append(String.Format("Quantity: {0}" + vbNewLine, Quantity))
sb.Append(String.Format("USPrice: {0}" + vbNewLine, USPrice))
If (Comment <> Nothing) Then
sb.Append(String.Format("Comment: {0}" + vbNewLine, Comment))
End If
If (ShipDate <> DateTime.MinValue) Then
sb.Append(String.Format("ShipDate: {0:d}" + vbNewLine,
ShipDate))
End If
Return sb.ToString()
End Function
End Class
Class PurchaseOrder
Private purchaseOrderNumberVal As String
Private orderDateVal As DateTime
Private commentVal As String
Private addressesVal As List(Of Address)
Private itemsVal As List(Of PurchaseOrderItem)
Public Property PurchaseOrderNumber() As String
Get
Return purchaseOrderNumberVal
End Get
Set(ByVal value As String)
purchaseOrderNumberVal = value
End Set

MCT: Luis Dueñas Pag 155 de 388


Manual de LINQ

End Property
Public Property OrderDate() As DateTime
Get
Return orderDateVal
End Get
Set(ByVal value As DateTime)
orderDateVal = value
End Set
End Property
Public Property Comment() As String
Get
Return commentVal
End Get
Set(ByVal value As String)
commentVal = value
End Set
End Property
Public Property Addresses() As List(Of Address)
Get
Return addressesVal
End Get
Set(ByVal value As List(Of Address))
addressesVal = value
End Set
End Property
Public Property Items() As List(Of PurchaseOrderItem)
Get
Return itemsVal
End Get
Set(ByVal value As List(Of PurchaseOrderItem))
itemsVal = value
End Set
End Property
Public Overrides Function ToString() As String
Dim sb As StringBuilder = New StringBuilder()
sb.Append(String.Format("PurchaseOrderNumber: {0}" _
+ vbNewLine, PurchaseOrderNumber))
sb.Append(String.Format("OrderDate: {0:d}" + vbNewLine,
OrderDate))
sb.Append(vbNewLine)
sb.Append("Addresses" + vbNewLine)
sb.Append("=====" + vbNewLine)
For Each address As Address In Addresses
sb.Append(address)
sb.Append(vbNewLine)
Next
sb.Append("Items" + vbNewLine)
sb.Append("=====" + vbNewLine)
For Each item As PurchaseOrderItem In Items
sb.Append(item)

MCT: Luis Dueñas Pag 156 de 388


Manual de LINQ

sb.Append(vbNewLine)
Next
Return sb.ToString()
End Function
End Class
Sub Main()
Dim po As XElement = XElement.Load("PurchaseOrder.xml")
Dim purchOrder = New PurchaseOrder With { _
.PurchaseOrderNumber = po.@<PurchaseOrderNumber>, _
.OrderDate = Convert.ToDateTime(po.@<OrderDate>), _
.Addresses = ( _
From a In po.<Address> _
Select New Address With { _
.AddressType = IIf(a.@<Type> = "Shipping", _
Address.AddressUse.Shipping, _
Address.AddressUse.Billing), _
.Name = a.<Name>.Value, _
.Street = a.<Street>.Value, _
.City = a.<City>.Value, _
.State = a.<State>.Value, _
.Zip = a.<Zip>.Value, _
.Country = a.<Country>.Value _
} _
).ToList(), _
.Items = ( _
From i In po.<Items>.<Item> _
Select New PurchaseOrderItem With { _
.PartNumber = i.@<PartNumber>, _
.ProductName = i.<ProductName>.Value, _
.Quantity = i.<Quantity>.Value, _
.USPrice = i.<USPrice>.Value, _
.Comment = i.<Comment>.Value, _
.ShipDate = IIf(i.<ShipDate>.Value <> Nothing, _
Convert.ToDateTime(i.<ShipDate>.Value), _
DateTime.MinValue) _
} _
).ToList() _
}
Console.WriteLine(purchOrder)
End Sub
En este ejemplo el resultado de la consulta de LINQ se devuelve como un IEnumerable<(Of <(T>)>) de
PurchaseOrderItem. Los elementos de la clase PurchaseOrder son del tipo IEnumerable<(Of <(T>)>) de
PurchaseOrderItem. El código usa el método de extensión ToList<(Of <(TSource>)>) para crear una
recopilación List<(Of <(T>)>) de los resultados de la consulta.

7.2.6.2.6. Cómo Proyectar un Tipo Anónimo


En algunos casos quizás desee proyectar una consulta a un nuevo tipo, aunque sepa que sólo utilizará
este tipo durante un breve período. Crear un nuevo tipo solamente para usarlo en la proyección conlleva
mucho trabajo adicional. En este caso un enfoque más eficiente consiste en proyectar en un tipo

MCT: Luis Dueñas Pag 157 de 388


Manual de LINQ

anónimo. Los tipos anónimos permiten definir una clase y, a continuación, declarar e inicializar un objeto
de esa clase, sin dar un nombre a la clase.

Los tipos anónimos son la implementación C# del concepto matemático de una tupla. El término
matemático tupla se originó de la secuencia único, doble, triple, cuádruple, quíntuple, n-tuple. Se refiere
a una secuencia finita de objetos, cada uno de un tipo específico. A veces se denomina una lista de pares
nombre/valor. Por ejemplo, el contenido de una dirección en el documento XML Pedido de compra típico
se puede expresar de la siguiente manera:
Name: Ellen Adams
Street: 123 Maple Street
City: Mill Valley
State: CA
Zip: 90952
Country: USA
Cuando se crea una instancia de un tipo anónimo, resulta práctico pensar en ella como si se creara una
tupla de orden n. Si escribe una consulta que crea una tupla en la cláusula select, la consulta devuelve
un IEnumerable de la tupla.

Ejemplo
En este ejemplo, la cláusula select proyecta un tipo anónimo. A continuación, el ejemplo utiliza var para
crear el objeto IEnumerable. En el bucle foreach, la variable de iteración se convierte en una instancia
del tipo anónimo creado en la expresión de consulta.
Dim custOrd As XElement = XElement.Load("CustomersOrders.xml")
Dim custList = From el In custOrd.<Customers>.<Customer> _
Select New With { _
.CustomerID = el.@<CustomerID>, _
.CompanyName = el.<CompanyName>.Value, _
.ContactName = el.<ContactName>.Value }
For Each cust In custList
Console.WriteLine("{0}:{1}:{2}", cust.CustomerID, cust.CompanyName,
cust.ContactName)
Next

7.2.6.2.7. Cómo Generar Archivos de Texto a partir de XML


Este ejemplo muestra cómo generar un archivo de valores separados por comas (CSV) a partir de un
archivo XML.

Ejemplo
La versión en C# de este ejemplo utiliza sintaxis de métodos y el operador Aggregate para generar un
archivo CSV a partir de un documento XML en una única expresión.

La versión para Visual Basic utiliza un código basado en procedimientos para agrupar la colección de
cadenas en una única cadena.
Dim custOrd As XElement = XElement.Load("CustomersOrders.xml")
Dim strCollection As IEnumerable(Of String) = _
From el In custOrd.<Customers>.<Customer> _
Select _
String.Format("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9}{10}", _
el.@CustomerID, _

MCT: Luis Dueñas Pag 158 de 388


Manual de LINQ

el.<CompanyName>.Value, _
el.<ContactName>.Value, _
el.<ContactTitle>.Value, _
el.<Phone>.Value, _
el.<FullAddress>.<Address>.Value, _
el.<FullAddress>.<City>.Value, _
el.<FullAddress>.<Region>.Value, _
el.<FullAddress>.<PostalCode>.Value, _
el.<FullAddress>.<Country>.Value, _
Environment.NewLine )
Dim sb As StringBuilder = New StringBuilder()
For Each str As String In strCollection
sb.Append(str)
Next
Console.WriteLine(sb.ToString())

7.2.6.2.8. Cómo Generar un XML a partir de Archivos CSV


Este ejemplo muestra el uso de Language-Integrated Query (LINQ) y LINQ a XML para generar un
archivo XML a partir de un archivo de valores separados por comas (CSV).

Ejemplo
El siguiente código realiza una consulta LINQ sobre una matriz de cadenas.
' Create the text file.
Dim csvString As String = "GREAL,Great Lakes Food Market,Howard
Snyder,Marketing Manager,(503) 555-7555,2732 Baker
Blvd.,Eugene,OR,97403,USA" & vbCrLf & _
"HUNGC,Hungry Coyote Import Store,Yoshi Latimer,Sales
Representative,(503) 555-6874,City Center Plaza 516 Main
St.,Elgin,OR,97827,USA" & vbCrLf & _
"LAZYK,Lazy K Kountry Store,John Steel,Marketing Manager,(509) 555-
7969,12 Orchestra Terrace,Walla Walla,WA,99362,USA" & vbCrLf & _
"LETSS,Let's Stop N Shop,Jaime Yorres,Owner,(415) 555-5938,87 Polk
St. Suite 5,San Francisco,CA,94117,USA"
File.WriteAllText("cust.csv", csvString)
' Read into an array of strings.
Dim source As String() = File.ReadAllLines("cust.csv")
Dim cust As XElement = _
<Root>
<%= From strs In source _
Let fields = Split(strs, ",") _
Select _
<Customer CustomerID=<%= fields(0) %>>
<CompanyName><%= fields(1) %></CompanyName>
<ContactName><%= fields(2) %></ContactName>
<ContactTitle><%= fields(3) %></ContactTitle>
<Phone><%= fields(4) %></Phone>
<FullAddress>
<Address><%= fields(5) %></Address>
<City><%= fields(6) %></City>

MCT: Luis Dueñas Pag 159 de 388


Manual de LINQ

<Region><%= fields(7) %></Region>


<PostalCode><%= fields(8) %></PostalCode>
<Country><%= fields(9) %></Country>
</FullAddress>
</Customer> %>
</Root>
Console.WriteLine(cust)

7.2.6.3. Técnicas Avanzadas de Consulta LINQ a XML


En esta sección se proporcionan ejemplos de técnicas de consulta de LINQ a XML más avanzadas.

7.2.6.3.1. Cómo Crear una Jerarquía con la Agrupación


Este ejemplo muestra cómo agrupar datos y después generar XML basado en la agrupación.

Ejemplo
Este ejemplo agrupa primero los datos por categorías y, a continuación, genera un nuevo archivo XML en
el que la jerarquía XML refleja la agrupación.
Dim doc As XElement = XElement.Load("Data.xml")
Dim newData As XElement = _
<Root>
<%= From data In doc.<Data> _
Group By category = data.<Category>(0).Value _
Into groupedData = Group _
Select <Group ID=<%= category %>>
<%= _
From g In groupedData _
Select _
<Data>
<%= g.<Quantity>(0) %>
<%= g.<Price>(0) %>
</Data> _
%>
</Group> _
%>
</Root>
Console.WriteLine(newData)

7.2.6.3.2. Cómo Realizar Consultas de LINQ a XML con


XPath
Este tema presenta los métodos de extensión que permiten consultar un árbol XML con XPath.

A menos que tenga un motivo muy específico para realizar consultas con XPath, como en el caso del uso
intensivo de código heredado, no se recomienda usar XPath con LINQ a XML. Las consultas XPath no se
realizarán tan bien como las consultas LINQ a XML.

Ejemplo

MCT: Luis Dueñas Pag 160 de 388


Manual de LINQ

En el ejemplo siguiente se crea un árbol XML pequeño y se utiliza XPathSelectElements para seleccionar
un conjunto de elementos.
Dim root As XElement = _
<Root>
<Child1>1</Child1>
<Child1>2</Child1>
<Child1>3</Child1>
<Child2>4</Child2>
<Child2>5</Child2>
<Child2>6</Child2>
</Root>
Dim list As IEnumerable(Of XElement)=root.XPathSelectElements("./Child2")
For Each el As XElement In list
Console.WriteLine(el)
Next

7.2.6.3.3. Cómo Enumerar Todos los Nodos de un Arbol


A veces resulta útil enumerar todos los nodos de un árbol. Puede resultar útil cuando se aprende
exactamente cómo afecta al árbol un método o propiedad. Un enfoque para enumerar todos los nodos en
formato de texto consiste en generar una expresión XPath que identifique de forma exacta y específica
cualquier nodo del árbol.

No resulta particularmente útil ejecutar expresiones XPath mediante LINQ a XML. Las expresiones XPath
tienen un rendimiento menor que las consultas de LINQ a XML y las consultas de LINQ a XML son mucho
más eficaces. No obstante, como forma de identificar nodos en el árbol XML, XPath funciona bien.

Ejemplo
Este ejemplo muestra una función con el nombre GetXPath que genera una expresión XPath específica
para cualquier nodo en el árbol XML. Genera expresiones XPath adecuadas incluso cuando hay nodos en
un espacio de nombres. Las expresiones XPath se generan usando prefijos de espacio de nombres.

A continuación el ejemplo crea un pequeño árbol de texto que contiene un ejemplo de varios tipos de
nodos. Después recorre en iteración los nodos descendientes y muestra la expresión XPath para cada
nodo.

Observará que la declaración XML no es un nodo del árbol.

A continuación se muestra un archivo XML que contiene varios tipos de nodos:


Xml
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<?target data?>
<Root AttName="An Attribute" xmlns:aw="http://www.adventure-works.com">
<!--This is a comment-->
<Child>Text</Child>
<Child>Other Text</Child>
<ChildWithMixedContent>text<b>BldTxt</b>otherText</ChildWithMixedContent>
<aw:ElementInNamespace>
<aw:ChildInNamespace />
</aw:ElementInNamespace>
</Root>

MCT: Luis Dueñas Pag 161 de 388


Manual de LINQ

A continuación se muestra la lista de nodos del árbol XML anterior, expresados como expresiones XPath:
/processing-instruction()
/Root
/Root/@AttName
/Root/@xmlns:aw
/Root/comment()
/Root/Child[1]
/Root/Child[1]/text()
/Root/Child[2]
/Root/Child[2]/text()
/Root/ChildWithMixedContent
/Root/ChildWithMixedContent/text()[1]
/Root/ChildWithMixedContent/b
/Root/ChildWithMixedContent/b/text()
/Root/ChildWithMixedContent/text()[2]
/Root/aw:ElementInNamespace
/Root/aw:ElementInNamespace/aw:ChildInNamespace

Module Module1
<System.Runtime.CompilerServices.Extension()> _
Private Function StrCat(Of T)(ByVal source As IEnumerable(Of T), _
ByVal separator As String) As String
Return _
source.Aggregate(New StringBuilder(), _
Function(sb, i) sb _
.Append(i.ToString()) _
.Append(separator), _
Function(s) s.ToString())
End Function
<System.Runtime.CompilerServices.Extension()> _
Public Function GetXPath(ByVal xobj As XObject) As String
Dim retStr As String
If xobj.Parent Is Nothing Then
Dim doc As XDocument = TryCast(xobj, XDocument)
If doc IsNot Nothing Then
Return "."
End If
Dim el As XElement = TryCast(xobj, XElement)
If el IsNot Nothing Then
Return ("/" & NameWithPredicate(el))
End If
' The XPath data model does not include white space text nodes
' that are children of a document, so this method returns null.
Dim xt As XText = TryCast(xobj, XText)
If xt IsNot Nothing Then
Return Nothing
End If
Dim com As XComment = TryCast(xobj, XComment)
If com IsNot Nothing Then
If com.Document().Nodes().OfType(Of XComment)().Count() <> 1 Then

MCT: Luis Dueñas Pag 162 de 388


Manual de LINQ

Return "/comment()[" & (com.NodesBeforeSelf().OfType _


(Of XComment)().Count() + 1) & "]"
Else
Return "/comment()"
End If
End If
Dim pi As XProcessingInstruction = TryCast(xobj,
XProcessingInstruction)
If pi IsNot Nothing Then
If pi.Document.Nodes().OfType(Of
XProcessingInstruction)(). _
Count() <> 1 Then
Return "/processing-instruction()[" & _
(pi.NodesBeforeSelf().OfType(Of
XProcessingInstruction)() _
.Count() + 1) & "]"
Else
Return "/processing-instruction()"
End If
End If
Else
Dim el As XElement = TryCast(xobj, XElement)
If el IsNot Nothing Then
Return "/" & el.Ancestors().InDocumentOrder(). _
Select(Function(e) NameWithPredicate(e)) _
.StrCat("/") & NameWithPredicate(el)
End If
Dim at As XAttribute = TryCast(xobj, XAttribute)
If at IsNot Nothing Then
Return "/" &
at.Parent().AncestorsAndSelf().InDocumentOrder(). _
Select(Function(e) NameWithPredicate(e)).StrCat("/") & _
"@" & GetQName(at)
End If
Dim com As XComment = TryCast(xobj, XComment)
If com IsNot Nothing Then
retStr = "/" &
com.Parent.AncestorsAndSelf().InDocumentOrder(). _
Select(Function(e) NameWithPredicate(e)).StrCat("/") & "comment()"
If com.Parent().Nodes().OfType(Of XComment)().Count() <> 1 Then
retStr &= "[" & (com.NodesBeforeSelf().OfType(Of
XComment)().Count() + 1) & "]"
End If
Return retStr
End If
Dim cd As XCData = TryCast(xobj, XCData)
If cd IsNot Nothing Then
retStr = "/" &
cd.Parent.AncestorsAndSelf().InDocumentOrder(). _
Select(Function(e) NameWithPredicate(e)).StrCat("/")

MCT: Luis Dueñas Pag 163 de 388


Manual de LINQ

& "text()"
If cd.Parent.Nodes().OfType(Of XText)().Count() <> 1 Then
retStr &= "[" & (cd.NodesBeforeSelf().OfType(Of
XText)(). Count() + 1) & "]"
End If
Return retStr
End If
Dim tx As XText = TryCast(xobj, XText)
If tx IsNot Nothing Then
retStr = "/" &
tx.Parent.AncestorsAndSelf().InDocumentOrder(). _
Select(Function(e) NameWithPredicate(e)).StrCat("/") & "text()"
If tx.Parent.Nodes().OfType(Of XText)().Count() <> 1 Then
retStr &= "[" & (tx.NodesBeforeSelf().OfType(Of XText)(). _
Count() + 1) & "]"
End If
Return retStr
End If
Dim pi As XProcessingInstruction = TryCast(xobj,
XProcessingInstruction)
If pi IsNot Nothing Then
retStr = "/" &
pi.Parent.AncestorsAndSelf().InDocumentOrder(). _
Select(Function(e) NameWithPredicate(e)). _
StrCat("/") & "processing-instruction()"
If pi.Parent.Nodes().OfType(Of
XProcessingInstruction)().Count() <> 1 Then
retStr &= "[" & (pi.NodesBeforeSelf().OfType(Of
XProcessingInstruction)(). Count() + 1) & "]"
End If
End If
End If
Return Nothing
End Function
Private Function GetQName(ByVal xe As XElement) As String
Dim prefix As String = xe.GetPrefixOfNamespace(xe.Name.Namespace)
If xe.Name.Namespace = XNamespace.None Or prefix Is Nothing Then
Return xe.Name.LocalName.ToString()
Else
Return prefix + ":" & xe.Name.LocalName.ToString()
End If
End Function
Private Function GetQName(ByVal xa As XAttribute) As String
Dim prefix As String = _
xa.Parent.GetPrefixOfNamespace(xa.Name.Namespace)
If xa.Name.Namespace = XNamespace.None Or prefix Is Nothing Then
Return xa.Name.ToString()
Else
Return prefix + ":" & xa.Name.LocalName
End If

MCT: Luis Dueñas Pag 164 de 388


Manual de LINQ

End Function
Public Function NameWithPredicate(ByVal el As XElement) As String
If el.Parent IsNot Nothing AndAlso
el.Parent.Elements(el.Name).Count() <> 1 Then
Return GetQName(el) + "[" & _
(el.ElementsBeforeSelf(el.Name).Count() + 1) & "]"
Else
Return GetQName(el)
End If
End Function
Sub Main()
Dim aw As XNamespace = "http://www.adventure-works.com"
Dim doc As XDocument = _
<?xml version='1.0' encoding="utf-8" standalone='yes'?>
<?target data?>
<Root AttName='An Attribute' xmlns:aw='http://www.adventure-works.com'>
<!--This is a comment-->
<Child>Text</Child>
<Child>Other Text</Child>
<ChildWithMixedContent>text<b>BldTxt</b>otherText</ChildWithMixedContent>
<aw:ElementInNamespace>
<aw:ChildInNamespace/>
</aw:ElementInNamespace>
</Root>
doc.Save("Test.xml")
Console.WriteLine(File.ReadAllText("Test.xml"))
Console.WriteLine("------")
For Each obj As XObject In doc.DescendantNodes()
Console.WriteLine(obj.GetXPath())
Dim el As XElement = TryCast(obj, XElement)
If el IsNot Nothing Then
For Each at As XAttribute In el.Attributes()
Console.WriteLine(at.GetXPath())
Next
End If
Next
End Sub
End Module

7.2.6.3.4. Cómo Rellenar un Arbol XML a partir del Sistema


de Archivos
Una aplicación habitual y útil de los árboles XML es un almacén de datos de nombres y valores
jerárquicos. Puede rellenar un árbol XML con datos jerárquicos y, a continuación, consultarlo,
transformarlo y, si es necesario, serializarlo. En este escenario de uso, gran parte de la semántica
específica XML (por ejemplo, el comportamiento de los espacios en blanco y los espacios de nombres) no
es importante. En su lugar, usará el árbol XML como una base de datos jerárquica, pequeña y en
memoria, de usuario único.

MCT: Luis Dueñas Pag 165 de 388


Manual de LINQ

Ejemplo
El ejemplo siguiente rellena un árbol XML desde el sistema de archivos local mediante la recursividad. A
continuación, consulta el árbol y calcula el total de los tamaños de todos los archivos del árbol.
Module Module1
Function CreateFileSystemXmlTree(ByVal source As String) As XElement
Dim di As DirectoryInfo = New DirectoryInfo(source)
Return <Dir Name=<%= di.Name %>>
<%= From d In Directory.GetDirectories(source) _
Select CreateFileSystemXmlTree(d) %>
<%= From fi In di.GetFiles() _
Select <File>
<Name><%= fi.Name %></Name>
<Length><%= fi.Length %></Length>
</File> %>
</Dir>
End Function
Sub Main()
Dim fileSystemTree As XElement=CreateFileSystemXmlTree("C:/Tmp")
Console.WriteLine(fileSystemTree)
Console.WriteLine("------")
Dim totalFileSize As Long = _
( _
From f In fileSystemTree...<File> _
Select CLng(f.<Length>(0)) _
).Sum()
Console.WriteLine("Total File Size:{0}", totalFileSize)
End Sub
End Module

7.2.6.4. LINQ a XML para usuarios de XPath


En este conjunto de temas se muestran varias expresiones XPath y sus equivalentes de LINQ a XML.

Todos los ejemplos utilizan la funcionalidad XPath en LINQ a XML que está disponible con los métodos de
extensión de System.Xml.XPath..::.Extensions. Los ejemplos ejecutan la expresión XPath y la expresión
LINQ a XML. A continuación, cada ejemplo compara los resultados de ambas consultas, validando que la
expresión XPath sea funcionalmente equivalente a la consulta de LINQ a XML. Como ambos tipos de
consultas devuelven nodos del mismo árbol XML, la comparación del resultado de las consultas se realiza
mediante identidad referencial.

7.2.6.4.1. Comparación de XPath y LINQ a XML


XPath y LINQ a XML ofrecen una funcionalidad similar. Ambos se pueden usar para consultar un árbol
XML, devolviendo resultados como una recopilación de elementos, una recopilación de nodos o el valor
de un elemento o atributo. Sin embargo, también hay algunas diferencias.

Diferencias entre XPath y LINQ a XML


XPath no permite la proyección de nuevos tipos. Sólo puede devolver recopilaciones de nodos del árbol,
mientras que LINQ a XML puede ejecutar una consulta y proyectar un gráfico de objetos o un árbol XML

MCT: Luis Dueñas Pag 166 de 388


Manual de LINQ

en una nueva forma. Las consultas de LINQ a XML abarcan una funcionalidad mucho mayor y son mucho
más eficaces que las expresiones XPath.

Las expresiones XPath existen de forma aislada en una cadena. El compilador de C# o Visual Basic no
puede ayudar a analizar la expresión XPath en tiempo de compilación. Por el contrario, el compilador de
C# o Visual Basic analiza y compila las consultas de LINQ a XML. El compilador puede detectar muchos
errores de consulta.

Los resultados de XPath no tienen un establecimiento inflexible de tipos. En varias circunstancias, el


resultado de evaluar una expresión XPath es un objeto, y depende del desarrollador determinar el tipo
adecuado y convertir el resultado según sea necesario. Por el contrario, las proyecciones de una consulta
de LINQ a XML tienen un establecimiento inflexible.

Ordenación de resultados
La recomendación XPath 1.0 indica que una recopilación que es el resultado de evaluar una expresión
XPath está desordenada.

No obstante, cuando se recorre en iteración una recopilación devuelta por un método de eje XPath de
LINQ a XML, los nodos de la recopilación se devuelven en el orden del documento. Esto es aplicable
incluso cuando se tiene acceso a los ejes XPath donde se expresan los predicados en términos de orden
de documento inverso, como preceding y preceding-sibling.

Por el contrario, la mayoría de los ejes de LINQ a XML devuelven recopilaciones en el orden del
documento, pero dos de ellos, Ancestors y AncestorsAndSelf, devuelven recopilaciones en el orden
inverso del documento. En la siguiente tabla se enumeran los ejes y se indica el orden de la recopilación
para cada uno:

Eje LINQ a XML Ordenación

XContainer.DescendantNodes Orden del documento

XContainer.Descendants Orden del documento

XContainer.Elements Orden del documento

XContainer.Nodes Orden del documento

XContainer.NodesAfterSelf Orden del documento

XContainer.NodesBeforeSelf Orden del documento

XElement.AncestorsAndSelf Orden del documento inverso

XElement.Attributes Orden del documento

XElement.DescendantNodesAndSelf Orden del documento

XElement.DescendantsAndSelf Orden del documento

XNode.Ancestors Orden del documento inverso

XNode.ElementsAfterSelf Orden del documento

XNode.ElementsBeforeSelf Orden del documento

MCT: Luis Dueñas Pag 167 de 388


Manual de LINQ

XNode.NodesAfterSelf Orden del documento

XNode.NodesBeforeSelf Orden del documento

Predicados de posición
En una expresión XPath, los predicados de posición se expresan en términos de orden del documento
para muchos ejes, pero se expresan en el orden del documento inverso para los ejes inversos, que son
preceding, preceding-sibling, ancestor y ancestor-or-self. Por ejemplo, la expresión de XPath preceding-
sibling::*[1] devuelve el elemento del mismo nivel inmediatamente anterior. Esto es aplicable aunque el
conjunto de resultados finales se presenta en el orden del documento.

Por el contrario, todos los predicados de posición de LINQ a XML siempre se expresan en términos del
orden del eje. Por ejemplo, anElement.ElementsBeforeSelf().ToList()[0] devuelve el primer elemento
secundario del elemento primario del elemento consultado, no el elemento del mismo nivel
inmediatamente anterior. Otro ejemplo: anElement.Ancestors().ToList()[0] devuelve el elemento
primario.

Tenga en cuenta que el enfoque anterior materializa toda la recopilación. Ésta no es la forma más
eficiente de escribir esa consulta. Se ha escrito de esa forma para demostrar el comportamiento de los
predicados de posición. Una forma más adecuada de escribir la misma consulta es usar el método First
de la siguiente manera: anElement.ElementsBeforeSelf().First().

Si deseara buscar el elemento inmediatamente precedente de LINQ a XML escribiría la siguiente


expresión:

ElementsBeforeSelf().Last()

Diferencias de rendimiento
Las consultas de XPath que usan la funcionalidad en XPath LINQ a XML no tienen un rendimiento tan
bueno como las consultas de LINQ a XML.

Comparación de la composición
La composición de una consulta de LINQ a XML es en cierta medida paralela a la composición de una
expresión XPath, aunque tiene una sintaxis muy diferente.

Por ejemplo, si tiene un elemento de una variable con el nombre customers y desea buscar un elemento
inferior con el nombre CompanyName bajo todos los elementos secundarios con el nombre Customer,
debe escribir una expresión XPath de la siguiente manera:
customers.XPathSelectElements("./Customer/CompanyName")
La consulta de LINQ a XML equivalente es:
customers.Element("Customer").Elements("CompanyName")
Existen elementos similares para cada uno de los ejes XPath.

Eje XPath Eje LINQ a XML

secundario (el eje predeterminado) XContainer..::.Elements

Primario (..) XObject..::.Parent

eje de atributo (@) XElement..::.Attribute


o bien
XElement..::.Attributes

MCT: Luis Dueñas Pag 168 de 388


Manual de LINQ

eje antecesor XNode..::.Ancestors

eje antecesor o propio XElement..::.AncestorsAndSelf

eje descendiente (//) XContainer..::.Descendants


o bien
Xcontainer..::.DescendantNodes

descendiente o propio XElement..::.DescendantsAndSelf


o bien
XElement..::.DescendantNodesAndSelf

siguientes-relacionados XNode..::.ElementsAfterSelf
o bien
XNode..::.NodesAfterSelf

precedentes-relacionados XNode..::.ElementsBeforeSelf
o bien
XNode..::.NodesBeforeSelf

siguientes No hay equivalente directo.

precedentes No hay equivalente directo.

7.2.6.4.2. Cómo Buscar un Elemento Secundario


En este tema se compara el eje del elemento secundario XPath con el método LINQ a XML Element.

La expresión XPath es DeliveryNotes.

Ejemplo
Este ejemplo busca el elemento secundario DeliveryNotes.
Dim cpo As XDocument = XDocument.Load("PurchaseOrders.xml")
Dim po As XElement = cpo.Root.<PurchaseOrder>.FirstOrDefault
'LINQ a XML query
Dim el1 As XElement = po.<DeliveryNotes>.FirstOrDefault
' XPath expression
Dim el2 As XElement = po.XPathSelectElement("DeliveryNotes")
' same as "child::DeliveryNotes"
' same as "./DeliveryNotes"
If el1 Is el2 Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
Console.WriteLine(el1)

7.2.6.4.3. Cómo Buscar una Lista de Elementos


Secundarios
En este tema se compara el eje de los elementos secundarios de XPath con el eje LINQ a XML Elements.

La expresión XPath es: ./*

MCT: Luis Dueñas Pag 169 de 388


Manual de LINQ

Ejemplo
Este ejemplo busca todos los elementos secundarios del elemento Address.
Dim cpo As XDocument = XDocument.Load("PurchaseOrders.xml")
Dim po As XElement = cpo.Root.<PurchaseOrder>.<Address>.FirstOrDefault
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = po.Elements()
' XPath expression
Dim list2 As IEnumerable(Of XElement) = po.XPathSelectElements("./*")
If (list1.Count() = list2.Count()) AndAlso _
(list1.Intersect(list2).Count() = list1.Count()) Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list1
Console.WriteLine(el)
Next

7.2.6.4.4. Cómo Buscar el Elemento Raíz


Este tema muestra cómo obtener el elemento raíz con XPath y LINQ a XML.

La expresión XPath es: /PurchaseOrders

Ejemplo
Este ejemplo busca el elemento raíz.
Dim po As XDocument = XDocument.Load("PurchaseOrders.xml")
' LINQ a XML query
Dim el1 As XElement = po.Root
' XPath expression
Dim el2 As XElement = po.XPathSelectElement("/PurchaseOrders")
If el1 Is el2 Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
Console.WriteLine(el1.Name)

7.2.6.4.5. Cómo Buscar Elementos Descendientes


En este tema se muestra cómo obtener los elementos descendientes con un nombre particular.

La expresión XPath es: //Name.

Ejemplo
Este ejemplo busca todos los descendientes con el nombre Name.
Dim po As XDocument = XDocument.Load("PurchaseOrders.xml")
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = po...<Name>
' XPath expression
Dim list2 As IEnumerable(Of XElement) = po.XPathSelectElements("//Name")

MCT: Luis Dueñas Pag 170 de 388


Manual de LINQ

If (list1.Count() = list2.Count() And _


list1.Intersect(list2).Count() = list1.Count()) Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list1
Console.WriteLine(el)
Next

7.2.6.4.6. Cómo Filtrar por Atributo


Este tema muestra cómo obtener los elementos descendientes con un nombre especificado y con un
atributo con un valor especificado.

La expresión XPath es: .//Address[@Type='Shipping']

Ejemplo
Este ejemplo encuentra todos los elementos descendientes con el nombre Address y con un atributo
Type con el valor "Shipping".
Dim po As XDocument = XDocument.Load("PurchaseOrders.xml")
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = From el In po...<Address> Where
el.@Type = "Shipping" Select el
' XPath expression
Dim list2 As IEnumerable(Of XElement) = _
po.XPathSelectElements(".//Address[@Type='Shipping']")
If (list1.Count = list2.Count And _
list1.Intersect(list2).Count() = list1.Count()) Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list1
Console.WriteLine(el)
Next

7.2.6.4.7. Cómo Buscar Elementos Relacionados


Este tema muestra cómo obtener un elemento seleccionando en un atributo al que hace referencia el
valor de otro elemento.

La expresión XPath es: .//Customer[@CustomerID=/Root/Orders/Order[12]/CustomerID]

Ejemplo
Este ejemplo encuentra el duodécimo (12) elemento Order, y después encuentra el cliente de dicho
pedido.

Tenga en cuenta que la indización en una lista de .Net se basa en "cero". Sin embargo, la indización en
una colección de nodos de un predicado XPath se basa en "uno". Este ejemplo refleja esta diferencia.
Dim co As XDocument = XDocument.Load("CustomersOrders.xml")

MCT: Luis Dueñas Pag 171 de 388


Manual de LINQ

' LINQ a XML query


Dim customer1 As XElement = (From el In co...<Customer> Where
el.@CustomerID = co.<Root>.<Orders>.<Order>.
ToList()(11).<CustomerID>(0).Value Select el).First()
' An alternate way to write the query that avoids creation
' of a System.Collections.Generic.List:
Dim customer2 As XElement = (From el In co...<Customer> Where
el.@CustomerID = co.<Root>.<Orders>.<Order>.
Skip(11).First().<CustomerID>(0).Value Select el).First()
' XPath expression
Dim customer3 As XElement = co.XPathSelectElement _
(".//Customer[@CustomerID=/Root/Orders/Order[12]/CustomerID]")
If customer1 Is customer2 And customer1 Is customer3 Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
Console.WriteLine(customer1)

7.2.6.4.8. Cómo Buscar Elementos en un Espacio de


Nombres
Las expresiones XPath pueden buscar nodos en un espacio de nombres específico. Las expresiones XPath
usan prefijos de espacio de nombres para especificar espacios de nombre. Para analizar una expresión
XPath que contiene prefijos de espacio de nombre, debe pasar un objeto a los métodos XPath que
implementan IXmlNamespaceResolver. Este ejemplo usa XmlNamespaceManager.

La expresión XPath es: ./aw:*

Ejemplo
En el ejemplo siguiente se lee un árbol XML que contiene dos espacios de nombres. Usa un objeto
XmlReader para leer el documento XML. Después obtiene un objeto XmlNameTable de XmlReader y un
objeto XmlNamespaceManager de XmlNameTable. Utiliza XmlNamespaceManager al seleccionar
elementos.
Dim reader As XmlReader=XmlReader.Create("ConsolidatedPurchaseOrder.xml")
Dim root As XElement = XElement.Load(reader)
Dim nameTable As XmlNameTable = reader.NameTable
Dim namespaceManager As XmlNamespaceManager = New
XmlNamespaceManager(nameTable)
namespaceManager.AddNamespace("aw", "http://www.adventure-works.com")
Dim list1 As IEnumerable(Of XElement) = _
root.XPathSelectElements("./aw:*", namespaceManager)
Dim list2 As IEnumerable(Of XElement) = From el In root.Elements() Where
el.Name.Namespace = "http://www.adventure-works.com" Select el
If list1.Count() = list2.Count() And list1.Intersect(list2).Count() =
list1.Count() Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")

MCT: Luis Dueñas Pag 172 de 388


Manual de LINQ

End If
For Each el As XElement In list2
Console.WriteLine(el)
Next

7.2.6.4.9. Cómo Encontrar Elementos Relacionados


Anteriores
En este tema se compara el eje XPath preceding-sibling con el eje secundario de LINQ a XML
XNode..::.ElementsBeforeSelf.

La expresión XPath es: preceding-sibling::*

Observe que los resultados de XPathSelectElements y de XNode..::.ElementsBeforeSelf aparecen en el


mismo orden que en el documento.

Ejemplo
El siguiente ejemplo busca el elemento FullAddress y, a continuación, recupera los elementos anteriores
utilizando el eje preceding-sibling.
Dim co As XElement = XElement.Load("CustomersOrders.xml")
Dim add As XElement = co.<Customers>.<Customer>. _
<FullAddress>.FirstOrDefault
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = add.ElementsBeforeSelf()
' XPath expression
Dim list2 As IEnumerable(Of XElement) =
add.XPathSelectElements("preceding-sibling::*")
If list1.Count() = list2.Count() And _
list1.Intersect(list2).Count() = list1.Count() Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list2
Console.WriteLine(el)
Next

7.2.6.4.10. Cómo Buscar Descendientes de un Elemento


Secundario
En este tema se muestra cómo obtener los elementos descendientes de un elemento secundario con un
nombre particular.

La expresión XPath es: ./Paragraph//Text/text()

Ejemplo
Este ejemplo simula los problemas de extraer texto de una representación XML de un documento de un
procesador de texto. Primero selecciona todos los elementos de Paragraph y después selecciona todos

MCT: Luis Dueñas Pag 173 de 388


Manual de LINQ

los elementos descendientes de Text de cada elemento Paragraph. Esto no selecciona los elementos Text
descendientes del elemento Comment.
Dim root As XElement = _
<Root>
<Paragraph>
<Text>This is the start of</Text>
</Paragraph>
<Comment>
<Text>This comment is not part of the paragraph text.</Text>
</Comment>
<Paragraph>
<Annotation Emphasis='true'>
<Text> a sentence.</Text>
</Annotation>
</Paragraph>
<Paragraph>
<Text> This is a second sentence.</Text>
</Paragraph>
</Root>
' LINQ a XML query
Dim str1 As String = _
root.<Paragraph>...<Text>.Select(Function(ByVal s) s.Value). _
Aggregate(New StringBuilder(),Function(ByVal s, ByVal i) s.Append(i),
Function(ByVal s) s.ToString())
' XPath expression
Dim str2 As String =
DirectCast(root.XPathEvaluate("./Paragraph//Text/text()"), IEnumerable) _
.Cast(Of XText)().Select(Function(ByVal s) s.Value) _
.Aggregate(New StringBuilder(),Function(ByVal s, ByVal i)
s.Append(i), Function(ByVal s) s.ToString())
If str1 = str2 Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
Console.WriteLine(str2)

7.2.6.4.11. Cómo Buscar una Unión de dos Rutas de


Ubicación
XPath permite buscar la unión de los resultados de dos rutas de ubicación XPath.

La expresión XPath es: //Category|//Price

Puede conseguir los mismos resultados usando el operador de consulta estándar Concat<(Of
<(TSource>)>).

Ejemplo
Este ejemplo busca todos los elementos Category y todos los elementos Price y los concatena en una
única recopilación. Tenga en cuenta que la consulta LINQ a XML llama a InDocumentOrder<(Of <(T>)>)

MCT: Luis Dueñas Pag 174 de 388


Manual de LINQ

para ordenar los resultados. Los resultados de la evaluación de la expresión XPath también están en el
orden del documento.
Dim data As XDocument = XDocument.Load("Data.xml")
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = _
data...<Category>.Concat(data...<Price>).InDocumentOrder()
' XPath expression
Dim list2 As IEnumerable(Of XElement) = _
data.XPathSelectElements("//Category|//Price")
If list1.Count() = list2.Count() And _
list1.Intersect(list2).Count() = list1.Count() Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list1
Console.WriteLine(el)
Next

7.2.6.4.12. Cómo Buscar Nodos Relacionados


Quizás desea buscar todos los elementos relacionados de un nodo que tienen un nombre específico. La
recopilación resultante puede incluir el nodo de contexto si también tiene el nombre específico.

La expresión XPath es: ../Book

Ejemplo
Este ejemplo primero busca un elemento Book y después busca todos los elementos secundarios con el
nombre Book. La recopilación resultante incluye el nodo de contexto.
Dim books As XDocument = XDocument.Load("Books.xml")
Dim book As XElement = books.Root.<Book>.Skip(1).First()
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = book.Parent.<Book>
' XPath expression
Dim list2 As IEnumerable(Of XElement)=book.XPathSelectElements("../Book")
If list1.Count() = list2.Count() And _
list1.Intersect(list2).Count() = list1.Count() Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list1
Console.WriteLine(el)
Next

7.2.6.4.13. Cómo Buscar un Atributo del Elemento Primario


En este tema se muestra cómo desplazarse hasta el elemento primario y buscar un atributo en él.

La expresión XPath es: ../@id

MCT: Luis Dueñas Pag 175 de 388


Manual de LINQ

Ejemplo
Este ejemplo busca primero un elemento Author. Después busca el atributo id del elemento primario.
Dim books As XDocument = XDocument.Load("Books.xml")
Dim author As XElement = books.Root.<Book>.<Author>.FirstOrDefault()
' LINQ a XML query
Dim att1 As XAttribute = author.Parent.Attribute("id")
' XPath expression
Dim att2 As XAttribute = DirectCast(author.XPathEvaluate("../@id"), _
IEnumerable).Cast(Of XAttribute)().First()
If att1 Is att2 Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
Console.WriteLine(att1)

7.2.6.4.14. Cómo Buscar Atributos de Elementos


Secundarios con un Nombre Específico
En este tema se muestra cómo buscar todos los atributos de los elementos secundarios del nodo de
contexto. Sólo se devuelven los atributos con un nombre específico en la recopilación.

La expresión XPath es: ../Book/@id

Ejemplo
Este ejemplo primero busca un elemento Book, después busca todos los elementos secundarios con el
nombre Book y después busca todos los atributos con el nombre id. El resultado es una recopilación de
atributos.
Dim books as XDocument = XDocument.Load("Books.xml")
Dim book As XElement = books.Root.<Book>(0)
' LINQ a XML query
Dim list1 As IEnumerable(Of XAttribute) = From el In book.Parent.<Book>
Select el.Attribute("id")
' XPath expression
Dim list2 As IEnumerable(Of XAttribute) = DirectCast(book. _
XPathEvaluate("../Book/@id"), IEnumerable).Cast(Of XAttribute)()
If list1.Count() = list2.Count() And _
(list1.Intersect(list2)).Count() = list1.Count() Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XAttribute In list1
Console.WriteLine(el)
Next

7.2.6.4.15. Cómo Buscar Elementos que Tienen un Atributo


Especifico

MCT: Luis Dueñas Pag 176 de 388


Manual de LINQ

En ocasiones, deseará buscar todos los elementos que tengan un atributo en particular. Pero no le
preocupa cuáles es el contenido del atributo. En vez de ello, desea realizar la selección en función de si
existe o no el atributo.

La expresión XPath es: ./*[@Select]

Ejemplo
El siguiente código selecciona únicamente los elementos que tengan el atributo Select.
Dim doc As XElement = _
<Root>
<Child1>1</Child1>
<Child2 Select='true'>2</Child2>
<Child3>3</Child3>
<Child4 Select='true'>4</Child4>
<Child5>5</Child5>
</Root>
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = From el In doc.Elements() Where
el.@Select <> Nothing Select el
' XPath expression
Dim list2 As IEnumerable(Of XElement) = DirectCast(doc.XPathEvaluate _
("./*[@Select]"), IEnumerable).Cast(Of XElement)()
If list1.Count() = list2.Count() And _
list1.Intersect(list2).Count() = list1.Count() Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list1
Console.WriteLine(el)
Next

7.2.6.4.16. Cómo Buscar Elementos Secundarios en base a


la Posición
En ocasiones, deseará buscar elementos en función de su posición. Quizá desee buscar el segundo
elemento o buscar el tercero en el quinto elemento.

La expresión XPath es:

Test[position() >= 2 and position() <= 4]

Existen dos aproximaciones posibles para escribir esta consulta de LINQ a XML de forma diferida. Puede
utilizar los operadores Skip<(Of <(TSource>)>) y Take<(Of <(TSource>)>), o bien utilizar la
sobrecarga Where que recibe un índice. Si utiliza la sobrecarga Where, estará utilizando una expresión
lambda que recibe dos argumentos. El siguiente ejemplo muestra ambos métodos para seleccionar
elementos en base a la posición.

Ejemplo

MCT: Luis Dueñas Pag 177 de 388


Manual de LINQ

Este ejemplo encontrará el segundo en el cuarto elemento de Test. El resultado es una colección de
elementos.
Dim testCfg As XElement = XElement.Load("TestConfig.xml")
' LINQ a XML query
Dim list1 As IEnumerable(Of XElement) = _
testCfg.Elements("Test").Skip(1).Take(3)
'LINQ a XML query
Dim list2 As IEnumerable(Of XElement) = testCfg.Elements("Test"). _
Where(Function(ByVal el, ByVal idx) idx >= 1 And idx <= 3)
' XPath expression
Dim list3 As IEnumerable(Of XElement) = _
testCfg.XPathSelectElements("Test[position()>=2 and position()<=4]")
If list1.Count() = list2.Count() And list1.Count() = list3.Count() And _
list1.Intersect(list2).Count() = list1.Count() And _
list1.Intersect(list3).Count() = list1.Count() Then
Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
For Each el As XElement In list1
Console.WriteLine(el)
Next

7.2.6.4.17. Cómo Buscar el Elemento del Mismo Nivel


Inmediatamente Anterior
Es posible que en alguna ocasión desee buscar el elemento del mismo nivel inmediatamente anterior a
un nodo. Debido a las diferencias de semántica de los predicados posicionales para los ejes de los
elementos del mismo nivel anteriores de XPath en comparación con LINQ a XML, ésta es una de las
comparaciones más interesantes.

Ejemplo
En este ejemplo, la consulta LINQ a XML usa el operador Last para buscar el último nodo de la
recopilación devuelto por ElementsBeforeSelf. Por el contrario, la expresión XPath usa un predicado con
un valor de 1 para buscar el elemento inmediatamente anterior.
Dim root As XElement = _
<Root>
<Child1/>
<Child2/>
<Child3/>
<Child4/>
</Root>
Dim child4 As XElement = root.Element("Child4")
' LINQ a XML query
Dim el1 As XElement = child4.ElementsBeforeSelf().Last()
' XPath expression
Dim el2 As XElement = _
DirectCast(child4.XPathEvaluate("preceding-sibling::*[1]"), _
IEnumerable).Cast(Of XElement)().First()

MCT: Luis Dueñas Pag 178 de 388


Manual de LINQ

If el1 Is el2 Then


Console.WriteLine("Results are identical")
Else
Console.WriteLine("Results differ")
End If
Console.WriteLine(el1)

7.2.7. Modificar Arboles XML


LINQ a XML es el almacén de datos en memoria para un árbol XML. Una vez haya cargado o analizado un
árbol XML a partir de un origen, LINQ a XML le permitirá modificarlo en el momento, para luego
serializarlo, quizá con el objetivo de guardarlo en un archivo o de enviarlo a un servidor remoto.

A la hora de modificar un árbol, puede utilizar ciertos métodos, como por ejemplo, Add.

No obstante, existe otra aproximación posible, la cual consiste en utilizar una construcción funcional para
generar un árbol nuevo que tenga un aspecto diferente. Dependiendo de los tipos de cambios que
necesite hacer en el árbol XML y del tamaño de éste, es posible que esta aproximación resulte más
robusta y sencilla de desarrollar. El primer punto de esta sección compara ambas aproximaciones.

7.2.7.1. Diferencias Entre la Modificación del Arbol XML en


Memoria y la Construcción Funcional
Modificar un árbol XML directamente es un enfoque tradicional para cambiar la forma de un documento
XML. Una aplicación típica carga un documento en un almacén de datos como DOM o LINQ a XML; utiliza
una interfaz de programación para insertar nodos, eliminar nodos o cambiar el contenido de los nodos y,
a continuación, guarda el XML en un archivo o lo transmite a través de una red.

LINQ a XML permite otro enfoque es útil en muchos escenarios: construcción funcional. La construcción
funcional trata la modificación de datos como un problema de transformación en lugar de una
manipulación detallada de un almacén de datos. Si puede tomar una representación de datos y
transformarla eficientemente de una a otra, el resultado es el mismo que si ha tomado un almacén de
datos y lo ha manipulado de alguna manera para que tome otra forma. Un aspecto fundamental del
enfoque de construcción funcional es pasar los resultados de las consultas a los constructores
XDocument y XElement.

En muchos casos puede escribir el código de transformación en una fracción del tiempo que tardaría en
manipular el almacén de datos y ese código es más eficaz y fácil de mantener. En esos casos, aunque el
enfoque de transformación puede necesitar más potencia de procesamiento, es una forma más efectiva
de modificar datos. Si un desarrollador está familiarizado con el enfoque funcional, el código resultante
es en muchos casos más fácil de entender. Resulta sencillo buscar el código que modifica cada parte del
árbol.

El enfoque en el que se modifica un árbol XML directamente es mucho más familiar para muchos
programadores de DOM, mientras que el código escrito usando el enfoque funcional puede ser poco
familiar para un desarrollador que aún no comprende este enfoque. Si sólo tiene que realizar una
pequeña modificación en un árbol XML grande, el enfoque en el que se modifica un árbol directamente
en muchos casos consumirá menos tiempo de CPU.

Este tema proporciona un ejemplo que se implementa con ambos enfoques.

MCT: Luis Dueñas Pag 179 de 388


Manual de LINQ

Transformar atributos en elementos


Para este ejemplo, supongamos que desea modificar el siguiente documento XML sencillo para que los
atributos se conviertan en elementos. Este tema primero presenta el enfoque tradicional de modificación
directa. Después muestra el enfoque de construcción funcional.
Xml
<?xml version="1.0" encoding="utf-8" ?>
<Root Data1="123" Data2="456">
<Child1>Content</Child1>
</Root>
Modificar el árbol XML
Puede escribir código de procedimientos para crear elementos a partir de atributos y después eliminar los
atributos de la siguiente manera:
Dim root As XElement = XElement.Load("Data.xml")
For Each att As XAttribute In root.Attributes()
root.Add(New XElement(att.Name, att.Value))
Next
root.Attributes().Remove()
Console.WriteLine(root)
Enfoque de construcción funcional
Por el contrario, un enfoque funcional consta de un código para formar un nuevo árbol, eligiendo y
seleccionando elementos y atributos del árbol de origen y transformándolos, según convenga, a medida
que se agregan al nuevo árbol. El enfoque funcional tiene el siguiente aspecto:
Dim root As XElement = XElement.Load("Data.xml")
Dim newTree As XElement = _
<Root>
<%= root.<Child1> %>
<%= From att In root.Attributes() _
Select New XElement(att.Name, att.Value) %>
</Root>
Console.WriteLine(newTree)
Este ejemplo produce el mismo XML que el primer ejemplo. No obstante, tenga en cuenta que puede ver
realmente la estructura resultante del nuevo XML en el enfoque funcional. Puede ver la creación del
elemento Root, el código que extrae el elemento Child1 del árbol de origen y el código que transforma
los atributos del árbol de origen a los elementos del nuevo árbol.

El ejemplo funcional de este caso no es más corto que el primer ejemplo y en realidad no es más
sencillo. No obstante, si tiene muchos cambios que realizar a un árbol XML, el enfoque no funcional se
hará más complejo y difícil de entender. Por el contrario, cuando se usa el enfoque funcional, se sigue
formando el XML deseado, incrustando consultas y expresiones según convenga, para extraer el
contenido deseado. El enfoque funcional produce código que es más fácil de mantener.

Tenga en cuenta que el enfoque funcional probablemente no tendrá un rendimiento tan bueno como la
manipulación del árbol. El principal problema es que el enfoque funcional crea objetos de corta duración.
No obstante, la contrapartida es eficacia si el uso del enfoque funcional permite una mayor productividad
del programador.

Éste es un ejemplo muy sencillo, pero sirve para mostrar la diferencia de filosofía entre los dos enfoques.
El enfoque funcional ofrece mayores ganancias de productividad para transformar documentos XML de
gran tamaño.

MCT: Luis Dueñas Pag 180 de 388


Manual de LINQ

7.2.7.2. Agregar Elementos, Atributos y Nodos a un Arbol


XML
Puede agregar contenidos (elementos, atributos, comentarios, instrucciones de procesamiento, texto y
bloques CDATA) a un árbol XML existente.

Métodos para agregar contenidos


Los métodos siguientes agregan contenidos secundarios a un XElement o a un XDocument:

Método Descripción

Add Agrega un contenido al final de los contenidos secundarios del XContainer.

AddFirst Agrega un contenido al comienzo de los contenidos secundarios del XContainer.

Los métodos siguientes agregan contenidos como nodos relacionados de un XNode. El nodo al que se
agregan habitualmente contenidos relacionados es XElement, aunque es posible agregar contenidos
relacionados válidos a otros tipos de nodos, como por ejemplo, al nodo XText o al nodo XComment.

Método Descripción

AddAfterSelf Añade un contenido detrás de XNode.

AddBeforeSelf Añade un contenido antes de XNode.

Ejemplo
Descripción
El siguiente ejemplo crear dos árboles XML y, a continuación, modifica uno de ellos.
Código
Dim srcTree As XElement = _
<Root>
<Element1>1</Element1>
<Element2>2</Element2>
<Element3>3</Element3>
<Element4>4</Element4>
<Element5>5</Element5>
</Root>
Dim xmlTree As XElement = _
<Root>
<Child1>1</Child1>
<Child2>2</Child2>
<Child3>3</Child3>
<Child4>4</Child4>
<Child5>5</Child5>
</Root>
xmlTree.Add(<NewChild>new content</NewChild>)
xmlTree.Add(From el In srcTree.Elements() Where CInt(el) > 3 Select el)
' Even though Child9 does not exist in srcTree, the following statement
' will not throw an exception, and nothing will be added to xmlTree.
xmlTree.Add(srcTree.Element("Child9"))
Console.WriteLine(xmlTree)

MCT: Luis Dueñas Pag 181 de 388


Manual de LINQ

7.2.7.3. Modificar Elementos, Atributos y Nodos en un


Arbol XML
La tabla siguiente resume los métodos y las propiedades que puede usar para modificar un elemento,
sus elementos secundarios o sus atributos.

Los métodos siguientes modifican un elemento XElement.

Método Descripción

XElement..::.Parse Reemplaza un elemento por XML analizado.

XElement..::.RemoveAll Quita todo el contenido (atributos y nodos secundarios) de un


elemento.

XElement..::.RemoveAttributes Quita los atributos de un elemento.

XElement..::.ReplaceAll Reemplaza todo el contenido (nodos secundarios y atributos) de un


elemento.

XElement..::.ReplaceAttributes Reemplaza los atributos de un elemento.

XElement..::.SetAttributeValue Establece el valor de un atributo. Crea el atributo si no existe. Si el


valor se establece en null, quita el atributo.

XElement..::.SetElementValue Establece el valor de un elemento secundario. Crea el elemento si


no existe. Si el valor se establece en null, quita el elemento.

XElement..::.Value Reemplaza el contenido (nodos secundarios) de un elemento por el


texto especificado.

XElement..::.SetValue Establece el valor de un elemento.

Los métodos siguientes modifican un elemento XAttribute.

Método Descripción

XAttribute..::.Value Establece el valor de un atributo.

XAttribute..::.SetValue Establece el valor de un atributo.

Los métodos siguientes modifican un elemento XNode (incluyendo XElement o XDocument).

Método Descripción

XNode..::.ReplaceWith Reemplaza un nodo por contenido nuevo.

Los métodos siguientes modifican un elemento XContainer (XElement o XDocument).

Método Descripción

XContainer..::.ReplaceNodes Reemplaza los nodos secundarios por contenido nuevo.

7.2.7.4. Quitar Elementos, Atributos y Nodos de un Arbol


XML

MCT: Luis Dueñas Pag 182 de 388


Manual de LINQ

Puede modificar un árbol XML mediante la eliminación de elementos, atributos y otros tipos de nodos.

Quitar un elemento o un atributo de un documento XML resulta sencillo. Sin embargo, al quitar
colecciones de elementos o atributos, primero debe materializar una colección en una lista y, a
continuación, eliminar los elementos o los atributos de ésta. El mejor método consiste en usar el método
de extensión Remove, que se ocupará de todo esto.

El motivo principal radica en que la mayoría de las colecciones que se recuperan de un árbol XML se
producen con una ejecución aplazada. Si no las materializa primero en una lista, o bien si no usa los
métodos de extensión, es posible que aparezca una clase determinada de errores.

Los siguientes métodos sirven para quitar nodos y atributos de un árbol XML.

Método Descripción

[M:System.Xml.Linq.XAttribute.Remove()] Quita un elemento XAttribute de su elemento


primario.

[M:System.Xml.Linq.XContainer.RemoveNodes()] Quita los nodos secundarios de un elemento


XContainer de la colección.

XElement..::.RemoveAll Quita el contenido y los atributos de un


elemento XElement.

XElement..::.RemoveAttributes Quita los atributos de un elemento XElement.

XElement..::.SetAttributeValue Si pasa null para el valor, quita el atributo.

XElement..::.SetElementValue Si pasa null para el valor, quita el elemento


secundario.

XNode..::.Remove Quita un elemento XNode de su elemento


primario.

Extensions..::.Remove Quita todos los atributos o elementos de la


colección de origen de su elemento primario.

Ejemplo
Descripción
Este ejemplo demuestra tres métodos para quitar elementos. Primero, quita un solo elemento. En
segundo lugar, recupera una colección de elementos, los materializa con el operador
Enumerable..::.ToList<(Of <(TSource>)>) y quita la colección. Por último, recupera una colección de
elementos y los quita con el método de extensión Remove.
Código
Dim root As XElement = _
<Root>
<Child1>
<GrandChild1/>
<GrandChild2/>
<GrandChild3/>
</Child1>
<Child2>
<GrandChild4/>
<GrandChild5/>
<GrandChild6/>

MCT: Luis Dueñas Pag 183 de 388


Manual de LINQ

</Child2>
<Child3>
<GrandChild7/>
<GrandChild8/>
<GrandChild9/>
</Child3>
</Root>
root.<Child1>.<GrandChild1>.Remove()
root.<Child2>.Elements().ToList().Remove()
root.<Child3>.Elements().Remove()
Console.WriteLine(root)

7.2.7.5. Mantener los Pares Nombre/Valor


Son muchas las aplicaciones que necesitan mantener información que se almacena mejor en forma de
pares de nombre/valor. Esta información podría contener datos sobre configuración o valores globales.
LINQ a XML incluye métodos que facilitan la operación de mantener un conjunto de pares nombre/valor.
Puede almacenar la información como atributos o como un conjunto de elementos secundarios.

Una diferencia existente entre almacenar la información como atributos o como elementos secundarios
es que los atributos tienen, como limitación, que sólo puede existir un atributo con un nombre en
particular para un atributo. Esto no se aplica a los elementos secundarios.

SetAttributeValue y SetElementValue
Los dos métodos que facilitan el mantenimiento de pares nombre/valor son SetAttributeValue y
SetElementValue. La semántica de ambos métodos es muy similar.

SetAttributeValue permite agregar, modificar o eliminar atributos de un elemento.

Si llama al método SetAttributeValue con el nombre de un atributo que no existe, éste creará un
nuevo atributo y lo agregará al elemento especificado.

Si llama al método SetAttributeValue con el nombre de un atributo ya existente y con un


contenido en particular, se sobrescribirán los contenidos del atributo con el contenido
especificado.

Si llama al método SetAttributeValue con el nombre de un atributo ya existente y pasando nulo


en el contenido, se eliminará el atributo de su elemento primario.

SetElementValue permite agregar, modificar o eliminar elementos secundarios de un elemento.

Si llama al método SetElementValue con el nombre de un elemento secundario que no existe,


éste creará un nuevo elemento y lo agregará al elemento especificado.

Si llama al método SetElementValue con el nombre de un elemento ya existente y con un


contenido en particular, se sobrescribirán los contenidos del elemento con el contenido
especificado.

Si llama al método SetElementValue con el nombre de un elemento ya existente y pasando nulo


en el contenido, se eliminará el elemento de su elemento primario.

Ejemplo

MCT: Luis Dueñas Pag 184 de 388


Manual de LINQ

El siguiente ejemplo crea un elemento que no tiene atributos. A continuación, utiliza el método
SetAttributeValue para crear y mantener una lista de pares nombre/valor.
' Create an element with no content.
Dim root As XElement = <Root/>
' Add a number of name/value pairs as attributes.
root.SetAttributeValue("Top", 22)
root.SetAttributeValue("Left", 20)
root.SetAttributeValue("Bottom", 122)
root.SetAttributeValue("Right", 300)
root.SetAttributeValue("DefaultColor", "Color.Red")
Console.WriteLine(root)
' Replace the value of Top.
root.SetAttributeValue("Top", 10)
Console.WriteLine(root)
' Remove DefaultColor.
root.SetAttributeValue("DefaultColor", Nothing)
Console.WriteLine(root)
Ejemplo
El siguiente ejemplo crea un elemento que no tiene elementos secundarios. A continuación, utiliza el
método SetElementValue para crear y mantener una lista de pares nombre/valor.
' Create an element with no content.
Dim root As XElement = <Root/>
' Add a number of name/value pairs as elements.
root.SetElementValue("Top", 22)
root.SetElementValue("Left", 20)
root.SetElementValue("Bottom", 122)
root.SetElementValue("Right", 300)
root.SetElementValue("DefaultColor", "Color.Red")
Console.WriteLine(root)
Console.WriteLine("----")
' Replace the value of Top.
root.SetElementValue("Top", 10)
Console.WriteLine(root)
Console.WriteLine("----")
' Remove DefaultColor.
root.SetElementValue("DefaultColor", Nothing)
Console.WriteLine(root)

7.2.7.6. Cómo Cambiar el Espacio de Nombres de un Arbol


XML Completo
En ocasiones, tendrá que cambiar, mediante programación, el espacio de nombres de un elemento o de
un atributo. Esto resulta sencillo con LINQ a XML. Es posible modificar la propiedad XElement..::.Name.
No es posible modificar la propiedad XAttribute..::.Name, pero es posible copiar con facilidad los
atributos en un System.Collections.Generic..::.List<(Of <(T>)>), eliminar los atributos existentes y, a
continuación, agregar los atributos nuevos que se encuentran en el espacio de nombres deseado.

Ejemplo

MCT: Luis Dueñas Pag 185 de 388


Manual de LINQ

En el código siguiente se crean dos árboles XML que no están en ningún espacio de nombres. A
continuación, se cambia el espacio de nombres de cada árbol y se combinan ambos árboles en uno sólo.
Dim tree1 As XElement = _
<Data>
<Child MyAttr="content">content</Child>
</Data>
Dim tree2 As XElement = _
<Data>
<Child MyAttr="content">content</Child>
</Data>
Dim aw As XNamespace = "http://www.adventure-works.com"
Dim ad As XNamespace = "http://www.adatum.com"
' change the namespace of every element and attribute in the first tree
For Each el As XElement In tree1.DescendantsAndSelf
el.Name = aw.GetName(el.Name.LocalName)
Dim atList As List(Of XAttribute) = el.Attributes().ToList()
el.Attributes().Remove()
For Each at As XAttribute In atList
el.Add(New XAttribute(aw.GetName(at.Name.LocalName), at.Value))
Next
Next
' change the namespace of every element and attribute in the second tree
For Each el As XElement In tree2.DescendantsAndSelf()
el.Name = ad.GetName(el.Name.LocalName)
Dim atList As List(Of XAttribute) = el.Attributes().ToList()
el.Attributes().Remove()
For Each at As XAttribute In atList
el.Add(New XAttribute(ad.GetName(at.Name.LocalName), at.Value))
Next
Next
' add attribute namespaces so that the tree will be serialized with
' the aw and ad namespace prefixes
tree1.Add(New XAttribute(XNamespace.Xmlns + "aw", "http://www.adventure-
works.com"))
tree2.Add(New XAttribute(XNamespace.Xmlns+"ad","http://www.adatum.com"))
' create a new composite tree
Dim root As XElement = _
<Root>
<%= tree1 %>
<%= tree2 %>
</Root>
Console.WriteLine(root)

7.2.8. Programación Avanzada de LINQ a XML


Esta sección proporciona información que sólo será aplicable a desarrolladores avanzados en
determinados escenarios de LINQ a XML.

7.2.8.1. Anotaciones en LINQ a XML

MCT: Luis Dueñas Pag 186 de 388


Manual de LINQ

Las anotaciones de LINQ a XML le permite asociar cualquier objeto de cualquier tipo con un componente
XML de un árbol XML.

Si desea agregar una anotación a un componente XML, como puede ser un XElement o un XAttribute,
deberá llamar al método AddAnnotation. Las anotaciones se obtienen por tipos.

Tenga en cuenta que las anotaciones no forman parte del conjunto de información de XML, por lo que no
se serializan o deserializan.

Métodos
Puede utilizar los siguientes métodos a la hora de trabajar con anotaciones:

Método Descripción

AddAnnotation Agrega un objeto a la lista de anotaciones de un XObject.

Annotation Obtiene el primer objeto de anotación del tipo especificado a partir de un


XObject.

Annotations Obtiene una colección de anotaciones del tipo especificado a partir de un


XObject.

RemoveAnnotations Elimina las anotaciones del tipo especificado de un XObject.

7.2.8.2. Eventos de LINQ a XML


Los eventos de LINQ a XML permiten recibir notificaciones cuando se modifica un árbol XML.

Puede agregar eventos a una instancia de cualquier XObject. El controlador de eventos recibirá entonces
eventos relacionados con las modificaciones realizadas en ese XObject y en cualquiera de sus
descendientes. Por ejemplo, puede agregar un controlador de evento a la raíz del árbol y controlar todas
las modificaciones que se realicen al árbol desde ese controlador de eventos.

Para consultar ejemplos de eventos LINQ a XML, vea Changing y Changed.

Tipos y eventos
Puede utilizar los siguientes tipos a la hora de trabajar con eventos:

Tipo Descripción

XObjectChange Especifica el tipo de evento cuando se produce éste para un XObject.

XObjectChangeEventArgs Proporciona datos para los eventos Changing y Changed.

Los siguientes eventos tienen lugar cuando se modifica un árbol XML:

Evento Descripción

Changing Se produce justo antes de que este XObject o cualquiera de sus descendientes se vayan
a modificar.

Changed Se produce cuando ha cambiado un XObject o cualquiera de sus descendientes.

Ejemplo
Descripción

MCT: Luis Dueñas Pag 187 de 388


Manual de LINQ

Los eventos resultan útiles cuando desea mantener cierta información de agregado en un árbol XML. Por
ejemplo, quizá desee mantener el total de una factura que es la suma de los conceptos de la factura.
Este ejemplo utiliza eventos para mantener el total de todos los elementos secundarios que se
encuentran bajo el elemento complejo Items.
Código
Module Module1
Dim WithEvents items As XElement = Nothing
Dim total As XElement = Nothing
Dim root As XElement = _
<Root>
<Total>0</Total>
<Items></Items>
</Root>
Private Sub XObjectChanged(ByVal sender As Object, _
ByVal cea As XObjectChangeEventArgs) Handles items.Changed
Select Case cea.ObjectChange
Case XObjectChange.Add
If sender.GetType() Is GetType(XElement) Then
total.Value = CStr(CInt(total.Value) + _
CInt((DirectCast(sender, XElement)).Value))
End If
If sender.GetType() Is GetType(XText) Then
total.Value = CStr(CInt(total.Value) + _
CInt((DirectCast(sender, XText)).Value))
End If
Case XObjectChange.Remove
If sender.GetType() Is GetType(XElement) Then
total.Value = CStr(CInt(total.Value) - _
CInt((DirectCast(sender, XElement)).Value))
End If
If sender.GetType() Is GetType(XText) Then
total.Value = CStr(CInt(total.Value) - _
CInt((DirectCast(sender, XText)).Value))
End If
End Select
Console.WriteLine("Changed {0} {1}",sender.GetType().ToString(),_
cea.ObjectChange.ToString())
End Sub
Sub Main()
total = root.<Total>(0)
items = root.<Items>(0)
items.SetElementValue("Item1", 25)
items.SetElementValue("Item2", 50)
items.SetElementValue("Item2", 75)
items.SetElementValue("Item3", 133)
items.SetElementValue("Item1", Nothing)
items.SetElementValue("Item4", 100)
Console.WriteLine("Total:{0}", CInt(total))
Console.WriteLine(root)
End Sub

MCT: Luis Dueñas Pag 188 de 388


Manual de LINQ

End Module

7.2.8.3. Programar con Nodos


Los desarrolladores de LINQ a XML que deben escribir programar como un editor de XML, un sistema de
transformación o un sistema de escritura de informes a menudo deben escribir programas que funcionan
en un nivel de granularidad más fino que los elementos y los atributos. A menudo deben trabajar en el
nivel del nodo, manipulando nodos de texto, procesando instrucciones y comentarios. En este tema se
proporcionan algunos detalles acerca de la programación en el nivel del nodo.

Detalles del nodo


Existen varios detalles de programación que un programador que trabaja en el nivel de nodo debe
conocer.

La propiedad primaria de los nodos secundarios de XDocument está establecida en NULL


La propiedad Parent contiene el XElement primario, no el nodo primario. Los nodos secundarios de
XDocument no tienen XElement primario. Su elemento primario es el documento, de forma que la
propiedad Parent para esos nodos se establece en NULL.

En el siguiente ejemplo se muestra esto:


Dim doc As XDocument = XDocument.Parse("<!-- a comment --><Root/>")
Console.WriteLine(doc.Nodes().OfType(Of XComment).First().Parent Is
Nothing)
Console.WriteLine(doc.Root.Parent Is Nothing)
Los nodos de texto adyacentes son posibles
En varios modelos de programación XML, los nodos de texto adyacente siempre están combinados. A
veces se denomina normalización de los nodos de texto. LINQ a XML no normaliza nodos de texto. Si
agrega dos nodos de texto al mismo elemento, tendrá como resultado nodos de texto adyacentes. No
obstante, si agrega contenido especificado como una cadena en lugar de un nodo XText, LINQ a XML
puede combinar la cadena con un nodo de texto adyacente.

En el siguiente ejemplo se muestra esto:


Dim xmlTree As XElement = <Root>Content</Root>
Console.WriteLine(xmlTree.Nodes().OfType(Of XText)().Count())
' This does not add a new text node.
xmlTree.Add("new content")
Console.WriteLine(xmlTree.Nodes().OfType(Of XText)().Count())
'// This does add a new, adjacent text node.
xmlTree.Add(New XText("more text"))
Console.WriteLine(xmlTree.Nodes().OfType(Of XText)().Count())
Los nodos de texto adyacentes vacíos son posibles
En algunos modelos de programación XML, se garantiza que los nodos de texto no contienen la cadena
vacía. El razonamiento es que ese nodo de texto no tiene ninguna incidencia en la serialización de XML.
No obstante, por la misma razón que los nodos de texto adyacentes son posibles, si quita el texto de un
nodo de texto estableciendo su valor en la cadena vacía, el nodo de texto en sí no se eliminará.
Dim xmlTree As XElement = <Root>Content</Root>
Dim textNode As XText = xmlTree.Nodes().OfType(Of XText)().First()
' The following line does not cause the removal of the text node.
textNode.Value = ""
Dim textNode2 As XText = xmlTree.Nodes().OfType(Of XText)().First()

MCT: Luis Dueñas Pag 189 de 388


Manual de LINQ

Console.WriteLine(">>{0}<<", textNode2)
Un nodo de texto vacío incide sobre la serialización
Si un elemento contiene solamente un nodo de texto que está vacío, se serializa con la sintaxis de
etiqueta larga: <Child></Child>. Si un elemento no contiene ningún nodo secundario, se serializa con la
sintaxis de etiqueta corta: <Child />.
Dim child1 As XElement = New XElement("Child1", New XText(""))
Dim child2 As XElement = New XElement("Child2")
Console.WriteLine(child1)
Console.WriteLine(child2)
Los espacios de nombres son atributos en el árbol LINQ a XML
Aunque las declaraciones del espacio de nombres tienen una sintaxis idéntica a los atributos, en algunas
interfaces de programación como XSLT y XPath, las declaraciones de espacios de nombres no se
consideran atributos. No obstante, en LINQ a XML, los espacios de nombres se almacenan como objetos
XAttribute en el árbol XML. Si recorre en iteración los atributos de un elemento que contiene una
declaración de espacio de nombres, verá la declaración de espacio de nombres como uno de los
elementos de la recopilación devuelta.

La propiedad IsNamespaceDeclaration indica si un atributo es una declaración de espacio de nombre.


Dim root As XElement = _
<Root
xmlns='http://www.adventure-works.com'
xmlns:fc='www.fourthcoffee.com'
AnAttribute='abc'/>
For Each att As XAttribute In root.Attributes()
Console.WriteLine("{0} IsNamespaceDeclaration:{1}", att, _
att.IsNamespaceDeclaration)
Next
Los métodos del eje XPath no devuelven un espacio en blanco secundario de XDocument
LINQ a XML permite nodos de texto secundarios de un XDocument mientras los nodos de texto
contengan solamente espacios en blanco. No obstante, el modelo del objeto XPath no incluye el espacio
en blanco como nodos secundarios de un documento, así que cuando recorra en iteración los elementos
secundarios de XDocument usando el eje Nodes, se devolverán los nodos de texto del espacio en blanco.
Sin embargo, cuando recorra en iteración los elementos secundarios de XDocument usando los métodos
del eje de XPath, no se devolverán los nodos de texto de espacio en blanco.
' Create a document with some white space child nodes of the document.
Dim root As XDocument = XDocument.Parse( _
"<?xml version='1.0' encoding='utf-8' standalone='yes'?>" & _
vbNewLine & "<Root/>" & vbNewLine & "<!--a comment-->" & vbNewLine, _
LoadOptions.PreserveWhitespace)
' Count the white space child nodes using LINQ to XML.
Console.WriteLine(root.Nodes().OfType(Of XText)().Count())
' Count the white space child nodes using XPathEvaluate.
Dim nodes As IEnumerable=CType(root.XPathEvaluate("text()"),IEnumerable)
Console.WriteLine(nodes.OfType(Of XText)().Count())
Los objetos XDeclaration no son nodos
Cuando recorra en iteración los nodos secundarios de XDocument, no verá el objeto de declaración XML.
Es una propiedad del documento, no un nodo secundario de él.
Dim doc As XDocument = _
<?xml version='1.0' encoding='utf-8' standalone='yes'?>

MCT: Luis Dueñas Pag 190 de 388


Manual de LINQ

<Root/>
doc.Save("Temp.xml")
Console.WriteLine(File.ReadAllText("Temp.xml"))
' This shows that there is only one child node of the document.
Console.WriteLine(doc.Nodes().Count())

7.2.8.4. Cómo Leer y Escribir un Documento Codificado


Para crear un documento XML codificado, se agrega un XDeclaration al árbol XML, estableciendo la
codificación en el nombre de página de códigos deseado.

Los valores devueltos por WebName son valores válidos.

Si lee un documento codificado, la propiedad Encoding estará establecida en el nombre de la página de


códigos.

Si establece Encoding en un nombre de página de códigos válido, LINQ a XML se serializará con la
codificación especificada.

Ejemplo
En el ejemplo siguiente se crean dos documentos, uno con codificación utf-8 y otro con codificación utf-
16. A continuación se cargan los documentos y se imprime la codificación en la consola.
Console.WriteLine("Creating a document with utf-8 encoding")
Dim encodedDoc8 As XDocument = _
<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Root>Content</Root>
encodedDoc8.Save("EncodedUtf8.xml")
Console.WriteLine("Encoding is:{0}", encodedDoc8.Declaration.Encoding)
Console.WriteLine()
Console.WriteLine("Creating a document with utf-16 encoding")
Dim encodedDoc16 As XDocument = _
<?xml version='1.0' encoding='utf-16' standalone='yes'?>
<Root>Content</Root>
encodedDoc16.Save("EncodedUtf16.xml")
Console.WriteLine("Encoding is:{0}", encodedDoc16.Declaration.Encoding)
Console.WriteLine()
Dim newDoc8 As XDocument = XDocument.Load("EncodedUtf8.xml")
Console.WriteLine("Encoded document:")
Console.WriteLine(File.ReadAllText("EncodedUtf8.xml"))
Console.WriteLine()
Console.WriteLine("Encoding of loaded document is:{0}",
newDoc8.Declaration.Encoding)
Console.WriteLine()
Dim newDoc16 As XDocument = XDocument.Load("EncodedUtf16.xml")
Console.WriteLine("Encoded document:")
Console.WriteLine(File.ReadAllText("EncodedUtf16.xml"))
Console.WriteLine()
Console.WriteLine("Encoding of loaded document is:{0}",
newDoc16.Declaration.Encoding)

MCT: Luis Dueñas Pag 191 de 388


Manual de LINQ

7.2.8.5. Usar XSLT para Transformar un Arbol XML


Puede crear un árbol XML, crear un objeto XmlReader desde el árbol XML, crear un nuevo documento y
crear un objeto XmlWriter que escribirá en el nuevo documento. A continuación, puede invocar la
transformación XSLT, pasando XmlReader y XmlWriter a la transformación. Después de que se complete
correctamente la transformación, se rellenará el nuevo árbol XML con los resultados de la
transformación.

Ejemplo
Dim xslMarkup As XDocument = _
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
version='1.0'>
<xsl:template match='/Parent'>
<Root>
<C1>
<xsl:value-of select='Child1'/>
</C1>
<C2>
<xsl:value-of select='Child2'/>
</C2>
</Root>
</xsl:template>
</xsl:stylesheet>
Dim xmlTree As XElement = _
<Parent>
<Child1>Child1 data</Child1>
<Child2>Child2 data</Child2>
</Parent>
Dim newTree As XDocument = New XDocument()
Using writer As XmlWriter = newTree.CreateWriter()
' Load the style sheet.
Dim xslt As XslCompiledTransform = _
New XslCompiledTransform()
xslt.Load(xslMarkup.CreateReader())
' Execute the transform and output the results to a writer.
xslt.Transform(xmlTree.CreateReader(), writer)
End Using
Console.WriteLine(newTree)

7.2.9. Seguridad de LINQ a XML


Este tema describe los problemas de seguridad asociados a LINQ a XML. Además, proporciona algunas
indicaciones para reducir la exposición a esos problemas de seguridad.

Información general de seguridad de LINQ a XML


LINQ a XML se ha diseñado más para la comodidad de programación que para aplicaciones de servidor
con estrictos requisitos de seguridad. La mayoría de escenarios XML consisten en el procesamiento de
documentos XML de confianza en lugar del procesamiento de documentos XML que no son de confianza
que se suben a un servidor. LINQ a XML está optimizado para esos escenarios.

MCT: Luis Dueñas Pag 192 de 388


Manual de LINQ

Si debe procesar datos que no son de confianza de orígenes desconocidos, Microsoft recomienda usar
una instancia de la clase XmlReader que se ha configurado para filtrar ataques de denegación de servicio
(DoS) XML.

Si ha configurado XmlReader para mitigar los ataques de denegación de servicio, puede usar ese lector
para rellenar un árbol de LINQ a XML y seguir sacando provecho de las mejoras de productividad del
programador de LINQ a XML. Muchas técnicas de reducción de riesgo implican crear lectores
configurados para mitigar el problema de seguridad y crear a continuación instancias de un árbol XML
mediante el lector configurado.

XML es intrínsecamente vulnerable a ataques de denegación de servicio porque los documentos no tienen
límite en el tamaño, la profundidad, el tamaño de nombre de elemento, etc. Independientemente del
componente que use para procesar XML, siempre debería estar preparado para reciclar el dominio de la
aplicación si utiliza demasiados recursos.

Reducción del riesgo de ataques de XML, XSD, XPath y XSLT


LINQ a XML se basa en XmlReader y XmlWriter. LINQ a XML admite XSD y XPath mediante métodos de
extensión en los espacios de nombres System.Xml.Schema y System.Xml.XPath. El uso de las clases
XmlReader, XPathNavigator y XmlWriter combinado con LINQ a XML, puede invocar XSLT para
transformar árboles XML.

Cuando trabaja en un entorno menos seguro, existen varios problemas de seguridad asociados con XML
y con el uso de clases en System.Xml, System.Xml.Schema, System.Xml.XPath y System.Xml.Xsl.
Algunos de los problemas son:

XSD, XPath y XSLT son lenguajes basados en cadenas en los que puede especificar operaciones
que consumen mucho tiempo o memoria. Es responsabilidad de los programadores que toman
cadenas XSD, XPath o XSLT de orígenes que no son de confianza validar que dichas cadenas no
son malintencionadas o supervisar y reducir la posibilidad de que la evaluación de esas cadenas
provoque un consumo excesivo de recursos del sistema.

Los esquemas XSD (incluyendo los esquemas en línea) son intrínsecamente vulnerables a
ataques de denegación de servicio. No se deben aceptar esquemas de orígenes que no sean de
confianza.

XSD y XSLT pueden incluir referencias a otros archivos y esas referencias pueden producir
ataques entre dominios y zonas.

Las entidades externas de DTD pueden tener como resultado ataques entre dominios y zonas.

Las DTD son vulnerables a ataques de denegación de servicio.

Los documentos XML con una profundidad excepcional pueden plantear problemas de
denegación de servicio. Es posible que desee limitar la profundidad de documentos XML.

No se admiten componentes como, por ejemplo, objetos NameTable, XmlNamespaceManager y


XmlResolver, de ensamblados que no sean de confianza.

Lectura de datos en fragmentos para mitigar ataques de documentos de gran tamaño.

Los bloques de scripts de las hojas de estilo XSLT pueden dar lugar a varios ataques.

MCT: Luis Dueñas Pag 193 de 388


Manual de LINQ

Validar cuidadosamente antes de crear expresiones XPath dinámicas.

Problemas de seguridad de LINQ a XML


Los problemas de seguridad de este tema no se presentan en ningún orden específico. Todos los
problemas son importantes y deben solucionarse de forma adecuada.

Un ataque de elevación de privilegios que tiene éxito proporciona a un ensamblado malintencionado más
control sobre su entorno. Un ataque de elevación de privilegios que tiene éxito puede tener como
resultado una revelación de datos, una denegación de servicio, etc.

Las aplicaciones no deben revelar datos a los usuarios que no están autorizados a verlos.

Los ataques de denegación de servicio hacen que el analizador XML o LINQ a XML consuman excesiva
memoria o tiempo de CPU. Los ataques de denegación de servicio se consideran menos graves que los
ataques de elevación de privilegios o los ataques de revelación de datos. No obstante, son importantes
en un escenario en el que un servidor debe procesar documentos XML de orígenes que no sean de
confianza.

Las excepciones y los mensajes de error pueden revelar datos


La descripción de un error puede revelar datos como la transformación de datos, los nombres de archivos
o los detalles de implementación. Los mensajes de error no se deben mostrar a los autores de la llamada
que no son de confianza. Debe detectar todos los errores e notificarlos con sus propios mensajes de
error.

No llamar a CodeAccessPermissions.Assert en un controlador de eventos


Un ensamblado puede tener más o menos permisos. Un ensamblado con más permisos cuenta con un
mayor control sobre el equipo y sus entornos.

Si el código de un ensamblado con más permisos llama a CodeAccessPermission..::.Assert en un


controlador de eventos y, a continuación, el árbol XML se pasa a un ensamblado malintencionado que
tiene permisos restringidos, el ensamblado malintencionado puede hacer que se genere un evento. Como
el evento ejecuta código que está en el ensamblado con más permisos, el ensamblado malintencionado
funcionará con privilegios elevados.

Microsoft recomienda no llamar nunca a CodeAccessPermission..::.Assert en un controlador de eventos.

Las DTD no son seguras


Las entidades de DTD son inseguras intrínsecamente. Un documento XML malintencionado que contiene
DTD puede hacer que el analizador use toda la memoria y todo el tiempo de CPU, lo que provocaría un
ataque de denegación de servicio. Por lo tanto, en LINQ a XML, el procesamiento de DTD está
desactivado de forma predeterminada. No debe aceptar DTD de orígenes que no sean de confianza.

Un ejemplo de DTD aceptadas de orígenes que no son de confianza es una aplicación web que permite a
los usuarios cargar un archivo XML que hace referencia a una DTD y un archivo de DTD. Al validar el
archivo, una DTD malintencionada puede ejecutar un ataque de denegación de servicio en el servidor.
Otro ejemplo de DTD aceptadas de orígenes que no son de confianza es hacer referencia a una DTD en
un recurso compartido de red que también permite el acceso FTP anónimo.

Evitar la asignación excesiva de búfer


Los desarrolladores de aplicaciones deben tener en cuenta que orígenes de datos extremadamente
grandes pueden provocar un agotamiento de los recursos y ataques de denegación de servicio.

MCT: Luis Dueñas Pag 194 de 388


Manual de LINQ

Si un usuario malintencionado envía o carga un documento XML de gran tamaño, puede hacer que LINQ
a XML consuma demasiados recursos del sistema. Esto puede representar un ataque de denegación de
servicio. Para evitarlo, puede ajustar la propiedad XmlReaderSettings..::.MaxCharactersInDocument y
crear un lector con un límite en el tamaño del documento que puede cargar. El lector se usa a
continuación para crear el árbol XML.

Por ejemplo, si sabe que el tamaño máximo previsto de los documentos XML que provienen de un origen
que no es de confianza va a ser inferior a 50 KB, establezca
XmlReaderSettings..::.MaxCharactersInDocument en 100.000. Esto no será un inconveniente para el
procesamiento de sus documentos XML y a la vez contrarrestará amenazas de denegación de servicio en
las que se podrían cargar documentos que consumirían grandes cantidades de memoria.

Evitar la expansión excesiva de entidades


Uno de los ataques de denegación de servicio conocidos cuando se usa una DTD es un documento que
provoca una expansión excesiva de entidades. Para evitar que esto se produzca, puede establecer la
propiedad XmlReaderSettings..::.MaxCharactersFromEntities y crear un lector que tenga un límite en el
número de caracteres producidos por la expansión de la entidad. El lector se usa a continuación para
crear el árbol XML.

Limitar la profundidad de la jerarquía XML


Un posible ataque de denegación de servicio consiste en enviar un documento que tiene una profundidad
de jerarquía excesiva. Para evitarlo, puede encapsular XmlReader en su propia clase que cuenta la
profundidad de los elementos. Si la profundidad supera un nivel razonable predeterminado, puede
finalizar el procesamiento del documento malintencionado.

Protegerse frente a implementaciones de XmlReader o XmlWriter que no son de confianza


Los administradores deben comprobar que las implementaciones de XmlReader o XmlWriter
proporcionadas de forma externa tienen nombres seguros y se han registrado en la configuración del
equipo. Esto evita que se cargue un código malintencionado que simula ser un lector o escritor.

Liberar periódicamente objetos que hacen referencia a XName


Para protegerse frente a cierto tipo de ataques, los programadores de aplicaciones deben liberar todos
los objetos que hacen referencia a un objeto XName en el dominio de la aplicación de forma periódica.

Protegerse frente a nombres XML aleatorios


Las aplicaciones que toman datos de orígenes que no son de confianza deben plantearse el uso de
XmlReader, que se incluye en el código personalizado para inspeccionar la posibilidad de nombres y
espacios de nombre XML aleatorios. Si se detectan nombres y espacios de nombres XML aleatorios, la
aplicación puede finalizar el procesamiento del documento malintencionado.

Es posible que desee limitar los nombres incluidos en cualquier espacio de nombres dado (incluyendo
nombres que no están en ningún espacio de nombres) a una cantidad razonable.

Los componentes de software que comparten un árbol de LINQ a XML pueden tener acceso a
las anotaciones
LINQ a XML se puede usar para crear canalizaciones de procesamiento en las que diferentes
componentes de la aplicación cargan, validan, consultan, transforman, actualizan y guardan datos XML
que se pasan entre componentes como árboles XML. Esto puede ayudarle a optimizar el rendimiento,
porque la sobrecarga de carga y serialización de objetos a texto XML se realiza únicamente en los
extremos de la canalización. Sin embargo, los desarrolladores deben tener en cuenta que otros
componentes puedes tener acceso a todas las anotaciones y controladores de eventos creados por un

MCT: Luis Dueñas Pag 195 de 388


Manual de LINQ

componente. Esto puede crear varias vulnerabilidades cuando los componentes tienen diferentes niveles
de confianza. Para crear canalizaciones seguras en componentes con un nivel de confianza inferior, debe
serializar los objetos LINQ a XML a texto XML antes de pasar los datos a un componente que no es de
confianza.

Common Language Runtime (CLR) proporciona cierta seguridad. Por ejemplo, un componente que no
incluye una clase privada no puede tener acceso a anotaciones con claves creadas por esa clase. No
obstante, las anotaciones pueden ser eliminadas por componentes que no las leen. Esto se podría usar
como ataque de manipulación.

8. LINQ a ADO .NET


Language-Integrated Query (LINQ) define un conjunto de operadores de consulta estándar generales
que se pueden usar en lenguajes de programación de .NET Framework 3.0. Estos operadores de consulta
estándar permiten proyectar, filtrar y recorrer recopilaciones en memoria o tablas de una base de datos.
Tenga en cuanta que las consultas de LINQ se expresan en el lenguaje de programación mismo y no
como literales de cadena incrustados en el código de la aplicación. Se trata de un cambio significativo
respecto a la forma en que la mayoría de aplicaciones se han escrito en versiones anteriores de .NET
Framework. Escribir consultas desde el lenguaje de programación ofrece varias ventajas fundamentales.
Simplifica las consultas eliminando la necesidad de usar un lenguaje de consulta independiente. Y si
utiliza el IDE de Visual Studio 2008, LINQ también permite aprovechar la comprobación en tiempo de
compilación, los tipos estáticos e IntelliSense.

LINQ se integra en varios aspectos del acceso a datos de .NET Framework, incluyendo el modelo de
programación desconectada del DataSet y los esquemas de base de datos de SQL Server existentes. En
esta sección se describe LINQ a ADO.NET, la implementación ADO.NET de LINQ.

El siguiente diagrama proporciona una visión general de cómo se relaciona LINQ a ADO.NET con
lenguajes de programación de alto nivel, diferentes de las tecnologías LINQ y orígenes de datos
compatibles con LINQ.

8.1. Información General de LINQ a ADO .NET


Actualmente, muchos programadores empresariales deben usar dos (o más) lenguajes de programación:
un lenguaje de alto nivel para las capas de presentación y lógica empresarial (como Visual C# o Visual

MCT: Luis Dueñas Pag 196 de 388


Manual de LINQ

Basic) y un lenguaje de consulta para interactuar con la base de datos (como Transact-SQL). Esto
requiere que el programador tenga conocimientos de varios idiomas para ser efectivo y también causa
discrepancias de idiomas en el entorno de desarrollo. Por ejemplo, una aplicación que utiliza API de
acceso a datos para ejecutar una consulta en una base de datos especifica la consulta como un literal de
cadena usando comillas. Esta cadena de consulta es ilegible y no se comprueba si contiene errores, tales
como una sintaxis no válida o si existen las columnas o las filas a las que hace referencia. No hay
ninguna comprobación de tipo de los parámetros de consulta y tampoco hay compatibilidad con
IntelliSense.

Language-Integrated Query (LINQ) permite a los programadores formar consultas basadas en conjuntos
en el código de su aplicación, sin tener que usar un lenguaje de consulta independiente. Se puede
escribir consultas de LINQ en varios orígenes de datos enumerables (es decir, un origen de datos que
implementa la interfaz IEnumerable), como estructuras de datos en memoria, documentos XML, bases
de datos SQL y objetos DataSet. Aunque esos orígenes de datos enumerables se implementan de varias
formas, todos revelan las mismas construcciones de lenguaje y sintaxis. Como las consultas se pueden
formar en el lenguaje de programación mismo, no es necesario utilizar otro lenguaje de consultas que
esté incrustado como literales de cadena que el compilador no pueda entender o verificar. La integración
de consultas en el lenguaje de programación también permite a los programadores de Visual Studio ser
más productivos proporcionando comprobación de sintaxis y tipo en tiempo de compilación e
IntelliSense. Estas características reducen la necesidad de depuración y corrección de errores de
consultas.

LINQ a ADO.NET consta de dos tecnologías diferentes: LINQ a DataSet y LINQ a SQL. LINQ a DataSet
proporciona consultas más enriquecidas y optimizadas en DataSet y LINQ a SQL permite consultar
directamente esquemas de base de datos de SQL Server.

La transferencia de datos de las tablas de SQL a objetos de memoria a menudo es una tarea tediosa y
propensa a errores. El proveedor de LINQ implementado por LINQ a DataSet y LINQ a SQL convierte el
origen de datos en recopilaciones de objetos basadas en IEnumerable. El programador siempre ve los
datos como una recopilación de IEnumerable cuando se realiza la consulta y la actualización. Se
proporciona compatibilidad completa con IntelliSense para escribir consultas en esas colecciones.

En las siguientes secciones se proporciona más información acerca de LINQ a DataSet y LINQ a SQL.

LINQ a DataSet
DataSet es un elemento fundamental del modelo de programación desconectada sobre el que se basa
ADO.NET y se usa ampliamente. LINQ a DataSet permite a los programadores crear capacidades de
consulta más complejas en DataSet utilizando el mismo mecanismo de formulación de consultas que está
disponible para muchos otros orígenes de datos.

LINQ a SQL
LINQ a SQL es una herramienta útil para programadores que no requieren la asignación a un modelo
conceptual. Si utiliza LINQ a SQL, puede usar el modelo de programación de LINQ directamente en un
esquema de base de datos existente. LINQ a SQL permite a los programadores generar clases de .NET
Framework que representan datos. En lugar de la asignación a un modelo de datos conceptual, esas
clases generadas se asignan directamente a tablas de bases de datos, vistas, procedimientos
almacenados y funciones definidas por el usuario.

MCT: Luis Dueñas Pag 197 de 388


Manual de LINQ

Con LINQ a SQL, los programadores pueden escribir código directamente en el esquema de
almacenamiento usando el mismo modelo de programación de LINQ que las recopilaciones en memoria y
DataSet, además de otros orígenes de datos como XML.

8.2. LINQ a DataSet


LINQ a DataSet facilita y acelera las consultas en datos almacenados en caché en un objeto DataSet. En
concreto, LINQ a DataSet simplifica la consulta permitiendo a los desarrolladores escribir consultas a
partir del lenguaje de programación mismo, en lugar de utilizar un lenguaje de consulta diferente. Esto
resulta especialmente útil para desarrolladores de Visual Studio, que ahora pueden aprovechar la
comprobación de sintaxis en tiempo de compilación, los tipos estáticos y la compatibilidad con
IntelliSense que proporciona Visual Studio en las consultas.

LINQ a DataSet también se puede utilizar para consultar en datos que se han consolidado de uno o más
orígenes de datos. Esto permite muchos casos que requieren flexibilidad en la forma de representar y
controlar los datos, como consultar datos agregados localmente y almacenar en caché en el nivel medio
en aplicaciones web. En concreto, las aplicaciones de Business Intelligence, análisis e informes genéricos
requieren este método de manipulación.

La funcionalidad LINQ a DataSet se expone principalmente mediante métodos de extensión en las clases
DataRowExtensions y DataTableExtensions. LINQ a DataSet se basa y utiliza la arquitectura existente
ADO.NET 2.0, y no está destinada a reemplazar ADO.NET 2.0 en el código de aplicación. El código de
ADO.NET 2.0 existente continuará funcionando en una aplicación LINQ a DataSet. La relación de LINQ a
DataSet con ADO.NET 2.0 y los datos almacenados se muestran en el diagrama siguiente.

8.2.1. Introducción LINQ a DataSet


En esta sección se ofrece información preliminar acerca de la programación con LINQ a DataSet.

8.2.1.1. Información General de LINQ a DataSet


DataSet es uno de los componentes más ampliamente utilizados de ADO.NET. Es un elemento
fundamental del modelo de programación desconectado en el que se basa ADO.NET y permite almacenar

MCT: Luis Dueñas Pag 198 de 388


Manual de LINQ

explícitamente en caché datos de diferentes orígenes de datos. Para el nivel de presentación, DataSet
está estrechamente integrado en los controles de GUI para el enlace de datos. Para el nivel medio,
proporciona una caché que conserva la forma relacional de los datos e incluye servicios de exploración de
jerarquías y consultas rápidos y sencillos. Una técnica común que se usa para reducir el número de
solicitudes de una bases de datos consiste en utilizar DataSet para el almacenamiento en caché en el
nivel medio. Por ejemplo, piense en una aplicación web de ASP.NET controlada por datos. A menudo una
parte importante de los datos de aplicación no cambia frecuentemente y es común entre sesiones o
usuarios. Estos datos se pueden conservar en memoria o en un servidor web, lo que reduce el número
de solicitudes en la base de datos y acelera las interacciones del usuario. Otro aspecto útil de DataSet es
que permite que una aplicación lleve subconjuntos de datos de uno o más orígenes de datos al espacio
de la aplicación. La aplicación puede manipular los datos en memoria mientras retiene su forma
relacional.

A pesar de su importancia, DataSet tiene capacidades de consulta limitadas. El método Select se puede
usar para filtrar y ordenar y los métodos GetChildRows y GetParentRow se pueden usar para la
exploración de jerarquías. Sin embargo, para cualquier tarea más compleja, el programador debe escribir
una consulta personalizada. Esto puede tener como resultado aplicaciones con un bajo rendimiento y con
un mantenimiento difícil.

LINQ a DataSet facilita y acelera las consultas en datos almacenados en caché en un objeto DataSet.
Esas consultas se expresan en el lenguaje de programación mismo, en lugar de como literales de cadena
incrustados en el código de la aplicación. Esto significa que los desarrolladores no tienen que aprender un
lenguaje de consultas diferente. Adicionalmente, LINQ a DataSet permite a los desarrolladores de Visual
Studio trabajar de forma más productiva, ya que la IDE de Visual Studio proporciona comprobación de
sintaxis en tiempo de compilación, tipos estáticos y compatibilidad de IntelliSense con LINQ. LINQ a
DataSet también se puede usar para consultar los datos que se han consolidado de uno o más orígenes
de datos. Esto permite muchos casos que requieren flexibilidad en la forma de representar y controlar los
datos. En concreto, las aplicaciones de inteligencia empresaria, análisis e informes genéricos requieren
este método de manipulación.

Consultar conjuntos de datos usando LINQ a DataSet


Antes de poder empezar a consultar un objeto DataSet usando LINQ a DataSet, se debe rellenar el
DataSet. Existen varias formas de cargar datos en un DataSet, como usar la clase DataAdapter o LINQ a
SQL. Cuando se han cargado los datos en un objeto DataSet, se puede empezar a realizar consultas en
él. La formulación de consultas usando LINQ a DataSet es similar a usar Language-Integrated Query
(LINQ) en otros orígenes de datos habilitados para LINQ. se pueden realizar consultas de LINQ en tablas
únicas en un DataSet o en más de una tabla usando los operadores de consulta estándar Join y
GroupJoin.

Se admiten consultas de LINQ en objetos DataSet con tipo y sin tipo. Si el esquema de DataSet es
desconocido en tiempo de diseño, se recomienda un DataSet con tipo. En un DataSet con tipo, las tablas
y las filas tienen miembros con tipo para cada una de las columnas, lo que hace que las consultas sean
más sencillas y legibles.

Además de los operadores de consulta estándar implementados en System.Core.dll, LINQ a DataSet


agrega varias extensiones específicas de DataSet que hacen que realizar consultas en un conjunto de
objetos DataRow sea sencillo. Estas extensiones específicas de DataSet incluyen operadores para
comparar secuencias de filas, así como métodos que proporcionan acceso a los valores de columna de un
DataRow.

MCT: Luis Dueñas Pag 199 de 388


Manual de LINQ

Aplicaciones con n niveles y LINQ a DataSet


Las aplicaciones de datos con n niveles son aplicaciones centradas en datos separadas en varias capas
lógicas (o niveles). Una aplicación con n niveles típica incluye un nivel de presentación, un nivel medio y
un nivel de datos. Al separar los componentes de la aplicación en diferentes niveles, se aumenta el
mantenimiento y la escalabilidad de la aplicación.

En las aplicaciones con n niveles, a menudo se utiliza DataSet en el nivel medio para almacenar en caché
información para una aplicación web. La funcionalidad de consulta de LINQ a DataSet se implementa
mediante los métodos de extensión y DataSet de ADO.NET 2.0 existente.

En el siguiente diagrama se muestra cómo se relaciona LINQ a DataSet con el DataSet y cómo encaja en
una aplicación de nivel n:

8.2.1.2. Cargar Datos en un DataSet


Un objeto DataSet se debe rellenar primero antes de poderlo consultar con LINQ a DataSet. Existen
varias formas de rellenar DataSet. Por ejemplo, se puede utilizar LINQ a SQL para consultar la base de
datos y cargar los resultados en DataSet.

Otra forma habitual de cargar datos en DataSet es utilizar la clase DataAdapter para recuperar datos
desde la base de datos. Esto se muestra en el ejemplo siguiente.

Ejemplo
En este ejemplo se utiliza DataAdapter para consultar en la base de datos AdventureWorks la
información de ventas del año 2002, y cargar los resultados en un DataSet. Una vez que se ha rellenado
DataSet, se pueden escribir consultas en él mediante LINQ a DataSet. En este ejemplo, se utiliza el
método FillDataSet en las consultas de ejemplo en Ejemplos de LINQ a DataSet.
Try
Dim connectionString As String
connectionString = "Data Source=localhost;Initial
Catalog=AdventureWorks;Integrated Security=true;"
' Create a new adapter and give it a query to fetch sales order,
' contact, address, and product information for sales in the year
' 2002. Point connection information to the configuration setting
' "AdventureWorks".
Dim da = New SqlDataAdapter( _
"SELECT SalesOrderID, ContactID, OrderDate, OnlineOrderFlag, " & _
"TotalDue, SalesOrderNumber, Status, ShipToAddressID,
BillToAddressID " & _
"FROM Sales.SalesOrderHeader " & _
"WHERE DATEPART(YEAR, OrderDate) = @year; " & _
"SELECT d.SalesOrderID, d.SalesOrderDetailID, d.OrderQty, " & _
"d.ProductID, d.UnitPrice " & _

MCT: Luis Dueñas Pag 200 de 388


Manual de LINQ

"FROM Sales.SalesOrderDetail d " & _


"INNER JOIN Sales.SalesOrderHeader h " & _
"ON d.SalesOrderID = h.SalesOrderID " & _
"WHERE DATEPART(YEAR, OrderDate) = @year; " & _
"SELECT p.ProductID, p.Name, p.ProductNumber, p.MakeFlag, " & _
"p.Color, p.ListPrice, p.Size, p.Class, p.Style " & _
"FROM Production.Product p; " & _
"SELECT DISTINCT a.AddressID, a.AddressLine1, a.AddressLine2, " & _
"a.City, a.StateProvinceID, a.PostalCode " & _
"FROM Person.Address a " & _
"INNER JOIN Sales.SalesOrderHeader h " & _
"ON a.AddressID = h.ShipToAddressID OR a.AddressID =
h.BillToAddressID " & _
"WHERE DATEPART(YEAR, OrderDate) = @year; " & _
"SELECT DISTINCT c.ContactID, c.Title, c.FirstName, " & _
"c.LastName, c.EmailAddress, c.Phone " & _
"FROM Person.Contact c " & _
"INNER JOIN Sales.SalesOrderHeader h " & _
"ON c.ContactID = h.ContactID " & _
"WHERE DATEPART(YEAR, OrderDate) = @year;", _
connectionString)
' Add table mappings.
da.SelectCommand.Parameters.AddWithValue("@year", 2002)
da.TableMappings.Add("Table", "SalesOrderHeader")
da.TableMappings.Add("Table1", "SalesOrderDetail")
da.TableMappings.Add("Table2", "Product")
da.TableMappings.Add("Table3", "Address")
da.TableMappings.Add("Table4", "Contact")
da.Fill(ds)
' Add data relations.
Dim orderHeader As DataTable = ds.Tables("SalesOrderHeader")
Dim orderDetail As DataTable = ds.Tables("SalesOrderDetail")
Dim co As DataRelation = New DataRelation("SalesOrderHeaderDetail", _
orderHeader.Columns("SalesOrderID"), _
orderDetail.Columns("SalesOrderID"), True)
ds.Relations.Add(co)
Dim contact As DataTable = ds.Tables("Contact")
Dim orderContact As DataRelation =
New DataRelation("SalesOrderContact", contact.Columns("ContactID"), _
orderHeader.Columns("ContactID"), True)
ds.Relations.Add(orderContact)
Catch ex As SqlException
Console.WriteLine("SQL exception occurred: " & ex.Message)
End Try

8.2.1.3. Cómo crear un Proyecto LINQ a DataSet en Visual


Studio

MCT: Luis Dueñas Pag 201 de 388


Manual de LINQ

Los distintos tipos de proyecto LINQ requieren determinados espacios de nombres importados (Visual
Basic) o directivas using (C#) y referencias. El requisito mínimo es una referencia a System.Core.dll y
una directiva using para System.Linq. De manera predeterminada, estos requisitos se proporcionan al
crear un proyecto Visual C# 2008 nuevo. LINQ a DataSet también requiere una referencia a
System.Data.dll y System.Data.DataSetExtensions.dll y una directiva Imports (VBasic) o using (C#).

Si está actualizando a un proyecto desde una versión anterior de Visual Studio, es posible que tenga que
proporcionar estas referencias relacionadas con LINQ de forma manual. También es posible que tenga
que configurar de forma manual el proyecto para establecer como destino la versión 3.5 de .NET
Framework.

Nota:

Si está generando el proyecto desde el símbolo del sistema, debe hacer referencia manualmente a
las DLL relacionadas con LINQ en drive:\Archivos de programa\Reference
Assemblies\Microsoft\Framework\v3.5.

Para establecer como destino .NET Framework


1. En Visual Studio 2008, cree un proyecto Visual Basic o C# nuevo. Como alternativa, también
puede abrir un proyecto Visual Basic o C# creado en Visual Studio 2005 y seguir las indicaciones
para convertirlo en un proyecto Visual Studio 2008.

2. Para un proyecto, haga clic en el menú Proyecto y, a continuación, haga clic en Propiedades.

a. En la página de propiedades Aplicación, seleccione .NET Framework 3.5 en la lista


desplegable Marco de trabajo de destino.

3. Para un proyecto Visual Basic, haga clic en el menú Proyecto y, a continuación, haga clic en
Propiedades.

a. En la página de propiedades Compilación, haga clic en Opciones de compilación


avanzadas y, a continuación, seleccione .NET Framework 3.5 en la lista desplegable Marco
de trabajo de destino (todas las configuraciones).

4. En el menú Proyecto, haga clic en Agregar referencia, haga clic en la ficha .NET, desplácese
hasta System.Core, haga clic en él y, a continuación, haga clic en Aceptar.

5. Agregue una directiva using o un espacio de nombres importado para System.Linq en el archivo
de código origen o proyecto.

Para habilitar la funcionalidad LINQ a DataSet

1. Si es necesario, siga las instrucciones descritas anteriormente para agregar una referencia a
System.Core.dll y una directiva using o un espacio de nombres importado para System.Linq.

2. En C# o Visual Basic, haga clic en el menú Proyecto y, a continuación, haga clic en Agregar
referencia.

3. En el cuadro de diálogo Agregar referencia, haga clic en la ficha .NET si no está en la parte
superior. Desplácese hasta System.Data y System.Data.DataSetExtensions y haga clic en
ellos. Haga clic en el botón Aceptar.

MCT: Luis Dueñas Pag 202 de 388


Manual de LINQ

4. Agregue una directiva using o un espacio de nombres importado para System.Data en el


archivo de código origen o proyecto.

5. Agregue una referencia a System.Data.DataSetExtensions.dll para la funcionalidad LINQ a


Dataset. Agregue una referencia a System.Data.dll si ésta no existe aún.

6. Si lo desea, agregue una directiva using o un espacio de nombres importado para


System.Data.Common o System.Data.SqlClient, según cómo se conecte a la base de datos.

8.2.2. Guía de Programación LINQ a DataSet


En esta sección se ofrece información conceptual y ejemplos de programación con LINQ a DataSet.

8.2.2.1. Consultas en LINQ a DataSet


Una consulta es una expresión que recupera datos de un origen de datos. Las consultas se suelen
expresar en un lenguaje de consulta especializado, como SQL para bases de datos relacionales y XQuery
para XML. Así pues, los desarrolladores han tenido que aprender un nuevo lenguaje de consulta para
cada tipo de origen de datos o formato de datos que consultan. Language-Integrated Query (LINQ)
ofrece un modelo más sencillo y coherente para trabajar con datos en varios tipos de orígenes y
formatos de datos. En una consulta de LINQ siempre se trabaja con objetos de programación.

Una operación de consulta de LINQ consta de tres acciones: obtener el origen o los orígenes de datos,
crear la consulta y ejecutar la consulta.

Los orígenes de datos que implementan la interfaz genérica IEnumerable<(Of <(T>)>) se pueden
consultar a través de LINQ. La llamada a AsEnumerable en DataTable devuelve un objeto que
implementa la interfaz IEnumerable<(Of <(T>)>) genérica, que actúa como origen de datos para las
consultas de LINQ a DataSet.

En la consulta se especifica exactamente la información que se desea recuperar del origen de datos. Una
consulta también puede especificar cómo se debe ordenar, agrupar y qué forma debe tener esa
información antes de que se devuelva. En LINQ, una consulta se almacena en una variable. Si la consulta
está diseñada para devolver una secuencia de valores, la variable misma de la consulta debe ser de tipo
enumerable. Esta variable de consulta no realiza ninguna acción y no devuelve datos; solamente
almacena la información de la consulta. Tras crear una consulta debe ejecutarla para recuperar los datos.

En una consulta que devuelve una secuencia de valores, la variable de consulta por sí misma nunca
conserva los resultados de la consulta y sólo almacena los comandos de la misma. La ejecución de la
consulta se aplaza hasta que la variable de consulta se recorre en iteración en un bucle foreach o For
Each. Esto se denomina ejecución aplazada; es decir, la ejecución de la consulta se produce después de
que se cree la consulta. Esto significa que se puede ejecutar una consulta con la frecuencia que se desee.
Esto es útil cuando, por ejemplo, se tiene una base de datos que otras aplicaciones están actualizando.
En su aplicación puede crear una consulta para recuperar la información más reciente y ejecutar de
forma repetida la consulta, devolviendo cada vez la información actualizada.

A diferencia de las consultas aplazadas, que devuelven una secuencia de valores, las consultas que
devuelven un valor singleton se ejecutan inmediatamente. Algunos ejemplos de consultas singleton son
Count, Max, Average y First. Se ejecutan inmediatamente porque se necesitan los resultados de la
consulta para calcular el resultado singleton. Por ejemplo, para buscar la media de los resultados de
consultas, debe ejecutarse la consulta para que la función de cálculo de media tenga datos de entrada

MCT: Luis Dueñas Pag 203 de 388


Manual de LINQ

con los que trabajar. También puede usar los métodos ToList<(Of <(TSource>)>) o ToArray<(Of
<(TSource>)>) en una consulta para forzar la ejecución inmediata de una consulta que no crea un valor
singleton. Esas técnicas para forzar la ejecución inmediata pueden ser útiles si desea almacenar en caché
los resultados de una consulta.

Consultas
Las consultas de LINQ a DataSet se pueden formular en dos sintaxis diferentes: sintaxis de expresiones
de consulta y sintaxis de consultas basadas en métodos.

Sintaxis de expresiones de consulta


Las expresiones de consulta son una sintaxis de consulta declarativa. Esta sintaxis permite a un
desarrollador escribir consultas en C# o Visual Basic en un formato similar a SQL. Si se utiliza la sintaxis
de expresiones de consulta, se pueden realizar incluso operaciones complejas de filtrado, ordenación y
agrupamiento en orígenes de datos con código mínimo.

La sintaxis de expresiones de consulta es nueva en C# 3.0 y en Visual Basic 2008. No obstante, Common
Language Runtime (CLR) de .NET Framework no puede leer la sintaxis de expresiones de consulta en sí.
Por lo tanto, en tiempo de compilación, las expresiones de consulta se traducen a algo que CLR no
comprende: las llamadas a métodos. Esos métodos se conocen como operadores de consulta estándar.
Como desarrollador, tiene la opción de llamarlos directamente utilizando la sintaxis de método en lugar
de la sintaxis de consulta.

El siguiente ejemplo usa Select para devolver todas las filas de la tabla Product y mostrar los nombres de
producto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Select product
Console.WriteLine("Product Names:")
For Each p In query
Console.WriteLine(p.Field(Of String)("Name"))
Next
Sintaxis de consultas basadas en métodos
La otra forma de formular consultas de LINQ a DataSet es usar las consultas basadas en métodos. La
sintaxis de consultas basadas en métodos es una secuencia de llamadas a métodos directas a los
métodos de operador de LINQ, pasando expresiones lambda como parámetros.

Este ejemplo usa Select para devolver todas las filas de Product y mostrar los nombres de producto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = products.AsEnumerable() _
.Select(Function(product As DataRow) New With _
{ .ProductName = product.Field(Of String)("Name"), _

MCT: Luis Dueñas Pag 204 de 388


Manual de LINQ

.ProductNumber = product.Field(Of String)("ProductNumber"), _


.Price = product.Field(Of Decimal)("ListPrice")})
Console.WriteLine("Product Info:")
For Each product In query
Console.Write("Product name: " & product.ProductName)
Console.Write("Product number: " & product.ProductNumber)
Console.WriteLine("List price: $ " & product.Price)
Next
Crear consultas
Tal y como se ha mencionado anteriormente en este tema, la variable de consulta sólo almacena los
comandos de la consulta cuando ésta se diseña para devolver una secuencia de valores. Si la consulta no
contiene un método que cause una ejecución inmediata, la ejecución real de la consulta se aplaza hasta
que la variable de consulta se recorra en iteración en un bucle foreach o For Each. La ejecución
aplazada permite combinar varias consultas o ampliar una consulta. Cuando se amplía una consulta, se
modifica para incluir las nuevas operaciones. La ejecución eventual reflejará los cambios. En el siguiente
ejemplo, la primera consulta devuelve todos los productos. La segunda consulta amplía la primera
usando Where para devolver todos los productos del tamaño "L":
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim productsQuery = From product In products.AsEnumerable() _
Select product
Dim largeProducts = _
productsQuery.Where(Function(p) p.Field(Of String)("Size") = "L")
Console.WriteLine("Products of size 'L':")
For Each product In largeProducts
Console.WriteLine(product.Field(Of String)("Name"))
Next
Tras ejecutar una consulta no se pueden crear consultas adicionales y todas las consultas posteriores
usarán los operadores de LINQ en memoria. La ejecución de la consulta se producirá cuando se recorra
en iteración la variable de consulta en una instrucción foreach o For Each, o mediante una llamada a
uno de los operadores de conversión de LINQ que provocará una ejecución inmediata. Entre estos
operadores se incluyen los siguientes: ToList<(Of <(TSource>)>), ToArray<(Of <(TSource>)>),
ToLookup y ToDictionary.

En el siguiente ejemplo, la primera consulta devuelve todos los productos ordenados por el precio de la
lista. El método ToArray<(Of <(TSource>)>) se usa para forzar la ejecución inmediata de la consulta:
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Order By
product.Field(Of Decimal)("ListPrice") Descending Select product
' Force immediate execution of the query.

MCT: Luis Dueñas Pag 205 de 388


Manual de LINQ

Dim productsArray = query.ToArray()


Console.WriteLine("Every price From highest to lowest:")
For Each prod In productsArray
Console.WriteLine(prod.Field(Of Decimal)("ListPrice"))
Next

8.2.2.2. Consultar DataSets


Después de rellenar un objeto DataSet con datos se puede empezar a realizar consultas sobre él.
Formular consultas con LINQ a DataSet es similar a usar Language-Integrated Query (LINQ) contra otros
orígenes de datos habilitados para LINQ. No obstante, recuerde que cuando use las consultas de LINQ en
un objeto DataSet estará consultando una enumeración de objetos DataRow en lugar de una
enumeración de un tipo personalizado. Esto significa que puede usar cualquier de los miembros de la
clase DataRow en sus consultas de LINQ. Esto permite crear consultas amplias y complejas.

Al igual que con otras implementaciones de LINQ, puede crear consultas de LINQ a DataSet de dos
formas diferentes: sintaxis de expresión de consulta y sintaxis de consulta basada en métodos. Puede
usar la sintaxis de expresión de consulta o la sintaxis de consulta basada en métodos para realizar
consultas en tablas únicas en un DataSet, en tablas múltiples en un DataSet o en tablas en un DataSet
con tipo.

8.2.2.2.1. Consultas de Tabla Unica


Las consultas Language-Integrated Query (LINQ) operan en orígenes de datos que implementan la
interfaz IEnumerable<(Of <(T>)>) o la interfaz IQueryable. La clase DataTable no se implementa en
ninguna de estas interfaces, por lo que se debe llamar al método AsEnumerable si desea utilizar
DataTable como un origen en la cláusula From de una consulta LINQ.

En el ejemplo siguiente se obtienen todos los pedidos en línea desde la tabla SalesOrderHeader y se
envían los resultados de id., fecha y número de pedido a la consola.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = _
From order In orders.AsEnumerable() _
Where order.Field(Of Boolean)("OnlineOrderFlag") = True _
Select New With { _
.SalesOrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate"), _
.SalesOrderNumber = order.Field(Of String)("SalesOrderNumber")}
For Each onlineOrder In query
Console.Write("Order ID: " & onlineOrder.SalesOrderID)
Console.Write(" Order date: " & onlineOrder.OrderDate)
Console.WriteLine(" Order number: " & onlineOrder.SalesOrderNumber)
Next
La consulta variable local se inicializa con una expresión de consulta, la cual opera en una o varias
fuentes de información aplicando uno o varios operadores de consulta desde el operador estándar de

MCT: Luis Dueñas Pag 206 de 388


Manual de LINQ

consulta o, en el caso de LINQ a DataSet, desde el operador específico de la clase DataSet. La expresión
de consulta del ejemplo anterior utiliza dos de los operadores estándar de consulta: Where y Select.

La cláusula Where filtra la secuencia basándose en una condición, en este caso que OnlineOrderFlag se
establezca en true. El operador Select asigna y devuelve un objeto enumerable que captura los
argumentos pasados al operador. En el ejemplo anterior, se crea un tipo anónimo con tres propiedades:
SalesOrderID, OrderDate y SalesOrderNumber. Los valores de estas tres propiedades se establecen en
los valores de las columnas SalesOrderID, OrderDate y SalesOrderNumber a partir de la tabla
SalesOrderHeader.
A continuación, el bucle foreach enumera el objeto enumerable devuelto por Select y produce los
resultados de la consulta. Dado que una consulta es un tipo Enumerable, que implementa
IEnumerable<(Of <(T>)>), la evaluación de la consulta se aplaza hasta que se procesa una iteración en
la variable de la consulta mediante el bucle foreach. El aplazamiento de la evaluación de consultas
permite que éstas se mantengan como valores que se pueden evaluar varias veces, y cada vez
produciendo resultados potencialmente diferentes.

El método Field proporciona acceso a los valores de columna de DataRow y SetField (que no se mostraba
en el ejemplo anterior) establece los valores de columna en DataRow. Tanto el método Field como el
método SetField controlan tipos que admiten valores null, por lo que no es necesario comprobar
explícitamente si hay valores null. Además, ambos son métodos genéricos, lo que significa que no es
necesario convertir el tipo de valor devuelto. Puede utilizar el descriptor de acceso de columna
preexistente en DataRow (por ejemplo, o["OrderDate"]), pero hacerlo le exigiría convertir el objeto de
valor devuelto al tipo apropiado. Si la columna admite valores null, debe comprobar si el valor es null
utilizando el método IsNull.

Observe que el tipo de datos especificado en el parámetro T genérico de los métodos Field y SetField
deben coincidir con el tipo del valor subyacente; en caso contrario, se producirá una
InvalidCastException. El nombre de columna especificado debe también coincidir con el nombre de una
columna en DataSet, en caso contrario, se producirá una ArgumentException. En ambos casos, la
excepción se produce en tiempo de ejecución de enumeración de datos, cuando se ejecuta la consulta.

8.2.2.2.2. Consultas Entre Tablas


Además de realizar consultas a una tabla única, en LINQ a DataSet se pueden efectuar consultas entre
tablas. Esto se consigue mediante una combinación. Una combinación es la asociación de objetos en un
origen de datos con objetos que comparten un atributo común en otro origen de datos, como un id. de
contacto o de producto. En la programación orientada a objetos, las relaciones entre dichos objetos son
relativamente fáciles de navegar debido a que cada uno de ellos tiene un miembro que hace referencia a
otro. Sin embargo, en tablas de bases de datos externas, navegar por relaciones no es tan sencillo. Las
tablas de bases de datos no contienen relaciones integradas. En estos casos, la operación de
combinación se puede utilizar para hacer coincidir elementos de cada origen. Por ejemplo, con dos tablas
que contienen información de producto y de ventas, se puede utilizar una operación de combinación para
hacer coincidir información de ventas y producto del mismo pedido de ventas.

El marco de trabajo Language-Integrated Query (LINQ) proporciona dos operadores de combinación, Join
y GroupJoin. Estos operadores efectúan combinaciones de igualdad, es decir, combinaciones que hacen
coincidir dos orígenes de datos únicamente cuando sus claves son iguales (por el contrario, Transact-SQL
admite operadores de combinación distintos de equals, como el operador less than).

MCT: Luis Dueñas Pag 207 de 388


Manual de LINQ

En cuanto a las bases de datos relacionales, Join implementa una combinación interna. Una combinación
interna es un tipo de combinación en la que solamente se devuelven los objetos que tienen una
correspondencia en el conjunto de datos opuesto.

Los operadores GroupJoin no tienen equivalente directo en términos de bases de datos relacionales;
implementan un superconjunto de combinaciones internas y combinaciones externas izquierdas. Una
combinación externa izquierda es una combinación que devuelve cada elemento de la primera colección
(izquierda), aunque éste no tenga elementos correlacionados en la segunda colección.

Ejemplo
El ejemplo siguiente realiza una combinación tradicional de las tablas SalesOrderHeader y
SalesOrderDetail de la base de datos de ejemplo AdventureWorks para obtener pedidos en línea del mes
de agosto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim details As DataTable = ds.Tables("SalesOrderDetail")
Dim query = From order In orders.AsEnumerable() _
Join detail In details.AsEnumerable() _
On order.Field(Of Integer)("SalesOrderID") Equals _
detail.Field(Of Integer)("SalesOrderID") _
Where order.Field(Of Boolean)("OnlineOrderFlag") = True And _
order.Field(Of DateTime)("OrderDate").Month = 8 _
Select New With _
{ .SalesOrderID = order.Field(Of Integer)("SalesOrderID"), _
.SalesOrderDetailID = detail.Field(Of Integer)("SalesOrderDetailID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate"), _
.ProductID = detail.Field(Of Integer)("ProductID") _
}
For Each order In query
Console.WriteLine(order.SalesOrderID & vbTab & _
order.SalesOrderDetailID & vbTab & _
order.OrderDate & vbTab & _
order.ProductID)
Next

8.2.2.2.3. Consultar DataSets con Establecimiento de Tipos


Si el esquema de DataSet se conoce en tiempo de diseño de la aplicación, se recomienda usar un
DataSet con tipo al utilizar LINQ a DataSet. Un DataSet con tipo es una clase que se deriva de un
DataSet. Como tal, hereda todos los métodos, eventos y propiedades de un DataSet. Además, un
DataSet con tipo proporciona métodos, eventos y propiedades con establecimiento inflexible de tipos.
Esto significa que se puede tener acceso a tablas y columnas por su nombre, en lugar de utilizar métodos
de una colección. Esto hace que las consultas sean más sencillas y más legibles.

LINQ a DataSet también admite las consultas en un DataSet con tipo. Con un DataSet con tipo no hay
que usar el método Field genérico o el método SetField para tener acceso a los datos de la columna. Los

MCT: Luis Dueñas Pag 208 de 388


Manual de LINQ

nombres de propiedad están disponibles en el tiempo de compilación porque la información de tipo se


incluye en DataSet. LINQ a DataSet proporciona acceso a valores de columna como tipo correcto para
que los errores de no coincidencia de tipos se detecten cuando se compile el código en lugar de en
tiempo de ejecución.

Antes de poder empezar a consultar un DataSet con tipo se debe generar la clase usando el Diseñador
de DataSet de Visual Studio 2008.

Ejemplo
En el siguiente ejemplo se muestra una consulta sobre un DataSet con tipo:
Dim orders = ds.Tables("SalesOrderHeader")
Dim query = From o In orders Where o.OnlineOrderFlag = True _
Select New {SalesOrderID:=o.SalesOrderID,OrderDate:=o.OrderDate, _
SalesOrderNumber := o.SalesOrderNumber}
For Each Dim onlineOrder In query
Console.WriteLine("{0}\t{1:d}\t{2}", onlineOrder.SalesOrderID, _
onlineOrder.OrderDate, onlineOrder.SalesOrderNumber)
Next

8.2.2.3. Comparar DataRows


Language-Integrated Query (LINQ) define varios operadores de conjuntos para comparar elementos de
origen y ver si son iguales. LINQ proporciona los siguientes operadores de conjuntos:
Distinct
Union
Intersect
Except
Estos operadores comparan elementos origen llamando a los métodos GetHashCode y Equals de cada
colección de elementos. En el caso de DataRow, estos operadores realizan una comparación de
referencia, lo que en general no constituye un comportamiento ideal para operaciones de conjunto en
datos tabulares. Para las operaciones de conjuntos, por lo general deseará determinar si los valores del
elemento son iguales o no a las referencias del elemento. Por ello, se ha agregado la clase
DataRowComparer a LINQ a DataSet. Esta clase se puede utilizar para comparar valores de fila.

La clase DataRowComparer contiene una implementación de comparación del valor para DataRow, de
modo que esta clase se puede utilizar para operaciones de conjuntos como Distinct. No se puede crear
una instancia directamente de esta clase. En su lugar, debe utilizarse la propiedad Default para devolver
una instancia de DataRowComparer. Entonces, se llama al método Equals(DataRow, DataRow) y los dos
objetos DataRow que se van a comparar se pasan como parámetros de entrada. El método
Equals(DataRow, DataRow) devuelve true si los conjuntos ordenados de valores de columna de ambos
objetos DataRow son iguales; de lo contrario, devuelve false.

Ejemplo
Este ejemplo usa Intersect para devolver contactos que aparecen en ambas tablas.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contactTable As DataTable = ds.Tables("Contact")

MCT: Luis Dueñas Pag 209 de 388


Manual de LINQ

Dim query1 = From contact In contactTable.AsEnumerable() _


Where contact.Field(Of String)("Title") = "Ms." Select contact
Dim query2 = From contact In contactTable.AsEnumerable() _
Where contact.Field(Of String)("FirstName") = "Sandra" Select contact
Dim contacts1 = query1.CopyToDataTable()
Dim contacts2 = query2.CopyToDataTable()
Dim contacts = contacts1.AsEnumerable() _
.Intersect(contacts2.AsEnumerable(), DataRowComparer.Default)
Console.WriteLine("Intersect of employees tables")
For Each row In contacts
Console.WriteLine("Id: {0} {1} {2} {3}", _
row("ContactID"), row("Title"), row("FirstName"), row("LastName"))
Next
Ejemplo
El ejemplo siguiente compara dos filas y obtiene sus códigos hash.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
' Get two rows from the SalesOrderHeader table.
Dim table As DataTable = ds.Tables("SalesOrderHeader")
Dim left = table.Rows(0)
Dim right = table.Rows(1)
' Compare the two different rows.
Dim comparer As IEqualityComparer(Of DataRow) = DataRowComparer.Default
Dim bEqual = comparer.Equals(left, right)
If (bEqual = True) Then
Console.WriteLine("Two rows are equal")
Else
Console.WriteLine("Two rows are not equal")
End If
' Output the hash codes of the two rows.
Console.WriteLine("The hashcodes for the two rows are {0}, {1}", _
comparer.GetHashCode(left), comparer.GetHashCode(right))

8.2.2.4. Crear DataTable desde una Consulta


El enlace de datos es una utilización muy frecuente del objeto DataTable. El método CopyToDataTable
toma los resultados de una consulta y los copia en DataTable, para así poder ser utilizados para el enlace
de datos. Cuando las operaciones de datos se han realizado, el DataTable nuevo se vuelve a combinar en
el DataTable de origen.

El método CopyToDataTable utiliza el siguiente proceso para crear un DataTable a partir de una consulta:

1. El método CopyToDataTable clona un DataTable a partir de la tabla origen (un objeto DataTable
que implementa la interfaz IQueryable<(Of <(T>)>)). El origen de IEnumerable se ha originado de
una expresión LINQ a DataSet o una consulta de método.

MCT: Luis Dueñas Pag 210 de 388


Manual de LINQ

2. El esquema del DataTable clonado se genera a partir de las columnas del primer objeto DataRow
enumerado en la tabla origen; el nombre de la tabla clonada es el nombre de la tabla origen con la
palabra "query" anexada.

3. En cada fila de la tabla origen, el contenido de la fila se copia en un objeto DataRow nuevo, el
cual, a continuación, se inserta en la tabla clonada. Las propiedades RowState y RowError se
mantienen en toda la operación de copia. Si los objetos DataRow de origen pertenecen a tablas
diferentes, se produce un ArgumentException.

4. Cuando todos los objetos DataRow de la tabla de entrada que se puede consultar se han
copiado, se devuelve el DataTable clonado. Si la secuencia origen no contiene objetos DataRow, el
método devuelve un DataTable vacío.

Observe que, si se llama al método CopyToDataTable, la consulta se enlaza a la tabla origen para
ejecutarse.

Cuando el método CopyToDataTable encuentra una referencia NULL o un tipo de valor que admite
valores NULL en una fila de la tabla origen, reemplaza el valor con Value. De este modo, los valores NULL
se controlan correctamente en el DataTable devuelto.

En el ejemplo siguiente se consultan en la tabla SalesOrderHeader los pedidos posteriores al 8 de agosto


de 2001 y se utiliza el método CopyToDataTable para crear un DataTable a partir de esa consulta. A
continuación, se enlaza DataTable a un BindingSource, que actúa como proxy para un objeto
DataGridView.
' Bind the System.Windows.Forms.DataGridView object
' to the System.Windows.Forms.BindingSource object.
dataGridView.DataSource = bindingSource
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
' Query the SalesOrderHeader table for orders placed
' after August 8, 2001.
Dim query = From order In orders.AsEnumerable() Where order.Field(Of
DateTime)("OrderDate") > New DateTime(2001, 8, 1) Select order
' Create a table from the query.
Dim boundTable As DataTable = query.CopyToDataTable()
' Bind the table to a System.Windows.Forms.BindingSource object,
' which acts as a proxy for a System.Windows.Forms.DataGridView object.
bindingSource.DataSource = boundTable
Crear un método CopyToDataTable<T> personalizado
Los métodos CopyToDataTable existentes solo funcionan en un origen IEnumerable<(Of <(T>)>) en el
que el parámetro genérico T es de tipo DataRow. Aunque esto es útil, no permite la creación de tablas a
partir de una secuencia de tipos escalares, a partir de consultas que devuelven tipos anónimos, o a partir
de consultas que realizan combinaciones de tablas.
Los ejemplos de esta sección utilizan los tipos personalizados siguientes:
Public Class Item
Private _Id As Int32

MCT: Luis Dueñas Pag 211 de 388


Manual de LINQ

Private _Price As Double


Private _Genre As String
Public Property Id() As Int32
Get
Return Id
End Get
Set(ByVal value As Int32)
_Id = value
End Set
End Property
Public Property Price() As Double
Get
Return _Price
End Get
Set(ByVal value As Double)
_Price = value
End Set
End Property
Public Property Genre() As String
Get
Return _Genre
End Get
Set(ByVal value As String)
_Genre = value
End Set
End Property
End Class
Public Class Book
Inherits Item
Private _Author As String
Public Property Author() As String
Get
Return _Author
End Get
Set(ByVal value As String)
_Author = value
End Set
End Property
End Class
Public Class Movie
Inherits Item
Private _Director As String
Public Property Director() As String
Get
Return _Director
End Get
Set(ByVal value As String)
_Director = value
End Set
End Property

MCT: Luis Dueñas Pag 212 de 388


Manual de LINQ

End Class
Ejemplo
En el ejemplo siguiente se consulta una colección de elementos cuyo precio es superior a $ 9,99 y se
crea una tabla a partir de los resultados de la consulta.
Dim book1 As New Book()
book1.Id = 1
book1.Price = 13.5
book1.Genre = "Comedy"
book1.Author = "Gustavo Achong"
Dim book2 As New Book
book2.Id = 2
book2.Price = 8.5
book2.Genre = "Drama"
book2.Author = "Jessie Zeng"
Dim movie1 As New Movie
movie1.Id = 1
movie1.Price = 22.99
movie1.Genre = "Comedy"
movie1.Director = "Marissa Barnes"
Dim movie2 As New Movie
movie2.Id = 1
movie2.Price = 13.4
movie2.Genre = "Action"
movie2.Director = "Emmanuel Fernandez"
Dim items(3) As Item
items(0) = book1
items(1) = book2
items(2) = movie1
items(3) = movie2
' Query for items with price greater than 9.99.
Dim query = From i In items Where i.Price > 9.99 _
Order By i.Price Select New With {i.Price, i.Genre}
Dim table As DataTable
table = query.CopyToDataTable()
Ejemplo
En el ejemplo siguiente se consulta una colección de elementos cuyo precio es superior a $ 9,99 y se
proyectan los resultados. La secuencia de tipos anónimos devuelta se carga en una tabla existente.
' Create a table with a schema that matches that of the query results.
Dim table As DataTable = New DataTable()
table.Columns.Add("Price", GetType(Integer))
table.Columns.Add("Genre", GetType(String))
' Query for items with price greater than 9.99.
Dim query = From i In items Where i.Price > 9.99 Order By i.Price _
Select New With {i.Price, i.Genre}
query.CopyToDataTable(table, LoadOption.PreserveChanges)
Ejemplo
En el ejemplo siguiente se consulta una colección de elementos cuyo precio es superior a $ 9,99 y se
proyectan los resultados. La secuencia de tipos anónimos devuelta se carga en una tabla existente. El
esquema de la tabla se amplía automáticamente porque los tipos Book y Movies se derivan del tipo Item.

MCT: Luis Dueñas Pag 213 de 388


Manual de LINQ

' Load into an existing DataTable, expand the schema and


' autogenerate a new Id.
Dim table As DataTable = New DataTable()
Dim dc As DataColumn = table.Columns.Add("NewId", GetType(Integer))
dc.AutoIncrement = True
table.Columns.Add("ExtraColumn", GetType(String))
Dim query = From i In items Where i.Price > 9.99 Order By i.Price _
Select New With {i.Price, i.Genre}
query.CopyToDataTable(table, LoadOption.PreserveChanges)
Ejemplo
En el ejemplo siguiente se consulta una colección de elementos cuyo precio es superior a $ 9,99 y se
devuelve una secuencia de Double, que se carga en una tabla nueva.
Dim query = From i In items Where i.Price > 9.99 Order By i.Price _
Select i.Price
Dim table As DataTable
table = query.CopyToDataTable()

8.2.2.5. Métodos Genéricos Field y SetField


LINQ a DataSet proporciona métodos de extensión a la clase DataRow para obtener acceso a los valores
de columna: el método Field y el método SetField. Estos métodos facilitan el acceso a los valores de
columna a los desarrolladores, sobre todo en lo relativo a valores NULL. DataSet utiliza Value para
representar valores NULL, en tanto que LINQ utiliza la compatibilidad con tipos que admiten valores
NULL introducida en .NET Framework 2.0. Utilizar el descriptor de acceso de columna preexistente en
DataRow exige convertir el objeto de valor devuelto al tipo apropiado. Si un campo determinado de
DataRow puede ser NULL, se debe comprobar de manera explícita si hay un valor NULL porque la
devolución de Value y su conversión implícita a otro tipo produce una InvalidCastException. En el
ejemplo siguiente, si no se utilizara el método IsNull para comprobar si hay un valor NULL, se produciría
una excepción si el indizador devolviera Value e intentara convertirlo en String.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() _
Where product!Color IsNot DBNull.Value AndAlso product!Color = "Red"
Select New With _
{ .Name = product!Name, _
.ProductNumber = product!ProductNumber, _
.ListPrice = product!ListPrice }
For Each product In query
Console.WriteLine("Name: " & product.Name)
Console.WriteLine("Product number: " & product.ProductNumber)
Console.WriteLine("List price: $" & product.ListPrice & vbNewLine)
Next
El método Field proporciona acceso a los valores de columna de DataRow y SetField establece los valores
de columna en DataRow. Tanto el método Field como el método SetField controlan tipos que admiten
valores NULL, por lo que no es necesario comprobar explícitamente si hay valores NULL, como en el

MCT: Luis Dueñas Pag 214 de 388


Manual de LINQ

ejemplo anterior. Además, ambos son métodos genéricos, lo que significa que no es necesario convertir
el tipo de valor devuelto.

El siguiente ejemplo utiliza el método Field.


' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() _
Where product.Field(Of String)("Color") = "Red" _
Select New With _
{ .Name = product.Field(Of String)("Name"), _
.ProductNumber = product.Field(Of String)("ProductNumber"), _
.ListPrice = product.Field(Of Decimal)("ListPrice") }
For Each product In query
Console.WriteLine("Name: " & product.Name)
Console.WriteLine("Product number: " & product.ProductNumber)
Console.WriteLine("List price: $ " & product.ListPrice & vbNewLine)
Next
Observe que el tipo de datos especificado en el parámetro T genérico de los métodos Field y SetField
deben coincidir con el tipo del valor subyacente. De lo contrario, se producirá una excepción
InvalidCastException. El nombre de columna especificado debe también coincidir con el nombre de una
columna en DataSet; en caso contrario, se producirá una ArgumentException. En ambos casos, la
excepción se produce en tiempo de ejecución durante la enumeración de datos cuando se ejecuta la
consulta.
El método SetField en sí no realiza ninguna conversión de tipos. Sin embargo, esto no significa que no se
realizará una conversión de tipos. El método SetField expone el comportamiento ADO.NET 2.0 de la clase
DataRow. El objeto DataRow consiguió realizar una conversión de tipos y el valor convertido se guardará
en el objeto DataRow.

8.2.2.6. Enlace de Datos y LINQ a DataSet


Enlace de datos es el proceso que establece una conexión entre la interfaz de usuario de la aplicación y
la lógica de negocios. Si el enlace está configurado correctamente y los datos proporcionan la
notificaciones adecuadas, al cambiar los valores de los datos, los elementos enlazados a los mismos
reflejarán de manera automática dichos cambios. DataSet es una representación de datos residente en
memoria que proporciona un modelo de programación relacional coherente independientemente del
origen de datos que contiene. DataView de ADO.NET 2.0 permite ordenar y filtrar los datos almacenados
en DataTable. Esta funcionalidad se utiliza con frecuencia en aplicaciones de enlace de datos. Mediante
DataView puede exponer los datos de una tabla con distintos criterios de ordenación y filtrar los datos
por el estado de fila o basándose en una expresión de filtro.

LINQ a DataSet permite a los programadores crear consultas complejas y eficaces en DataSet utilizando
Language-Integrated Query (LINQ). No obstante, una consulta LINQ a DataSet devuelve una
enumeración de objetos DataRow, que no se utiliza con facilidad en un caso de enlace. Para facilitar el
proceso de enlace, se puede crear un objeto DataView a partir de una consulta LINQ a DataSet. Este
objeto DataView usa el filtrado y la ordenación especificados en la consulta, pero es más adecuado para
el enlace de datos. LINQ a DataSet amplía la funcionalidad del objeto DataView proporcionando filtrado y

MCT: Luis Dueñas Pag 215 de 388


Manual de LINQ

ordenación basados en expresiones de LINQ, lo que permite operaciones de filtrado y ordenación mucho
más complejas y eficaces que las basadas en cadenas.

Observe que DataView representa la propia consulta y no es una vista encima de la consulta. DataView
se enlaza a un control de la interfaz de usuario, como DataGrid o DataGridView, proporcionando un
modelo de enlace de datos simple. DataView se puede crear también a partir de DataTable,
proporcionando una vista predeterminada de esa tabla.

8.2.2.6.1. Crear un Objeto DataView


Existen dos maneras de crear DataView en el contexto de LINQ a DataSet . Se puede crear DataView a
partir de una consulta LINQ a DataSet en DataTable o a partir de DataTable con o sin tipo. En ambos
casos, se crea DataView utilizando uno de los métodos de extensión AsDataView; DataView no se puede
construir directamente en el contexto de LINQ a DataSet.

Cuando se ha creado DataView, puede enlazarlo con un control de la interfaz de usuario en una
aplicación de formularios Windows o en una aplicación ASP.NET, o cambiar la configuración de filtro y
ordenación.

DataView construye un índice, que mejora considerablemente el rendimiento de las operaciones que
pueden utilizarlo, como filtro y ordenación. El índice de DataView se genera cuando se crea DataView y
cuando se modifica cualquier información de filtro u ordenación. La creación de DataView y el posterior
establecimiento de información de filtro y ordenación hace que el índice se genere al menos dos veces:
una cuando se crea DataView y la otra cuando se modifica cualquiera de las propiedades de ordenación y
filtrado.

Crear DataView desde una consulta LINQ a DataSet


Un objeto DataView puede crearse a partir de los resultados de una consulta LINQ a DataSet, en la que
los resultados son una proyección de objetos DataRow. El objeto DataView que se acaba de crear hereda
la información de filtrado y ordenación de la consulta a partir de la cual se creó.

Nota:

En la mayor parte de los casos, las expresiones utilizadas en el filtrado y la ordenación no deben
tener efectos secundarios y deben ser deterministas. Además, las expresiones no deben tener
ninguna lógica que dependa de un número de conjunto de ejecuciones, ya que las operaciones de
filtrado y ordenación se pueden ejecutar un número ilimitado de veces.

No se admite la creación de DataView a partir de una consulta que devuelve tipos anónimos o consultas
que realizan operaciones de combinación.

Sólo se admiten los siguientes operadores de consulta en una consulta utilizada para crear DataView:
Cast<(Of <(TResult>)>)
OrderBy
OrderByDescending
Select<(Of <(TRow, S>)>)
ThenBy
ThenByDescending
Where<(Of <(TRow>)>)
Tenga en cuenta que, cuando se crea un objeto DataView a partir de una consulta LINQ a DataSet, el
método Select<(Of <(TRow, S>)>) deber ser el método final que se invoca en la consulta. Esto se

MCT: Luis Dueñas Pag 216 de 388


Manual de LINQ

muestra en el ejemplo siguiente, en el que se crea un objeto DataView de pedidos en línea ordenados
por el total a pagar:
Dim orders As DataTable = dataSet.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() _
Where order.Field(Of Boolean)("OnlineOrderFlag") = True _
Order By order.Field(Of Decimal)("TotalDue") Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
También se pueden utilizar las propiedades basadas en cadena RowFilter y Sort para filtrar y ordenar
DataView después de haber sido creado a partir de una consulta. Observe que con esto se borrará la
información de ordenación y filtro heredada de la consulta. El ejemplo siguiente crea DataView a partir
de una consulta LINQ a DataSet que filtra por apellidos que empiezan por "S". La propiedad basada en
cadena Sort se establece para ordenar por apellidos en orden ascendente y después por nombres en
orden descendente:
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() _
Where contact.Field(Of String)("LastName").StartsWith("S") _
Select contact
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
view.Sort = "LastName desc, FirstName asc"
Crear DataView a partir de DataTable
Además de poderse crear a partir de una consulta LINQ a DataSet, un objeto DataView se puede crear a
partir de DataTable utilizando el método AsDataView.

En el ejemplo siguiente, se crea DataView a partir de la tabla SalesOrderDetail y se establece ese objeto
como origen de datos de un objeto BindingSource: Este objeto actúa como proxy para un control
DataGridView.
Dim orders As DataTable = dataSet.Tables("SalesOrderDetail")
Dim view As DataView = orders.AsDataView()
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
El filtro y la ordenación pueden establecerse en DataView después de que se haya creado a partir de
DataTable. El ejemplo siguiente crea DataView a partir de la tabla Contact y establece la propiedad Sort
para ordenar por apellidos en orden ascendente y luego por nombres en orden descendente:
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim view As DataView = contacts.AsDataView()
view.Sort = "LastName desc, FirstName asc"
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
No obstante, se produce una pérdida de rendimiento cuando se establece la propiedad RowFilter o Sort
después de haberse creado DataView a partir de una consulta, debido a que DataView construye un
índice para admitir operaciones de filtro y ordenación. El establecimiento de la propiedad RowFilter o Sort
hace que se vuelva a generar el índice de los datos, lo que agrega sobrecarga a la aplicación y reduce el
rendimiento. Siempre que sea posible, se recomienda especificar la información sobre filtro y ordenación
cuando se cree por primera vez DataView y evitar modificaciones posteriores.

8.2.2.6.2. Filtrar con DataView

MCT: Luis Dueñas Pag 217 de 388


Manual de LINQ

La capacidad de filtrar datos utilizando criterios específicos y después presentarlos a un cliente mediante
un control de IU es un aspecto importante del enlace de datos. DataView proporciona varias maneras de
filtrar datos y devolver subconjuntos de filas de datos que reúnan determinados criterios. Además de
capacidades de filtro basado en cadena, DataView también proporciona la capacidad de utilizar
expresiones LINQ para los criterios de filtro. Las expresiones LINQ permiten operaciones de filtro más
complejas y eficaces que el filtro basado en cadena.

Existen dos maneras de filtrar datos utilizando DataView:


Crear un DataView a partir de una consulta LINQ a DataSet con una cláusula WHERE.
Utilizar las capacidades de filtro basado en cadena de DataView existentes.
Crear DataView desde una consulta con información de filtro
Se puede crear un objeto DataView desde una consulta LINQ a DataSet. Si dicha consulta tiene una
cláusula Where, DataView se crea con información de filtro desde la consulta. La expresión de la
cláusula Where se utiliza para determinar qué filas de datos se incluirán en DataView, y constituye la
base para el filtro.

Los filtros basados en expresión son más eficaces y complejos que los sencillos filtros basados en
cadena. Los filtros basados en cadena y los basados en expresión se excluyen mutuamente. Cuando el
RowFilter basado en cadena se establece después de haber creado DataView desde una consulta, se
borra el filtro basado en expresión inferido a partir de la consulta.

Nota:

En la mayor parte de los casos, las expresiones utilizadas en el filtro no deben tener efectos
secundarios y deben ser deterministas. Además, las expresiones no deben tener ninguna lógica que
dependa de un número de conjunto de ejecuciones, porque las operaciones de filtrado se pueden
ejecutar un número ilimitado de veces.

Ejemplo
El siguiente ejemplo consulta en la tabla SalesOrderDetail los pedidos con una cantidad superior a 2 e
inferior a 6; crea un DataView desde una consulta y enlaza DataView a un BindingSource:
Dim orders As DataTable = dataSet.Tables("SalesOrderDetail")
Dim query = From order In orders.AsEnumerable() Where order.Field(Of
Int16)("OrderQty")>2 And order.Field(Of Int16)("OrderQty")<6 Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
Ejemplo
El siguiente ejemplo crea un DataView desde una consulta de pedidos efectuados con posterioridad al 6
de junio de 2001:
Dim orders As DataTable = dataSet.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Where order.Field(Of
DateTime)("OrderDate") > New DateTime(2002, 6, 1) Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
Ejemplo
El filtro se puede combinar también con la ordenación. El siguiente ejemplo crea un DataView desde una
consulta de contactos cuyos apellidos empiezan por la letra "S", y los ordena primero por el apellido y
después por el nombre:
Dim contacts As DataTable = dataSet.Tables("Contact")

MCT: Luis Dueñas Pag 218 de 388


Manual de LINQ

Dim query=From contact In contacts.AsEnumerable() Where contact.Field(Of


String)("LastName").StartsWith("S") Order By contact.Field(Of String)
("LastName"), contact.Field(Of String)("FirstName") Select contact
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
Ejemplo
El siguiente ejemplo utiliza el algoritmo SoundEx para encontrar contactos cuyo apellido es similar a
"Zhu". El algoritmo SoundEx se implementa en el método SoundEx.
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim soundExCode As String = SoundEx("Zhu")
Dim query=From contact In contacts.AsEnumerable() Where SoundEx(contact.
Field(Of String)("LastName")) = soundExCode Select contact
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
SoundEx es un algoritmo fonético utilizado para indizar nombres según el sonido, tal como se pronuncian
en inglés, que fue desarrollado originalmente por la Oficina del Censo de Estados Unidos. El método
SoundEx devuelve un código de cuatro caracteres para un nombre que consiste en una letra inglesa
seguida de tres números. La letra es la primera letra del nombre y los números codifican el resto de las
consonantes del mismo. Los nombres que suenan parecidos comparten el mismo código SoundEx. A
continuación se muestra la implementación del SoundEx utilizado en el método SoundEx del ejemplo
anterior:
Private Function SoundEx(ByVal word As String) As String
Dim length As Integer = 4
' Value to return
Dim value As String = ""
' Size of the word to process
Dim size As Integer = word.Length
' Make sure the word is at least two characters in length
If (size > 1) Then
' Convert the word to all uppercase
word = word.ToUpper(System.Globalization.CultureInfo.InvariantCulture)
' Convert the word to character array for faster processing
Dim chars As Char() = word.ToCharArray()
' Buffer to build up with character codes
Dim buffer As StringBuilder = New StringBuilder()
' The current and previous character codes
Dim prevCode As Integer = 0
Dim currCode As Integer = 0
' Append the first character to the buffer
buffer.Append(chars(0))
' Loop through all the characters and convert them to the proper
' character code
For i As Integer = 1 To size - 1
Select Case chars(i)
Case "A", "E", "I", "O", "U", "H", "W", "Y"
currCode = 0
Case "B", "F", "P", "V"

MCT: Luis Dueñas Pag 219 de 388


Manual de LINQ

currCode = 1
Case "C", "G", "J", "K", "Q", "S", "X", "Z"
currCode = 2
Case "D", "T"
currCode = 3
Case "L"
currCode = 4
Case "M", "N"
currCode = 5
Case "R"
currCode = 6
End Select
' Check to see if the current code is the same as the last one
If (currCode <> prevCode) Then
' Check to see if the current code is 0 (a vowel); do not process vowels
If (currCode <> 0) Then
buffer.Append(currCode)
End If
End If
' Set the new previous character code
prevCode = currCode
' If the buffer size meets the length limit, then exit the loop
If (buffer.Length = length) Then
Exit For
End If
Next
' Pad the buffer, if required
size = buffer.Length
If (size < length) Then
buffer.Append("0", (length - size))
End If
' Set the value to return
value = buffer.ToString()
End If
' Return the value
Return value
End Function
Utilizar la propiedad RowFilter
La funcionalidad de filtro basado en cadena DataView existente funciona también en el contexto de LINQ
a DataSet.

El siguiente ejemplo crea un DataView desde la tabla Contact y, a continuación, establece la propiedad
RowFilter para que devuelva filas cuando el apellido del contacto sea "Zhu":
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim view As DataView = contacts.AsDataView()
view.RowFilter = "LastName='Zhu'"
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
Después de haberse creado un DataView desde una consulta DataTable o LINQ a DataSet, se puede
utilizar la propiedad RowFilter para especificar subconjuntos de filas basados en sus valores de columna.

MCT: Luis Dueñas Pag 220 de 388


Manual de LINQ

Los filtros basados en cadena y los basados en expresión se excluyen mutuamente. Al establecer la
propiedad RowFilter se borrará la expresión de filtro inferida a partir de la consulta LINQ a DataSet, y la
expresión de filtro no se podrá volver a restablecer.
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() Where
contact.Field(Of String)("LastName") = "Hernandez" Select contact
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
view.RowFilter = "LastName='Zhu'"
Si desea devolver los resultados de una consulta determinada en los datos, en lugar de proporcionar una
vista dinámica de un subconjunto de los datos, para conseguir el máximo rendimiento puede utilizar los
métodos Find o FindRows de la DataView, en lugar de establecer la propiedad RowFilter. La propiedad
RowFilter es más idónea en una aplicación enlazada a datos donde un control enlazado muestra
resultados filtrados. El establecimiento de la propiedad RowFilter hace que se vuelva a generar el índice
de los datos, lo que agrega sobrecarga a la aplicación y reduce el rendimiento. Los métodos Find y
FindRows utilizan el índice actual, sin necesidad de volver a generarlo. Si va a llamar a Find o a FindRows
una única vez, entonces debería utilizar el DataView existente. Si va a llamar a Find o a FindRows varias
veces, debería crear un nuevo DataView para volver a generar el índice en la columna en la que desea
buscar y, a continuación, llamar a los métodos Find o FindRows.

Borrar el filtro
Después de haber configurado el filtro de DataView, éste se puede borrar mediante la propiedad
RowFilter. El filtro de DataView se pude borrar de dos maneras:
Establecer la propiedad RowFilter en null.
Establecer la propiedad RowFilter en una cadena vacía.
Ejemplo
El siguiente ejemplo crea un DataView desde una consulta y, a continuación, borra el filtro estableciendo
la propiedad RowFilter en null:
Dim orders As DataTable = dataSet.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Where order.Field(Of
DateTime)("OrderDate") > New DateTime(2002, 11, 20) And order.Field(Of
Decimal)("TotalDue") < New Decimal(60.0) Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
view.RowFilter = Nothing
Ejemplo
El siguiente ejemplo crea un DataView desde una tabla, establece la propiedad RowFilter y, a
continuación, borra el filtro estableciendo la propiedad RowFilter en una cadena vacía:
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim view As DataView = contacts.AsDataView()
view.RowFilter = "LastName='Zhu'"
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
' Clear the row filter.
view.RowFilter = ""

8.2.2.6.3. Ordenar con DataView

MCT: Luis Dueñas Pag 221 de 388


Manual de LINQ

La capacidad de ordenar datos basándose en criterios específicos y después presentarlos a un cliente


mediante un control de interfaz de usuario es un aspecto importante del enlace de datos. DataView
proporciona varias maneras de ordenar datos y devolver filas de datos ordenados según criterios de
ordenación específicos. Además de capacidades de ordenación basada en cadena, DataView también
permite utilizar expresiones Language-Integrated Query (LINQ) para los criterios de ordenación. Las
expresiones LINQ permiten operaciones de ordenación más complejas y eficaces que la ordenación
basada en cadena. En este tema se describen ambos enfoques de ordenación utilizando DataView.

Crear DataView desde una consulta con información de ordenación


Se puede crear un objeto DataView desde una consulta LINQ a DataSet. Si dicha consulta contiene una
cláusula OrderBy, OrderByDescending, ThenBy o ThenByDescending las expresiones de dicha cláusula se
utilizan como base para la ordenación de los datos en DataView. Por ejemplo, si la consulta contiene las
cláusulas Order By… y Then By…, el DataView resultante ordenará los datos por ambas columnas
especificadas.

La ordenación basada en expresión es más eficaz y compleja que la simple ordenación basada en
cadena. Observe que la ordenación basada en cadena y expresión se excluyen mutuamente. Cuando el
Sort basado en cadena se establece después de haber creado DataView desde una consulta, el filtro
basado en expresión inferido a partir de la consulta se borra y no se puede volver a restablecer.

El índice de DataView se genera cuando se crea DataView y cuando se modifica cualquier información de
filtro u ordenación. Obtendrá un rendimiento óptimo si suministra criterios de ordenación en la consulta
LINQ a DataSet a partir de la cual se crea DataView y no modifica posteriormente la información de
ordenación.

Nota:

En la mayor parte de los casos, las expresiones utilizadas en la ordenación no deben tener efectos
secundarios y deben ser deterministas. Además, las expresiones no deben tener ninguna lógica que
dependa de un número de conjunto de ejecuciones, porque las operaciones de ordenación se pueden
ejecutar un número ilimitado de veces.

Ejemplo
El siguiente ejemplo consulta en la tabla SalesOrderHeader y ordena las filas devueltas por fecha de
pedido; crea un DataView desde una consulta y enlaza DataView a un BindingSource:
Dim orders As DataTable = dataSet.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Order By order.Field(Of
DateTime)("OrderDate") Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
Ejemplo
El siguiente ejemplo consulta en la tabla SalesOrderHeader y ordena la fila devuelta por el importe total
a pagar; crea un DataView desde una consulta y enlaza DataView a un BindingSource:
Dim orders As DataTable = dataSet.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Order By order.Field(Of
Decimal)("TotalDue") Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
Ejemplo
El siguiente ejemplo consulta en la tabla SalesOrderDetail y ordena las filas devueltas por cantidad de
pedido y, a continuación, por id. del pedido de ventas; crea un DataView desde una consulta y enlaza
DataView a un BindingSource:

MCT: Luis Dueñas Pag 222 de 388


Manual de LINQ

Dim orders As DataTable = dataSet.Tables("SalesOrderDetail")


Dim query = From order In orders.AsEnumerable() Order By order.Field(Of
Int16)("OrderQty"), order.Field(Of Integer)("SalesOrderID") Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
Utilizar la propiedad Sort basada en cadena
La funcionalidad de ordenación basada en cadena DataView funciona también con LINQ a DataSet.
Después de haberse creado DataView desde una consulta LINQ a DataSet, se puede utilizar la propiedad
Sort para establecer la ordenación en DataView.

Las funcionalidades de ordenación basada en cadena y expresión se excluyen mutuamente. Al establecer


la propiedad Sort se borrará la ordenación basada en expresión heredada de la consulta a partir de la
que se ha creado DataView.

Ejemplo
El ejemplo siguiente crea DataView a partir de la tabla Contact y ordena las filas por apellido en orden
descendente y luego por nombres en orden ascendente:
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim view As DataView = contacts.AsDataView()
view.Sort = "LastName desc, FirstName asc"
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
Ejemplo
En el siguiente ejemplo se consultan en la tabla Contact los apellidos que empiezan por la letra “S”. Se
crea DataView a partir de dicha consulta y se enlaza a un objeto BindingSource.
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() Where
contact.Field(Of String)("LastName").StartsWith("S") Select contact
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
view.Sort = "LastName desc, FirstName asc"
Borrar la ordenación
Se puede borrar la información de ordenación de DataView después de haberla establecido mediante la
propiedad Sort. Hay dos formas de borrar información de ordenación en DataView:
Establecer la propiedad Sort en null.
Establecer la propiedad Sort en una cadena vacía.
Ejemplo
El siguiente ejemplo crea un DataView desde una consulta y borra la ordenación mediante el
establecimiento de la propiedad Sort en una cadena vacía:
Dim orders As DataTable = dataSet.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Order By order.Field(Of
Decimal)("TotalDue") Select order
Dim view As DataView = query.AsDataView()
bindingSource1.DataSource = view
view.Sort = ""
Ejemplo
El siguiente ejemplo crea un DataView desde la tabla Contact y establece la propiedad Sort para ordenar
por apellidos en orden descendente. A continuación, la información de ordenación se borra estableciendo
la propiedad Sort en null:

MCT: Luis Dueñas Pag 223 de 388


Manual de LINQ

Dim contacts As DataTable = dataSet.Tables("Contact")


Dim view As DataView = contacts.AsDataView()
view.Sort = "LastName desc"
bindingSource1.DataSource = view
dataGridView1.AutoResizeColumns()
'Clear the sort.
view.Sort = Nothing

8.2.2.6.4. Rendimiento del DataView


En este tema se abordan las ventajas del uso de los métodos Find y FindRows de la clase DataView, y del
almacenamiento en memoria caché de DataView en una aplicación web.

Find y FindRows
DataView construye un índice. Un índice contiene claves generadas a partir de una o varias columnas de
la tabla o la vista. Dichas claves están almacenadas en una estructura que permite que DataView busque
de forma rápida y eficiente la fila o filas asociadas a los valores de cada clave. Las operaciones que
utilizan el índice, como las de filtro y ordenación, ven aumentar significativamente el rendimiento. El
índice de DataView se genera cuando se crea DataView y cuando se modifica cualquier información de
filtro u ordenación. La creación de DataView y el posterior establecimiento de información de filtro y
ordenación hace que el índice se genere al menos dos veces: una cuando se crea DataView y la otra
cuando se modifica cualquiera de las propiedades de ordenación y filtrado.

Si desea devolver los resultados de una consulta determinada en los datos, en lugar de proporcionar una
vista dinámica de un subconjunto de los datos, para conseguir el máximo rendimiento puede utilizar los
métodos Find o FindRows de la DataView, en lugar de establecer la propiedad RowFilter. La propiedad
RowFilter es más idónea en una aplicación enlazada a datos donde un control enlazado muestra
resultados filtrados. El establecimiento de la propiedad RowFilter hace que se vuelva a generar el índice
de los datos, lo que agrega sobrecarga a la aplicación y reduce el rendimiento. Los métodos Find y
FindRows utilizan el índice actual, sin necesidad de volver a generarlo. Si va a llamar a Find o a FindRows
una única vez, entonces debería utilizar el DataView existente. Si va a llamar a Find o a FindRows varias
veces, debería crear un nuevo DataView para volver a generar el índice en la columna en la que desea
buscar y, a continuación, llamar a los métodos Find o FindRows.

El ejemplo siguiente utiliza el método Find para buscar un contacto con el apellido "Zhu".
Dim contacts As DataTable = dataSet.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() Order By
contact.Field(Of String)("LastName") Select contact
Dim view As DataView = query.AsDataView()
Dim found As Integer = view.Find("Zhu")
En el ejemplo siguiente se usa el método FindRows para buscar todos los productos de color rojo.
Dim products As DataTable = dataSet.Tables("Product")
Dim query = From product In products.AsEnumerable() Order By
product.Field(Of Decimal)("ListPrice"), product.Field(Of String)("Color")
Select product
Dim view As DataView = query.AsDataView()
view.Sort = "Color"
Dim criteria As Object() = New Object() {"Red"}
Dim foundRowsView As DataRowView() = view.FindRows(criteria)
ASP.NET

MCT: Luis Dueñas Pag 224 de 388


Manual de LINQ

ASP.NET dispone de un mecanismo de almacenamiento en memoria caché de objetos cuya creación en


memoria requiere una gran cantidad de recursos del servidor. Si se almacenan estos tipos de recursos en
la memoria caché se puede mejorar de forma significativa el rendimiento de la aplicación. La clase Cache
implementa el almacenamiento en memoria caché, con instancias de la memoria caché privadas para
cada aplicación. Dado que la creación de un objeto DataView nuevo puede consumir muchos recursos,
quizás desee utilizar esta funcionalidad de almacenamiento en caché en aplicaciones web para que
DataView no tenga que volver a crearse cada vez que se actualiza la página web.

En el ejemplo siguiente, DataView se almacena en caché para que los datos no tengan que volver a
ordenarse cuando la página se actualice.
If (Cache("ordersView") = Nothing) Then
Dim dataSet As New DataSet()
FillDataSet(dataSet)
Dim orders As DataTable = dataSet.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Where order.Field(Of
Boolean)("OnlineOrderFlag") = True Order By order.Field(Of
Decimal)("TotalDue") Select order
Dim view As DataView = query.AsDataView()
Cache.Insert("ordersView", view)
End If
Dim ordersView = CType(Cache("ordersView"), DataView)
GridView1.DataSource = ordersView
GridView1.DataBind()

8.2.2.6.5. Cómo Enlazar un Objeto DataView al Control


DataGridView de Windows Forms
El control DataGridView proporciona una forma eficaz y flexible de mostrar datos en formato de tabla. El
control DataGridView admite el modelo de enlace de datos de los formularios Windows Forms estándar,
por lo que se enlazará a DataView y a otros orígenes de datos. Sin embargo, en la mayoría de las
situaciones se enlazará a un componente BindingSource que administrará los detalles de la interacción
con el origen de datos.

Para conectar un control DataGridView a DataView

1. Implemente un método que controle los detalles de recuperación de datos desde una base de
datos. En el ejemplo de código siguiente se implementa un método GetData que inicializa un
componente SqlDataAdapter y lo utiliza para rellenar un DataSet. Asegúrese de establecer la
variable connectionString en un valor que sea adecuado para la base de datos. Necesitará tener
acceso a un servidor que tenga la base de datos de ejemplo AdventureWorks de SQL Server
instalada.
Private Sub GetData()
Try
' Initialize the DataSet.
dataSet = New DataSet()
dataSet.Locale = CultureInfo.InvariantCulture
' Create the connection string for the AdventureWorks sample database
Dim connectionString As String = "Data Source=localhost;
Initial Catalog=AdventureWorks;Integrated Security=true;"

MCT: Luis Dueñas Pag 225 de 388


Manual de LINQ

' Create the command strings for querying the Contact table.
Dim contactSelectCommand As String = "SELECT ContactID,
Title, FirstName, LastName, EmailAddress, Phone FROM Person.Contact"
' Create the contacts data adapter.
contactsDataAdapter = New SqlDataAdapter( _
contactSelectCommand,connectionString)
' Create a command builder to generate SQL update, insert, and
' delete commands based on the contacts select command. These
' are used to update the database.
Dim contactsCommandBuilder As SqlCommandBuilder = _
New SqlCommandBuilder(contactsDataAdapter)
' Fill the data set with the contact information.
contactsDataAdapter.Fill(dataSet, "Contact")
Catch ex As SqlException
MessageBox.Show(ex.Message)
End Try
End Sub
2. En el controlador de eventos Load del formulario, enlace el control DataGridView al componente
BindingSource y llame al método GetData para recuperar los datos de la base de datos. DataView
se crea a partir de una consulta LINQ a DataSet en DataTable Contact y luego se enlaza al
componente BindingSource.
Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
' Connect to the database and fill the DataSet.
GetData()
contactDataGridView.DataSource = contactBindingSource
' Create a LinqDataView from a LINQ to DataSet query and bind it
' to the Windows forms control.
Dim contactQuery = From row In dataSet.Tables("Contact").
AsEnumerable Where row.Field(Of String)("EmailAddress")<>Nothing
Order By row.Field(Of String)("LastName") Select row
contactView = contactQuery.AsDataView()
' Bind the DataGridView to the BindingSource.
contactBindingSource.DataSource = contactView
contactDataGridView.AutoResizeColumns()
End Sub

8.2.2.7. Depurar Consultas LINQ a DataSet


Visual Studio 2008 permite depurar el código LINQ a DataSet. Sin embargo, existen algunas diferencias
entre depurar código LINQ a DataSet y depurar código administrado que no es LINQ a DataSet. La
mayor parte de las características de depuración funcionan con instrucciones LINQ a DataSet, entre ellas
la función de examen, de establecimiento de puntos de interrupción y el examen de resultados que se
muestran en la ventana del depurador. Sin embargo, la ejecución aplazada de consultas conlleva algunos
efectos secundarios que se deben tener en cuenta al depurar código LINQ a DataSet y hay algunas
limitaciones para utilizar Editar y continuar. En este tema se tratan aspectos de depuración únicos para
LINQ a DataSet en comparación con código administrado que no es LINQ a DataSet

Ver los resultados

MCT: Luis Dueñas Pag 226 de 388


Manual de LINQ

Puede ver el resultado de una instrucción LINQ a DataSet mediante Información sobre datos, ventana
Inspección y cuadro de diálogo Inspección rápida. Al usar una ventana de código fuente, puede pausar el
puntero en una consulta en la ventana de código fuente para que aparezca una información sobre datos.
Puede copiar una variable LINQ a DataSet y pegarla en la ventana Inspección o en el cuadro de diálogo
Inspección rápida. En LINQ a DataSet, una consulta no se evalúa cuando se crea o declara, pero
únicamente cuando se ejecuta. Esto se denomina ejecución aplazada. Por consiguiente, la variable de
consulta no tiene un valor hasta que se evalúe.

Para mostrar el resultado de una consulta, el depurador debe evaluarla. Esta evaluación implícita se
produce cuando aparece un resultado de la consulta LINQ a DataSet en el depurador y tiene algunos
efectos que debería considerar. Cada evaluación de la consulta lleva tiempo. Expandir el nodo de
resultados lleva tiempo. Para algunas consultas, la evaluación repetida podría producir una reducción
notable del rendimiento. Evaluar una consulta también puede producir efectos secundarios, que son
cambios en el valor de los datos o en el estado del programa. No todas las consultas tienen los efectos
secundarios. Para determinar si una consulta se puede evaluar sin ningún riesgo ni efectos secundarios,
debe entender el código que implementa la consulta.

Editar y continuar
Editar y continuar no admite los cambios a las consultas LINQ a DataSet. Si agrega, quita o cambia una
instrucción LINQ a DataSet durante una sesión de depuración, aparece un cuadro de diálogo indicando
que Editar y continuar no admite el cambio. En ese momento, puede deshacer los cambios o detener la
sesión de depuración y reiniciar una nueva sesión con el código revisado.

Además, Editar y continuar no permite cambiar el tipo o el valor de una variable que se usa en una
instrucción LINQ a DataSet. De nuevo, puede deshacer los cambios o detener y reiniciar la sesión de
depuración.

En Visual C# 2008, no puede usar Editar y continuar en cualquier código en un método que contiene una
consulta LINQ a DataSet.

En Visual Basic 2008, puede usar Editar y continuar en código sin LINQ a DataSet, incluso en un método
que contiene una consulta LINQ a DataSet. Puede agregar o quitar el código antes de la instrucción LINQ
a DataSet, incluso si los cambios afectan al número de línea de la consulta LINQ a DataSetLINQ. La
experiencia en depuración de Visual Basic para código sin LINQ a DataSet es igual que antes de incluir
LINQ a DataSet. No se puede cambiar, agregar o quitar una consulta LINQ a DataSet, a menos que
desee detener la depuración para aplicar los cambios.

8.2.2.8. Seguridad
En este tema se tratan temas de seguridad en LINQ a DataSet.

Pasar una consulta a un componente que no sea de confianza


Una consulta LINQ a DataSet se puede formular en un punto de un programa y ejecutarse en otro
diferente. En el momento en que se formula la consulta, ésta puede hacer referencia a cualquier
elemento visible en ese momento, como miembros privados de la clase a la que pertenece el método de
llamada o símbolos que representan variables o argumentos locales. En tiempo de ejecución, la consulta
podrá obtener acceso a los miembros a los que la consulta hizo referencia durante la formulación, incluso
si el código de llamada no puede verlos. Al código que ejecuta la consulta no se le ha agregado
visibilidad de manera arbitraria por lo que no puede elegir a qué miembro debe obtener acceso. Sólo
podrá obtener acceso a lo que la consulta obtenga acceso y sólo a través de la misma consulta.

MCT: Luis Dueñas Pag 227 de 388


Manual de LINQ

Esto implica que, al pasar una referencia a una consulta realizada a otra parte del código, el componente
que recibe la consulta tiene acceso a todos los miembros públicos y privados a los que hace referencia la
consulta. En general, las consultas LINQ a DataSet no se deben pasar a componentes que no sean de
confianza a menos que la consulta se haya construido cuidadosamente para que no muestre la
información que debe ser privada.

Entrada externa
A menudo, las aplicaciones obtienen entradas externas (de un usuario o de otro agente externo) y
realizan acciones basadas en dicha entrada. En el caso de LINQ a DataSet, es posible que la aplicación
construya una consulta de determinada manera en función de la entrada externa o utilice dicha entrada
en la consulta. Las consultas LINQ a DataSet aceptan parámetros siempre que se acepten literales. Los
desarrolladores de aplicaciones deben utilizar consultas parametrizadas en lugar de insertar literales en
la consulta procedentes de un agente externo.

Cualquier entrada derivada directa o indirectamente del usuario o de un agente externo puede inyectar
contenido que aproveche la sintaxis del lenguaje de destino para realizar acciones no autorizadas. Esto
se conoce como ataque de inyección de SQL porque en el modelo de ataque el idioma de destino es
Transact-SQL. La entrada de usuario inyectada directamente en la consulta se utiliza para eliminar una
tabla de base de datos, provocar un ataque de denegación de servicio o cambiar la naturaleza de la
operación que se está realizando. Aunque la composición de consultas es posible en LINQ a DataSet,
ésta se realiza a través de la API del modelo de objetos. Las consultas LINQ a DataSet no se construyen
manipulando ni concatenando cadenas como ocurre en Transact-SQL y no son susceptibles de ataques
de inyección de SQL en el sentido tradicional.

8.2.2.9. Ejemplos de LINQ a DataSet


Esta sección proporciona ejemplos de programación de LINQ a DataSet que usan los operadores de
consulta estándar. El DataSet usado en estos ejemplos se rellena usando el método FillDataSet, que se
especifica en Cargar datos en DataSet.

8.2.2.9.1. Ejemplos de Sintaxis de Expresiones de Consulta


Esta sección proporciona ejemplos de programación de LINQ a DataSet en la sintaxis de expresión de
consultas que usan los operadores de consulta estándar. El DataSet usado en estos ejemplos se rellena
usando el método FillDataSet, que se especifica en Cargar datos en DataSet.

8.2.2.9.1.1. Proyección
Los ejemplos de este tema muestran cómo usar los métodos Select y SelectMany para consultar DataSet
usando la sintaxis de expresión de consultas.

El método FillDataSet utilizado en estos ejemplos se especifica en Cargar datos en DataSet.

Los ejemplos de este tema usan las tablas Contact, Address, Product, SalesOrderHeader y
SalesOrderDetail en la base de datos de ejemplo de AdventureWorks.

Los ejemplos de este tema usan las siguientes instrucciones using/Imports:


Option Explicit On
Imports System
Imports System.Linq
Imports System.Linq.Expressions

MCT: Luis Dueñas Pag 228 de 388


Manual de LINQ

Imports System.Collections.Generic
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.Common
Imports System.Globalization
Select
Ejemplo
Este ejemplo utiliza Select para devolver todas las filas de la tabla Product y mostrar los nombres de
producto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Select product
Console.WriteLine("Product Names:")
For Each p In query
Console.WriteLine(p.Field(Of String)("Name"))
Next
Ejemplo
Este ejemplo utiliza Select para devolver una secuencia de nombres de producto solamente.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Select product.Field
(Of String)("Name")
Console.WriteLine("Product Names:")
For Each productName In query
Console.WriteLine(productName)
Next
SelectMany
Ejemplo
En este ejemplo se utiliza From …, … (el equivalente del método SelectMany) para seleccionar todos los
pedidos en los que TotalDue es inferior a 500,00.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From contact In contacts.AsEnumerable() From order In
orders.AsEnumerable() Where (contact.Field(Of Integer)("ContactID") =
order.Field(Of Integer)("ContactID")) And (order.Field(Of Decimal)
("TotalDue") < 500D) Select New With _

MCT: Luis Dueñas Pag 229 de 388


Manual de LINQ

{
.ContactID = contact.Field(Of Integer)("ContactID"), _
.LastName = contact.Field(Of String)("LastName"), _
.FirstName = contact.Field(Of String)("FirstName"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.TotalDue = order.Field(Of Decimal)("TotalDue")}
For Each smallOrder In query
Console.Write("ContactID: " & smallOrder.ContactID)
Console.Write(" Name: {0}, {1}", smallOrder.LastName, _
smallOrder.FirstName)
Console.Write(" OrderID: " & smallOrder.OrderID)
Console.WriteLine(" TotalDue: $" & smallOrder.TotalDue)
Next
Ejemplo
En este ejemplo se utiliza From …, … (el equivalente del método SelectMany) para seleccionar todos los
pedidos efectuados a partir del 1 de octubre.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From contact In contacts.AsEnumerable() From order In
orders.AsEnumerable() Where contact.Field(Of Integer)("ContactID") =
order.Field(Of Integer)("ContactID") And order.Field(Of DateTime)
("OrderDate") >= New DateTime(2002, 10, 1) Select New With _
{ .ContactID = contact.Field(Of Integer)("ContactID"), _
.LastName = contact.Field(Of String)("LastName"), _
.FirstName = contact.Field(Of String)("FirstName"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate") }
For Each order In query
Console.Write("Contact ID: " & order.ContactID)
Console.Write(" Name: " & order.LastName & ", " & order.FirstName)
Console.Write(" Order ID: " & order.OrderID)
Console.WriteLine(" Order date: {0:d} ", order.OrderDate)
Next
Ejemplo
En este ejemplo se utiliza From …, … (el equivalente al método SelectMany) para seleccionar todos los
pedidos en los que el total del pedido es superior a 10.000,00 y utiliza la asignación From para evitar
que se solicite dos veces el total.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From contact In contacts.AsEnumerable() From order In
orders.AsEnumerable() Let total = order.Field(Of Decimal)("TotalDue")

MCT: Luis Dueñas Pag 230 de 388


Manual de LINQ

Where contact.Field(Of Integer)("ContactID") = order.Field(Of


Integer)("ContactID") And total >= 10000D Select New With _
{ .ContactID = contact.Field(Of Integer)("ContactID"), _
.LastName = contact.Field(Of String)("LastName"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate"), _
total }
For Each order In query
Console.Write("Contact ID: " & order.ContactID)
Console.Write(" Last Name: " & order.LastName)
Console.Write(" Order ID: " & order.OrderID)
Console.WriteLine(" Total: $" & order.total)
Next

8.2.2.9.1.2. Restricción
Los ejemplos de este tema muestran cómo utilizar el método Where para consultar DataSet utilizando
sintaxis de expresión de consultas.

Where
Ejemplo
Este ejemplo devuelve todos los pedidos en línea.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Where order.Field(Of
Boolean)("OnlineOrderFlag") = True Select New With _
{ .SalesOrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate"), _
.SalesOrderNumber = order.Field(Of String)("SalesOrderNumber") }
For Each onlineOrder In query
Console.Write("Order ID: " & onlineOrder.SalesOrderID)
Console.Write(" Order date: " & onlineOrder.OrderDate)
Console.WriteLine(" Order number: " & onlineOrder.SalesOrderNumber)
Next
Ejemplo
Este ejemplo devuelve los pedidos en los que la cantidad de pedido es superior a 2 e inferior a 6.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderDetail")
Dim query = From order In orders.AsEnumerable() Where order.Field(Of
Short)("OrderQty") > 2 And order.Field(Of Short)("OrderQty") < 6 _
Select New With _
{ .SalesOrderID = order.Field(Of Integer)("SalesOrderID"), _

MCT: Luis Dueñas Pag 231 de 388


Manual de LINQ

.OrderQty = order.Field(Of Short)("OrderQty") }


For Each order In query
Console.Write("Order ID: " & order.SalesOrderID)
Console.WriteLine(" Order quantity: " & order.OrderQty)
Next
Ejemplo
Este ejemplo devuelve todos los productos de color rojo.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Where product.Field
(Of String)("Color") = "Red" Select New With _
{ .Name = product.Field(Of String)("Name"), _
.ProductNumber = product.Field(Of String)("ProductNumber"), _
.ListPrice = product.Field(Of Decimal)("ListPrice") }
For Each product In query
Console.WriteLine("Name: " & product.Name)
Console.WriteLine("Product number: " & product.ProductNumber)
Console.WriteLine("List price: $ " & product.ListPrice & vbNewLine)
Next
Ejemplo
En este ejemplo se utiliza el método Where para buscar pedidos efectuados con posterioridad al 1 de
diciembre de 2002 y, a continuación, se utiliza el método GetChildRows para obtener los detalles de cada
pedido.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query As IEnumerable(Of DataRow) = From order In orders.AsEnumerable
Where order.Field(Of DateTime)("OrderDate") >= New DateTime(2002, 12, 1)
Select order
Console.WriteLine("Orders that were made after 12/1/2002:")
For Each order As DataRow In query
Console.WriteLine("OrderID {0} Order date: {1:d} ", _
order.Field(Of Integer)("SalesOrderID"), order.Field(Of
DateTime)("OrderDate"))
For Each orderDetail As DataRow In
order.GetChildRows("SalesOrderHeaderDetail")
Console.WriteLine(" Product ID: {0} Unit Price {1}", _
orderDetail("ProductID"), orderDetail("UnitPrice"))
Next
Next

8.2.2.9.1.3. Creación de Particiones

MCT: Luis Dueñas Pag 232 de 388


Manual de LINQ

Los ejemplos de este tema demuestran cómo usar los métodos Skip<(Of <(TSource>)>) y Take<(Of
<(TSource>)>) para consultar un DataSet usando la sintaxis de expresión de consultas.

Skip
Ejemplo
En este ejemplo se utiliza el método Skip<(Of <(TSource>)>) para obtener las dos primeras direcciones
de Seattle.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim addresses As DataTable = ds.Tables("Address")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = (From address In addresses.AsEnumerable() From order In
orders.AsEnumerable() Where (address.Field(Of Integer)("AddressID") =
order.Field(Of Integer)("BillToAddressID")) And address.Field(Of
String)("City") = "Seattle" Select New With _
{ .City = address.Field(Of String)("City"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate") _
}).Skip(2)
Console.WriteLine("All but first 2 orders in Seattle:")
For Each addOrder In query
Console.Write("City: " & addOrder.City)
Console.Write(" Order ID: " & addOrder.OrderID)
Console.WriteLine(" Order date: " & addOrder.OrderDate)
Next
Take
Ejemplo
En este ejemplo se utiliza el método Take<(Of <(TSource>)>) para obtener las tres primeras direcciones
de Seattle.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim addresses As DataTable = ds.Tables("Address")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = (From address In addresses.AsEnumerable() From order In
orders.AsEnumerable() Where (address.Field(Of Integer)("AddressID") =
order.Field(Of Integer)("BillToAddressID")) And address.Field(Of
String)("City") = "Seattle" Select New With _
{ .City = address.Field(Of String)("City"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate") _
}).Take(3)
Console.WriteLine("First 3 orders in Seattle:")
For Each order In query
Console.Write("City: " & order.City)

MCT: Luis Dueñas Pag 233 de 388


Manual de LINQ

Console.Write(" Order ID: " & order.OrderID)


Console.WriteLine(" Order date: " & order.OrderDate)
Next

8.2.2.9.1.4. Ordenación
Los ejemplos de este tema muestran cómo se utilizan los métodos OrderBy, OrderByDescending,
Reverse<(Of <(TSource>)>) y ThenByDescending para consultar DataSet y ordenar los resultados
utilizando la sintaxis de expresiones de consultas.

OrderBy
Ejemplo
En este ejemplo se utiliza OrderBy para devolver una lista de contactos organizados por apellido.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() Select contact Order
By contact.Field(Of String)("LastName")
Console.WriteLine("The sorted list of last names:")
For Each contact In query
Console.WriteLine(contact.Field(Of String)("LastName"))
Next
Ejemplo
En este ejemplo se utiliza OrderBy para ordenar una lista de contactos por longitud del apellido.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() Select contact Order
By contact.Field(Of String)("LastName").Length
Console.WriteLine("The sorted list of last names (by length):")
For Each contact In query
Console.WriteLine(contact.Field(Of String)("LastName"))
Next
OrderByDescending
Ejemplo
En este ejemplo se utiliza orderby… descending (Order By … Descending), equivalente al método
OrderByDescending, para ordenar el precio de venta de mayor a menor.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")

MCT: Luis Dueñas Pag 234 de 388


Manual de LINQ

Dim query = From product In products.AsEnumerable() Select product Order


By product.Field(Of Decimal)("ListPrice") Descending
Console.WriteLine("The list price From highest to lowest:")
For Each product In query
Console.WriteLine(product.Field(Of Decimal)("ListPrice"))
Next
Reverse
Ejemplo
En este ejemplo se utiliza Reverse<(Of <(TSource>)>) para crear una lista de pedidos en los que
OrderDate es anterior al 20 de febrero de 2002.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = (From order In orders.AsEnumerable() Where order.Field(Of
DateTime)("OrderDate") < New DateTime(2002, 2, 20) Select order).Reverse
Console.WriteLine("A backwards list orders where OrderDate<Feb 20, 2002")
For Each order In query
Console.WriteLine(order.Field(Of DateTime)("OrderDate"))
Next
ThenByDescending
Ejemplo
En este ejemplo se utiliza OrderBy… Descending, que es equivalente al método ThenByDescending,
para ordenar una lista de productos, primero por nombre y después por precio de venta, de mayor a
menor.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Order By
product.Field(Of String)("Name"), product.Field(Of Decimal)("ListPrice")
Descending Select product
For Each product In query
Console.Write("Product ID: " & product.Field(Of Integer)("ProductID"))
Console.Write("Product Name: " & product.Field(Of String)("Name"))
Console.WriteLine("Price: " & product.Field(Of Decimal)("ListPrice"))
Next

8.2.2.9.1.5. Operadores de Elementos


Los ejemplos de este tema muestran cómo usar los métodos First y ElementAt<(Of <(TSource>)>) para
obtener elementos DataRow de un DataSet usando la sintaxis de expresión de consultas.

ElementAt
Ejemplo

MCT: Luis Dueñas Pag 235 de 388


Manual de LINQ

Este ejemplo usa el método ElementAt<(Of <(TSource>)>) para recuperar la quinta dirección donde
PostalCode == "M4B 1V7".
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim addresses As DataTable = ds.Tables("Address")
Dim fifthAddress = (From address In addresses.AsEnumerable() Where
address.Field(Of String)("PostalCode") = "M4B 1V7" Select
address.Field(Of String)("AddressLine1")).ElementAt(5)
Console.WriteLine("5Address where PostalCode='M4B 1V7':" & fifthAddress)
First
Ejemplo
Este ejemplo usar el método First para devolver el primer contacto cuyo nombre es 'Brooke'.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim query = (From contact In contacts.AsEnumerable() Where
contact.Field(Of String)("FirstName") = "Brooke" Select contact).First()
Console.WriteLine("ContactID: " & query.Field(Of Integer)("ContactID"))
Console.WriteLine("FirstName: " & query.Field(Of String)("FirstName"))
Console.WriteLine("LastName: " & query.Field(Of String)("LastName"))

8.2.2.9.1.6. Operadores de Agregado


Los ejemplos de este tema muestran cómo utilizar los métodos Average, Count, Max, Min y Sum para
consultar un DataSet y agregar datos utilizando sintaxis de expresiones de consulta.

Average
Ejemplo
En este ejemplo se utiliza el método Average para encontrar el precio de venta promedio de cada estilo
de productos.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As IEnumerable(Of DataRow) = _
ds.Tables("Product").AsEnumerable()
Dim query = From product In products Group product By style =
product.Field(Of String)("Style") Into g = Group Select New With _
{ .Style = style, _
.AverageListPrice = g.Average(Function(product) _
product.Field(Of Decimal)("ListPrice")) }
For Each product In query
Console.WriteLine("Product style: {0} Average list price: {1}", _

MCT: Luis Dueñas Pag 236 de 388


Manual de LINQ

product.Style, product.AverageListPrice)
Next
Ejemplo
En este ejemplo se utiliza Average para obtener el importe total a pagar promedio para cada id. de
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.averageTotalDue = g.Average(Function(order) order. _
Field(Of Decimal)("TotalDue")) }
For Each order In query
Console.WriteLine("ContactID = {0} " & vbTab & " Average TotalDue =
{1}", order.Category, order.averageTotalDue)
Next
Ejemplo
En este ejemplo se utiliza Average para obtener los pedidos con el TotalDue promedio para cada de
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Let averageTotalDue =
g.Average(Function(order) order.Field(Of Decimal)("TotalDue"))
Select New With _
{ .Category = contactID, _
.CheapestProducts = g.Where(Function(order) order. _
Field(Of Decimal)("TotalDue") = averageTotalDue) }
For Each orderGroup In query
Console.WriteLine("ContactID: " & orderGroup.Category)
For Each order In orderGroup.CheapestProducts
Console.WriteLine("Average total due for SalesOrderID {1} is: {0}", _
order.Field(Of Decimal)("TotalDue"),order.Field(Of Int32)("SaleOrderID"))
Next
Console.WriteLine("")
Next
Count
Ejemplo
En este ejemplo se utiliza Count para devolver una lista de id. de contactos y el número de pedidos que
tiene cada uno de ellos.
' Fill the DataSet.

MCT: Luis Dueñas Pag 237 de 388


Manual de LINQ

Dim ds As New DataSet()


ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() Select New With _
{ .ContactID = contact.Field(Of Integer)("ContactID"), _
.OrderCount = contact.GetChildRows("SalesOrderContact").Count()}
For Each contact In query
Console.Write("CustomerID = " & contact.ContactID)
Console.WriteLine(vbTab & "OrderCount = " & contact.OrderCount)
Next
Ejemplo
En este ejemplo se agrupan los productos por colores y se utiliza Count para devolver el número de
productos de cada grupo de color.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Group product By
color = product.Field(Of String)("Color") Into g = Group Select New With
{.Color = color, .ProductCount = g.Count()}
For Each product In query
Console.WriteLine("Color = {0} " & vbTab & "ProductCount = {1}", _
product.Color, product.ProductCount)
Next
Max
Ejemplo
En este ejemplo se utiliza el método Max para obtener el mayor importe total a pagar de cada id. de
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.maxTotalDue = _
g.Max(Function(order) order.Field(Of Decimal)("TotalDue")) }
For Each order In query
Console.WriteLine("ContactID={0}" & vbTab & "Maximum TotalDue={1}", _
order.Category, order.maxTotalDue)
Next
Ejemplo
En este ejemplo se utiliza el método Max para obtener los pedidos con el TotalDue mayor de cada id.
contacto.

MCT: Luis Dueñas Pag 238 de 388


Manual de LINQ

' Fill the DataSet.


Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Let maxTotalDue =
g.Max(Function(order) order.Field(Of Decimal)("TotalDue")) Select New
With { .Category = contactID, _
.CheapestProducts = _
g.Where(Function(order) order. _
Field(Of Decimal)("TotalDue") = maxTotalDue)}
For Each orderGroup In query
Console.WriteLine("ContactID: " & orderGroup.Category)
For Each order In orderGroup.CheapestProducts
Console.WriteLine("MaxTotalDue {0} for SalesOrderID {1} ", _
order.Field(Of Decimal)("TotalDue"), _
order.Field(Of Int32)("SalesOrderID"))
Next
Next
Min
Ejemplo
En este ejemplo se utiliza el método Min para obtener el menor importe total a pagar de cada id. de
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.smallestTotalDue = g.Min(Function(order) _
order.Field(Of Decimal)("TotalDue")) }
For Each order In query
Console.WriteLine("ContactID = {0} " & vbTab & _
"Minimum TotalDue = {1}", order.Category, order.smallestTotalDue)
Next
Ejemplo
En este ejemplo se utiliza el método Min para obtener el pedido con el menor importe total a pagar de
cada contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")

MCT: Luis Dueñas Pag 239 de 388


Manual de LINQ

Dim query = From order In orders.AsEnumerable() Group order By contactID


= order.Field(Of Int32)("ContactID") Into g = Group Let minTotalDue =
g.Min(Function(order) order.Field(Of Decimal)("TotalDue")) Select New
With { _
.Category = contactID, _
.smallestTotalDue = g.Where(Function(order) order. _
Field(Of Decimal)("TotalDue") = minTotalDue) }
For Each orderGroup In query
Console.WriteLine("ContactID: " & orderGroup.Category)
For Each order In orderGroup.smallestTotalDue
Console.WriteLine("Mininum TotalDue {0} for SalesOrderID {1} ", _
order.Field(Of Decimal)("TotalDue"), _
order.Field(Of Int32)("SalesOrderID"))
Next
Console.WriteLine("")
Next
Sum
Ejemplo
En este ejemplo se utiliza el método Sum para obtener el importe total a pagar de cada id. de contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.TotalDue = g.Sum(Function(order) order. _
Field(Of Decimal)("TotalDue")) }
For Each order In query
Console.WriteLine("ContactID = {0} " & vbTab & "TotalDue sum = {1}", _
order.Category, order.TotalDue)
Next

8.2.2.9.1.7. Operadores de Combinación


La combinación es una operación importante de las consultas dirigidas a orígenes de datos que no tienen
relaciones navegables entre ellos, como las tablas de bases de datos relacionales. Una combinación de
dos orígenes de datos es la asociación de objetos en un origen de datos con objetos que comparten un
atributo común en el otro origen de datos.

Los ejemplos de este tema demuestran cómo usar los métodos GroupJoin y Join para consultar un
DataSet usando la sintaxis de expresión de consultas.

GroupJoin
Ejemplo
Este ejemplo realiza un GroupJoin en las tablas SalesOrderHeader y SalesOrderDetail para buscar el
número de pedidos por cliente. Una combinación de grupo es el equivalente de una combinación externa

MCT: Luis Dueñas Pag 240 de 388


Manual de LINQ

izquierda, que devuelve cada elemento del primer origen de datos (izquierdo), incluso si no hay
elementos correlacionados en el otro origen de datos.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders = ds.Tables("SalesOrderHeader").AsEnumerable()
Dim details = ds.Tables("SalesOrderDetail").AsEnumerable()
Dim query = From order In orders Group Join detail In details On
order.Field(Of Integer)("SalesOrderID") Equals detail.Field(Of
Integer)("SalesOrderID") Into ords = Group Select New With _
{ .CustomerID = order.Field(Of Integer)("SalesOrderID"), _
.ords = ords.Count() }
For Each order In query
Console.WriteLine("CustomerID: {0} Orders Count: {1}", _
order.CustomerID, order.ords)
Next
Ejemplo
Este ejemplo realiza un GroupJoin en las tablas Contact y SalesOrderHeader. Una combinación de grupo
es el equivalente de una combinación externa izquierda, que devuelve cada elemento del primer origen
de datos (izquierdo), incluso si no hay elementos correlacionados en el otro origen de datos.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From contact In contacts.AsEnumerable(), order In
orders.AsEnumerable() Where (contact.Field(Of Integer)("ContactID") =
order.Field(Of Integer)("ContactID")) Select New With _
{ .ContactID = contact.Field(Of Integer)("ContactID"), _
.SalesOrderID = order.Field(Of Integer)("SalesOrderID"), _
.FirstName = contact.Field(Of String)("FirstName"), _
.Lastname = contact.Field(Of String)("Lastname"), _
.TotalDue = order.Field(Of Decimal)("TotalDue") }
For Each contact_order In query
Console.Write("ContactID: " & contact_order.ContactID)
Console.Write(" SalesOrderID: " & contact_order.SalesOrderID)
Console.Write(" FirstName: " & contact_order.FirstName)
Console.Write(" Lastname: " & contact_order.Lastname)
Console.WriteLine(" TotalDue: " & contact_order.TotalDue)
Next
Join
Ejemplo
Este ejemplo realiza una combinación en las tablas SalesOrderHeader y SalesOrderDetail para obtener
pedidos en línea del mes de agosto.
' Fill the DataSet.

MCT: Luis Dueñas Pag 241 de 388


Manual de LINQ

Dim ds As New DataSet()


ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim details As DataTable = ds.Tables("SalesOrderDetail")
Dim query = From order In orders.AsEnumerable() Join detail In
details.AsEnumerable() On order.Field(Of Integer)("SalesOrderID") Equals
detail.Field(Of Integer)("SalesOrderID") Where order.Field(Of
Boolean)("OnlineOrderFlag") = True And order.Field(Of
DateTime)("OrderDate").Month = 8 Select New With _
{ .SalesOrderID = order.Field(Of Integer)("SalesOrderID"), _
.SalesOrderDetailID = detail.Field(Of
Integer)("SalesOrderDetailID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate"), _
.ProductID = detail.Field(Of Integer)("ProductID") }
For Each order In query
Console.WriteLine(order.SalesOrderID & vbTab & _
order.SalesOrderDetailID & vbTab & _
order.OrderDate & vbTab & order.ProductID)
Next

8.2.2.9.2. Ejemplos de Sintaxis de Consulta Basada en


Métodos
Esta sección proporciona ejemplos de programación de LINQ a DataSet en la sintaxis de consulta basada
en métodos que usan los operadores de consulta estándar.

8.2.2.9.2.1. Proyección
Los ejemplos de este tema muestran cómo usar los métodos Select y SelectMany para consultar DataSet
usando la sintaxis de consulta basada en métodos.

Select
Ejemplo
En este ejemplo se utiliza el método Select para proyectar las propiedades Name, ProductNumber y
ListPrice en una secuencia de tipos anónimos. El nombre de la propiedad ListPrice se cambia a Price en el
tipo resultante.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = products.AsEnumerable() .Select(Function(product As DataRow)
New With _
{ .ProductName = product.Field(Of String)("Name"), _
.ProductNumber = product.Field(Of String)("ProductNumber"), _
.Price = product.Field(Of Decimal)("ListPrice") })

MCT: Luis Dueñas Pag 242 de 388


Manual de LINQ

Console.WriteLine("Product Info:")
For Each product In query
Console.Write("Product name: " & product.ProductName)
Console.Write("Product number: " & product.ProductNumber)
Console.WriteLine("List price: $ " & product.Price)
Next
SelectMany
Ejemplo
En este ejemplo se utiliza el método SelectMany para seleccionar todos los pedidos en los que TotalDue
es inferior a 500,00.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts = ds.Tables("Contact").AsEnumerable()
Dim orders = ds.Tables("SalesOrderHeader").AsEnumerable()
Dim query = contacts.SelectMany(Function(contact)
orders.Where(Function(order)(contact.Field(Of Int32)("ContactID") =
order.Field(Of Int32)("ContactID")) And order.Field(Of Decimal)
("TotalDue") < 500D) .Select(Function(order) New With _
{ .ContactID = contact.Field(Of Integer)("ContactID"), _
.LastName = contact.Field(Of String)("LastName"), _
.FirstName = contact.Field(Of String)("FirstName"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.Total = order.Field(Of Decimal)("TotalDue") }))
For Each smallOrder In query
Console.Write("ContactID: " & smallOrder.ContactID)
Console.Write(" Name: " & smallOrder.LastName & ", " &
smallOrder.FirstName)
Console.Write(" Order ID: " & smallOrder.OrderID)
Console.WriteLine(" Total Due: $" & smallOrder.Total)
Next
Ejemplo
En este ejemplo se utiliza el método SelectMany para seleccionar todos los pedidos efectuados a partir
del 1 de octubre.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts = ds.Tables("Contact").AsEnumerable()
Dim orders = ds.Tables("SalesOrderHeader").AsEnumerable()
Dim query = contacts.SelectMany(Function(contact)
orders.Where(Function(order)(contact.Field(Of Int32)("ContactID") =
order.Field(Of Int32)("ContactID")) And order.Field(Of DateTime)
("OrderDate")>=New DateTime(2002, 10, 1)) .Select(Function(order) New
With { .ContactID = contact.Field(Of Integer)("ContactID"),
.LastName = contact.Field(Of String)("LastName"), _

MCT: Luis Dueñas Pag 243 de 388


Manual de LINQ

.FirstName = contact.Field(Of String)("FirstName"), _


.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate")}))
For Each order In query
Console.Write("Contact ID: " & order.ContactID)
Console.Write(" Name: " & order.LastName & ", " & order.FirstName)
Console.Write(" Order ID: " & order.OrderID)
Console.WriteLine(" Order date: {0:d}", order.OrderDate)
Next

8.2.2.9.2.2. Creación de Particiones


Los ejemplos de este tema muestran cómo usar los métodos Skip<(Of <(TSource>)>), SkipWhile,
Take<(Of <(TSource>)>) y TakeWhile para consultar un DataSet usando la sintaxis de expresión de
consultas.

Skip
Ejemplo
En este ejemplo se utiliza el método Skip<(Of <(TSource>)>) para obtener todos los contactos menos
los cinco primeros de la tabla Contact.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim allButFirst5Contacts = contacts.AsEnumerable().Skip(5)
Console.WriteLine("All but first 5 contacts:")
For Each contact In allButFirst5Contacts
Console.Write("FirstName = {0} ", contact.Field(Of
String)("FirstName"))
Console.WriteLine(vbTab & " LastName = " & contact.Field(Of
String)("LastName"))
Next
Ejemplo
En este ejemplo se utiliza el método Skip<(Of <(TSource>)>) para obtener las dos primeras direcciones
de Seattle.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim addresses As DataTable = ds.Tables("Address")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = (From address In addresses.AsEnumerable() From order In
orders.AsEnumerable() Where (address.Field(Of Integer)("AddressID") =
order.Field(Of Integer)("BillToAddressID")) And address.Field(Of
String)("City") = "Seattle" Select New With _
{ .City = address.Field(Of String)("City"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _

MCT: Luis Dueñas Pag 244 de 388


Manual de LINQ

.OrderDate = order.Field(Of DateTime)("OrderDate") _


}).Skip(2)
Console.WriteLine("All but first 2 orders in Seattle:")
For Each addOrder In query
Console.Write("City: " & addOrder.City)
Console.Write(" Order ID: " & addOrder.OrderID)
Console.WriteLine(" Order date: " & addOrder.OrderDate)
Next
SkipWhile
Ejemplo
En este ejemplo se utilizan los métodos OrderBy y SkipWhile para devolver productos de la tabla Product
con un precio de venta superior a 300,00.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim skipWhilePriceLessThan300 As IEnumerable(Of DataRow) = _
products.AsEnumerable().OrderBy(Function(listprice) listprice.Field(Of
Decimal)("ListPrice")).SkipWhile(Function(product) product.Field(Of
Decimal)("ListPrice") < 300D)
Console.WriteLine("First ListPrice less than 300.00:")
For Each product As DataRow In skipWhilePriceLessThan300
Console.WriteLine(product.Field(Of Decimal)("ListPrice"))
Next
Take
Ejemplo
En este ejemplo se utiliza el método Take<(Of <(TSource>)>) para obtener únicamente los cinco
primeros contactos de la tabla Contact.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim first5Contacts = contacts.AsEnumerable().Take(5)
Console.WriteLine("First 5 contacts:")
For Each contact In first5Contacts
Console.Write("Title = " & contact.Field(Of String)("Title"))
Console.Write(vbTab & "FirstName = " & contact.Field(Of
String)("FirstName"))
Console.WriteLine(vbTab & "LastName = " & contact.Field(Of
String)("LastName"))
Next
Ejemplo
En este ejemplo se utiliza el método Take<(Of <(TSource>)>) para obtener las tres primeras direcciones
de Seattle.
' Fill the DataSet.

MCT: Luis Dueñas Pag 245 de 388


Manual de LINQ

Dim ds As New DataSet()


ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim addresses As DataTable = ds.Tables("Address")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = (From address In addresses.AsEnumerable() From order In
orders.AsEnumerable() Where (address.Field(Of Integer)("AddressID") =
order.Field(Of Integer)("BillToAddressID")) And address.Field(Of
String)("City") = "Seattle" Select New With _
{ .City = address.Field(Of String)("City"), _
.OrderID = order.Field(Of Integer)("SalesOrderID"), _
.OrderDate = order.Field(Of DateTime)("OrderDate") _
}).Take(3)
Console.WriteLine("First 3 orders in Seattle:")
For Each order In query
Console.Write("City: " & order.City)
Console.Write(" Order ID: " & order.OrderID)
Console.WriteLine(" Order date: " & order.OrderDate)
Next
TakeWhile
Ejemplo
En este ejemplo se utilizan los métodos OrderBy y TakeWhile para devolver de la tabla productos Product
con un precio de venta inferior a 300,00.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim takeWhileListPriceLessThan300 As IEnumerable(Of DataRow) = _
products.AsEnumerable().OrderBy(Function(listprice) listprice.Field(Of
Decimal)("ListPrice")).TakeWhile(Function(product) product.Field(Of
Decimal)("ListPrice") < 300D)
Console.WriteLine("First ListPrice less than 300.00:")
For Each product As DataRow In takeWhileListPriceLessThan300
Console.WriteLine(product.Field(Of Decimal)("ListPrice"))
Next

8.2.2.9.2.3. Ordenación
Los ejemplos de este tema muestran cómo se utilizan los métodos OrderBy, Reverse<(Of
<(TSource>)>) y ThenBy para consultar DataSet y ordenar los resultados utilizando la sintaxis de
consulta basada en métodos.

OrderBy
Ejemplo
Este ejemplo utiliza el método OrderBy con un comparador personalizado para realizar una ordenación
con distinción de mayúsculas y minúsculas de apellidos.
' Fill the DataSet.

MCT: Luis Dueñas Pag 246 de 388


Manual de LINQ

Dim ds As New DataSet()


ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim query As IEnumerable(Of DataRow) =
contacts.AsEnumerable().OrderBy(Function(contact)
contact.Field(Of String)("LastName"), New CaseInsensitiveComparer())
For Each contact As DataRow In query
Console.WriteLine(contact.Field(Of String)("LastName"))
Next
Reverse
Ejemplo
En este ejemplo se utiliza el método Reverse<(Of <(TSource>)>) para crear una lista de pedidos en los
que OrderDate es anterior al 20 de febrero de 2002.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = (From order In orders.AsEnumerable() Where order.Field(Of
DateTime)("OrderDate")<New DateTime(2002, 2, 20) Select order).Reverse()
Console.WriteLine("A backwards list orders where OrderDate<Feb 20, 2002")
For Each order In query
Console.WriteLine(order.Field(Of DateTime)("OrderDate"))
Next
ThenBy
Ejemplo
En este ejemplo se utilizan los métodos OrderBy y ThenBy con un comparador personalizado para
ordenar primero por precio de venta y después realizar una ordenación con distinción de mayúsculas y
minúsculas en orden descendente de nombres de producto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query As IEnumerable(Of DataRow) =
products.AsEnumerable().OrderBy(Function(product) product.Field(Of
Decimal)("ListPrice")).ThenBy(Function(product) product.Field(Of
String)("Name"),New CaseInsensitiveComparer())
For Each product As DataRow In query
Console.WriteLine("Product ID: {0} Product Name: {1} List Price {2}", _
product.Field(Of Integer)("ProductID"), _
product.Field(Of String)("Name"), _
product.Field(Of Decimal)("ListPrice"))
Next

MCT: Luis Dueñas Pag 247 de 388


Manual de LINQ

8.2.2.9.2.4. Operadores de Conjuntos


Los ejemplos de este tema muestran cómo se usan los operadores Distinct, Except, Intersect y Union
para realizar operaciones de comparación basadas en valores en conjuntos de filas de datos.

Distinct
Ejemplo
Este ejemplo usa el método Distinct para quitar elementos duplicados de una secuencia.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim rows As List(Of DataRow) = New List(Of DataRow)
Dim contacts As DataTable = ds.Tables("Contact")
' Get 100 rows from the Contact table.
Dim query = (From c In contacts.AsEnumerable() Select c).Take(100)
Dim contactsTableWith100Rows = query.CopyToDataTable()
' Add 100 rows to the list.
For Each row In contactsTableWith100Rows.Rows
rows.Add(row)
Next
' Create duplicate rows by adding the same 100 rows to the list.
For Each row In contactsTableWith100Rows.Rows
rows.Add(row)
Next
Dim table = _
System.Data.DataTableExtensions.CopyToDataTable(Of DataRow)(rows)
' Find the unique contacts in the table.
Dim uniqueContacts = _
table.AsEnumerable().Distinct(DataRowComparer.Default)
Console.WriteLine("Unique contacts:")
For Each uniqueContact In uniqueContacts
Console.WriteLine(uniqueContact.Field(Of Integer)("ContactID"))
Next
Except
Ejemplo
Este ejemplo usa el método Except para devolver contactos que aparecen en la primera tabla pero no en
la segunda.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contactTable As DataTable = ds.Tables("Contact")
Dim query1 = From contact In contactTable.AsEnumerable() Where
contact.Field(Of String)("Title") = "Ms." Select contact
Dim query2 = From contact In contactTable.AsEnumerable() Where
contact.Field(Of String)("FirstName") = "Sandra" Select contact
Dim contacts1 = query1.CopyToDataTable()

MCT: Luis Dueñas Pag 248 de 388


Manual de LINQ

Dim contacts2 = query2.CopyToDataTable()


' Find the contacts that are in the first
' table but not the second.
Dim contacts =
contacts1.AsEnumerable().Except(contacts2.AsEnumerable(),DataRowComparer.
Default)
Console.WriteLine("Except of employees tables")
For Each row In contacts
Console.WriteLine("Id: {0} {1} {2} {3}", _
row("ContactID"), row("Title"), row("FirstName"), row("LastName"))
Next
Intersect
Ejemplo
Este ejemplo usa el método Intersect para devolver contactos que aparecen en ambas tablas.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contactTable As DataTable = ds.Tables("Contact")
Dim query1 = From contact In contactTable.AsEnumerable() Where
contact.Field(Of String)("Title") = "Ms." Select contact
Dim query2 = From contact In contactTable.AsEnumerable() Where
contact.Field(Of String)("FirstName") = "Sandra" Select contact
Dim contacts1 = query1.CopyToDataTable()
Dim contacts2 = query2.CopyToDataTable()
Dim contacts = contacts1.AsEnumerable() _
.Intersect(contacts2.AsEnumerable(), DataRowComparer.Default)
Console.WriteLine("Intersect of employees tables")
For Each row In contacts
Console.WriteLine("Id: {0} {1} {2} {3}", _
row("ContactID"), row("Title"), row("FirstName"), row("LastName"))
Next
Union
Ejemplo
Este ejemplo usa el método Union para devolver contactos únicos de cualquiera de las dos tablas.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contactTable As DataTable = ds.Tables("Contact")
Dim query1 = From contact In contactTable.AsEnumerable() Where
contact.Field(Of String)("Title") = "Ms." Select contact
Dim query2 = From contact In contactTable.AsEnumerable() Where
contact.Field(Of String)("FirstName") = "Sandra" Select contact
Dim contacts1 = query1.CopyToDataTable()
Dim contacts2 = query2.CopyToDataTable()

MCT: Luis Dueñas Pag 249 de 388


Manual de LINQ

Dim contacts = contacts1.AsEnumerable().Union(contacts2.AsEnumerable(), _


DataRowComparer.Default)
Console.WriteLine("Union of employees tables")
For Each row In contacts
Console.WriteLine("Id: {0} {1} {2} {3}", _
row("ContactID"), row("Title"), row("FirstName"), row("LastName"))
Next

8.2.2.9.2.5. Operadores de Conversión


Los ejemplos de este tema demuestran cómo usar los métodos ToArray<(Of <(TSource>)>),
ToDictionary y ToList<(Of <(TSource>)>) para ejecutar inmediatamente una expresión de consulta.

ToArray
Ejemplo
En este ejemplo se utiliza el método ToArray<(Of <(TSource>)>) para evaluar de forma inmediata una
secuencia en una matriz.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim productsArray = products.AsEnumerable().ToArray()
Dim query = From product In productsArray Select product Order By
product.Field(Of Decimal)("ListPrice") Descending
Console.WriteLine("Every price From highest to lowest:")
For Each product In query
Console.WriteLine(product.Field(Of Decimal)("ListPrice"))
Next
ToDictionary
Ejemplo
En este ejemplo se utiliza el método ToDictionary para evaluar de forma inmediata una secuencia y una
expresión de clave relacionada en un diccionario.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim scoreRecordsDict = products.AsEnumerable(). _
ToDictionary(Function(record) record.Field(Of String)("Name"))
Console.WriteLine("Top Tube's ProductID: {0}", _
scoreRecordsDict("Top Tube")("ProductID"))
ToList
Ejemplo
En este ejemplo se utiliza el método ToList<(Of <(TSource>)>) para evaluar inmediatamente una
secuencia en List<(Of <(T>)>), donde T es de tipo DataRow.
' Fill the DataSet.

MCT: Luis Dueñas Pag 250 de 388


Manual de LINQ

Dim ds As New DataSet()


ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim productList = products.AsEnumerable().ToList()
Dim query = From product In productList Select product Order By
product.Field(Of String)("Name")
Console.WriteLine("The sorted name list:")
For Each product In query
Console.WriteLine(product.Field(Of String)("Name").ToLower _
(CultureInfo.InvariantCulture))
Next

8.2.2.9.2.6. Operadores de Elementos


Los ejemplos de este tema muestran cómo usar el método First para obtener elementos DataRow de un
DataSet usando la sintaxis de consulta de métodos.

First
Ejemplo
Este ejemplo usa el método First para buscar la primera dirección de correo electrónico que empieza por
"caroline".
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim startsWith As DataRow = contacts.AsEnumerable(). _
First(Function(contact) contact.Field(Of String) _
("EmailAddress").StartsWith("caroline"))
Console.WriteLine("An email address starting with 'caroline': {0}", _
startsWith.Field(Of String)("EmailAddress"))

8.2.2.9.2.7. Operadores de Agregado


Los ejemplos de este tema muestran cómo utilizar los operadores Aggregate, Average, Count,
LongCount, Max, Min y Sum para consultar DataSet y agregar datos utilizando sintaxis de consulta
basada en métodos.

Aggregate
Ejemplo
En este ejemplo se utiliza el método Aggregate para obtener los cinco primeros contactos de la tabla
Contact y generar una lista de apellidos delimitada por comas.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)

MCT: Luis Dueñas Pag 251 de 388


Manual de LINQ

Dim contacts As IEnumerable(Of DataRow)=ds.Tables("Contact").AsEnumerable


Dim nameList As String = contacts.Take(5).Select(Function(contact)
contact.Field(Of String)("LastName")).Aggregate(Function(workingList,
next1) workingList + "," + next1)
Console.WriteLine(nameList)
Average
Ejemplo
En este ejemplo se utiliza el método Average para encontrar el precio de venta promedio de los
productos.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As IEnumerable(Of DataRow) = _
ds.Tables("Product").AsEnumerable()
Dim averageListPrice As Decimal = products.Average(Function(product)
product.Field(Of Decimal)("ListPrice"))
Console.WriteLine("The average list price of all the products is $" & _
averageListPrice)
Ejemplo
En este ejemplo se utiliza el método Average para encontrar el precio de venta promedio de cada estilo
de productos.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As IEnumerable(Of DataRow)=ds.Tables("Product").AsEnumerable
Dim query = From product In products Group product By style =
product.Field(Of String)("Style") Into g = Group Select New With _
{ .Style = style,
.AverageListPrice = g.Average(Function(product) _
product.Field(Of Decimal)("ListPrice")) }
For Each product In query
Console.WriteLine("Product style: {0} Average list price: {1}", _
product.Style, product.AverageListPrice)
Next
Ejemplo
En este ejemplo se utiliza el método Average para encontrar el importe total a pagar promedio.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim averageTotalDue As Decimal = orders.AsEnumerable(). _
Average(Function(order) order.Field(Of Decimal)("TotalDue"))
Console.WriteLine("The average TotalDue is {0}.", averageTotalDue)

MCT: Luis Dueñas Pag 252 de 388


Manual de LINQ

Ejemplo
En este ejemplo se utiliza el método Average para obtener el importe total a pagar promedio de cada id.
de contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.averageTotalDue = g.Average(Function(order) order. _
Field(Of Decimal)("TotalDue")) }
For Each order In query
Console.WriteLine("ContactID = {0} " & vbTab & _
" Average TotalDue = {1}", order.Category,order.averageTotalDue)
Next
Ejemplo
En este ejemplo se utiliza el método Average para obtener los pedidos con el TotalDue promedio de cada
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Let averageTotalDue =
g.Average(Function(order) order.Field(Of Decimal)("TotalDue")) Select New
With { .Category = contactID, _
.CheapestProducts = g.Where(Function(order) order. _
Field(Of Decimal)("TotalDue") = averageTotalDue) }
For Each orderGroup In query
Console.WriteLine("ContactID: " & orderGroup.Category)
For Each order In orderGroup.CheapestProducts
Console.WriteLine("Average total due for SalesOrderID {1} is: {0}", _
order.Field(Of Decimal)("TotalDue"), _
order.Field(Of Int32)("SalesOrderID"))
Next
Console.WriteLine("")
Next
Count
Ejemplo
En este ejemplo se utiliza el método Count para devolver el número de productos de la tabla Product.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.

MCT: Luis Dueñas Pag 253 de 388


Manual de LINQ

FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim numProducts = products.AsEnumerable().Count()
Console.WriteLine("There are " & numProducts & " products.")
Ejemplo
En este ejemplo se utiliza el método Count para devolver una lista de id. de contactos y el número de
pedidos que tiene cada uno de ellos.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim query = From contact In contacts.AsEnumerable() Select New With _
{ .ContactID = contact.Field(Of Integer)("ContactID"), _
.OrderCount = contact.GetChildRows("SalesOrderContact").Count()}
For Each contact In query
Console.Write("CustomerID = " & contact.ContactID)
Console.WriteLine(vbTab & "OrderCount = " & contact.OrderCount)
Next
Ejemplo
En este ejemplo se agrupan los productos por colores y se utiliza el método Count para devolver el
número de productos de cada grupo de color.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim products As DataTable = ds.Tables("Product")
Dim query = From product In products.AsEnumerable() Group product By
color = product.Field(Of String)("Color") Into g = Group Select New With
{.Color = color, .ProductCount = g.Count()}
For Each product In query
Console.WriteLine("Color = {0} " & vbTab & "ProductCount = {1}", _
product.Color, product.ProductCount)
Next
LongCount
Ejemplo
En este ejemplo se obtiene el recuento de contactos como un entero largo.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim numberOfContacts = contacts.AsEnumerable().LongCount()
Console.WriteLine("There are {0} Contacts", numberOfContacts)
Max
Ejemplo
En este ejemplo se utiliza el método Max para obtener el mayor importe total a pagar.

MCT: Luis Dueñas Pag 254 de 388


Manual de LINQ

' Fill the DataSet.


Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim maxTotalDue As Decimal = orders.AsEnumerable(). _
Max(Function(w) w.Field(Of Decimal)("TotalDue"))
Console.WriteLine("The maximum TotalDue is {0}.", maxTotalDue)
Ejemplo
En este ejemplo se utiliza el método Max para obtener el mayor importe total a pagar de cada id. de
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.maxTotalDue = _
g.Max(Function(order) order.Field(Of Decimal)("TotalDue")) _
}
For Each order In query
Console.WriteLine("ContactID = {0} " & vbTab & _
" Maximum TotalDue = {1}", order.Category, order.maxTotalDue)
Next
Ejemplo
En este ejemplo se utiliza el método Max para obtener los pedidos con el TotalDue mayor de cada
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Let maxTotalDue =
g.Max(Function(order) order.Field(Of Decimal)("TotalDue")) Select New
With { .Category = contactID, _
.CheapestProducts = g.Where(Function(order) order. _
Field(Of Decimal)("TotalDue") = maxTotalDue) }
For Each orderGroup In query
Console.WriteLine("ContactID: " & orderGroup.Category)
For Each order In orderGroup.CheapestProducts
Console.WriteLine("MaxTotalDue {0} for SalesOrderID {1} ", _
order.Field(Of Decimal)("TotalDue"), _
order.Field(Of Int32)("SalesOrderID"))
Next

MCT: Luis Dueñas Pag 255 de 388


Manual de LINQ

Next
Min
Ejemplo
En este ejemplo se utiliza el método Min para obtener el menor importe total a pagar.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim smallestTotalDue As Decimal = orders.AsEnumerable(). _
Min(Function(totalDue) totalDue.Field(Of Decimal)("TotalDue"))
Console.WriteLine("The smallest TotalDue is {0}.", smallestTotalDue)
Ejemplo
En este ejemplo se utiliza el método Min para obtener el menor importe total a pagar de cada id. de
contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.smallestTotalDue = g.Min(Function(order) _
order.Field(Of Decimal)("TotalDue")) _
}
For Each order In query
Console.WriteLine("ContactID = {0} " & vbTab & _
"Minimum TotalDue = {1}", order.Category, order.smallestTotalDue)
Next
Ejemplo
En este ejemplo se utiliza el método Min para obtener el pedido con el menor importe total a pagar de
cada contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Let minTotalDue =
g.Min(Function(order) order.Field(Of Decimal)("TotalDue")) Select New
With { .Category = contactID, _
.smallestTotalDue = g.Where(Function(order) order. _
Field(Of Decimal)("TotalDue") = minTotalDue) }
For Each orderGroup In query
Console.WriteLine("ContactID: " & orderGroup.Category)

MCT: Luis Dueñas Pag 256 de 388


Manual de LINQ

For Each order In orderGroup.smallestTotalDue


Console.WriteLine("Mininum TotalDue {0} for SalesOrderID {1} ", _
order.Field(Of Decimal)("TotalDue"), _
order.Field(Of Int32)("SalesOrderID"))
Next
Console.WriteLine("")
Next
Sum
Ejemplo
En este ejemplo se utiliza el método Sum para obtener el número total de cantidades de pedido de la
tabla SalesOrderDetail.
// Fill the DataSet.
DataSet ds = new DataSet();
ds.Locale = CultureInfo.InvariantCulture;
FillDataSet(ds);
DataTable orders = ds.Tables["SalesOrderDetail"];
double totalOrderQty=orders.AsEnumerable().
Sum(o => o.Field<Int16>("OrderQty"));
Console.WriteLine("There are a total of {0} OrderQty.",totalOrderQty);
Ejemplo
En este ejemplo se utiliza el método Sum para obtener el importe total a pagar de cada id. de contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = From order In orders.AsEnumerable() Group order By contactID
= order.Field(Of Int32)("ContactID") Into g = Group Select New With _
{ .Category = contactID, _
.TotalDue = g.Sum(Function(order) order. _
Field(Of Decimal)("TotalDue")) }
For Each order In query
Console.WriteLine("ContactID = {0} " & vbTab & "TotalDue sum = {1}", _
order.Category, order.TotalDue)
Next

8.2.2.9.2.8. Combinación
La combinación es una operación importante de las consultas dirigidas a orígenes de datos que no tienen
relaciones navegables entre ellos, como las tablas de bases de datos relacionales. Una combinación de
dos orígenes de datos es la asociación de objetos en un origen de datos con objetos que comparten un
atributo común en el otro origen de datos.

Los ejemplos de este tema muestran cómo usar el método Join para consultar DataSet usando la sintaxis
de consulta basada en métodos.

Join
Ejemplo
En este ejemplo se realiza una combinación en las tablas Contact y SalesOrderHeader.

MCT: Luis Dueñas Pag 257 de 388


Manual de LINQ

' Fill the DataSet.


Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = contacts.AsEnumerable().Join(orders.AsEnumerable(), _
Function(order) order.Field(Of Int32)("ContactID"), _
Function(contact) contact.Field(Of Int32)("ContactID"), _
Function(contact, order) New With _
{ .ContactID = contact.Field(Of Int32)("ContactID"), _
.SalesOrderID = order.Field(Of Int32)("SalesOrderID"), _
.FirstName = contact.Field(Of String)("FirstName"), _
.Lastname = contact.Field(Of String)("Lastname"), _
.TotalDue = order.Field(Of Decimal)("TotalDue") })
For Each contact_order In query
Console.WriteLine("ContactID: {0} " & "SalesOrderID: {1} " _
& "FirstName: {2} " & "Lastname: {3} " & "TotalDue: {4}", _
contact_order.ContactID, contact_order.SalesOrderID, _
contact_order.FirstName, contact_order.Lastname, _
contact_order.TotalDue)
Next
Ejemplo
En este ejemplo se realiza una combinación en las tablas Contact y SalesOrderHeader, agrupando los
resultados por id. de contacto.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contacts As DataTable = ds.Tables("Contact")
Dim orders As DataTable = ds.Tables("SalesOrderHeader")
Dim query = contacts.AsEnumerable().Join(orders.AsEnumerable(), _
Function(order) order.Field(Of Int32)("ContactID"), _
Function(contact) contact.Field(Of Int32)("ContactID"), _
Function(contact, order) New With _
{ .ContactID = contact.Field(Of Int32)("ContactID"), _
.SalesOrderID = order.Field(Of Int32)("SalesOrderID"), _
.FirstName = contact.Field(Of String)("FirstName"), _
.Lastname = contact.Field(Of String)("Lastname"), _
.TotalDue = order.Field(Of Decimal)("TotalDue") }) _
.GroupBy(Function(record) record.ContactID)
For Each group In query
For Each contact_order In group
Console.WriteLine("ContactID: {0} " & "SalesOrderID: {1} " _
& "FirstName: {2} " & "Lastname: {3} " & "TotalDue: {4}", _
contact_order.ContactID, contact_order.SalesOrderID, _
contact_order.FirstName, contact_order.Lastname, _
contact_order.TotalDue)

MCT: Luis Dueñas Pag 258 de 388


Manual de LINQ

Next
Next

8.2.2.9.3. Ejemplos de Operadores Específicos de DataSet


Los ejemplos de este tema muestran cómo usar los métodos CopyToDataTable y la clase
DataRowComparer.

CopyToDataTable
Ejemplo
En este ejemplo se carga un DataTable con resultados de consulta utilizando el método
CopyToDataTable.
' Fill the DataSet.
Dim ds As New DataSet()
ds.Locale = CultureInfo.InvariantCulture
' See the FillDataSet method in the Loading Data Into a DataSet topic.
FillDataSet(ds)
Dim contactTable As DataTable = ds.Tables("Contact")
Dim query = From contact In contactTable.AsEnumerable() Where
contact.Field(Of String)("Title") = "Ms." And contact.Field(Of
String)("FirstName") = "Carla" Select contact
Dim contacts = query.CopyToDataTable().AsEnumerable()
For Each contact In contacts
Console.Write("ID: " & contact.Field(Of Integer)("ContactID"))
Console.WriteLine(" Name: " & contact.Field(Of String)("LastName") &
", " & contact.Field(Of String)("FirstName"))
Next
DataRowComparer
Ejemplo
En este ejemplo se comparan dos filas distintas de datos utilizando DataRowComparer.
// Fill the DataSet.
DataSet ds = new DataSet();
ds.Locale = CultureInfo.InvariantCulture;
FillDataSet(ds);
// Get two rows from the SalesOrderHeader table.
DataTable table = ds.Tables["SalesOrderHeader"];
DataRow left = (DataRow)table.Rows[0];
DataRow right = (DataRow)table.Rows[1];
// Compare the two different rows.
IEqualityComparer<DataRow> comparer = DataRowComparer.Default;
bool bEqual = comparer.Equals(left, right);
if (bEqual)
Console.WriteLine("The two rows are equal");
else
Console.WriteLine("The two rows are not equal");
// Get the hash codes of the two rows.
Console.WriteLine("The hashcodes for the two rows are {0}, {1}",
comparer.GetHashCode(left),comparer.GetHashCode(right));

MCT: Luis Dueñas Pag 259 de 388


Manual de LINQ

8.3. LINQ a SQL


LINQ a SQL es un componente de .NET Framework 3.5 que proporciona una infraestructura en tiempo de
ejecución para administrar los datos relacionales como objetos.

Nota:

Los datos relacionales se muestran como una colección de tablas bidimensionales (relaciones o
archivos sin formato), donde las columnas comunes relacionan las tablas entre sí. Para utilizar LINQ
a SQL con eficacia, debe estar un poco familiarizado con los principios subyacentes de las bases de
datos relacionales.

En LINQ a SQL, el modelo de datos de una base de datos relacional se asigna a un modelo de objetos
expresado en el lenguaje de programación del programador. Cuando la aplicación se ejecuta, LINQ a SQL
convierte a SQL las consultas integradas en el lenguaje en el modelo de objetos y las envía a la base de
datos para su ejecución. Cuando la base de datos devuelve los resultados, LINQ a SQL los vuelve a
convertir en objetos con los que pueda trabajar en su propio lenguaje de programación.

Los programadores de Visual Studio normalmente utilizan el Diseñador relacional de objetos, que
proporciona una interfaz de usuario para implementar muchas de las características de LINQ a SQL.

8.3.1. Introducción LINQ a SQL


En LINQ a SQL, puede utilizar la tecnología LINQ para tener acceso a las bases de datos de SQL igual
que obtendría acceso a una colección en memoria.

Por ejemplo, en el código siguiente, se crea el objeto nw para representar la base de datos Northwind, el
destino es la tabla Customers, las filas se filtran para Customers de London y se selecciona para la
recuperación una cadena de CompanyName.

Cuando se ejecuta el bucle, se recupera la colección de valores CompanyName.


' Northwnd inherits from System.Data.Linq.DataContext.
Dim nw As New Northwnd("c:\northwnd.mdf")
Dim companyNameQuery = From cust In nw.Customers Where cust.City =
"London" Select cust.CompanyName
For Each customer In companyNameQuery
Console.WriteLine(customer)
Next

8.3.1.1. Qué se Puede Hacer con LINQ a SQL


LINQ a SQL admite toda la funcionalidad clave que puede esperar un programador de SQL. Puede
consultar, insertar, actualizar y eliminar información en las tablas.

Selección
Para la selección (proyección), basta con que escriba una consulta LINQ en su propio lenguaje de
programación y, después, la ejecute para recuperar los resultados. LINQ a SQL convierte todas las
operaciones necesarias en las operaciones SQL con las que está familiarizado.

En el ejemplo siguiente se recuperan los nombres de las compañías de los clientes de Londres y se
muestran en la ventana de la consola.
' Northwnd inherits from System.Data.Linq.DataContext.

MCT: Luis Dueñas Pag 260 de 388


Manual de LINQ

Dim nw As New Northwnd("c:\northwnd.mdf")


Dim companyNameQuery = From cust In nw.Customers Where cust.City =
"London" Select cust.CompanyName
For Each customer In companyNameQuery
Console.WriteLine(customer)
Next
Inserción
Para realizar una operación Insert de SQL, basta con que agregue objetos al modelo de objetos que ha
creado y llame a SubmitChanges en DataContext.

En el ejemplo siguiente se agrega un nuevo cliente, e información sobre el cliente, a la tabla Customers
utilizando el método InsertOnSubmit.
' Northwnd inherits from System.Data.Linq.DataContext.
Dim nw As New Northwnd("c:\northwnd.mdf")
Dim cust As New Customer With {.CompanyName="SomeCompany",.City="London",
.CustomerID = 98128,.PostalCode = 55555, .Phone = "555-555-5555"}
nw.Customers.InsertOnSubmit(cust)
' At this point, the new Customer object is added in the object model.
' In LINQ to SQL, the change is not sent to the database until
' SubmitChanges is called.
nw.SubmitChanges()
Actualización
Para realizar una operación Update en una entrada de base de datos, primero recupere el elemento y
modifíquelo directamente en el modelo de objetos. Después de haber modificado el objeto, llame a
SubmitChanges en DataContext para actualizar la base de datos.

En el ejemplo siguiente se recuperan todos los clientes que son de Londres. A continuación, el nombre de
la ciudad se cambia de "London" a "London - Metro". Finalmente, se llama a SubmitChanges para enviar
los cambios a la base de datos.
Dim nw As New Northwnd("c:\northwnd.mdf")
Dim cityNameQuery=From cust In nw.Customers Where
cust.City.Contains("London") Select cust
For Each customer In cityNameQuery
If customer.City = "London" Then
customer.City = "London - Metro"
End If
Next
nw.SubmitChanges()
Eliminar
Para realizar una operación Delete en un elemento, quite el elemento de la colección a la que pertenece
y, a continuación, llame al método SubmitChanges en DataContext para confirmar el cambio.

Nota:

LINQ a SQL no reconoce las operaciones de eliminación en cascada.

En el ejemplo siguiente, se recupera el cliente cuyo CustomerID es 98128 de la base de datos. A


continuación, después de confirmar que se recuperó la fila del cliente, se llama a DeleteOnSubmit para
quitar ese objeto de la colección. Finalmente, se llama a SubmitChanges para transmitir la eliminación a
la base de datos.
Dim nw As New Northwnd("c:\northwnd.mdf")

MCT: Luis Dueñas Pag 261 de 388


Manual de LINQ

Dim deleteIndivCust = From cust In nw.Customers Where cust.CustomerID =


98128 Select cust
If deleteIndivCust.Count > 0 Then
nw.Customers.DeleteOnSubmit(deleteIndivCust.First)
nw.SubmitChanges()
End If

8.3.1.2. Procedimientos Típicos para Usar LINQ a SQL


Para implementar una aplicación LINQ a SQL, debe seguir los pasos que se describen más adelante en
este tema. Observe que muchos pasos son opcionales. Es muy posible que pueda utilizar su modelo de
objetos en su estado predeterminado.

Para agilizar el proceso, utilice el Diseñador relacional de objetos para crear su modelo de objetos y
poder empezar a codificar sus consultas.

Crear el modelo de objetos


El primer paso es crear un modelo de objetos a partir de los metadatos de una base de datos relacional
existente. El modelo de objetos representa la base de datos según el lenguaje de programación del
desarrollador.

1. Seleccionar una herramienta para crear el modelo


Tres herramientas están disponibles para crear el modelo.

El Diseñador relacional de objetos

Este diseñador proporciona una interfaz de usuario completa para crear un modelo de objetos a
partir de una base de datos existente. Esta herramienta forma parte del IDE de Visual Studio y
es perfecta para bases de datos pequeñas o medianas.

Herramienta de generación de código SQLMetal

Esta herramienta de línea de comandos proporciona un conjunto de opciones ligeramente


diferentes de las del Diseñador relacional de objetos. Las bases de datos grandes se modelan
mejor con esta herramienta.

Editor de código

Puede escribir su propio código utilizando el editor de código de Visual Studio u otro editor. No
recomendamos este enfoque, que puede ser susceptible a errores, cuando se tiene una base de
datos existente y se puede utilizar el Diseñador relacional de objetos o la herramienta SQLMetal.
Sin embargo, el editor de código puede ser muy útil para perfeccionar o modificar el código ya
generado con otras herramientas.

2. Seleccionar el tipo de código que se desea generar


Un archivo de código fuente de C# o Visual Basic para la asignación basada en atributos.

Después, incluirá este archivo de código en su proyecto de Visual Studio.

Un archivo XML para la asignación externa.

Con este enfoque puede mantener los metadatos de la asignación fuera del código de aplicación.

MCT: Luis Dueñas Pag 262 de 388


Manual de LINQ

Nota:

El Diseñador relacional de objetos no admite la generación de archivos de asignación externos. Debe


utilizar la herramienta SQLMetal para implementar esta característica.

Un archivo DBML, que se puede modificar antes de generar el archivo de código definitivo.

Ésta es una característica avanzada.

3. Perfeccionar el archivo de código para reflejar las necesidades de una aplicación


Para este propósito, puede utilizar el Diseñador relacional de objetos o el editor de código.

Utilizar el modelo de objetos


La ilustración siguiente muestra la relación entre el programador y los datos en un escenario de dos
niveles.

Ahora que tiene un modelo de objetos, debe describir las solicitudes de información y manipular los
datos dentro de ese modelo. Debe pensar en términos de los objetos y las propiedades del modelo de
objetos, y no en términos de las filas y columnas de la base de datos. No tratará directamente con la
base de datos.

Al indicar a LINQ a SQL que ejecute una consulta que ha descrito o que llame a SubmitChanges() en los
datos que ha manipulado, LINQ a SQL se comunica con la base de datos en el lenguaje de la misma.

8.3.2. Guía de Programación LINQ a SQL


Esta sección contiene información sobre cómo crear y utilizar un modelo de objetos LINQ a SQL. Si utiliza
Visual Studio, también puede utilizar Diseñador relacional de objetos para realizar muchas de estas
mismas tareas.

8.3.2.1. Crear el Modelo de Objetos


Puede crear un modelo de objetos a partir de una base de datos existente y utilizar el modelo en su
estado predeterminado. También puede personalizar muchos aspectos del modelo y su comportamiento.

Si utiliza Visual Studio, puede usar el Diseñador relacional de objetos para crear el modelo de objetos.

8.3.2.1.1. Cómo: Generar el Modelo de Objetos en Visual


Basic o C#
En LINQ a SQL, un modelo de objetos en un lenguaje de programación se asigna a una base de datos
relacional. Hay dos herramientas disponibles para generar automáticamente un modelo de Visual Basic o
C# a partir de los metadatos de una base de datos existente.

MCT: Luis Dueñas Pag 263 de 388


Manual de LINQ

Si programa en Visual Studio, puede utilizar el Diseñador relacional de objetos para generar un
modelo de objetos. El Diseñador relacional de objetos proporciona una interfaz de usuario
completa para ayudarle a generar un modelo de objetos LINQ a SQL.

Herramienta de línea de comandos SQLMetal.

Nota:

Si no tiene una base de datos existente y desear crear una a partir de un modelo de objetos, puede
crear el modelo de objetos mediante el editor de código y CreateDatabase.

La documentación del Diseñador relacional de objetos proporciona ejemplos de cómo generar un modelo
de objetos de Visual Basic o de C# mediante el Diseñador relacional de objetos. La información siguiente
proporciona ejemplos de cómo utilizar la herramienta de línea de comandos SQLMetal.

Ejemplo
La línea de comandos de SQLMetal mostrada en el ejemplo siguiente genera código de Visual Basic como
el modelo de objetos basado en atributos de la base de datos de ejemplo Northwind. Se representan
también los procedimientos almacenados y las funciones.
sqlmetal /code:northwind.vb /language:vb "c:\northwnd.mdf" /sprocs
/functions
La línea de comandos de SQLMetal mostrada en el ejemplo siguiente genera código de C# como el
modelo de objetos basado en atributos de la base de datos de ejemplo Northwind. Se representan
también los procedimientos almacenados y las funciones, y los nombres de tabla se pluralizan
automáticamente.
sqlmetal /code:northwind.cs /language:csharp "c:\northwnd.mdf" /sprocs
/functions /pluralize

8.3.2.1.2. Cómo: Generar el Modelo de Objetos como un


Archivo Externo
Como alternativa a la asignación basada en atributos, puede generar su modelo de objetos como un
archivo XML externo mediante la herramienta de línea de comandos SQLMetal. Utilizando un archivo de
asignación XML externo, se reduce el desorden en su código. También puede cambiar el comportamiento
modificando el archivo externo sin volver a compilar los binarios de su aplicación.

Nota:

El Diseñador relacional de objetos no admite la generación de un archivo de asignación externo.

Ejemplo
El comando siguiente genera un archivo de asignación externo a partir de la base de datos de ejemplo
Northwind.
sqlmetal /server:myserver /database:northwind /map:externalfile.xml

8.3.2.1.3. Cómo: Generar Código Personalizado Mediante la


Modificación de un Archivo DBML
Puede generar código fuente de Visual Basic o C# a partir de un archivo de metadatos de lenguaje de
marcado de base de datos (.dbml). Este enfoque proporciona una oportunidad de personalizar el archivo

MCT: Luis Dueñas Pag 264 de 388


Manual de LINQ

.dbml predeterminado antes de generar el código de asignación de la aplicación. Ésta es una


característica avanzada.

Los pasos de este proceso son los siguientes.

1. Genere un archivo .dbml.

2. Utilice un editor para modificar el archivo .dbml. Tenga en cuenta que el archivo .dbml debe
validarse correctamente con el archivo de definición de esquema (.xsd) de los archivos .dbml de
LINQ a SQL.

3. Genere el código fuente de Visual Basic o de C#.

En los ejemplos siguientes se utiliza la herramienta de línea de comandos SQLMetal.

Ejemplo
El código siguiente genera un archivo .dbml a partir de la base de datos de ejemplo Northwind. Como
origen de los metadatos de la base de datos, puede utilizar el nombre de la base de datos o el nombre
del archivo .mdf.
sqlmetal /server:myserver /database:northwind /dbml:mymeta.dbml
sqlmetal /dbml:mymeta.dbml mydbfile.mdf
El código siguiente genera un archivo de código fuente de Visual Basic o C# a partir de un archivo .dbml.
sqlmetal /namespace:nwind /code:nwind.vb /language:vb DBMLFile.dbml
sqlmetal /namespace:nwind /code:nwind.cs /language:csharp DBMLFile.dbml

8.3.2.1.4. Cómo: Validar Archivos de Asignación Externa y


DBML
Los archivos de asignación externa y los archivos .dbml que se modifican se deben validar con sus
respectivas definiciones de esquema. En este tema se proporciona a los usuarios de Visual Studio los
pasos necesarios para implementar el proceso de validación.

Nota:

Es posible que su equipo muestre nombres o ubicaciones diferentes para algunos de los elementos de
la interfaz de usuario de Visual Studio incluidos en las instrucciones siguientes. La edición de Visual
Studio que se tenga y la configuración que se utilice determinan estos elementos.

Para validar un archivo .dbml o XML

1. En el menú Archivo de Visual Studio, elija Abrir y, a continuación, haga clic en Archivo.

2. En el cuadro de diálogo Abrir archivo, haga clic en el archivo de asignación .dbml o XML que
desea validar.

El archivo se abre en el Editor XML.

3. Haga clic con el botón secundario del mouse en la ventana y, a continuación, haga clic en
Propiedades.

4. En la ventana Propiedades, haga clic en el botón de puntos suspensivos (…) en la propiedad


Esquemas.

MCT: Luis Dueñas Pag 265 de 388


Manual de LINQ

Se abrirá el cuadro de diálogo Esquemas XML.

5. Observe cuál es la definición de esquema adecuada para lo que desea.

DbmlSchema.xsd es la definición de esquema para validar un archivo .dbml.

LinqToSqlMapping.xsd es la definición de esquema para validar un archivo XML de


asignación externa.

6. En la columna Uso de la fila de definición de esquema que desee, haga clic para abrir el cuadro
desplegable y, a continuación, haga clic en Usar este esquema.

El archivo de definición de esquema está asociado ahora con su archivo de asignación DBML o XML.

Asegúrese de que no hay otras definiciones de esquema seleccionadas.

7. En el menú Ver, haga clic en Lista de errores.

Determine si se han generado errores, advertencias o mensajes. Si no, el archivo XML es válido
respecto a la definición de esquema.

Método alternativo para proporcionar la definición de esquema


Si por alguna razón el archivo .xsd adecuado no aparece en el cuadro de diálogo Esquemas XML, puede
descargarlo de un tema de Ayuda. Los pasos siguientes le indicarán cómo guardar el archivo descargado
en el formato Unicode que requiere el Editor XML de Visual Studio.

Para copiar un archivo de definición de esquema de un tema de Ayuda

1. Busque el tema de Ayuda que contiene la definición de esquema tal como se ha descrito
anteriormente en este tema.

En el caso de los archivos .dbml, vea Generación de código en LINQ a SQL.

En el caso de los archivos de asignación externa, vea Referencia de asignación externa


(LINQ a SQL).

2. Haga clic en Copiar código en el Portapapeles para copiar el archivo de código en el


Portapapeles.

3. Inicie Bloc de notas para crear un nuevo archivo.

4. Pegue el código del Portapapeles en el archivo del Bloc de notas.

5. En el menú Archivo del Bloc de notas, haga clic en Guardar como.

6. En el cuadro Codificación, seleccione Unicode.

Nota importante:

Esta selección garantiza que el marcador de orden de bytes Unicode-16 (FFFE) se antepone al
archivo de texto.

7. En el cuadro Nombre de archivo, cree un nombre de archivo con extensión .xsd.

MCT: Luis Dueñas Pag 266 de 388


Manual de LINQ

8.3.2.1.5. Cómo: Convertir Entidades en Serializables


Puede hacer que las entidades sean serializables al generar el código. Las clases de entidad se decoran
con el atributo DataContractAttribute y las columnas con el atributo DataMemberAttribute.

Los programadores que utilizan Visual Studio pueden utilizar Diseñador relacional de objetos para este
propósito.

Si utiliza la herramienta de línea de comandos SQLMetal, utilice la opción /serialization con el


argumento unidirectional.
Ejemplo
Con las siguientes líneas de comandos de SQLMetal se generan archivos que tienen entidades
serializables.
sqlmetal /code:nwserializable.vb /language:vb "c:\northwnd.mdf" /sprocs
/functions /pluralize /serialization:unidirectional

sqlmetal /code:nwserializable.cs /language:csharp "c:\northwnd.mdf"


/sprocs /functions /pluralize /serialization:unidirectional

8.3.2.1.6. Cómo: Personalizar Clases de Entidad Mediante


el Editor de Código
Los programadores que utilizan Visual Studio pueden usar Diseñador relacional de objetos para crear o
personalizar las clases de entidad.

También puede utilizar el editor de código de Visual Studio para escribir su propio código de asignación o
para personalizar código que ya se ha generado.

8.3.2.1.6.1. Cómo: Especificar Nombres de Base de Datos


Utilice la propiedad Name en un atributo DatabaseAttribute para especificar el nombre de una base de
datos cuando la conexión no proporciona ninguno.

Para especificar el nombre de la base de datos

1. Agregue el atributo DatabaseAttribute a la declaración de clase para la base de datos.

2. Agregue la propiedad Name al atributo DatabaseAttribute.

3. Establezca el valor de la propiedad Name en el nombre que desea especificar.

8.3.2.1.6.2. Cómo: Representar Tablas como Clases


Utilice el atributo TableAttribute de LINQ a SQL para designar una clase como una clase de entidad
asociada a una tabla de base de datos.

Para asignar una clase a una tabla de base de datos

Agregue el atributo TableAttribute a la declaración de la clase.

MCT: Luis Dueñas Pag 267 de 388


Manual de LINQ

Ejemplo
El código siguiente establece la clase Customer como una clase de entidad que está asociada a la tabla
de base de datos Customers.
<Table(Name:="Customers")> _
Public Class Customer
' ...
End Class
No tiene que especificar la propiedad Name si se puede deducir el nombre. Si no especifica ningún
nombre, se supone que es el mismo que el de la propiedad o campo.

8.3.2.1.6.3. Cómo: Representar Columnas como Miembros


de Clase
Utilice el atributo ColumnAttribute de LINQ a SQL para asociar un campo o una propiedad a una columna
de base de datos.

Para asignar un campo o una propiedad a una columna de base de datos

Agregue el atributo ColumnAttribute a la propiedad o declaración de campo.

Ejemplo
El código siguiente asigna el campo CustomerID de la clase Customer a la columna CustomerID de la
tabla de base de datos Customers.
<Table(Name:="Customers")> _
Public Class Customer
<Column(Name:="CustomerID")> _
Public CustomerID As String
' ...
End Class
No tiene que especificar la propiedad Name si se puede deducir el nombre. Si no especifica ningún
nombre, se supone que es el mismo que el de la propiedad o campo.

8.3.2.1.6.4. Cómo: Representar Claves Principales


Utilice la propiedad IsPrimaryKey de LINQ a SQL en el atributo ColumnAttribute para designar que una
propiedad o campo representa la clave principal de una columna de base de datos.

Nota:

LINQ a SQL no admite las columnas calculadas como claves principales.

Para designar una propiedad o un campo como clave principal

1. Agregue la propiedad IsPrimaryKey al atributo ColumnAttribute.

2. Especifique el valor como true.

8.3.2.1.6.5. Cómo: Asignar Relaciones de Base de Datos

MCT: Luis Dueñas Pag 268 de 388


Manual de LINQ

Puede codificar como referencias de propiedad en la clase de entidad cualquier relación de datos que
vaya a ser siempre la misma. En la base de datos de ejemplo Northwind, por ejemplo, dado que los
clientes normalmente realizan pedidos, hay siempre una relación en el modelo entre los clientes y sus
pedidos.

LINQ a SQL define un atributo AssociationAttribute para ayudar a representar tales relaciones. Este
atributo se utiliza junto con los tipos EntitySet<(Of <(TEntity>)>) y EntityRef<(Of <(TEntity>)>) para
representar lo que sería una relación de clave externa en una base de datos.

La mayoría de las relaciones son de uno a varios, como en el ejemplo que se incluye más adelante en
este tema. También puede representar relaciones uno a uno y varios a varios de la manera siguiente:

Uno a uno: para representar este tipo de relación incluya EntitySet<(Of <(TEntity>)>) en
ambos lados.

Por ejemplo, considere una relación Customer-SecurityCode, creada de forma que el código de
seguridad del cliente no se encuentre en la tabla Customer y sólo esté accesible para las
personas autorizadas.

Varios a varios: en las relaciones varios a varios, la clave principal de la tabla de vínculos
(también conocida como tabla de unión ) suele estar formada por un conjunto de claves
externas de las otras dos tablas.

Por ejemplo, considere una relación varios a varios Employee-Project formada mediante la tabla
de vínculos EmployeeProject. LINQ a SQL requiere que tal relación se modele utilizando tres
clases: Employee, Project y EmployeeProject. En este caso, al cambiar la relación entre
Employee y Project puede parecer necesario actualizar la clave principal de EmployeeProject. Sin
embargo, esta situación se modela mejor eliminando un EmployeeProject existente y creando un
nuevo EmployeeProject.

Nota:

Las relaciones en las bases de datos relacionales se modelan normalmente como valores de
clave externa que hacen referencia a claves principales de otras tablas. Para navegar entre
ellas, las dos tablas se asocian explícitamente, utilizando una operación de combinación
relacional.
Por otra parte, las referencias mutuas entre los objetos de LINQ a SQL se realizan mediante
referencias de propiedad o colecciones de referencias por las que se navega mediante la
notación de punto.

Ejemplo
En el siguiente ejemplo de relación uno a varios, la clase Customer tiene una propiedad que declara la
relación entre los clientes y sus pedidos. La propiedad Orders es de tipo EntitySet<(Of <(TEntity>)>).
Este tipo indica que esta relación es de uno a varios (entre un cliente y varios pedidos). La propiedad
OtherKey se utiliza para describir cómo se logra esta asociación, a saber, especificando el nombre de la
propiedad de la clase relacionada que se va a comparar con ésta. En este ejemplo, se compara la
propiedad CustomerID, de igual forma que una combinación de base de datos compararía ese valor de
columna.

Nota:

Si utiliza Visual Studio, puede utilizar el Diseñador relacional de objetos para crear una asociación
entre clases.

MCT: Luis Dueñas Pag 269 de 388


Manual de LINQ

<Table(Name:="Customers")> _
Public Class Customer
<Column(IsPrimaryKey:=True)> _
Public CustomerID As String
' ...
Private _Orders As EntitySet(Of Order)
<Association(Storage:="_Orders", OtherKey:="CustomerID")> _
Public Property Orders() As EntitySet(Of Order)
Get
Return Me._Orders
End Get
Set(ByVal value As EntitySet(Of Order))
Me._Orders.Assign(value)
End Set
End Property
End Class
También puede invertir la situación. En lugar de utilizar la clase Customer para describir la asociación
entre los clientes y los pedidos, puede utilizar la clase Order. La clase Order utiliza el tipo EntityRef<(Of
<(TEntity>)>) para describir paso a paso la relación hasta el cliente, como se observa en el ejemplo de
código siguiente.

Nota:

La clase EntityRef<(Of <(TEntity>)>) admite la carga diferida.

<Table(Name:="Orders")> _
Public Class Order
<Column(IsPrimaryKey:=True)> _
Public OrderID As Integer
<Column()> _
Public CustomerID As String
Private _Customer As EntityRef(Of Customer)
<Association(Storage:="Customer", ThisKey:="CustomerID")> _
Public Property Customer() As Customer
Get
Return Me._Customer.Entity
End Get
Set(ByVal value As Customer)
Me._Customer.Entity = value
End Set
End Property
End Class

8.3.2.1.6.6. Cómo: Representar Columnas como Generadas


por la Base de Datos
Utilice la propiedad IsDbGenerated de LINQ a SQL en el atributo ColumnAttribute para designar que un
campo o propiedad representa una columna generada por base de datos.

MCT: Luis Dueñas Pag 270 de 388


Manual de LINQ

Para designar que un campo o propiedad representa una columna generada por
base de datos

1. Agregue la propiedad IsDbGenerated al atributo ColumnAttribute.

2. Establezca el valor de la propiedad en true.

8.3.2.1.6.7. Cómo: Representar Columnas como Columnas


de Marca de Tiempo o Versión
Utilice la propiedad IsVersion de LINQ a SQL en el atributo ColumnAttribute para designar que un campo
o propiedad representa una columna de base de datos que contiene marcas de tiempo o números de
versión de base de datos.

Para designar que un campo o propiedad representa una columna de marca de


tiempo o versión

1. Agregue la propiedad IsVersion al atributo ColumnAttribute.

2. Establezca el valor de la propiedad IsVersion en true.

8.3.2.1.6.8. Cómo: Especificar Tipos de Datos de una Base


de Datos
Utilice la propiedad DbType de LINQ a SQL en un atributo ColumnAttribute para especificar el texto
exacto que define la columna en una declaración de tabla de T-SQL.

Debe especificar la propiedad DbType sólo si piensa utilizar CreateDatabase para crear una instancia de
la base de datos.

Para especificar texto para definir un tipo de datos en una tabla T-SQL

1. Agregue la propiedad DbType al atributo ColumnAttribute.

2. Establezca el valor de la propiedad DbType en el texto exacto que T-SQL utiliza.

8.3.2.1.6.9. Cómo: Representar Columnas Calculadas


Utilice la propiedad Expression de LINQ a SQL en un atributo ColumnAttribute para representar una
columna cuyo contenido es el resultado de un cálculo.

Nota:

LINQ a SQL no admite las columnas calculadas como claves principales.

Para representar una columna calculada

1. Agregue la propiedad Expression al atributo ColumnAttribute.

MCT: Luis Dueñas Pag 271 de 388


Manual de LINQ

2. Asigne una representación de cadena de la fórmula a la propiedad Expression.

8.3.2.1.6.10. Cómo: Especificar Campos de


Almacenamiento Privado
Utilice la propiedad Storage de LINQ a SQL en el atributo DataAttribute para designar el nombre de un
campo de almacenamiento subyacente.

Para especificar el nombre de un campo de almacenamiento subyacente

1. Agregue la propiedad Storage al atributo ColumnAttribute.

2. Asigne el nombre del campo como valor de la propiedad Storage.

8.3.2.1.6.11. Cómo: Representar Columnas que Permitan


Valores Null
Utilice la propiedad CanBeNull de LINQ a SQL en el atributo ColumnAttribute para especificar que la
columna de base de datos asociada puede contener valores nulos.

Para designar que una columna permite valores nulos

1. Agregue la propiedad CanBeNull al atributo ColumnAttribute.

2. Establezca el valor de la propiedad CanBeNull en true.

8.3.2.1.6.12. Cómo: Asignar Jerarquías de Herencia


Para implementar la asignación de herencia en LINQ, debe especificar los atributos y las propiedades de
atributo en la clase raíz de la jerarquía de herencia, tal como se describe en los pasos siguientes. Los
programadores de Visual Studio pueden utilizar el Diseñador relacional de objetos para asignar
jerarquías de herencia.

Nota:

No se requieren atributos o propiedades especiales en las subclases. Observe sobre todo que las
subclases no tienen el atributo TableAttribute.

Para asignar una jerarquía de herencia

1. Agregue el atributo TableAttribute a la clase raíz.

2. También en la clase raíz, agregue un atributo InheritanceMappingAttribute para cada clase de la


estructura jerárquica.

3. Para cada atributo InheritanceMappingAttribute, defina una propiedad Code.

Esta propiedad contiene un valor que aparece en la tabla de base de datos, en la columna
IsDiscriminator, para indicar a qué clase o subclase pertenece esta fila de datos.

MCT: Luis Dueñas Pag 272 de 388


Manual de LINQ

4. Para cada atributo InheritanceMappingAttribute, agregue también una propiedad Type.

Esta propiedad contiene un valor que especifica a qué clase o subclases representa el valor de
clave.

5. En uno de los atributos InheritanceMappingAttribute, agregue una propiedad IsDefault.

Esta propiedad sirve para designar una asignación de reserva cuando el valor de discriminador de
la tabla de base de datos no coincide con ningún valor Code de las asignaciones de herencia.

6. Agregue una propiedad IsDiscriminator para un atributo ColumnAttribute.

Esta propiedad indica que ésta es la columna que contiene el valor Code.

Ejemplo

Nota:

Si utiliza Visual Studio, puede usar el Diseñador relacional de objetos para configurar la herencia.

En el ejemplo de código siguiente, Vehicle se define como la clase raíz y se han implementado los pasos
anteriores para describir la jerarquía para LINQ. En la ilustración siguiente se muestran las relaciones.

<Table()> _
<InheritanceMapping(Code:="C", Type:=GetType(Car))> _
<InheritanceMapping(Code:="T", Type:=GetType(Truck))> _
<InheritanceMapping(Code:="V", Type:=GetType(Vehicle),IsDefault:=True)> _
Public Class Vehicle
<Column(IsDiscriminator:=True)> _
Private DiscKey As String
<Column(IsPrimaryKey:=True)> _
Private VIN As String
<Column()> _
Private MfgPlant As String
End Class
Public Class Car
Inherits Vehicle
<Column()> _
Private TrimCode As Integer
<Column()> _

MCT: Luis Dueñas Pag 273 de 388


Manual de LINQ

Private ModelName As String


End Class
Public Class Truck
Inherits Vehicle
<Column()> _
Private Tonnage As Integer
<Column()> _
Private Axles As Integer
End Class

8.3.2.1.6.13. Cómo: Especificar la Comprobación de


Conflictos de Simultaneidad
Puede especificar en qué columnas de la base de datos se deben comprobar los conflictos de
simultaneidad al llamar a SubmitChanges.

Ejemplo
El código siguiente especifica que el miembro HomePage nunca se debería comprobar durante las
búsquedas de actualizaciones.
<Column(Storage:="_HomePage", DbType:="NText",
UpdateCheck:=UpdateCheck.Never)> _
Public Property HomePage() As String
Get
Return Me._HomePage
End Get
Set(ByVal value As String)
If ((Me._HomePage <> value) = false) Then
Me.OnHomePageChanging(value)
Me.SendPropertyChanging
Me._HomePage = value
Me.SendPropertyChanged("HomePage")
Me.OnHomePageChanged
End If
End Set
End Property

8.3.2.2. Comunicar con la Base de Datos


Los temas de esta sección describen algunos aspectos básicos de cómo establecer y mantener la
comunicación con la base de datos.

8.3.2.2.1. Cómo: Conectarse a una Base de Datos


DataContext es la canalización principal mediante la cual se conecta a una base de datos, recupera los
objetos de ella y le vuelve a enviar los cambios. DataContext se utiliza igual que si se tratase de un
objeto SqlConnection de ADO.NET. De hecho, DataContext se inicializa con la conexión o cadena de
conexión que proporcione.

El propósito de DataContext es convertir sus solicitudes de objetos en consultas SQL que se van a
ejecutar en la base de datos y, a continuación, ensamblar los objetos a partir de los resultados.

MCT: Luis Dueñas Pag 274 de 388


Manual de LINQ

DataContext habilita Language-Integrated Query (LINQ) al implementar el mismo modelo de operador


que los operadores de consulta estándar, como Where y Select.

Nota de seguridad:

Es fundamental mantener una conexión segura.

Ejemplo
En el ejemplo siguiente, se utiliza DataContext para establecer una conexión con la base de datos de
ejemplo Northwind y recuperar las filas de clientes cuya ciudad es Londres (London).
' DataContext takes a connection string.
Dim db As New DataContext("…\Northwnd.mdf")
' Get a typed table to run queries.
Dim Customers As Table(Of Customer) = db.GetTable(Of Customer)()
' Query for customer from London.
Dim Query = From cust In Customers Where cust.City = "London" Select cust
For Each cust In Query
Console.WriteLine("id=" & cust.CustomerID & ", City=" & cust.City)
Next
Cada tabla de base de datos se representa como una colección Table disponible a través del método
GetTable, utilizando la clase de entidad para su identificación.

El procedimiento recomendado es declarar un DataContext con establecimiento inflexible de tipos en


lugar de confiar en la clase DataContext básica y el método GetTable. Un DataContext con
establecimiento inflexible de tipos declara todas las colecciones Table como miembros del contexto, como
en el ejemplo siguiente.
Partial Public Class Northwind
Inherits DataContext
Public Customers As Table(Of Customer)
Public Orders As Table(Of Order)
Public Sub New(ByVal connection As String)
MyBase.New(connection)
End Sub
End Class
Después, puede expresar la consulta de clientes de Londres de manera más sencilla:
Dim db As New Northwind("...\Northwnd.mdf")
Dim query=From cust In db.Customers Where cust.City="London" Select cust
For Each cust In query
Console.WriteLine("id=" & cust.CustomerID & ", City=" & cust.City)
Next

8.3.2.2.2. Cómo: Ejecutar Directamente Comandos SQL


En el supuesto de que tuviese una conexión DataContext, podría utilizar ExecuteCommand para ejecutar
comandos SQL que no devuelven objetos.

Ejemplo
El ejemplo siguiente hace que SQL Server aumente el precio unitario (UnitPrice) en 1,00.
db.ExecuteCommand("UPDATE Products SET UnitPrice = UnitPrice + 1.00")

MCT: Luis Dueñas Pag 275 de 388


Manual de LINQ

8.3.2.2.3. Cómo: Volver a Usar una Conexión entre un


Comando ADO.NET y DataContext
Dado que LINQ a SQL es parte de la familia de tecnologías ADO.NET y se basa en los servicios
proporcionados por ADO.NET, se puede reutilizar una conexión entre un comando ADO.NET y
DataContext.

Ejemplo
En el ejemplo siguiente se muestra cómo reutilizar la misma conexión entre un comando ADO.NET y
DataContext.
Dim conString = "Data
Source=.\SQLEXPRESS;AttachDbFilename=c:\northwind.mdf; Integrated
Security=True;Connect Timeout=30;User Instance=True"
Dim northwindCon = New SqlConnection(conString)
northwindCon.Open()
Dim db = New Northwind("...")
Dim northwindTransaction = northwindCon.BeginTransaction()
Try
Dim cmd = New SqlCommand("UPDATE Products SET QuantityPerUnit =
'single item' " & "WHERE ProductID = 3")
cmd.Connection = northwindCon
cmd.Transaction = northwindTransaction
cmd.ExecuteNonQuery()
db.Transaction = northwindTransaction
Dim prod1 = (From prod In db.Products Where prod.ProductID = 4).First
Dim prod2 = (From prod In db.Products Where prod.ProductID = 5).First
prod1.UnitsInStock -= 3
prod2.UnitsInStock -= 5
db.SubmitChanges()
northwindTransaction.Commit()
Catch e As Exception
Console.WriteLine(e.Message)
Console.WriteLine("Error submitting changes all changes rolled back")
End Try
northwindCon.Close()

8.3.2.3. Realizar Consultas en la Base de Datos


Este grupo de temas describe cómo desarrollar y ejecutar consultas en proyectos LINQ a SQL.

8.3.2.3.1. Cómo: Consultar Información


Las consultas en LINQ a SQL utilizan la misma sintaxis que las consultas en LINQ. La única diferencia es
que los objetos a los que se hace referencia en las consultas LINQ a SQL se asignan a elementos de una
base de datos.

LINQ a SQL convierte las consultas que se escriben en consultas SQL equivalentes y las envía al servidor
para su procesamiento.

MCT: Luis Dueñas Pag 276 de 388


Manual de LINQ

Algunas características de las consultas LINQ podrían requerir una atención especial en las aplicaciones
LINQ a SQL.

Ejemplo
La consulta siguiente solicita una lista de clientes de Londres. En este ejemplo, Customers es una tabla
de la base de datos de ejemplo Northwind.
Dim db As New Northwnd("c:\northwnd.mdf")
' Query for customers in London.
Dim custQuery=From cus In db.Customers Where cus.City="London" Select cus

8.3.2.3.2. Cómo: Recuperar Información como de Sólo


Lectura
Cuando no se tiene pensado cambiar los datos, puede aumentar el rendimiento de las consultas
buscando resultados de sólo lectura.

El procesamiento de sólo lectura se implementa estableciendo ObjectTrackingEnabled en false.

Nota:

Cuando ObjectTrackingEnabled se establece en false, DeferredLoadingEnabled se establece


implícitamente en false.

Ejemplo
El código siguiente recupera una colección de sólo lectura de fechas de contratación de empleados.
Dim db As New Northwnd("c:\northwnd.mdf")
db.ObjectTrackingEnabled = False
Dim hireQuery = From emp In db.Employees Select emp Order By emp.HireDate
For Each empObj As Employee In hireQuery
Console.WriteLine("EmpID = {0}, Date Hired = {1}", _
empObj.EmployeeID, empObj.HireDate)
Next

8.3.2.3.3. Cómo: Controlar la Cantidad de Datos


Relacionados que se Recuperan
Utilice el método LoadWith para especificar qué datos relacionados con el destino principal deben
recuperarse al mismo tiempo. Por ejemplo, si sabe que va a necesitar información sobre los pedidos de
los clientes, puede utilizar LoadWith para asegurarse de que la información de los pedidos se va a
recuperar al mismo tiempo que la información de los clientes. Con este enfoque, sólo se requiere un viaje
a la base de datos para ambos conjuntos de información.

Nota:

Puede recuperar datos relacionados con el destino principal de la consulta recuperando un producto
cruzado como una gran proyección; por ejemplo, puede recuperar los pedidos cuando el destino de la
consulta son los clientes. Sin embargo, este enfoque a menudo tiene desventajas. Por ejemplo, los
resultados son simples proyecciones, no entidades que se puedan cambiar y conservar mediante
LINQ a SQL. Además, podría recuperar una gran cantidad de datos que no necesita.

Ejemplo

MCT: Luis Dueñas Pag 277 de 388


Manual de LINQ

En el siguiente ejemplo, se recuperan todos los Orders de todos los Customers de Londres cuando se
ejecuta la consulta. En consecuencia, el acceso posterior a la propiedad Orders de un objeto Customer no
desencadena una nueva consulta de base de datos.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim dlo As DataLoadOptions = New DataLoadOptions()
dlo.LoadWith(Of Customer)(Function(c As Customer) c.Orders)
db.LoadOptions = dlo
Dim londonCustomers = From cust In db.Customers Where cust.City =
"London" Select cust
For Each custObj In londonCustomers
Console.WriteLine(custObj.CustomerID)
Next

8.3.2.3.4. Cómo: Filtrar Datos Relacionados


Utilice el método AssociateWith para especificar subconsultas para limitar la cantidad de los datos
recuperados.

Ejemplo
En el ejemplo siguiente, el método AssociateWith limita los Orders recuperados a los que no se han
enviado hoy. Sin este enfoque, se habrían recuperado todos los Orders aunque sólo se requiere un
subconjunto.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim dlo As DataLoadOptions = New DataLoadOptions()
dlo.AssociateWith(Of Customer)(Function(c As Customer) _
c.Orders.Where(Function(p) p.ShippedDate <> DateTime.Today))
db.LoadOptions = dlo
Dim custOrderQuery = From cust In db.Customers Where cust.City = "London"
Select cust
For Each custObj In custOrderQuery
Console.WriteLine(custObj.CustomerID)
For Each ord In custObj.Orders
Console.WriteLine("{0}{1}", vbTab, ord.OrderDate)
Next
Next

8.3.2.3.5. Cómo: Desactivar la Carga Aplazada


Puede desactivar la carga diferida al establecer DeferredLoadingEnabled en false.

Nota:

La carga diferida se desactiva al desactivar el seguimiento de los objetos.

Ejemplo
En el ejemplo siguiente se muestra cómo desactivar la carga diferida al establecer
DeferredLoadingEnabled en false.
Dim db As New Northwnd("c:\northwnd.mdf")
db.DeferredLoadingEnabled = False
Dim ds As New DataLoadOptions()
ds.LoadWith(Function(c As Customer) c.Orders)

MCT: Luis Dueñas Pag 278 de 388


Manual de LINQ

ds.LoadWith(Of Order)(Function(o) o.OrderDetails)


db.LoadOptions = ds
Dim custQuery=From cus In db.Customers Where cus.City="London" Select cus
For Each custObj In custQuery
Console.WriteLine("Customer ID: {0}", custObj.CustomerID)
For Each ord In custObj.Orders
Console.WriteLine(vbTab & "Order ID: {0}", ord.OrderID)
For Each detail In ord.OrderDetails
Console.WriteLine(vbTab & vbTab & "Product ID: {0}", _
detail.ProductID)
Next
Next
Next

8.3.2.3.6. Cómo: Ejecutar Directamente Consultas SQL


LINQ a SQL convierte las consultas que se escriben en consultas SQL parametrizadas (en formato de
texto) y las envía al servidor SQL Server para su procesamiento.

SQL no puede ejecutar todos los métodos que podrían estar localmente disponibles para una aplicación.
LINQ a SQL intenta convertir estos métodos locales en operaciones y funciones equivalentes que estén
disponibles en el entorno de SQL. La mayoría de los métodos y operadores de los tipos integrados de
.NET Framework tienen comandos SQL que son equivalentes directos. Algunos se pueden generar a
partir de las funciones que están disponibles. Aquéllos que no se pueden generar, inician excepciones en
tiempo de ejecución.

Cuando una consulta LINQ a SQL no es suficiente para una tarea especializada, puede utilizar el método
ExecuteQuery para ejecutar una consulta SQL y después convertir el resultado de la consulta
directamente en objetos.

Ejemplo
En el ejemplo siguiente, supongamos que los datos de la clase Customer ocupan dos tablas (customer1 y
customer2). La consulta devuelve una secuencia de objetos Customer.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim results As IEnumerable(Of Customer) = db.ExecuteQuery(Of Customer) _
("SELECT c1.custID as CustomerID,c2.custName as ContactName" & _
"FROM customer1 AS c1, customer2 as c2 WHERE c1.custid = c2.custid")
Siempre y cuando los nombres de columna de los resultados tabulares coincidan con las propiedades de
columna de la clase de entidad, LINQ a SQL crea objetos a partir de cualquier consulta SQL.

El método ExecuteQuery también permite el uso de parámetros. Utilice código como el siguiente para
ejecutar una consulta parametrizada.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim results As IEnumerable(Of Customer) = db.ExecuteQuery(Of
Customer)("SELECT contactname FROM customers WHERE city = {0}, 'London'")
Los parámetros se expresan en el texto de la consulta con la misma notación con llaves que
Console.WriteLine() y String.Format(). De hecho, se llama a String.Format() en la cadena de consulta
proporcionada, sustituyendo los parámetros entre llaves por nombres de parámetro generados, como
@p0, @p1 …, @p(n).

MCT: Luis Dueñas Pag 279 de 388


Manual de LINQ

8.3.2.3.7. Cómo: Almacenar y Volver a Usar Consultas


Cuando una aplicación ejecuta muchas veces consultas que tienen una estructura similar, a menudo se
mejora el rendimiento si se compila la consulta una vez y se ejecuta varias veces con parámetros
diferentes. Por ejemplo, una aplicación podría tener que recuperar todos los clientes que están en una
ciudad determinada, que es especificada por el usuario en un formulario en tiempo de ejecución. LINQ a
SQL admite el uso de consultas compiladas para este propósito.

Nota:

Este modelo de utilización representa el uso más común de las consultas compiladas, pero también
son posibles otros enfoques. Por ejemplo, las consultas compiladas se pueden almacenar como
miembros estáticos en una clase parcial que extiende el código generado por el diseñador.

Ejemplo
En muchos escenarios podrían reutilizarse las consultas entre límites de subprocesos. En tales casos,
almacenar las consultas compiladas en variables estáticas es particularmente eficaz. En el ejemplo de
código siguiente se da por supuesto que hay una clase Queries diseñada para almacenar las consultas
compiladas y que hay una clase Northwind que representa un objeto DataContext con establecimiento
inflexible de tipos.
Class Queries
Public Shared CustomersByCity As Func(Of Northwnd, String,
IQueryable(Of Customer)) = CompiledQuery.Compile(Function(db As
Northwnd, city As String) From c In db.Customers Where c.City = city
Select c)
Public Shared CustomersById As Func(Of Northwnd,String,
IQueryable(Of Customer))=CompiledQuery.Compile(Function(db As
Northwnd,id As String) db.Customers.Where(Function(c) c.CustomerID=id))
End Class
' The following example invokes such a compiled query in the main program
Public Function GetCustomersByCity(ByVal city As String) As _
IEnumerable(Of Customer)
Dim myDb = GetNorthwind()
Return Queries.CustomersByCity(myDb, city)
End Function
En estos momentos, no se pueden almacenar (en variables estáticas) las consultas que devuelven un
tipo anónimo porque el tipo no tiene un nombre que se pueda proporcionar como argumento genérico.
En el ejemplo siguiente se muestra cómo se puede solucionar el problema creando un tipo que pueda
representar el resultado y, a continuación, cómo se utiliza como argumento genérico.
Class SimpleCustomer
Private _ContactName As String
Public Property ContactName() As String
Get
Return _ContactName
End Get
Set(ByVal value As String)
_ContactName = value
End Set
End Property
End Class
Class Queries2

MCT: Luis Dueñas Pag 280 de 388


Manual de LINQ

Public Shared CustomersByCity As Func(Of Northwnd, String,


IEnumerable(Of SimpleCustomer)) = CompiledQuery.Compile(Of Northwnd,
String, IEnumerable(Of SimpleCustomer))(Function(db As Northwnd, city
As String) From c In db.Customers Where c.City = city Select New
SimpleCustomer With {.ContactName = c.ContactName})
End Class

8.3.2.3.8. Cómo: Administrar Claves Compuestas en


Consultas
Algunos operadores sólo pueden aceptar un argumento. Si su argumento debe incluir más de una
columna de la base de datos, debe crear un tipo anónimo para representar la combinación.

Ejemplo
En el ejemplo siguiente se muestra una consulta que invoca al operador GroupBy, que sólo acepta un
argumento key.
Dim query = From cust In db.Customers Group cust.ContactName By Key = New
With {cust.City, cust.Region} Into Group
For Each grp In query
Console.WriteLine("Location Key: {0}", grp.Key)
For Each listing In grp.Group
Console.WriteLine(vbTab & "0}", listing)
Next
Next
La misma situación se da con las combinaciones, como en el ejemplo siguiente:
Dim query = From ord In db.Orders, prod In db.Products Join det In
db.OrderDetails On New With {ord.OrderID, prod.ProductID} Equals New With
{det.OrderID,det.ProductID} Select ord.OrderID,prod.ProductID,
det.UnitPrice

8.3.2.3.9. Cómo: Recuperar Muchos Objetos de Una Vez


Puede recuperar muchos objetos en una consulta mediante LoadWith.

Ejemplo
El código siguiente utiliza el método LoadWith para recuperar los objetos Customer y Order.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim ds As DataLoadOptions = New DataLoadOptions()
ds.LoadWith(Of Customer)(Function(c) c.Orders)
ds.LoadWith(Of Order)(Function(o) o.OrderDetails)
db.LoadOptions = ds
Dim custQuery = From cust In db.Customers() Where cust.City = "London"
Select cust
For Each custObj In custQuery
Console.WriteLine("Customer ID: " & custObj.CustomerID)
For Each ord In custObj.Orders
Console.WriteLine(vbTab & "Order ID: " & ord.OrderID)
For Each detail In ord.OrderDetails
Console.WriteLine(vbTab & vbTab & _

MCT: Luis Dueñas Pag 281 de 388


Manual de LINQ

"Product ID: {0}", detail.ProductID)


Next
Next
Next

8.3.2.3.10. Cómo: Filtrar en el Nivel de Contexto de Datos


Puede filtrar EntitySets en el nivel de DataContext. Tales filtros se aplican a todas las consultas
realizadas con esa instancia de DataContext.

Ejemplo
En el ejemplo siguiente, se usa DataLoadOptions..::.AssociateWith(LambdaExpression) para filtrar los
pedidos de clientes previamente cargados por ShippedDate.
Dim db As New Northwnd("c:\northwnd.mdf")
' Preload Orders for Customer.
' One directive per relationship to be preloaded.
Dim ds As DataLoadOptions = New DataLoadOptions()
ds.LoadWith(Of Customer)(Function(cust) cust.Orders)
ds.AssociateWith(Of Customer)(Function(cust) From ord In cust.Orders
Where ord.ShippedDate <> DateTime.Today)
db.LoadOptions = ds
Dim custQuery = From cust In db.Customers Where cust.City = "London"
Select cust
For Each custObj In custQuery
Console.WriteLine("Customer ID: " & custObj.CustomerID)
For Each ord In custObj.Orders
Console.WriteLine(vbTab & "Order ID: " & ord.OrderID)
For Each detail In ord.OrderDetails
Console.WriteLine(vbTab & vbTab & _
"Product ID: " & detail.ProductID)
Next
Next
Next

8.3.2.3.11. Ejemplos de Consultas


En esta sección se proporcionan ejemplos de consultas LINQ a SQL típicas en Visual Basic y C#. Los
programadores que utilicen Visual Studio encontrarán muchos más ejemplos en la solución de ejemplo
que está disponible en la sección Ejemplos.

Nota importante:

db se utiliza a menudo en los ejemplos de código de la documentación de LINQ a SQL. Se supone


que db es una instancia de una clase Northwind, que hereda de DataContext.

8.3.2.3.11.1. Consultas de Funciones Agregadas


LINQ a SQL admite los operadores de agregado Average, Count, Max, Min y Sum. Tener en cuenta las
características siguientes de los operadores de agregado en LINQ a SQL:

Las consultas de funciones agregadas se ejecutan inmediatamente.

MCT: Luis Dueñas Pag 282 de 388


Manual de LINQ

Las consultas de funciones agregadas normalmente devuelven un número en lugar de una


colección.

No se puede llamar a funciones de agregado en tipos anónimos.

Los ejemplos de los temas siguientes se derivan de la base de datos de ejemplo Northwind.

8.3.2.3.11.1.1. Cómo: Devolver el Promedio de una


Secuencia Numérica
El operador Average calcula el promedio de una secuencia de valores numéricos.

Nota:

La conversión de Average de valores enteros en LINQ a SQL se calcula como un entero, no como
double.

Ejemplo
En el ejemplo siguiente se devuelve el promedio de los valores Freight de la tabla Orders.

Los resultados de la base de datos de ejemplo Northwind serían 78.2442.


Dim averageFreight = Aggregate ord In db.Orders Into Average(ord.Freight)
Console.WriteLine(averageFreight)
En el ejemplo siguiente se devuelve el precio unitario de todos los Products de la tabla Products.

Los resultados de la base de datos de ejemplo Northwind serían 28.8663.


Dim averageUnitPrice = Aggregate prod In db.Products Into
Average(prod.UnitPrice)
Console.WriteLine(averageUnitPrice)
En el ejemplo siguiente se utiliza al operador Average para buscar Products cuyo precio unitario es más
alto que el precio unitario promedio de la categoría a la que pertenece. A continuación, el ejemplo
muestra los resultados en grupos.

Observe que este ejemplo requiere el uso de la palabra clave var en C#, porque el tipo de valor devuelto
es anónimo.
Dim priceQuery = From prod In db.Products() Group prod By prod.CategoryID
Into grouping = Group Select CategoryID, ExpensiveProducts = (From prod2
In grouping Where prod2.UnitPrice > grouping.Average(Function(prod3)
prod3.UnitPrice) Select prod2)
For Each grp In priceQuery
Console.WriteLine(grp.CategoryID)
For Each listing In grp.ExpensiveProducts
Console.WriteLine(listing.ProductName)
Next
Next

8.3.2.3.11.1.2. Cómo: Contar el Número de Elementos de


una Secuencia
Utilice al operador Count para contar el número de elementos de una secuencia.

MCT: Luis Dueñas Pag 283 de 388


Manual de LINQ

Al ejecutar esta consulta en la base de datos de ejemplo Northwind, se genera un resultado de 91.

Ejemplo
En el ejemplo siguiente se cuenta el número de Customers en la base de datos.
Dim customerCount = db.Customers.Count()
Console.WriteLine(customerCount)
En el ejemplo siguiente se cuenta el número de productos de la base de datos que no han dejado de
fabricarse.

Al ejecutar este ejemplo en la base de datos de ejemplo Northwind, se genera un resultado de 69.
Dim notDiscontinuedCount = Aggregate prod In db.Products Into Count(Not
prod.Discontinued)
Console.WriteLine(notDiscontinuedCount)

8.3.2.3.11.1.3. Cómo: Buscar el Valor Máximo en una


Secuencia Numérica
Utilice el operador Max para buscar el valor máximo de una secuencia de valores numéricos.

Ejemplo
En el ejemplo siguiente se busca la fecha de la última contratación de cualquier empleado.

Si ejecuta esta consulta en la base de datos de ejemplo Northwind, el resultado es: 11/15/1994
12:00:00 AM.
Dim latestHireDate = Aggregate emp In db.Employees Into Max(emp.HireDate)
Console.WriteLine(latestHireDate)
En el ejemplo siguiente se busca el número máximo de unidades en existencias para cualquier producto.

Si ejecuta este ejemplo en la base de datos de ejemplo Northwind, el resultado es: 125.
Dim maxUnitsInStock=Aggregate prod In db.Products Into Max(prod.UnitsInS)
Console.WriteLine(maxUnitsInStock)
En el ejemplo siguiente se utiliza Max para encontrar los Products que tienen el precio unitario más alto
en cada categoría. En este caso, los resultados se muestran por categoría.
Dim maxQuery = From prod In db.Products() Group prod By prod.CategoryID
Into grouping = Group Select CategoryID, MostExpensiveProducts = (From
prod2 In grouping Where prod2.UnitPrice = grouping.Max(Function(prod3)
prod3.UnitPrice))
For Each grp In maxQuery
Console.WriteLine(grp.CategoryID)
For Each listing In grp.MostExpensiveProducts
Console.WriteLine(listing.ProductName)
Next
Next

8.3.2.3.11.1.4. Cómo: Buscar el Valor Mínimo en una


Secuencia Numérica
Utilice al operador Min para devolver el valor mínimo de una secuencia de valores numéricos.

MCT: Luis Dueñas Pag 284 de 388


Manual de LINQ

Ejemplo
En el ejemplo siguiente se busca el precio unitario más bajo de cualquier producto.

Si ejecuta esta consulta en la base de datos de ejemplo Northwind, el resultado es: 2.5000.
Dim lowestUnitPrice=Aggregate prod In db.Products Into Min(prod.UnitPric)
Console.WriteLine(lowestUnitPrice)
En el ejemplo siguiente se busca el importe con flete más bajo de cualquier pedido.

Si ejecuta esta consulta en la base de datos de ejemplo Northwind, el resultado es: 0.0200.
Dim lowestFreight = Aggregate ord In db.Orders Into Min(ord.Freight)
Console.WriteLine(lowestFreight)
En el ejemplo siguiente se utiliza Min para encontrar los Products que tienen el precio unitario más bajo
en cada categoría. El resultado se organiza por categoría.
Dim minQuery = From prod In db.Products() Group prod By prod.CategoryID
Into grouping = Group Select CategoryID, LeastExpensiveProducts = From
prod2 In grouping Where prod2.UnitPrice = grouping.Min(Function(prod3)
prod3.UnitPrice)
For Each grp In minQuery
Console.WriteLine(grp.CategoryID)
For Each listing In grp.LeastExpensiveProducts
Console.WriteLine(listing.ProductName)
Next
Next

8.3.2.3.11.1.5. Cómo: Calcular la Suma de los Valores de


una Secuencia Numérica
Utilice el operador Sum para calcular la suma de los valores numéricos de una secuencia.

Tenga en cuenta las características siguientes del operador Sum en LINQ a SQL:

El operador de consulta estándar de agregado Sum se evalúa como cero para una secuencia
vacía o una secuencia que sólo contiene valores nulos. En LINQ a SQL, la semántica de SQL se
mantiene invariable. Por esta razón, Sum se evalúa como Null en lugar de cero en el caso de
secuencias vacías o secuencias que sólo contienen valores nulos.

En LINQ a SQL se aplican las restricciones de SQL para los agregados en los resultados
intermedios. La suma de cantidades enteras de 32 bits no se calcula utilizando resultados de 64
bits, y puede producirse un desbordamiento en la conversión que LINQ a SQL realiza de Sum.
Esta posibilidad existe aun cuando la implementación del operador de consulta estándar no
produzca un desbordamiento para la secuencia en memoria correspondiente.

Ejemplo
En el ejemplo siguiente se busca el flete total de todos los pedidos de la tabla Order.

Si ejecuta esta consulta en la base de datos de ejemplo Northwind, el resultado es: 64942.6900.
Dim totalFreight = Aggregate ord In db.Orders Into Sum(ord.Freight)
Console.WriteLine(totalFreight)
En el ejemplo siguiente se busca el número total de unidades pedidas para todos los productos.

MCT: Luis Dueñas Pag 285 de 388


Manual de LINQ

Si ejecuta esta consulta en la base de datos de ejemplo Northwind, el resultado es: 780.

Tenga en cuenta que debe convertir los tipos short (por ejemplo, UnitsOnOrder) porque Sum no tiene
una sobrecarga para ellos.
Dim totalUnitsOnOrder = Aggregate prod In db.Products Into
Sum(prod.UnitsOnOrder)
Console.WriteLine(totalUnitsOnOrder)

8.3.2.3.11.2. Cómo: Devolver el Primer Elemento de una


Secuencia
Utilice el operador First para devolver el primer elemento de una secuencia. Las consultas que usan First
se ejecutan inmediatamente.

Nota:

LINQ a SQL no admite el operador Last.

Ejemplo
El código siguiente busca el primer Shipper de una tabla:
Dim shipper As Shipper = db.Shippers.First()
Console.WriteLine("ID = {0}, Company = {1}", shipper.ShipperID, _
shipper.CompanyName)
El código siguiente busca el Customer único cuyo CustomerID es BONAP.
Dim custquery As Customer = (From c In db.Customers Where c.CustomerID =
"BONAP" Select c) .First()
Console.WriteLine("ID = {0}, Contact = {1}", custquery.CustomerID, _
custquery.ContactName)

8.3.2.3.11.3. Cómo: Devolver u Omitir Elementos de una


Secuencia
Utilice el operador Take<(Of <(TSource>)>) para devolver un número determinado de elementos de una
secuencia y omitir el resto.

Utilice el operador Skip<(Of <(TSource>)>) para omitir un número determinado de elementos de una
secuencia y devolver el resto.

Nota:

Take<(Of <(TSource>)>) y Skip<(Of <(TSource>)>) tienen ciertas limitaciones cuando se utilizan


en consultas en SQL Server 2000.

LINQ a SQL convierte Skip<(Of <(TSource>)>) utilizando una subconsulta con la cláusula NOT EXISTS
de SQL. Esta conversión tiene las limitaciones siguientes:

El argumento debe ser un conjunto. No se admiten los conjuntos múltiples, aunque estén
ordenados.

La consulta generada puede ser mucho más compleja que la consulta generada para la consulta
base en la que se aplica Skip<(Of <(TSource>)>). Esta complejidad puede mermar el
rendimiento o incluso hacer que se agote el tiempo de espera.

MCT: Luis Dueñas Pag 286 de 388


Manual de LINQ

Ejemplo
En el ejemplo siguiente se utiliza Take para seleccionar los cinco primeros Employees contratados.
Observe que la colección primero se ordena por HireDate.
Dim firstHiredQuery = From emp In db.Employees Select emp Order By
emp.HireDate Take 5
For Each empObj As Employee In firstHiredQuery
Console.WriteLine("{0}, {1}", empObj.EmployeeID, empObj.HireDate)
Next
En el ejemplo siguiente se utiliza Skip<(Of <(TSource>)>) para seleccionar todos los Products excepto
los 10 más caros.
Dim lessExpensiveQuery = From prod In db.Products Select prod Order By
prod.UnitPrice Descending Skip 10
For Each prodObj As Product In lessExpensiveQuery
Console.WriteLine(prodObj.ProductName)
Next
En el ejemplo siguiente se combinan los métodos Skip<(Of <(TSource>)>) y Take<(Of <(TSource>)>)
para omitir los 50 primeros registros y después devolver los 10 siguientes.
Dim custQuery2 = From cust In db.Customers Order By (cust.ContactName)
Select cust Skip 50 Take 10
For Each custRecord As Customer In custQuery2
Console.WriteLine(custRecord.ContactName)
Next
Las operaciones Take<(Of <(TSource>)>) y Skip<(Of <(TSource>)>) sólo están perfectamente
definidas para los conjuntos ordenados. La semántica para los conjuntos no ordenados o conjuntos
múltiples no está definida.

Debido a las limitaciones de la ordenación en SQL, LINQ a SQL intenta trasladar la ordenación del
argumento del operador Take<(Of <(TSource>)>) o Skip<(Of <(TSource>)>) al resultado del operador.

Nota:

La conversión es diferente para SQL Server 2000 y SQL Server 2005. Si piensa utilizar Skip<(Of
<(TSource>)>) con una consulta de cualquier complejidad, utilice SQL Server 2005.

Considere la siguiente consulta LINQ a SQL para SQL Server 2000:


Dim custQuery3 = From custs In db.Customers Where custs.City = "London"
Select custs Order By custs.CustomerID Skip 1 Take 1
For Each custObj In custQuery3
Console.WriteLine(custObj.CustomerID)
Next
LINQ a SQL traslada la operación de ordenación al final en el código de SQL, como se observa a
continuación:
SELECT TOP 1 [t0].[CustomerID], [t0].[CompanyName],
FROM [Customers] AS [t0]
WHERE (NOT (EXISTS(
SELECT NULL AS [EMPTY]
FROM (
SELECT TOP 1 [t1].[CustomerID]
FROM [Customers] AS [t1]
WHERE [t1].[City] = @p0
ORDER BY [t1].[CustomerID]

MCT: Luis Dueñas Pag 287 de 388


Manual de LINQ

) AS [t2]
WHERE [t0].[CustomerID] = [t2].[CustomerID]
))) AND ([t0].[City] = @p1)
ORDER BY [t0].[CustomerID]
Cuando se encadenan Take<(Of <(TSource>)>) y Skip<(Of <(TSource>)>), todos los criterios de
ordenación especificados deben ser coherentes. De lo contrario, los resultados no están definidos.

Para los argumentos integrales constantes no negativos basados en la especificación de SQL, tanto
Take<(Of <(TSource>)>) como Skip<(Of <(TSource>)>) están perfectamente definidos.

8.3.2.3.11.4. Cómo: Ordenar Elementos de una Secuencia


Utilice el operador OrderBy para ordenar una secuencia según una o más claves.

Nota:

LINQ a SQL está diseñado para permitir la ordenación según tipos primitivos simples, como string,
int, etc. No admite la ordenación de clases complejas con valores múltiples, como los tipos
anónimos. Tampoco admite los tipos de datos byte.

Ejemplo
En el ejemplo siguiente se ordena Employees por fecha de contratación.
Dim hireQuery = From emp In db.Employees Select emp Order By emp.HireDate
For Each empObj As Employee In hireQuery
Console.WriteLine("EmpID = {0}, Date Hired = {1}", _
empObj.EmployeeID, empObj.HireDate)
Next
En el ejemplo siguiente se utiliza where para ordenar los Orders distribuidos a London por flete.
Dim freightQuery = From ord In db.Orders Where ord.ShipCity = "London"
Select ord Order By ord.Freight
For Each ordObj In freightQuery
Console.WriteLine("Order ID = {0}, Freight = {1}", _
ordObj.OrderID, ordObj.Freight)
Next
En el ejemplo siguiente se ordenan los Products por precio unitario, de mayor a menor.
Dim priceQuery = From prod In db.Products Select prod Order By
prod.UnitPrice Descending
For Each prodObj In priceQuery
Console.WriteLine("Product ID = {0}, Unit Price = {1}", _
prodObj.ProductID, prodObj.UnitPrice)
Next
En el ejemplo siguiente se utiliza una cláusula OrderBy compuesta para ordenar los Customers por
ciudad y, después, por nombre de contacto.
Dim custQuery = From cust In db.Customers Select cust Order By cust.City,
cust.ContactName
For Each custObj In custQuery
Console.WriteLine("City = {0}, Name = {1}", custObj.City, _
custObj.ContactName)
Next
En el ejemplo siguiente se ordenan los pedidos de EmployeeID 1 por país de destino y, después, por
flete, de mayor a menor.

MCT: Luis Dueñas Pag 288 de 388


Manual de LINQ

Dim ordQuery = From ord In db.Orders Where CInt(ord.EmployeeID.Value) = 1


Select ord Order By ord.ShipCountry, ord.Freight Descending
For Each ordObj In ordQuery
Console.WriteLine("Country = {0}, Freight = {1}", _
ordObj.ShipCountry, ordObj.Freight)
Next
En el ejemplo siguiente se combinan los operadores OrderBy, Max y GroupBy para buscar los Products
que tienen el precio unitario más alto en cada categoría y, después, se ordena el grupo por identificador
de categoría.
Dim highPriceQuery = From prod In db.Products Group prod By
prod.CategoryID Into grouping = Group Order By CategoryID Select
CategoryID, MostExpensiveProducts = From prod2 In grouping Where
prod2.UnitPrice = grouping.Max(Function(p3) p3.UnitPrice)
For Each prodObj In highPriceQuery
Console.WriteLine(prodObj.CategoryID)
For Each listing In prodObj.MostExpensiveProducts
Console.WriteLine(listing.ProductName)
Next
Next

8.3.2.3.11.5. Cómo: Agrupar Elementos en una Secuencia


El operador GroupBy agrupa los elementos de una secuencia. En los ejemplos siguientes se utiliza la
base de datos Northwind.

Nota:

Los valores de columna nulos de las consultas GroupBy a veces pueden iniciar InvalidOperationException.

Ejemplo
En el ejemplo siguiente se crea una partición de Products por CategoryID.
Dim prodQuery = From prod In db.Products Group prod By prod.CategoryID
Into grouping = Group
For Each grp In prodQuery
Console.WriteLine(vbNewLine & "CategoryID Key={0}:", grp.CategoryID)
For Each listing In grp.grouping
Console.WriteLine(vbTab & listing.ProductName)
Next
Next
En el ejemplo siguiente se utiliza Max para buscar el precio unitario máximo para cada CategoryID.
Dim query = From p In db.Products Group p By p.CategoryID Into g = Group
Select CategoryID, MaxPrice = g.Max(Function(p) p.UnitPrice)
En el ejemplo siguiente se utiliza Average para buscar el UnitPrice promedio para cada CategoryID.
Dim q2 = From p In db.Products Group p By p.CategoryID Into g = Group
Select CategoryID, AveragePrice = g.Average(Function(p) p.UnitPrice)
En el ejemplo siguiente se usa Sum para buscar el UnitPrice total para cada CategoryID.
Dim priceQuery = From prod In db.Products Group prod By prod.CategoryID
Into grouping = Group Select CategoryID, TotalPrice =
grouping.Sum(Function(p) p.UnitPrice)
For Each grp In priceQuery
Console.WriteLine("Category = {0}, Total price = {1}", _

MCT: Luis Dueñas Pag 289 de 388


Manual de LINQ

grp.CategoryID, grp.TotalPrice)
Next
En el ejemplo siguiente se utiliza Count para buscar el número de Products que ya no se fabrican en cada
CategoryID.
Dim disconQuery = From prod In db.Products Group prod By prod.CategoryID
Into grouping = Group Select CategoryID, NumProducts =
grouping.Count(Function(p) p.Discontinued)
For Each prodObj In disconQuery
Console.WriteLine("CategoryID = {0}, Discontinued# = {1}", _
prodObj.CategoryID, prodObj.NumProducts)
Next
En el ejemplo siguiente se utiliza una cláusula where posterior para buscar todas las categorías que
tienen al menos 10 productos.
Dim prodCountQuery = From prod In db.Products Group prod By
prod.CategoryID Into grouping = Group Where grouping.Count >= 10 Select
CategoryID, ProductCount = grouping.Count
For Each prodCount In prodCountQuery
Console.WriteLine("CategoryID = {0}, Product count = {1}", _
prodCount.CategoryID, prodCount.ProductCount)
Next
En el ejemplo siguiente se agrupan los productos por CategoryID y SupplierID.
Dim prodQuery = From prod In db.Products Group prod By Key = New With
{prod.CategoryID, prod.SupplierID} Into grouping = Group
For Each grp In prodQuery
Console.WriteLine(vbNewLine & "CategoryID {0}, SupplierID {1}", _
grp.Key.CategoryID, grp.Key.SupplierID)
For Each listing In grp.grouping
Console.WriteLine(vbTab & listing.ProductName)
Next
Next
En el ejemplo siguiente se devuelven dos secuencias de productos. La primera secuencia contiene los
productos con precio unitario menor o igual que 10. La segunda secuencia contiene los productos con
precio unitario mayor que 10.
Dim priceQuery = From prod In db.Products Group prod By Key = New With
{.Criterion = prod.UnitPrice > 10} Into grouping = Group Select Key,
grouping
For Each prodObj In priceQuery
If prodObj.Key.Criterion = False Then
Console.WriteLine("Prices 10 or less:")
Else
Console.WriteLine("\nPrices greater than 10")
For Each listing In prodObj.grouping
Console.WriteLine("{0}, {1}", listing.ProductName, _
listing.UnitPrice)
Next
End If
Next
El operador GroupBy sólo acepta un argumento de clave única. Si necesita agrupar los elementos según
más de una clave, debe crear un tipo anónimo, como en el ejemplo siguiente:

MCT: Luis Dueñas Pag 290 de 388


Manual de LINQ

Dim custRegionQuery = From cust In db.Customers Group cust.ContactName By


Key = New With {cust.City, cust.Region} Into grouping = Group
For Each grp In custRegionQuery
Console.WriteLine(vbNewLine & "Location Key: {0}", grp.Key)
For Each listing In grp.grouping
Console.WriteLine(vbTab & "{0}", listing)
Next
Next

8.3.2.3.11.6. Cómo: Eliminar Elementos Duplicados de una


Secuencia
Utilice el operador Distinct para eliminar los elementos duplicados de una secuencia.

Ejemplo
En el ejemplo siguiente se utiliza Distinct para seleccionar una secuencia de las ciudades que tienen
clientes.
Dim cityQuery = (From cust In db.Customers Select cust.City).Distinct()
For Each cityString In cityQuery
Console.WriteLine(cityString)
Next

8.3.2.3.11.7. Cómo: Determinar si Algún Elemento o Todos


los Elementos de una Secuencia Cumplen una Condición
El operador All<(Of <(TSource>)>) devuelve true si todos los elementos de una secuencia satisfacen
una condición.

El operador Any devuelve true si cualquier elemento de una secuencia satisface una condición.

Ejemplo
En el ejemplo siguiente se devuelve una secuencia de clientes que tienen por lo menos un pedido. La
cláusula Where/where se evalúa como true si el Customer dado tiene cualquier Order.
Dim OrdersQuery = From cust In db.Customers Where cust.Orders.Any()
Select cust
El código de Visual Basic siguiente determina la lista de clientes que no han realizado pedidos y garantiza
que, para cada cliente de esa lista, se proporciona un nombre de contacto.
Public Sub ContactsAvailable()
Dim db As New Northwnd("c:\northwnd.mdf")
Dim result = (From cust In db.Customers Where Not cust.Orders.Any() _
Select cust).All(AddressOf ContactAvailable)
If result Then Console.WriteLine _
("All of the customers who have made no orders have a contact name")
Else
Console.WriteLine _
("Some customers who have made no orders have no contact name")
End If
End Sub
Function ContactAvailable(ByVal contact As Object) As Boolean

MCT: Luis Dueñas Pag 291 de 388


Manual de LINQ

Dim cust As Customer = CType(contact, Customer)


Return (cust.ContactTitle Is Nothing OrElse _
cust.ContactTitle.Trim().Length = 0)
End Function
En el ejemplo de C# siguiente se devuelve una secuencia de clientes cuyos pedidos tienen un valor de
ShipCity que empieza por "C". En el resultado devuelto se incluyen también los clientes que no tienen
pedidos. (Por diseño, el operador All<(Of <(TSource>)>) devuelve true para una secuencia vacía.) Los
clientes sin pedidos se eliminan en los resultados de la consola mediante el operador Count.
var custEmpQuery = from cust in db.Customers where cust.Orders.All(o =>
o.ShipCity.StartsWith("C")) orderby cust.CustomerID select cust;
foreach (Customer custObj in custEmpQuery)
{
if (custObj.Orders.Count > 0)
Console.WriteLine("CustomerID: {0}", custObj.CustomerID);
foreach (Order ordObj in custObj.Orders)
{
Console.WriteLine("\t OrderID: {0}; ShipCity: {1}",
ordObj.OrderID, ordObj.ShipCity);
}
}

8.3.2.3.11.8. Cómo: Concatenar Dos Secuencias


Utilice el operador Concat<(Of <(TSource>)>) para concatenar dos secuencias.

El operador Concat<(Of <(TSource>)>) se define para conjuntos múltiples ordenados en los que el
orden del receptor y del argumento son el mismo.

En SQL, la ordenación es el último paso antes de que se generen los resultados. Por esta razón, el
operador Concat<(Of <(TSource>)>) se implementa utilizando UNION ALL y no conserva el orden de
sus argumentos. Para garantizar que el orden de los resultados es correcto, no olvide ordenar los
resultados explícitamente.

Ejemplo
En este ejemplo se utiliza Concat<(Of <(TSource>)>) para devolver una secuencia de todos los números
de teléfono y fax de Customer y Employee.
Dim custQuery = (From c In db.Customers Select c.Phone) .Concat (From c
In db.Customers Select c.Fax) .Concat (From e In db.Employees Select
e.HomePhone)
For Each custData In custQuery
Console.WriteLine(custData)
Next
En este ejemplo se utiliza Concat<(Of <(TSource>)>) para devolver una secuencia de todas las
asignaciones de números de teléfono y nombres de Customer y Employee.
Dim infoQuery = (From cust In db.Customers Select Name =
cust.CompanyName, Phone = cust.Phone) .Concat (From emp In db.Employees
Select Name = emp.FirstName & " " & emp.LastName, Phone = emp.HomePhone)
For Each infoData In infoQuery
Console.WriteLine("Name=" & infoData.Name & ",Phone=" & infoData.Phone)
Next

MCT: Luis Dueñas Pag 292 de 388


Manual de LINQ

8.3.2.3.11.9. Cómo: Devolver la diferencia de conjuntos


entre dos secuencias
Utilice el operador Except para devolver la diferencia de conjuntos entre dos secuencias.

Ejemplo
En este ejemplo se utiliza Except para devolver una secuencia de todos los países en los que viven
Customers pero no Employees.
Dim infoQuery = (From cust In db.Customers Select cust.Country) .Except
From emp In db.Employees Select emp.Country)
En LINQ a SQL, la operación Except se define correctamente sólo en conjuntos. La semántica de
conjuntos múltiples no está definida.

8.3.2.3.11.10. Cómo: Devolver la Intersección de Conjuntos


de Dos Secuencias
Utilice el operador Intersect para devolver la intersección de conjuntos de dos secuencias.

Ejemplo
En este ejemplo se utiliza Intersect para devolver una secuencia de todos los países en los que viven
Customers y Employees.
Dim infoQuery = (From cust In db.Customers Select cust.Country) Intersect
(From emp In db.Employees Select emp.Country)
En LINQ a SQL, la operación Intersect se define correctamente sólo en conjuntos. La semántica de
conjuntos múltiples no está definida.

8.3.2.3.11.11. Cómo: Devolver la Unión de Conjuntos de


Dos Secuencias
Utilice el operador Union para devolver la unión de conjuntos de dos secuencias.

Ejemplo
En este ejemplo se utiliza Union para devolver una secuencia de todos los países en los que hay
Customers o Employees.
Dim infoQuery = (From cust In db.Customers Select cust.Country) .Union
(From emp In db.Employees Select emp.Country)
En LINQ a SQL, el operador Union se define para conjuntos múltiples como la concatenación no ordenada
de conjuntos múltiples (de hecho, el resultado de la cláusula UNION ALL en SQL).

8.3.2.3.11.12. Cómo: Convertir una Secuencia en una Matriz


Utilice ToArray<(Of <(TSource>)>) para crear una matriz a partir de una secuencia.

Ejemplo
En el ejemplo siguiente se utiliza ToArray<(Of <(TSource>)>) para evaluar inmediatamente una
consulta como una matriz y obtener el tercer elemento.

MCT: Luis Dueñas Pag 293 de 388


Manual de LINQ

Dim custQuery = From cust In db.Customers Where cust.City = "London"


Select cust
Dim qArray() As Customer = custQuery.ToArray()

8.3.2.3.11.13. Cómo: Convertir una Secuencia en una Lista


Genérica
Utilice ToList<(Of <(TSource>)>) para crear una lista genérica a partir de una secuencia.

Ejemplo
En el ejemplo siguiente se utiliza ToList<(Of <(TSource>)>) para evaluar una consulta inmediatamente
como una List<(Of <(T>)>) genérica.
Dim empQuery = From emp In db.Employees Where emp.HireDate.Value >=
#1/1/1994# Select emp
Dim qList As List(Of Employee) = empQuery.ToList()

8.3.2.3.11.14. Cómo: Convertir un Tipo en Datos


IEnumerable Genéricos
Utilice AsEnumerable<(Of <(TSource>)>) para devolver el argumento cuyo tipo es una interfaz genérica
IEnumerable.

Ejemplo
En este ejemplo, LINQ a SQL (mediante la Query genérica predeterminada) intentaría convertir la
consulta en SQL y ejecutarla en el servidor. Sin embargo, la cláusula where hace referencia a un
método de cliente definido por el usuario (isValidProduct), que no se puede convertir a SQL.

La solución es especificar la implementación de interfaz genérica IEnumerable<(Of <(T>)>) de where


en el cliente para reemplazar la interfaz genérica IQueryable<(Of <(T>)>). Para ello se invoca al
operador AsEnumerable<(Of <(TSource>)>).
Private Function isValidProduct(ByVal prod As Product) As Boolean
Return prod.ProductName.LastIndexOf("C") = 0
End Function
Sub ConvertToIEnumerable()
Dim db As New Northwnd("c:\northwnd.mdf")
Dim validProdQuery = From prod In db.Products.AsEnumerable Where
isValidProduct(prod) Select prod
End Sub

8.3.2.3.11.15. Cómo: Formular Uniones y Consultas de


Varios Productos
En los ejemplos siguientes se muestra cómo combinar los resultados procedentes de varias tablas.

Ejemplo
En el ejemplo siguiente se utiliza la navegación de clave externa en la cláusula From de Visual Basic
(cláusula from en C#) para seleccionar todos los pedidos de los clientes de Londres.

MCT: Luis Dueñas Pag 294 de 388


Manual de LINQ

Dim infoQuery = From cust In db.Customers, ord In cust.Orders Where


cust.City = "London" Select ord
En el ejemplo siguiente se utiliza la navegación de clave externa en la cláusula Where de Visual Basic
(cláusula where en C#) para aplicar un filtro y obtener los Products agotados cuyo Supplier se
encuentra en Estados Unidos.
Dim infoQuery = From prod In db.Products Where prod.Supplier.Country =
"USA" AndAlso CShort(prod.UnitsInStock) = 0 Select prod
En el ejemplo siguiente se utiliza la navegación de clave externa en la cláusula From de Visual Basic
(cláusula from en C#) para aplicar un filtro y obtener los empleados de Seattle y una lista de sus áreas.
var infoQuery = from emp in db.Employees from empterr in
emp.EmployeeTerritories where emp.City == "Seattle" select new
{ emp.FirstName,
emp.LastName,
empterr.Territory.TerritoryDescription };
En el ejemplo siguiente se utiliza la navegación de clave externa en la cláusula Select de Visual Basic
(cláusula select en C#) para aplicar un filtro y obtener pares de empleados de tal forma que uno de
ellos esté subordinado al otro, siendo ambos de la misma City.
Dim infoQuery = From e1 In db.Employees, e2 In e1.Employees Where e1.City
= e2.City Select FirstName1 = e1.FirstName, LastName1 = e1.LastName,
FirstName2 = e2.FirstName, LastName2 = e2.LastName, e1.City
En el ejemplo siguiente de Visual Basic se buscan todos los clientes y pedidos, se comprueba que los
pedidos estén asociados a clientes y se garantiza que para cada cliente de la lista se proporciona un
nombre de contacto.
Dim q1 = From c In db.Customers, o In db.Orders Where c.CustomerID =
o.CustomerID Select c.CompanyName, o.ShipRegion
' Note that because the O/R designer generates class
' hierarchies for database relationships for you,
' the following code has the same effect as the above
' and is shorter:
Dim q2 = From c In db.Customers, o In c.Orders Select c.CompanyName,
o.ShipRegion
For Each nextItem In q2
Console.WriteLine("{0} {1}", nextItem.CompanyName, nextItem.ShipRegion)
Next
En el ejemplo siguiente se combinan explícitamente dos tablas y se proyectan los resultados de ambas.
Dim q = From c In db.Customers Group Join o In db.Orders On c.CustomerID
Equals o.CustomerID Into orders = Group Select c.ContactName, OrderCount
= orders.Count()
En el ejemplo siguiente se combinan explícitamente tres tablas y se proyectan los resultados de cada una
de ellas.
Dim q = From c In db.Customers Group Join o In db.Orders On c.CustomerID
Equals o.CustomerID Into ords = Group Group Join e In db.Employees On
c.City Equals e.City Into emps = Group Select c.ContactName, ords =
ords.Count(), emps = emps.Count()
En el ejemplo siguiente se muestra cómo lograr una LEFT OUTER JOIN mediante DefaultIfEmpty(). El
método DefaultIfEmpty() devuelve null cuando no hay ningún Order para Employee.
Dim q = From e In db.Employees() Group Join o In db.Orders On e Equals
o.Employee Into ords = Group From o In ords.DefaultIfEmpty() Select
e.FirstName, e.LastName, Order = o

MCT: Luis Dueñas Pag 295 de 388


Manual de LINQ

En el ejemplo siguiente se proyecta una expresión let que es el resultado de una combinación.
Dim q = From c In db.Customers Group Join o In db.Orders On c.CustomerID
Equals o.CustomerID Into ords = Group Let z = c.City + c.Country From o
In ords Select c.ContactName, o.OrderID, z
En el ejemplo siguiente se muestra una join con una clave compuesta.
Dim q = From o In db.Orders From p In db.Products Group Join d In
db.OrderDetails On New With {o.OrderID, p.ProductID} Equals New With
{d.OrderID, d.ProductID} Into details = Group From d In details Select
o.OrderID, p.ProductID, d.UnitPrice
En el ejemplo siguiente se muestra cómo construir una join de tal forma que una parte acepte valores
NULL y la otra no.
Dim q=From o In db.Orders Group Join e In db.Employees On o.EmployeeID
Equals e.EmployeeID Into emps=Group From e In emps Select o.OrderID,
e.FirstName

8.3.2.3.11.16. Cómo: Formular Proyecciones


En los ejemplos siguientes se muestra cómo la instrucción select en C# y la instrucción Select en Visual
Basic se puede combinar con otras características para formar proyecciones de consulta.

Ejemplo
En el ejemplo siguiente se utiliza la cláusula Select de Visual Basic (cláusula select en C#) para
devolver una secuencia de nombres de contacto de Customers.
Dim nameQuery = From cust In db.Customers Select cust.ContactName
En el ejemplo siguiente se utilizan la cláusula Select de Visual Basic (cláusula select en C#) y tipos
anónimos para devolver una secuencia de nombres de contacto y números de teléfono de Customers.
Dim infoQuery = From cust In db.Customers Select cust.ContactName,
cust.Phone
En el ejemplo siguiente se utilizan la cláusula Select de Visual Basic (cláusula select en C#) y tipos
anónimos para devolver una secuencia de nombres de contacto y números de teléfono de empleados.
Los campos FirstName y LastName se combinan en un campo único (Name) y se cambia el nombre del
campo HomePhone a Phone en la secuencia resultante.
Dim info2Query = From emp In db.Employees Select Name = emp.FirstName & "
" & emp.LastName, Phone = emp.HomePhone
En el ejemplo siguiente se utiliza la cláusula Select de Visual Basic (cláusula select en C#) y tipos
anónimos para devolver una secuencia de todos los ProductID y un valor calculado denominado
HalfPrice. Este valor se establece en UnitPrice dividido entre 2.
Dim specialQuery = From prod In db.Products Select prod.ProductID,
HalfPrice = CDec(prod.UnitPrice) / 2
En el ejemplo siguiente se utiliza la cláusula Select de Visual Basic (cláusula select en C#) y una
instrucción condicional para devolver una secuencia de nombre de producto y disponibilidad de producto.
Dim prodQuery = From prod In db.Products Select prod.ProductName,
Availability = If(prod.UnitsInStock - prod.UnitsOnOrder < 0, "Out Of
Stock", "In Stock")
En el ejemplo siguiente se utiliza la cláusula Select de Visual Basic (cláusula select en C#) y un tipo
conocido (Name) para devolver una secuencia de nombres de empleados.
Public Class Name
Public FirstName As String
Public LastName As String

MCT: Luis Dueñas Pag 296 de 388


Manual de LINQ

End Class
Dim db As New Northwnd("c:\northwnd.mdf")
Dim empQuery = From emp In db.Employees Select New Name With {.FirstName
= emp.FirstName, .LastName = emp.LastName}
En el ejemplo siguiente se utilizan las cláusulas Select y Where de Visual Basic (select y where en C#)
para devolver una secuencia filtrada de los nombres de contacto de los clientes de Londres.
Dim contactQuery = From cust In db.Customers Where cust.City = "London"
Select cust.ContactName
En el ejemplo siguiente se utilizan una cláusula Select de Visual Basic (cláusula select en C#) y tipos
anónimos para devolver un subconjunto con forma de los datos de clientes.
Dim custQuery = From cust In db.Customers Select cust.CustomerID,
CompanyInfo = New With {cust.CompanyName, cust.City, cust.Country},
ContactInfo = New With {cust.ContactName, cust.ContactTitle}
En el ejemplo siguiente se utilizan consultas anidadas para devolver estos resultados:
Una secuencia de todos los pedidos y sus OrderID correspondientes.
Una subsecuencia de los elementos del pedido que tienen descuento.
La cantidad de dinero que se ahorra si no se incluye el envío en el precio.
Dim ordQuery = From ord In db.Orders Select ord.OrderID,
DiscountedProducts = (From od In ord.OrderDetails Where od.Discount > 0.0
Select od), FreeShippingDiscount = ord.Freight

8.3.2.4. Crear y Enviar Cambios en los Datos


En los temas de esta sección se describe cómo realizar y transmitir cambios en la base de datos y cómo
administrar los conflictos de simultaneidad optimista.

Nota:

Puede invalidar los métodos predeterminados de LINQ a SQL para las operaciones de base de datos
Insert, Update y Delete.
Los programadores de Visual Studio pueden usar el Diseñador relacional de objetos para desarrollar
procedimientos almacenados con el mismo propósito.

8.3.2.4.1. Cómo: Insertar Filas en la Base de Datos


Para insertar filas en una base de datos, se agregan objetos a la colección Table<(Of <(TEntity>)>) de
LINQ a SQL asociada y después se envían los cambios a la base de datos. LINQ a SQL convierte los
cambios en los comandos INSERT adecuados de SQL.

En los pasos siguientes se asume que un objeto DataContext válido le conecta a la base de datos
Northwind.

Para insertar una fila en la base de datos

1. Cree un nuevo objeto que incluya los datos de la columna que se van a enviar.

2. Agregue el nuevo objeto a la colección Table de LINQ a SQL que está asociada a la tabla de
destino en la base de datos.

3. Envíe el cambio a la base de datos.

MCT: Luis Dueñas Pag 297 de 388


Manual de LINQ

Ejemplo
En el ejemplo de código siguiente se crea un nuevo objeto de tipo Order y se llena con los valores
adecuados. Después, se agrega el nuevo objeto a la colección Order. Finalmente, se envía el cambio a la
base de datos, como una nueva fila de la tabla Orders.
' Create a new Order object.
Dim ord As New Order With {.OrderID = 12000, .ShipCity = "Seattle", _
.OrderDate = DateTime.Now}
' Add the new object to the Orders collection.
db.Orders.InsertOnSubmit(ord)
' Submit the change to the database.
Try
db.SubmitChanges()
Catch e As Exception
Console.WriteLine(e)
' Make some adjustments.
' ...
' Try again.
db.SubmitChanges()
End Try

8.3.2.4.2. Cómo: Actualizar Filas en la Base de Datos


Puede actualizar las filas de una base de datos modificando los valores de miembro de los objetos
asociados a la colección Table<(Of <(TEntity>)>) de LINQ a SQL y enviando después los cambios a la
base de datos. LINQ a SQL convierte los cambios en los comandos UPDATE adecuados de SQL.

Para actualizar una fila de la base de datos

1. Consulte en la base de datos la fila que se va a actualizar.

2. Realice los cambios deseados en los valores de miembro en el objeto LINQ a SQL resultante.

3. Envíe los cambios a la base de datos.

Ejemplo
En el ejemplo siguiente se consulta en la base de datos el pedido #11000 y, a continuación, se cambian
los valores de ShipName y ShipVia en el objeto Order resultante. Finalmente, los cambios en estos
valores de miembro se envían a la base de datos como cambios en las columnas ShipName y ShipVia.
' Query the database for the row to be updated.
Dim ordQuery = From ord In db.Orders Where ord.OrderID = 11000 Select ord
' Execute the query, and change the column values
' you want to change.
For Each ord As Order In ordQuery
ord.ShipName = "Mariner"
ord.ShipVia = 2
' Insert any additional changes to column values.
Next
' Submit the changes to the database.
Try
db.SubmitChanges()

MCT: Luis Dueñas Pag 298 de 388


Manual de LINQ

Catch e As Exception
Console.WriteLine(e)
' Make some adjustments.
' ...
' Try again
db.SubmitChanges()
End Try

8.3.2.4.3. Cómo: Eliminar Filas de la Base de Datos


Puede eliminar filas de una base de datos quitando los objetos de LINQ a SQL correspondientes de la
colección relacionada con la tabla. LINQ a SQL convierte los cambios en los comandos SQL DELETE
correspondientes.

LINQ a SQL no admite ni reconoce las operaciones de eliminación en cascada. Si desea eliminar una fila
de una tabla que tiene restricciones, deberá realizar una de las siguientes tareas:

Establezca la regla ON DELETE CASCADE en la restricción FOREIGN KEY de la base de datos.

Utilice su propio código para eliminar primero los objetos secundarios que impiden que se
elimine el objeto primario.

De lo contrario, se producirá una excepción. Vea el segundo ejemplo de código que se muestra más
adelante en este tema.

Para eliminar una fila en la base de datos

1. Consulte en la base de datos la fila que se va a eliminar.

2. Llame al método DeleteOnSubmit.

3. Envíe el cambio a la base de datos.

Ejemplo
En este primer ejemplo de código se consultan en la base de datos los detalles del pedido #11000, se
marcan dichos detalles para eliminarlos y se envían estos cambios a la base de datos.
' Query the database for the rows to be deleted.
Dim deleteOrderDetails = _
From details In db.OrderDetails() _
Where details.OrderID = 11000 _
Select details
For Each detail As OrderDetail In deleteOrderDetails
db.OrderDetails.DeleteOnSubmit(detail)
Next
Try
db.SubmitChanges()
Catch ex As Exception
Console.WriteLine(ex)
' Provide for exceptions
End Try

MCT: Luis Dueñas Pag 299 de 388


Manual de LINQ

En este segundo ejemplo, el objetivo es quitar un pedido (#10250). En primer lugar, el código examina
la tabla OrderDetails para comprobar si el pedido que se va a quitar tiene elementos secundarios en
ésta. Si el pedido tiene elementos secundarios, se marcan primero los elementos secundarios y después
el pedido para eliminarlos. El objeto DataContext coloca las eliminaciones en el orden correcto para que
los comandos de eliminación enviados a la base de datos cumplan las restricciones de la misma.
Dim db As New Northwnd("c:\northwnd.mdf")
db.Log = Console.Out
' Specify order to be removed from database.
Dim reqOrder As Integer = 10252
' Fetch OrderDetails for requested order.
Dim ordDetailQuery = _
From odq In db.OrderDetails _
Where odq.OrderID = reqOrder _
Select odq
For Each selectedDetail As OrderDetail In ordDetailQuery
Console.WriteLine(selectedDetail.Product.ProductID)
db.OrderDetails.DeleteOnSubmit(selectedDetail)
Next
' Display progress.
Console.WriteLine("Detail section finished.")
Console.ReadLine()
' Determine from Detail collection whether parent exists.
If ordDetailQuery.Any Then
Console.WriteLine("The parent is present in the Orders collection.")
' Fetch order.
Try
Dim ordFetch = _
(From ofetch In db.Orders _
Where ofetch.OrderID = reqOrder _
Select ofetch).First()
db.Orders.DeleteOnSubmit(ordFetch)
Console.WriteLine("{0} OrderID is marked for deletion.,",
ordFetch.OrderID)
Catch ex As Exception
Console.WriteLine(ex.Message)
Console.ReadLine()
End Try
Else
Console.WriteLine("There was no parent in the Orders collection.")
End If
' Display progress.
Console.WriteLine("Order section finished.")
Console.ReadLine()
Try
db.SubmitChanges()
Catch ex As Exception
Console.WriteLine(ex.Message)
Console.ReadLine()
End Try
' Display progress.

MCT: Luis Dueñas Pag 300 de 388


Manual de LINQ

Console.WriteLine("Submit finished.")
Console.ReadLine()

8.3.2.4.4. Cómo: Enviar Cambios a la Base de Datos


Con independencia de los cambios que se efectúen en los objetos, éstos sólo se realizan en las réplicas
en memoria. Los cambios no se aplican a los datos reales de la base de datos. Los cambios no se
transmiten al servidor hasta que se llama a SubmitChanges explícitamente en DataContext.

Al realizar esta llamada, DataContext intenta convertir los cambios a comandos SQL equivalentes. Puede
utilizar su propia lógica personalizada para invalidar estas acciones, pero el orden de envío lo controla un
servicio de DataContext que se conoce como procesador de cambios. Todo ocurre en este orden:

1. Al llamar a SubmitChanges, LINQ a SQL examina el conjunto de objetos conocidos para


determinar si se les han asociado nuevas instancias. En caso afirmativo, estas nuevas instancias se
agregan al conjunto de objetos con seguimiento.

2. Todos los objetos que tienen cambios pendientes se ordenan en una secuencia de objetos de
acuerdo con las dependencias entre ellos. Los objetos con cambios que dependen de otros objetos
se ordenan según sus dependencias.

3. Inmediatamente antes de que se transmitan los verdaderos cambios, LINQ a SQL inicia una
transacción para encapsular la serie de comandos individuales.

4. Los cambios en los objetos se convierten uno a uno en comandos SQL y se envían al servidor.

En este punto, cualquier error detectado por la base de datos hace que se detenga el proceso de envío, y
se inicia una excepción. Todos los cambios de la base de datos se revierten, como si no se hubiese
enviado nada. DataContext mantiene un registro completo de todos los cambios. Por lo tanto, se puede
intentar corregir el problema y llamar de nuevo a SubmitChanges, como en el ejemplo de código
siguiente.

Ejemplo
Cuando la transacción relacionada con el envío se completa correctamente, DataContext recibe los
cambios de los objetos sin tener en cuenta la información de seguimiento de cambios.
Dim db As New Northwnd("c:\northwnd.mdf")
' Make changes here.
Sub MakeChanges()
Try
db.SubmitChanges()
Catch e As ChangeConflictException
Console.WriteLine(e.Message)
' Make some adjustments
'...
' Try again.
db.SubmitChanges()
End Try
End Sub

MCT: Luis Dueñas Pag 301 de 388


Manual de LINQ

8.3.2.4.5. Cómo: Catalogar Envíos de Datos mediante


Transacciones
Puede utilizar TransactionScope para catalogar sus envíos a la base de datos.

Ejemplo
El código siguiente incluye el envío de base de datos en TransactionScope.
Dim db As New Northwnd("c:\northwnd.mdf")
Using ts = New TransactionScope()
Try
Dim prod1 = db.Products.First(Function(p) p.ProductID = 4)
Dim prod2 = db.Products.First(Function(p) p.ProductID = 5)
prod1.UnitsInStock -= 3
prod2.UnitsInStock -= 5
db.SubmitChanges()
Catch e As Exception
Console.WriteLine(e.Message)
End Try
End Using

8.3.2.4.6. Cómo: Crear Dinámicamente una Base de Datos


Las clases de entidad tienen atributos que describen la estructura de las tablas y las columnas de una
base de datos relacional. Puede utilizar esta información para crear nuevas instancias de la base de
datos. Al llamar al método CreateDatabase en DataContext, LINQ a SQL construye una nueva instancia
de la base de datos con una estructura definida por los objetos.

Podría utilizar esta característica en cualquier escenario, sobre todo si está disponible un proveedor de
datos conocido, como SQL Server Express 2005. Entre los escenarios típicos, se pueden citar los
siguientes:

Se genera una aplicación que se instala automáticamente en un sistema del cliente.

Se genera una aplicación cliente que necesita que una base de datos local guarde su estado sin
conexión.

Nota:

Es posible que los atributos de datos de su modelo de objetos no codifiquen todos los
elementos de la estructura de una base de datos existente. Los atributos no representan el
contenido de las funciones definidas por el usuario, los procedimientos almacenados, los
desencadenadores y las restricciones CHECK. La función CreateDatabase crea una réplica de la
base de datos sólo en lo que respecta a la información codificada en el modelo de objetos.
Este comportamiento es suficiente para varias bases de datos.

También puede utilizar CreateDatabase con SQL Server utilizando un archivo .mdf o simplemente un
nombre de catálogo, dependiendo de su cadena de conexión. LINQ a SQL utiliza la cadena de conexión
para definir la base de datos que se va a crear y en qué servidor será creada.

Ejemplo
El código siguiente proporciona un ejemplo de cómo se crearía una nueva base de datos denominada
MyDVDs.mdf.
Public Class MyDVDs

MCT: Luis Dueñas Pag 302 de 388


Manual de LINQ

Inherits DataContext
Public DVDs As Table(Of DVD)
Public Sub New(ByVal connection As String)
MyBase.New(connection)
End Sub
End Class
<Table(Name:="DVDTable")> _
Public Class DVD
<Column(IsPrimaryKey:=True)> _
Public Title As String
<Column()> _
Public Rating As String
End Class
Puede utilizar el modelo de objetos para crear una base de datos de la manera siguiente:
Public Sub CreateDatabase()
Dim db As New MyDVDs("c:\...\mydvds.mdf")
db.CreateDatabase()
End Sub
LINQ a SQL también proporciona una API para quitar una base de datos existente antes de crear una
nueva. Puede modificar el código del escenario 1 (incluido anteriormente en este documento) para
comprobar primero si existe alguna otra versión de la base de datos. Utilice los métodos DatabaseExists
y DeleteDatabase para implementar este enfoque. Después de llamar a CreateDatabase, se habrá creado
la nueva base de datos y aceptará las consultas y comandos habituales.

Puede implementar este enfoque utilizando código como el siguiente:


Public Sub CreateDatabase2()
Dim db As MyDVDs = New MyDVDs("c:\...\mydvds.mdf")
If db.DatabaseExists() Then
Console.WriteLine("Deleting old database...")
db.DeleteDatabase()
End If
db.CreateDatabase()
End Sub

8.3.2.4.7. Cómo: Administrar los Conflictos de Cambios


LINQ a SQL proporciona una colección de API para ayudarle a detectar, evaluar y solucionar conflictos de
simultaneidad.

8.3.2.4.7.1. Cómo: Detectar y Resolver Envíos de Datos en


Conflicto
LINQ a SQL proporciona muchos recursos para detectar y resolver los conflictos que ocasionan los
cambios que realizan varios usuarios en la base de datos.

Ejemplo
En el ejemplo siguiente se muestra un bloque try/catch que detecta una excepción
ChangeConflictException. La información de entidad y miembro de cada conflicto se muestra en la
ventana de la consola.

MCT: Luis Dueñas Pag 303 de 388


Manual de LINQ

Nota:

Debe incluir la directiva using System.Reflection (Imports System.Reflection en Visual Basic) para
habilitar la recuperación de la información.

' Imports System.Reflection


Dim newCust As New Customer()
newCust.City = "Auburn"
newCust.CustomerID = "AUBUR"
newCust.CompanyName = "AubCo"
db.Customers.InsertOnSubmit(newCust)
Try
db.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
Console.WriteLine("Optimistic concurrency error.")
Console.WriteLine(e.Message)
Console.ReadLine()
For Each occ In db.ChangeConflicts
Dim metatable As MetaTable =
db.Mapping.GetTable(occ.Object.GetType())
Dim entityInConflict = CType(occ.Object, Customer)
Console.WriteLine("Table name: {0}", metatable.TableName)
Console.Write("Customer ID: ")
Console.WriteLine(entityInConflict.CustomerID)
For Each mcc In occ.MemberConflicts
Dim currVal = mcc.CurrentValue
Dim origVal = mcc.OriginalValue
Dim databaseVal = mcc.DatabaseValue
Dim mi = mcc.Member
Console.WriteLine("Member: {0}", mi.Name)
Console.WriteLine("current value: {0}", currVal)
Console.WriteLine("original value: {0}", origVal)
Console.WriteLine("database value: {0}", databaseVal)
Next
Next
Catch ee As Exception
' Catch other exceptions.
Console.WriteLine(ee.Message)
Finally
Console.WriteLine("TryCatch block has finished.")
End Try

8.3.2.4.7.2. Cómo: Especificar Cuándo se Producen


Excepciones de Simultaneidad
En LINQ a SQL, se inicia una excepción ChangeConflictException cuando los objetos no se actualizan
debido a conflictos de simultaneidad optimista.

Antes de enviar cambios a la base de datos, puede especificar cuándo se deberían iniciar excepciones de
simultaneidad:

MCT: Luis Dueñas Pag 304 de 388


Manual de LINQ

Iniciar la excepción en el primer error (FailOnFirstConflict).

Finalizar todos los intentos de actualización, acumular todos los errores e informar de todos ellos
en la excepción (ContinueOnConflict).

Cuando se inicia, la excepción ChangeConflictException proporciona acceso a una colección


ChangeConflictCollection. Esta colección proporciona detalles sobre cada conflicto (asignado a un único
intento de actualización con error), incluido el acceso a la colección MemberConflicts. Cada conflicto de
miembro se asigna a un único miembro en la actualización que no pasó la comprobación de
simultaneidad.

Ejemplo
El código siguiente muestra ejemplos de ambos valores.
Dim db As New Northwnd("...") 'Create, update, delete code.
db.SubmitChanges(ConflictMode.FailOnFirstConflict) 'or
db.SubmitChanges(ConflictMode.ContinueOnConflict)

8.3.2.4.7.3. Cómo: Especificar los Miembros que se


Comprueban en Conflictos de Simultaneidad
Aplique una de las tres enumeraciones a la propiedad UpdateCheck de LINQ a SQL en un atributo
ColumnAttribute para especificar qué miembros estarán incluidos en las comprobaciones de actualización
para detectar conflictos de simultaneidad optimista.

La propiedad UpdateCheck (asignada en tiempo de diseño) se utiliza junto con las características de
simultaneidad en tiempo de ejecución de LINQ a SQL.

Nota:

Los valores de miembro originales se comparan con el estado de la base de datos actual siempre y
cuando ningún miembro se designe como IsVersion=true. Para obtener más información, consulte
IsVersion.

Para usar siempre este miembro para detectar los conflictos

1. Agregue la propiedad UpdateCheck al atributo ColumnAttribute.

2. Establezca el valor de la propiedad UpdateCheck en Always.

Para no usar nunca este miembro para detectar los conflictos

1. Agregue la propiedad UpdateCheck al atributo ColumnAttribute.

2. Establezca el valor de la propiedad UpdateCheck en Never.

Para utilizar este miembro para detectar conflictos sólo cuando la aplicación cambia
el valor del miembro

1. Agregue la propiedad UpdateCheck al atributo ColumnAttribute.

2. Establezca el valor de la propiedad UpdateCheck en WhenChanged.

MCT: Luis Dueñas Pag 305 de 388


Manual de LINQ

Ejemplo
En el ejemplo siguiente se especifica que nunca deberían probarse los objetos HomePage durante las
comprobaciones de actualización. Para obtener más información, consulte UpdateCheck.
<Column(Storage:="_HomePage", DbType:="NText",
UpdateCheck:=UpdateCheck.Never)> _
Public Property HomePage() As String
Get
Return Me._HomePage
End Get
Set(ByVal value As String)
If ((Me._HomePage <> value) = false) Then
Me.OnHomePageChanging(value)
Me.SendPropertyChanging
Me._HomePage = value
Me.SendPropertyChanged("HomePage")
Me.OnHomePageChanged
End If
End Set
End Property

8.3.2.4.7.4. Cómo: Recuperar Información sobre Conflictos


entre Entidades
Puede utilizar objetos de la clase ObjectChangeConflict para proporcionar información sobre los conflictos
que revelan las excepciones ChangeConflictException.

Ejemplo
En el ejemplo siguiente se recorre en iteración una lista de conflictos acumulados.
Dim db As New Northwnd("...")
Try
db.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch ex As ChangeConflictException
Console.WriteLine("Optimistic concurrency error.")
Console.WriteLine(ex.Message)
For Each occ As ObjectChangeConflict In db.ChangeConflicts
Dim metatable As MetaTable =
db.Mapping.GetTable(occ.Object.GetType())
Dim entityInConflict = occ.Object
Console.WriteLine("Table name: " & metatable.TableName)
Console.Write("Customer ID: ")
Console.WriteLine(entityInConflict.CustomerID)
Console.ReadLine()
Next
End Try

8.3.2.4.7.5. Cómo: Recuperar Información sobre Conflictos


entre Miembros

MCT: Luis Dueñas Pag 306 de 388


Manual de LINQ

Puede utilizar la clase MemberChangeConflict para recuperar información sobre cada uno de los
miembros en conflicto. En este mismo contexto, puede proporcionar el control personalizado del conflicto
para cualquier miembro.

Ejemplo
El código siguiente procesa una iteración en los objetos ObjectChangeConflict. Para cada objeto, procesa
una iteración en los objetos MemberChangeConflict.

Nota:

Incluya System.Reflection para proporcionar información de Member.

' Add 'Imports System.Reflection' for this section.


Dim db As New Northwnd("...")
'...
Try
db.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch ex As ChangeConflictException
Console.WriteLine("Optimistic concurrency error.")
Console.WriteLine(ex.Message)
For Each occ As ObjectChangeConflict In db.ChangeConflicts
Dim metatable As MetaTable =
db.Mapping.GetTable(occ.Object.GetType)
Dim entityInConflict As Object = occ.Object
Console.WriteLine("Table name: " & metatable.TableName)
Console.Write("Customer ID: ")
Console.WriteLine(entityInConflict.CustomerID)
For Each mcc As MemberChangeConflict In occ.MemberConflicts
Dim currVal = mcc.CurrentValue
Dim origVal = mcc.OriginalValue
Dim databaseVal = mcc.DatabaseValue
Dim mi As MemberInfo = mcc.Member
Console.WriteLine("Member: " & mi.Name)
Console.WriteLine("current value: " & currVal)
Console.WriteLine("original value: " & origVal)
Console.WriteLine("database value: " & databaseVal)
Console.ReadLine()
Next
Next
End Try

8.3.2.4.7.6. Cómo: Resolver Conflictos de Simultaneidad


mediante la Retención de Valores de Base de Datos
Para resolver las diferencias entre los valores de base de datos esperados y reales antes de intentar
reenviar los cambios, puede utilizar OverwriteCurrentValues para conservar los valores que se
encuentran en la base de datos. Los valores actuales del modelo de objetos se sobrescriben.

Nota:

MCT: Luis Dueñas Pag 307 de 388


Manual de LINQ

En todos los casos, al recuperar los datos actualizados de la base de datos se actualiza en primer
lugar el registro en el cliente. Esta acción garantiza que el siguiente intento de actualización no
producirá errores en las mismas comprobaciones de simultaneidad.

Ejemplo
En este escenario, se inicia una excepción ChangeConflictException cuando User1 intenta enviar los
cambios, porque User2 ha cambiado en ese período de tiempo las columnas Assistant y Department. En
la tabla siguiente se muestra la situación.

Administrador Assistant Department

Estado de la base de datos original cuando la Alfreds Maria Sales


consultan User1 y User2.

User1 se prepara para enviar los cambios. Alfred Marketing

User2 ya ha enviado los cambios. Mary Servicio

User1 decide resolver este conflicto haciendo que los valores más nuevos de la base de datos
sobrescriban los valores actuales del modelo de objetos.

Cuando User1 resuelve el conflicto utilizando OverwriteCurrentValues, el resultado en la base de datos es


el de la tabla siguiente:

Administrador Assistant Department

Nuevo estado tras la resolución del conflicto. Alfreds Mary Servicio


(original) (de User2) (de User2)

El código de ejemplo siguiente muestra cómo sobrescribir los valores actuales del modelo de objetos con
los valores de la base de datos. (No se produce ninguna inspección o control personalizado de los
conflictos entre miembros individuales.)
Dim db As New Northwnd("...")
Try
db.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch ex As ChangeConflictException
Console.WriteLine(ex.Message)
For Each occ As ObjectChangeConflict In db.ChangeConflicts
' All database values overwrite current values.
occ.Resolve(Data.Linq.RefreshMode.OverwriteCurrentValues)
Next
End Try

8.3.2.4.7.7. Cómo: Resolver Conflictos de Simultaneidad


mediante la Invalidación de Valores de Base de Datos
Para resolver las diferencias existentes entre los valores de base de datos esperados y reales antes de
intentar reenviar los cambios, puede utilizar KeepCurrentValues para sobrescribir los valores de base de
datos.

Ejemplo

MCT: Luis Dueñas Pag 308 de 388


Manual de LINQ

En este escenario, se inicia una excepción ChangeConflictException cuando User1 intenta enviar los
cambios, porque User2 ha cambiado en ese período de tiempo las columnas Assistant y Department. En
la tabla siguiente se muestra la situación.

Administrador Assistant Department

Estado de la base de datos original cuando la Alfreds Maria Sales


consultan User1 y User2.

User1 se prepara para enviar los cambios. Alfred Marketing

User2 ya ha enviado los cambios. Mary Servicio

User1 decide resolver este conflicto sobrescribiendo los valores de la base de datos con los valores de
miembro de cliente actuales.

Cuando User1 resuelve el conflicto utilizando KeepCurrentValues, el resultado en la base de datos es


como en la tabla siguiente:

Administrador Assistant Department

Nuevo estado tras la resolución del conflicto. Alfred Maria Marketing


(de User1) (original) (de User1)

En el siguiente ejemplo de código se muestra cómo sobrescribir los valores de base de datos con los
valores de miembro de cliente actuales. (No se produce ninguna inspección o control personalizado de
los conflictos entre miembros individuales.)
Try
db.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch ex As ChangeConflictException
Console.WriteLine(ex.Message)
For Each occ As ObjectChangeConflict In db.ChangeConflicts
' No database values are merged into current.
occ.Resolve(Data.Linq.RefreshMode.KeepCurrentValues)
Next
End Try

8.3.2.4.7.8. Cómo: Resolver Conflictos de Simultaneidad


mediante la Combinación con Valores de Base de Datos
Para resolver las diferencias entre los valores de base de datos esperados y reales antes de intentar
reenviar los cambios, puede utilizar KeepChanges para combinar los valores de base de datos con los
valores de miembro de cliente actuales.

Ejemplo
En este escenario, se inicia una excepción ChangeConflictException cuando User1 intenta enviar los
cambios, porque User2 ha cambiado en ese período de tiempo las columnas Assistant y Department. En
la tabla siguiente se muestra la situación.

Administrador Assistant Department

Estado de la base de datos original cuando la Alfreds Maria Sales


consultan User1 y User2.

MCT: Luis Dueñas Pag 309 de 388


Manual de LINQ

User1 se prepara para enviar los cambios. Alfred Marketing

User2 ya ha enviado los cambios. Mary Servicio

User1 decide resolver este conflicto combinando los valores de la base de datos con los valores de
miembro de cliente actuales. El resultado será que los valores de la base de datos se sobrescribirán
cuando el conjunto de cambios actual haya modificado también ese valor.

Cuando User1 resuelve el conflicto utilizando KeepChanges, el resultado en la base de datos es como en
la tabla siguiente:

Administrador Assistant Department

Nuevo estado tras la resolución del conflicto. Alfred Mary Marketing


(de User1) (de User2) (de User1)

En el ejemplo siguiente se muestra cómo combinar los valores de la base de datos con los valores de
miembro de cliente actuales (a menos que el cliente también haya cambiado ese valor). No se produce
ninguna inspección o control personalizado de los conflictos entre miembros individuales.
Try
db.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch ex As ChangeConflictException
Console.WriteLine(ex.Message)
For Each occ As ObjectChangeConflict In db.ChangeConflicts
' Automerge database values into current for members
' that client has not modified.
occ.Resolve(Data.Linq.RefreshMode.KeepChanges)
Next
End Try
' Submit succeeds on second try.
db.SubmitChanges(ConflictMode.FailOnFirstConflict)

8.3.2.5. Funcionalidad de Depuración


LINQ a SQL proporciona compatibilidad general para la depuración de proyectos LINQ a SQL.

LINQ a SQL también proporciona herramientas especiales para ver código SQL.

8.3.2.5.1. Cómo: Mostrar Código SQL Generado


Puede ver el código de SQL generado para las consultas y cambiar su procesamiento por medio de la
propiedad Log. Este enfoque puede ser útil para entender la funcionalidad de LINQ a SQL y para depurar
problemas concretos.

Ejemplo
En el ejemplo siguiente se utiliza la propiedad Log para mostrar el código de SQL en la ventana de la
consola antes de que se ejecute el código. Puede utilizar esta propiedad con comandos de consulta,
inserción, actualización y eliminación.
db.Log = Console.Out
Dim custQuery = From cust In db.Customers Where cust.City = "London"
Select cust
For Each custObj In custQuery

MCT: Luis Dueñas Pag 310 de 388


Manual de LINQ

Console.WriteLine(custObj.CustomerID)
Next

8.3.2.5.2. Cómo: Mostrar un Conjunto de Cambios


Puede ver los cambios de los que DataContext ha realizado un seguimiento mediante GetChangeSet.

Ejemplo
En el ejemplo siguiente se recuperan los clientes cuya ciudad es Londres, se cambia la ciudad a París y
se envían los cambios a la base de datos.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim custQuery = From cust In db.Customers Where (cust.City = "London")
Select cust
For Each custObj As Customer In custQuery
Console.WriteLine("CustomerID: {0}", custObj.CustomerID)
Console.WriteLine(vbTab & "Original value: {0}", custObj.City)
custObj.City = "Paris"
Console.WriteLine(vbTab & "Updated value: {0}", custObj.City)
Next
Dim cs As ChangeSet = db.GetChangeSet()
Console.Write("Total changes: {0}", cs)
' Freeze the console window.
Console.ReadLine()
db.SubmitChanges()

8.3.2.5.3. Cómo: Mostrar Comandos de LINQ a SQL


Utilice GetCommand para mostrar comandos SQL y otra información.

Ejemplo
En el ejemplo siguiente, la ventana de la consola muestra el resultado de la consulta, seguido de los
comandos SQL que se generan, el tipo de los comandos y el tipo de la conexión.
' Imports System.Data.Common
Dim db As New Northwnd("c:\northwnd.mdf")
Dim q = From cust In db.Customers Where cust.City = "London" Select cust
Console.WriteLine("Customers from London:")
For Each z As Customer In q
Console.WriteLine(vbTab & z.ContactName)
Next
Dim dc As DbCommand = db.GetCommand(q)
Console.WriteLine(vbNewLine & "Command Text: " & vbNewLine &
dc.CommandText)
Console.WriteLine(vbNewLine & "Command Type: {0}", dc.CommandType)
Console.WriteLine(vbNewLine & "Connection: {0}", dc.Connection)
Console.ReadLine()

8.3.2.5.4. Solución de Problemas


La siguiente información expone algunos problemas que podría encontrar en sus aplicaciones de LINQ a
SQL y proporciona sugerencias para evitar o reducir el efecto de estos problemas.

MCT: Luis Dueñas Pag 311 de 388


Manual de LINQ

Operadores de consulta estándar no admitidos


LINQ a SQL no admite todos los métodos de operador de consulta estándar (por ejemplo, ElementAt<(Of
<(TSource>)>)). Como resultado, aunque los proyectos se compilen correctamente, pueden producir
errores en tiempo de ejecución.

Problemas de memoria
Si una consulta utiliza una colección que reside en memoria y LINQ a SQL Table<(Of <(TEntity>)>), la
consulta se podría ejecutar en memoria, dependiendo del orden en el que se especifiquen las dos
colecciones. Si la consulta se debe ejecutar en memoria, entonces es necesario recuperar los datos de la
tabla de la base de datos.

Este enfoque es ineficiente y podría provocar un uso excesivo del procesador y de la memoria. Intente
evitar esas consultas multidominio.

Nombres de archivo y SQLMetal


Para especificar un nombre de archivo de entrada, agregue el nombre a la línea de comandos como
archivo de entrada. No se admite la inclusión del nombre de archivo en la cadena de conexión (mediante
la opción /conn ).

Proyectos de biblioteca de clases


El Diseñador relacional de objetos crea una cadena de conexión en el archivo app.config del proyecto. En
proyectos de biblioteca de clases, el archivo app.config no se utiliza. LINQ a SQL utiliza la cadena de
conexión proporcionada en los archivos en tiempo de diseño. Al cambiar el valor en app.config, la base
de datos a la que se conecta su aplicación no cambia.

Eliminación en cascada
LINQ a SQL no admite ni reconoce las operaciones de eliminación en cascada. Si desea eliminar una fila
de una tabla que tiene restringidas las eliminaciones, deberá hacerlo mediante una de las siguientes
opciones:

Establezca la regla ON DELETE CASCADE en la restricción de clave externa de la base de datos.

Utilice su propio código para eliminar primero los objetos secundarios que impiden que se
elimine el objeto primario.

En caso contrario, se producirá una excepción SqlException.

Expresión que no se puede consultar


Si obtiene el error "No se puede consultar una expresión de tipo [expresión]. Compruebe que no falta
ninguna referencia de ensamblado", asegúrese de lo siguiente:

Su aplicación va destinada a .NET Compact Framework 3.5.

Tiene una referencia a System.Core.dll y System.Data.Linq.dll.

Tiene una directiva Imports (Visual Basic) o using (C#) para System.Linq y System.Data.Linq.

DuplicateKeyException
Durante la depuración de un proyecto de LINQ a SQL, se podrían recorrer las relaciones de una entidad.
Al hacer esto, esos elementos se introducen en la caché, y LINQ a SQL se da cuenta de su presencia. Si,

MCT: Luis Dueñas Pag 312 de 388


Manual de LINQ

a continuación, intenta ejecutar Attach o InsertOnSubmit o un método similar que genere varias filas con
que presenten la misma clave, se producirá la excepción DuplicateKeyException.

Excepciones de concatenación de cadenas


No se admite la concatenación sobre operandos asignados a [n]text y otros [n][var]char. Se produce una
excepción para la concatenación de cadenas asignadas a los dos conjuntos diferentes de tipos.

Omitir y aceptar excepciones en SQL Server 2000


Debe usar miembros de identidad (IsPrimaryKey) cuando utilice Take<(Of <(TSource>)>) o Skip<(Of
<(TSource>)>) contra una base de datos de SQL Server 2000. La consulta debe realizarse contra una
sola tabla (es decir, no una unión), o ser una operación Distinct, Except, Intersect o Union, y no debe
incluir una operación Concat<(Of <(TSource>)>).

Este requisito no se aplica a SQL Server 2005.

GroupBy InvalidOperationException
Esta excepción se produce cuando un valor de columna es nulo en una consulta GroupBy que agrupa
mediante una expresión boolean, como group x by (Phone==@phone). Dado que la expresión es
boolean, se deduce que la clave será boolean, no nullableboolean. Cuando la comparación traducida
genera un valor nulo (null), se realizará un intento de asignar un nullableboolean a un boolean, y se
producirá la excepción.

Para evitar esta situación (suponiendo que desea tratar los valores null como falsos), utilice un enfoque
como el siguiente:

GroupBy="(Phone != null) && (Phone=@Phone)"

Método parcial OnCreated()


Se llama al método generado OnCreated() cada vez que se llama al constructor de objeto, incluido el
escenario en el que LINQ a SQL llama al constructor para realizar una copia para los valores originales.
Tenga en cuenta este comportamiento si implementa el método OnCreated() en su propia clase parcial.

8.3.2.6. Información General


En los temas de esta sección se analizan conceptos y procedimientos de LINQ a SQL que van más allá del
simple uso.

8.3.2.6.1. ADO .NET y LINQ a SQL


LINQ a SQL forma parte de la familia de tecnologías ADO.NET. Se basa en los servicios proporcionados
por el modelo de proveedor ADO.NET. Por lo tanto, se puede mezclar código LINQ a SQL con aplicaciones
ADO.NET existentes y migrar las soluciones ADO.NET actuales a LINQ a SQL. La ilustración siguiente
proporciona una visión de alto nivel de la relación.

Conexiones

MCT: Luis Dueñas Pag 313 de 388


Manual de LINQ

Puede proporcionar una conexión de ADO.NET existente al crear un DataContext de LINQ a SQL. Todas
las operaciones que se realicen en DataContext (incluidas las consultas) utilizan la conexión
proporcionada. Si la conexión ya está abierta, LINQ a SQL la deja como estaba cuando haya terminado
de usarla.
Dim conString = "Data Source=.\SQLEXPRESS; AttachDbFilename=
c:\northwind.mdf; Integrated Security=True;Connect Timeout=30;User
Instance=True"
Dim northwindCon = New SqlConnection(conString)
northwindCon.Open()
Dim db = New Northwnd("...")
Dim northwindTransaction = northwindCon.BeginTransaction()
Try
Dim cmd = New SqlCommand("UPDATE Products SET QuantityPerUnit =
'single item' WHERE ProductID = 3")
cmd.Connection = northwindCon
cmd.Transaction = northwindTransaction
cmd.ExecuteNonQuery()
db.Transaction = northwindTransaction
Dim prod1 = (From prod In db.Products Where prod.ProductID = 4).First
Dim prod2 = (From prod In db.Products Where prod.ProductID = 5).First
prod1.UnitsInStock -= 3
prod2.UnitsInStock -= 5
db.SubmitChanges()
northwindTransaction.Commit()
Catch e As Exception
Console.WriteLine(e.Message)
Console.WriteLine("Error submitting changes all changes rolled back")
End Try
northwindCon.Close()
Siempre puede tener acceso a la conexión y cerrarla usted mismo utilizando la propiedad Connection,
como en el código siguiente:
db.Connection.Close()
Transacciones
Puede proporcionar DataContext con su propia transacción de base de datos si la aplicación ya ha
iniciado la transacción y desea incluir en ella su DataContext.

El método preferido para realizar transacciones con .NET Framework es utilizar el objeto
TransactionScope. Con este enfoque, puede realizar transacciones distribuidas que funcionan entre bases
de datos y otros administradores de recursos residentes en memoria. Los ámbitos de transacción
requieren pocos recursos para iniciarse. Se promueven a sí mismos a transacciones distribuidas sólo
cuando hay varias conexiones en el ámbito de la transacción.
Using ts As New TransactionScope()
db.SubmitChanges()
ts.Complete()
End Using
No puede utilizar este enfoque para todas las bases de datos. Por ejemplo, la conexión de SqlClient no
puede promover las transacciones del sistema cuando funciona en un servidor SQL Server 2000. En su
lugar, se da de alta automáticamente en una transacción distribuida completa cada vez que detecta que
se utiliza un ámbito de transacción.

MCT: Luis Dueñas Pag 314 de 388


Manual de LINQ

Comandos SQL directos


A veces pueden darse situaciones en las que la capacidad de DataContext para realizar consultas o
enviar cambios es insuficiente para la tarea especializada que se desea realizar. En estas circunstancias,
se puede utilizar el método ExecuteQuery para emitir comandos SQL a la base de datos y convertir los
resultados de la consulta en objetos.

Por ejemplo, supongamos que los datos de la clase Customer ocupan dos tablas (customer1 y
customer2). La consulta siguiente devuelve una secuencia de objetos Customer:
Dim results As IEnumerable(Of Customer) = db.ExecuteQuery(Of Customer)
("SELECT c1.custID as CustomerID,c2.custName as ContactName" & _
"FROM customer1 AS c1, customer2 as c2 WHERE c1.custid = c2.custid")
Siempre y cuando los nombres de columna de los resultados tabulares coincidan con las propiedades de
columna de la clase de entidad, LINQ a SQL crea objetos a partir de cualquier consulta SQL.

Parameters
El método ExecuteQuery acepta parámetros: El código siguiente ejecuta una consulta parametrizada:
Dim results As IEnumerable(Of Customer) = db.ExecuteQuery(Of Customer)
("SELECT contactname FROM customers WHERE city = {0}, 'London'")
End Sub

Nota:

Los parámetros se expresan en el texto de la consulta con la misma notación con llaves utilizada por
Console.WriteLine() y String.Format(). String.Format() toma la cadena de consulta proporcionada y
sustituye los parámetros entre llaves por nombres de parámetros generados, como @p0, @p1 …,
@p(n).

8.3.2.6.2. Analizar el Código Fuente de LINQ a SQL


Con ayuda de los pasos siguientes, puede generar código fuente LINQ a SQL utilizando la base de datos
de ejemplo Northwind. Puede comparar elementos del modelo de objetos con elementos de la base de
datos para ver mejor cómo se asignan los distintos elementos.

Nota:

Los programadores que usen Visual Studio pueden utilizar el Diseñador relacional de objetos para
generar este código.

1. Si aún no tiene la base de datos de ejemplo Northwind en su equipo de desarrollo, puede


descargarla de forma gratuita.

2. Utilice la herramienta de línea de comandos SqlMetal para generar un archivo de código fuente
de Visual Basic o C#. Si escribe los comandos siguientes en un símbolo del sistema, puede generar
archivos de código fuente de Visual Basic y C# que incluyan procedimientos almacenados y
funciones:

sqlmetal /code:northwind.vb /language:vb "c:\northwnd.mdf" /sprocs /functions


/pluralize

sqlmetal /code:northwind.cs /language:csharp "c:\northwnd.mdf" /sprocs /functions


/pluralize

MCT: Luis Dueñas Pag 315 de 388


Manual de LINQ

8.3.2.6.3. Personalizar Operaciones de Inserción,


Actualización y Eliminación
De forma predeterminada, LINQ a SQL genera SQL dinámico para implementar operaciones de inserción,
lectura, actualización y eliminación. Sin embargo, en la práctica, normalmente se personaliza la
aplicación para satisfacer las necesidades de la empresa.

Nota:

Si utiliza Visual Studio, puede utilizar el Diseñador relacional de objetos para personalizar las
acciones de inserción, actualización y eliminación.

En esta sección de temas se describen las técnicas que LINQ a SQL proporciona para personalizar las
operaciones de inserción, lectura, actualización y eliminación en una aplicación.

8.3.2.6.3.1. Personalizar operaciones: Información General


De forma predeterminada, LINQ a SQL genera SQL dinámico para las operaciones de inserción,
actualización y eliminación por asignación. Sin embargo, en la práctica, normalmente se agrega lógica
empresarial propia para proporcionar seguridad, validación, etc.

Entre las técnicas de LINQ a SQL que están disponibles para personalizar estas operaciones se incluyen
las siguientes.

Opciones de carga
En las consultas es posible controlar la cantidad de datos relacionados con el destino principal que se
recuperan al establecer una conexión con la base de datos. Esta funcionalidad se implementa en gran
medida mediante el uso de DataLoadOptions.

Métodos Partial
En su asignación predeterminada, LINQ a SQL proporciona métodos parciales para ayudar a implementar
lógica empresarial propia.

Procedimientos almacenados y funciones definidas por el usuario


LINQ a SQL admite el uso de procedimientos almacenados y funciones definidas por el usuario. Los
procedimientos almacenados se utilizan con frecuencia para personalizar operaciones.

8.3.2.6.3.2. Operaciones de Inserción, Actualización y


Eliminación
En LINQ a SQL, las operaciones Insert, Update y Delete se realizan agregando, cambiando y quitando
objetos en el modelo de objetos. De forma predeterminada, LINQ a SQL convierte estas acciones a SQL y
envía los cambios a la base de datos.

LINQ a SQL proporciona la máxima flexibilidad para manipular y conservar los cambios realizados en los
objetos. En cuanto están disponibles los objetos de entidad (ya sea recuperándolos a través de una
consulta o construyéndolos nuevamente), puede cambiarlos como los objetos normales de la aplicación.
Es decir, puede cambiar sus valores, agregarlos a las colecciones y quitarlos de las mismas. LINQ a SQL
realiza un seguimiento de los cambios y está listo para volver a transmitirlos a la base de datos cuando
llame al método SubmitChanges.

MCT: Luis Dueñas Pag 316 de 388


Manual de LINQ

Nota:

LINQ a SQL no admite ni reconoce las operaciones de eliminación en cascada. Si desea eliminar una
fila de una tabla que tiene restricciones, deberá establecer la regla ON DELETE CASCADE en la
restricción FOREIGN KEY de la base de datos o bien utilizar su propio código para eliminar primero
los objetos secundarios que impiden que se elimine el objeto primario. De lo contrario, se producirá
una excepción.

En los siguientes extractos se utilizan las clases Customer y Order de la base de datos de ejemplo
Northwind. Para no extendernos demasiado, no incluimos las definiciones de clase.
Dim db As New Northwnd("…\Northwnd.mdf")
Dim cust As Customer = (From c In db.Customers Where c.CustomerID =
"ALFKI" Select c) .First()
' Change the name of the contact.
cust.ContactName = "New Contact"
' Create and add a new Order to Orders collection.
Dim ord As New Order With {.OrderDate = DateTime.Now}
cust.Orders.Add(ord)
' Delete an existing Order.
Dim ord0 As Order = cust.Orders(0)
' Removing it from the table also removes it from
' the Customer‟s list.
db.Orders.DeleteOnSubmit(ord0)
' Ask the DataContext to save all the changes.
db.SubmitChanges()
Al llamar a SubmitChanges, LINQ a SQL genera y ejecuta automáticamente los comandos SQL
necesarios para volver a transmitir los cambios a la base de datos.

Nota:

Puede invalidar este comportamiento utilizando su propia lógica personalizada, normalmente


mediante un procedimiento almacenado.
Los programadores de Visual Studio pueden usar el Diseñador relacional de objetos para desarrollar
procedimientos almacenados con este propósito.

8.3.2.6.3.3. Responsabilidades del Desarrollador en la


Invalidación del Comportamiento Predeterminado
LINQ a SQL no exige que se cumplan los requisitos siguientes, pero, en caso de no cumplirse, el
comportamiento no está definido.

El método de invalidación no debe llamar a SubmitChanges o Attach. LINQ a SQL inicia una
excepción si se llama a estos métodos en un método de invalidación.

No se pueden utilizar métodos de invalidación para iniciar, confirmar o detener una transacción.
La operación SubmitChanges se realiza bajo una transacción. Una transacción anidada interna
puede interferir con la transacción externa. Los métodos de invalidación de carga sólo pueden
iniciar una transacción después de determinar que la operación no se realiza en Transaction.

Se espera que los métodos de invalidación sigan la asignación de simultaneidad optimista


aplicable. Se espera que el método de invalidación inicie ChangeConflictException cuando se

MCT: Luis Dueñas Pag 317 de 388


Manual de LINQ

produzca un conflicto de simultaneidad optimista. LINQ a SQL detecta esta excepción para que
se pueda procesar correctamente la opción SubmitChanges proporcionada en SubmitChanges.

Se espera que los métodos de invalidación de creación (Insert) y de actualización (Update)


restablezcan los valores de las columnas generadas por la base de datos en los miembros de
objeto correspondientes cuando la operación se complete.

Por ejemplo, si Order.OrderID se asigna a una columna de identidad (clave principal


autoincrement), el método de invalidación InsertOrder() debe recuperar el identificador
generado por la base de datos y establecer el miembro Order.OrderID en ese identificador. De
igual forma, los miembros de marca de tiempo deben estar actualizados con los valores de
marca de tiempo generados por la base de datos para garantizar que los objetos actualizados
sean coherentes. Si no se propagan los valores generados por la base de datos, puede haber
incoherencias entre la base de datos y los objetos de los que DataContext realiza un
seguimiento.

Es el usuario quien debe invocar la API dinámica correcta. Por ejemplo, en el método de
invalidación de actualización, sólo se puede llamar al método ExecuteDynamicUpdate. LINQ a
SQL no detecta ni comprueba si el método dinámico invocado coincide con la operación
aplicable. Los resultados no están definidos si se llama a un método no aplicable (por ejemplo,
ExecuteDynamicDelete para un objeto que se va a actualizar).

Finalmente, se espera que el método de invalidación realice la operación indicada. Las semántica
de las operaciones de LINQ a SQL, como la carga rápida, la carga diferida y SubmitChanges
requieren que los métodos de invalidación proporcionen el servicio indicado. Por ejemplo, una
invalidación de carga que sólo devuelva una colección vacía sin comprobar el contenido en la
base de datos probablemente generará datos incoherentes.

8.3.2.6.3.4. Agregar Lógica Empresarial Utilizando Métodos


Partial
Puede personalizar el código de Visual Basic y C# generado en los proyectos de LINQ a SQL mediante el
uso de métodos parciales. El código que LINQ a SQL genera define las firmas como parte de un método
parcial. Si desea implementar el método, puede agregar un método parcial propio. Si no agrega su
propia implementación, el compilador descarta la firma de método parcial y llama a los métodos
predeterminados de LINQ a SQL.

Nota:

Si utiliza Visual Studio, puede utilizar el Diseñador relacional de objetos para agregar funciones de
validación y otras personalizaciones a las clases de entidad.

Por ejemplo, la asignación predeterminada para la clase Customer en la base de datos de ejemplo
Northwind incluye el método parcial siguiente:
Partial Private Sub OnAddressChanged()
End Sub
Puede implementar un método propio agregando código como el siguiente a su propia clase Customer
parcial:
Partial Class Customer
Private Sub OnAddressChanged()
' Insert business logic here.

MCT: Luis Dueñas Pag 318 de 388


Manual de LINQ

End Sub
End Class
Este enfoque se suele usar en LINQ a SQL para invalidar los métodos predeterminados para Insert,
Update y Delete, y para validar las propiedades durante los eventos de ciclo de vida de los objetos.

Código:
Descripción
En el ejemplo siguiente se muestra primero ExampleClass tal como se podría definir con una herramienta
que genera código, como SQLMetal, y, a continuación, se muestra cómo se podría implementar sólo uno
de los dos métodos.

Código
' Code-generating tool defines a partial class, including
' two partial methods.
Partial Class ExampleClass
Partial Private Sub OnFindingMaxOutput()
End Sub
Partial Private Sub OnFindingMinOutput()
End Sub
Sub ExportResults()
OnFindingMaxOutput()
OnFindingMinOutput()
End Sub
End Class
' Developer implements one of the partial methods. Compiler
' discards the other method.
Class ExampleClass
Private Sub OnFindingMaxOutput()
Console.WriteLine("Maximum has been found.")
End Sub
End Class
Código:
Descripción
En el ejemplo siguiente se utiliza la relación entre las entidades Shipper y Order. Observe, entre los
métodos, los métodos parciales, InsertShipper y DeleteShipper. Estos métodos invalidan los métodos
parciales predeterminados proporcionados por la asignación de LINQ a SQL.

Código
Public Shared LoadOrdersCalled As Integer = 0
Private Function LoadOrders(ByVal shipper As Shipper) As _
IEnumerable(Of Order)
LoadOrdersCalled += 1
Return Me.Orders.Where(Function(o) o.ShipVia = _
shipper.ShipperID)
End Function
Public Shared LoadShipperCalled As Integer = 0
Private Function LoadShipper(ByVal order As Order) As Shipper
LoadShipperCalled += 1
Return Me.Shippers.Single(Function(s) s.ShipperID = _
order.ShipVia)

MCT: Luis Dueñas Pag 319 de 388


Manual de LINQ

End Function
Public Shared InsertShipperCalled As Integer = 0
Private Sub InsertShipper(ByVal instance As Shipper)
InsertShipperCalled += 1
' Call a Web service to perform an insert operation.
InsertShipperService(shpr:=Nothing)
End Sub
Public Shared UpdateShipperCalled As Integer = 0
Private Sub UpdateShipper(ByVal original As Shipper, ByVal current _
As Shipper)
UpdateShipperCalled += 1
' Call a Web service to update shipper.
InsertShipperService(shpr:=Nothing)
End Sub
Public Shared DeleteShipperCalled As Boolean
Private Sub DeleteShipper(ByVal instance As Shipper)
DeleteShipperCalled = True
End Sub

8.3.2.6.4. Enlace de Datos


LINQ a SQL admite el enlace a controles comunes, como controles de cuadrícula. Concretamente, LINQ a
SQL define los modelos básicos para realizar un enlace a una cuadrícula de datos y controlar el enlace de
detalles principales, tanto para la visualización como para la actualización.

Principio subyacente
LINQ a SQL convierte las consultas LINQ a SQL para su ejecución en una base de datos. Los resultados
son IEnumerable con establecimiento inflexible de tipos. Dado que se trata de objetos CLR (Common
Language Runtime) normales, se puede usar el enlace de datos de objeto normal para mostrar los
resultados. Por otro lado, las operaciones de cambio (inserciones, actualizaciones y eliminaciones)
requieren pasos adicionales.

Operación
El enlace implícito a controles de formularios Windows Forms se logra mediante la implementación de
IListSource. Los elementos genéricos Table<(Of <(TEntity>)>) (Table<T> en C# o Table(Of T) en Visual
Basic) y DataQuery de los orígenes de datos se han actualizado para implementar IListSource. Los
motores de enlace de datos de la interfaz de usuario (formularios Windows Forms y Windows
Presentation Foundation) comprueban si su origen de datos implementa IListSource. Por consiguiente, al
escribir una modificación directa de una consulta en un origen de datos de un control, se llama
implícitamente a la generación de colecciones de LINQ a SQL, como en el ejemplo siguiente:
Dim dataGrid1 As New DataGrid()
Dim dataGrid2 As New DataGrid()
Dim dataGrid3 As New DataGrid()
Dim custQuery = From cust In db.Customers Select cust
dataGrid1.DataSource = custQuery
dataGrid2.DataSource = custQuery
dataGrid2.DataMember = "Orders"
Dim bs = New BindingSource()
bs.DataSource = custQuery
dataGrid3.DataSource = bs

MCT: Luis Dueñas Pag 320 de 388


Manual de LINQ

Lo mismo sucede con Windows Presentation Foundation:


Dim listView1 As New ListView()
Dim custQuery2 = From cust In db.Customers Select cust
Dim ItemsSource As New ListViewItem
ItemsSource = custQuery2
Las generaciones de colecciones se implementan mediante Table<(Of <(TEntity>)>) genérico y
DataQuery genérico en GetList.

Implementación de IListSource
LINQ a SQL implementa IListSource en dos ubicaciones:

El origen de datos es Table<(Of <(TEntity>)>): LINQ a SQL examina la tabla para rellenar una
colección DataBindingList que mantiene una referencia en la tabla.

El origen de datos es IQueryable<(Of <(T>)>). Hay dos escenarios:

Si LINQ a SQL encuentra el objeto Table<(Of <(TEntity>)>) subyacente de


IQueryable<(Of <(T>)>), el origen permite la edición y la situación es la misma que en el
primer punto de la lista.

Si LINQ a SQL no encuentra el objeto Table<(Of <(TEntity>)>) subyacente, el origen no


permite la edición (por ejemplo, groupby). LINQ a SQL examina la consulta para rellenar un
elemento SortableBindingList genérico, que es un objeto BindingList<(Of <(T>)>) simple
que implementa la característica de ordenación para las entidades T de una propiedad
determinada.

Colecciones especializadas
Para muchas de las características antes descritas en este documento, BindingList<(Of <(T>)>) se ha
especializado en algunas clases diferentes. Estas clases son SortableBindingList genérica y
DataBindingList genérica. Ambas se declaran como internas.

SortableBindingList genérica
Esta clase se hereda de BindingList<(Of <(T>)>) y es una versión de BindingList<(Of <(T>)>) que se
puede ordenar. La ordenación es una solución en memoria y nunca entra en contacto con la propia base
de datos. BindingList<(Of <(T>)>) implementa IBindingList, pero no admite la ordenación de forma
predeterminada. Sin embargo, BindingList<(Of <(T>)>) implementa IBindingList con los métodos
básicos virtuales. Puede invalidar estos métodos con facilidad. La clase SortableBindingList genérica
invalida SupportsSortingCore, SortPropertyCore, SortDirectionCore y ApplySortCore. ApplySort llama a
ApplySortCore y ordena la lista de elementos T de una propiedad dada.

Se producirá una excepción si la propiedad no pertenece a T.

Para la operación de ordenación, LINQ a SQL crea una clase SortableBindingList.PropertyComparer


genérica que hereda del método genérico IComparer..::.Compare e implementa un comparador
predeterminado para un tipo T dado, un objeto PropertyDescriptor y una dirección. Esta clase crea
dinámicamente un Comparer de T, donde T es PropertyType de PropertyDescriptor. Después, el
comparador predeterminado se recupera del Comparer genérico estático. Se obtiene una instancia
predeterminada mediante reflexión.

La clase SortableBindingList genérica es también la clase base para DataBindingList. La clase


SortableBindingList genérica proporciona dos métodos virtuales para suspender o reanudar el

MCT: Luis Dueñas Pag 321 de 388


Manual de LINQ

seguimiento de las operaciones de agregar o quitar elementos. Esos dos métodos se pueden utilizar para
operaciones básicas, como la ordenación, pero realmente serán implementados por clases de nivel
superior, como la clase DataBindingList genérica.

Clase DataBindingList genérica


Esta clase se hereda de la clase SortableBindingLIst genérica. La clase DataBindingList genérica
mantiene una referencia en el elemento Table genérico subyacente de la interfaz genérica IQueryable
que se utilizó para el llenado inicial de la colección. La clase DatabindingList genérica agrega el
seguimiento de las operaciones de agregar o quitar elementos a la colección mediante la invalidación de
InsertItem() y RemoveItem(). También implementa la característica de seguimiento de
suspensión/reanudación abstracta para que el seguimiento sea condicional. Esta característica permite
que la clase DataBindingList genérica se pueda aprovechar del uso polimórfico completo de la
característica de seguimiento de las clases primarias.

Enlace a EntitySets
El enlace a EntitySet es un caso especial, porque EntitySet ya es una colección que implementa
IBindingList. LINQ a SQL agrega compatibilidad con las operaciones de ordenación y cancelación
(ICancelAddNew). Una clase EntitySet utiliza una lista interna para almacenar las entidades. Esta lista
es una colección de nivel inferior basada en una matriz genérica, la clase ItemList genérica.

Agregar una característica de ordenación


Las matrices proporcionan un método de ordenación (Array.Sort()) que se puede utilizar con un
Comparer de T. LINQ a SQL utiliza la clase genérica SortableBindingList.PropertyComparer descrita
anteriormente en este tema para obtener el Comparer de la propiedad y la dirección de ordenación.
Para llamar a esta característica, se agrega un método ApplySort a la clase ItemList genérica.

En el lado EntitySet, ahora tiene que declarar la compatibilidad con la ordenación:

SupportsSorting devuelve true.

ApplySort llama a entities.ApplySort() y después a OnListChanged().

Las propiedades SortDirection y SortProperty exponen la definición de la ordenación actual, que


se almacena en miembros locales.

Almacenamiento en caché
Las consultas LINQ a SQL implementan GetList. Cuando la clase BindingSource de formularios Windows
Forms se encuentra con esta interfaz, llama a GetList() tres veces para la misma conexión. Para evitar
esta situación, LINQ a SQL implementa una caché por cada instancia para almacenar y devolver siempre
la misma colección generada.

Cancelación
IBindingList define un método AddNew que los controles utilizan para crear un nuevo elemento a partir
de una colección enlazada. El control DataGridView muestra esta característica perfectamente,
incluyendo un asterisco en el encabezado de la última fila visible. El asterisco indica que se puede
agregar un nuevo elemento.

Además de esta característica, una colección también puede implementar ICancelAddNew. Esta
característica permite a los controles cancelar o verificar la validación o no validación del nuevo elemento
editado.

MCT: Luis Dueñas Pag 322 de 388


Manual de LINQ

ICancelAddNew se implementa en todas las colecciones de LINQ a SQL enlazadas a datos


(SortableBindingList genérica y EntitySet genérica). En ambas implementaciones, el código actúa de
la forma siguiente:

Permite insertar y después quitar los elementos de la colección.

No realiza un seguimiento de los cambios hasta que la interfaz de usuario confirma la edición.

No realiza un seguimiento de los cambios hasta que la edición se cancela (CancelNew).

Permite el seguimiento de los cambios cuando se confirma la edición (EndNew).

Permite que la colección se comporte con normalidad si el nuevo elemento no procede de


AddNew.

Solución de problemas
En esta sección se describen varios elementos que podrían ayudarle a solucionar los problemas de las
aplicaciones de enlace de datos de LINQ a SQL.

Debe utilizar propiedades; utilizar sólo campos no es suficiente. Los formularios Windows Forms
requieren que se utilicen.

De forma predeterminada, los tipos de base de datos image, varbinary y timestamp se


asignan a una matriz de bytes. Dado que ToString() no se admite en este escenario, estos
objetos no se pueden mostrar.

Un miembro de clase asignado a una clave principal tiene un establecedor, pero LINQ a SQL no
admite el cambio de identidad del objeto. Por consiguiente, la clave principal/única que se usa
en la asignación no se puede actualizar en la base de datos. Un cambio en la cuadrícula produce
una excepción al llamar a SubmitChanges.

Si una entidad está enlazada en dos cuadrículas independientes (por ejemplo, una maestra y
otra de detalle), Delete en la cuadrícula maestra no se propaga a la cuadrícula de detalle.

8.3.2.6.5. Compatibilidad con la Herencia


LINQ a SQL admite la asignación de tabla única. En otras palabras, en una sola tabla de base de datos se
almacena una jerarquía de herencia completa. La tabla contiene la unión simplificada de todas las
posibles columnas de datos de toda la jerarquía. (Una unión es el resultado de combinar dos tablas en
una sola tabla que contiene las filas que estaban presentes en cualquiera de las tablas originales.) Cada
fila tiene valores nulos en las columnas que no corresponden al tipo de la instancia representada por la
fila.

La estrategia de asignación de tabla única es la representación más simple de la herencia y presenta


buenas características de rendimiento para muchas categorías diferentes de consultas.

Para implementar esta asignación en LINQ a SQL, debe especificar los atributos y las propiedades de los
atributos en la clase raíz de la jerarquía de herencia.

Los programadores que usen Visual Studio también pueden utilizar el Diseñador relacional de objetos
para asignar jerarquías de herencia.

MCT: Luis Dueñas Pag 323 de 388


Manual de LINQ

8.3.2.6.6. Llamadas a Métodos Locales


Las llamadas a métodos locales son las que se ejecutan dentro del modelo de objetos. Las llamadas a
métodos remotos son la que LINQ a SQL convierte a SQL y después transmite al motor de base de datos
para su ejecución. Las llamadas a métodos locales son necesarias cuando LINQ a SQL no puede convertir
una llamada a SQL. De lo contrario, se produce una excepción InvalidOperationException.

Ejemplo 1
En el ejemplo siguiente, se asigna una clase Order a la tabla Orders de la base de datos de ejemplo
Northwind. Se ha agregado a la clase un método de instancia local.

En la consulta 1, el constructor de la clase Order se ejecuta localmente. En la consulta 2, si LINQ a SQL


intentara convertir LocalInstanceMethod() a SQL, se produciría un error y se iniciaría una excepción
InvalidOperationException. Pero, dado que LINQ a SQL proporciona compatibilidad con las llamadas a
métodos locales, la consulta 2 no iniciará una excepción.
' Query 1.
Dim q0 = From ord In db.Orders Where ord.EmployeeID = 9 Select ord
For Each ordObj In q0
Console.WriteLine("{0}, {1}", ordObj.OrderID, ordObj.ShipVia.Value)
Next
' Query 2.
Public Function LocalInstanceMethod(ByVal x As Integer) As Integer
Return x + 1
End Function
Sub q2()
Dim db As New Northwnd("")
Dim q2 = From ord In db.Orders Where ord.EmployeeID = 9 Select
member0=ord.OrderID, member1=ord.LocalInstanceMethod(ord.ShipVia.Value)
End Sub

8.3.2.6.7. Aplicaciones Remotas y de n Niveles con LINQ a


SQL
Puede crear aplicaciones multinivel o de n niveles que utilicen LINQ a SQL. Normalmente, el contexto de
datos de LINQ a SQL, las clases de entidad y la lógica de construcción de consultas se encuentran en el
nivel intermedio como la capa de acceso a datos (DAL). La lógica empresarial y los datos no persistentes
se pueden implementar completamente en clases parciales y métodos de entidades y en el contexto de
los datos, o se pueden implementar en clases independientes.

La capa de presentación o de cliente llama a los métodos en la interfaz remota de nivel intermedio, y la
capa de acceso a datos (DAL) en ese nivel ejecutará consultas o procedimientos almacenados asignados
a métodos DataContext. El nivel intermedio devuelve los datos a los clientes generalmente como
representaciones XML de entidades u objetos proxy.

En el nivel intermedio, las entidades son creadas por el contexto de los datos, el cual realiza el
seguimiento de su estado, y administra la carga diferida desde, y el envío de los cambios a, la base de
datos. Estas entidades están "asociadas" al DataContext. Sin embargo, una vez que las entidades se
han enviado a otro nivel a través de la serialización, quedan desasociadas, lo cual significa que el
DataContext ya no realiza el seguimiento de su estado. Las entidades que el cliente devuelve para las

MCT: Luis Dueñas Pag 324 de 388


Manual de LINQ

actualizaciones se deben volver a asociar al contexto de los datos antes de que LINQ a SQL pueda enviar
los cambios a la base de datos. El cliente es responsable de devolver valores originales y/o marcas de
tiempo al nivel intermedio si se requieren para las comprobaciones de simultaneidad optimista.

En aplicaciones ASP.NET, LinqDataSource administra la mayor parte de esta complejidad.

La siguiente ilustración muestra la arquitectura básica de una aplicación de n niveles que utiliza LINQ a
SQL en la capa de acceso a datos.

8.3.2.6.7.1. Niveles de LINQ a SQL N Capas con ASP.NET


En aplicaciones de ASP.NET que usan LINQ a SQL, se utiliza el control de servidor web LinqDataSource.
El control administra la mayor parte de la lógica necesaria para consultar contra LINQ a SQL, pasar los
datos al explorador, recuperarlos y enviarlos a LINQ a SQLDataContext, que, a continuación, actualiza la
base de datos. El control, que se configura simplemente en el marcado, administra toda la transferencia
de datos entre LINQ a SQL y el explorador. Puesto que el control administra las interacciones con el nivel
de presentación, y LINQ a SQL administra la comunicación con el nivel de datos, el trabajo principal en
aplicaciones multinivel de ASP.NET se centra en escribir la lógica empresarial personalizada.

8.3.2.6.7.2. Niveles de LINQ a SQL con Servicios Web


LINQ a SQL está diseñado especialmente para su uso en el nivel intermedio de una capa de acceso a
datos (DAL) débilmente acoplada (por ejemplo, un servicio web). Si el nivel de presentación es una
página web ASP.NET, el control de servidor web LinqDataSource se utiliza para controlar la transferencia
de datos entre la interfaz de usuario y LINQ a SQL en el de nivel intermedio. Si el nivel de presentación
no es una página ASP.NET, entonces el nivel intermedio y el nivel de presentación deben realizar un
trabajo adicional para administrar la serialización y deserialización de datos.

Establecer LINQ a SQL en el nivel intermedio


En un servicio web o aplicación de n niveles, el nivel intermedio contiene el contexto de los datos y las
clases de entidad. Puede crear estas clases manualmente o mediante SQLMetal.exe o el Diseñador

MCT: Luis Dueñas Pag 325 de 388


Manual de LINQ

relacional de objetos, como se describe en otra parte de la documentación. En tiempo de diseño, tiene la
opción de hacer las clases de entidad serializables. Otra opción consiste en crear un conjunto
independiente de clases que encapsulan los datos que se van a serializar y, a continuación, proyectar en
esos tipos serializables cuando se devuelvan datos en las consultas de LINQ.

A continuación, se define la interfaz con los métodos a los que los clientes llamarán para recuperar,
insertar y actualizar datos. Los métodos de interfaz envuelven las consultas de LINQ. Puede utilizar
cualquier tipo de mecanismo de serialización para administrar las llamadas a métodos remotos y la
serialización de datos. El único requisito es que, si tiene relaciones cíclicas o bidireccionales en su modelo
de objetos, como las que existen entre Clientes y Pedidos en el modelo de objetos estándar de
Northwind, deberá utilizar un serializador que admita esa situación. Windows Communication Foundation
(WCF) DataContractSerializer admite relaciones bidireccionales, pero no así el serializador XmlSerializer
que se utiliza con servicios web que no son de WCF. Si selecciona el serializador XmlSerializer, deberá
asegurarse de que su modelo de objetos no contenga relaciones cíclicas.

Implemente sus reglas de empresa u otra lógica específica del dominio utilizando las clases parciales y
métodos de DataContext y las clases de entidad para conectar con eventos de tiempo de ejecución de
LINQ a SQL.

Definir los tipos serializables


El nivel de cliente o de presentación debe contar con las definiciones de tipo para las clases que recibirá
desde el nivel intermedio. Esos tipos pueden ser clases de entidad propiamente dichas o clases
especiales que envuelven sólo ciertos campos de las clases de entidad para la comunicación remota. En
cualquier caso, a LINQ a SQL le resulta completamente indiferente cómo el nivel de presentación
adquiere esas definiciones de tipo. Por ejemplo, el nivel de presentación podría utilizar WCF para generar
los tipos automáticamente, o podría tener una copia de una DLL en la que se definen esos tipos, o
simplemente podría definir sus propias versiones de los tipos.

Recuperar e insertar datos


El nivel intermedio define una interfaz que especifica cómo el nivel de presentación obtiene acceso a los
datos. Por ejemplo: GetProductByID(int productID) o GetCustomers(). En el nivel intermedio, el cuerpo
del método crea generalmente una nueva instancia de DataContext y ejecuta una consulta contra una o
más de su tabla. A continuación, el nivel intermedio devuelve el resultado como un IEnumerable<(Of
<(T>)>), donde T es una clase de entidad u otro tipo que se utiliza para la serialización. El nivel de
presentación nunca envía o recibe directamente variables de consulta a o desde el nivel intermedio. Los
dos niveles intercambian valores, objetos y colecciones de datos concretos. Después de haber recibido
una colección, el nivel de presentación puede utilizar LINQ a Objects para consultarla si es necesario.

Al insertar datos, el nivel de presentación puede construir un nuevo objeto y enviarlo al nivel intermedio,
o puede hacer que el nivel intermedio construya el objeto en función de los valores que proporciona. En
general, la recuperación e inserción de datos en aplicaciones de n niveles no difiere mucho del proceso
para aplicaciones de 2 niveles.

Seguimiento de cambios para actualizaciones y eliminaciones


LINQ a SQL admite simultaneidad optimista basada en marcas de tiempo (también denominadas
RowVersions) y en valores originales. Si las tablas de base de datos tienen marcas de tiempo, entonces
las actualizaciones y eliminaciones requieren poco trabajo adicional en el nivel intermedio o en el nivel de
presentación. Sin embargo, si debe utilizar valores originales para las comprobaciones de simultaneidad
optimista, entonces el nivel de presentación es el responsable de realizar el seguimiento de esos valores
y devolverlos cuando realiza las actualizaciones. Esto es debido a que en el nivel intermedio no se hace

MCT: Luis Dueñas Pag 326 de 388


Manual de LINQ

un seguimiento de los cambios que se realizan en entidades en el nivel de presentación. De hecho, la


recuperación original de una entidad y su posible actualización se realizan generalmente mediante dos
instancias completamente independientes de DataContext.

Cuanto mayor es el número de cambios que realiza el nivel de presentación, más complejo se hace
realizar el seguimiento de esos cambios y empaquetarlos de vuelta hacia el nivel intermedio. La
implementación de un mecanismo para comunicar los cambios depende completamente de la aplicación.
El único requisito es que se deben dar a LINQ a SQL aquellos valores originales que se requieren para las
comprobaciones de la simultaneidad optimista.

8.3.2.6.7.3. LINQ a SQL con Aplicaciones Cliente-Servidor


que se Corresponden Estrictamente
LINQ a SQL se puede utilizar en el nivel intermedio con clientes inteligentes fuertemente acoplados en la
capa de presentación. En escenarios que implican acceso a datos de sólo lectura, sin comprobaciones de
simultaneidad optimista, o simultaneidad optimista con marcas de tiempo, no existe mucha más
complejidad que con escenarios no remotos. Sin embargo, cuando una base de datos requiere
comprobaciones de simultaneidad optimista con valores originales, LINQ a SQL no proporciona el nivel de
compatibilidad para las conversiones de ida y vuelta de los datos que sí se encuentra en los conjuntos de
datos (DataSets). No obstante, un nivel intermedio de LINQ a SQL puede intercambiar datos con clientes
en cualquier plataforma.

LINQ a SQL en Visual Studio 2008 no proporciona ninguna infraestructura para realizar un seguimiento
del estado de las entidades una vez serializadas hacia un cliente. LINQ a SQL permite arquitecturas
orientadas a servicios donde las interacciones entre los datos y las capas de presentación son pequeñas
y relativamente atómicas, pero no realiza ninguna conversión de ida y vuelta de valores originales. Por
consiguiente, si desea utilizar un cliente inteligente fuertemente acoplado con LINQ a SQL, y una base de
datos utiliza simultaneidad optimista con valores originales, tendrá que implementar su propio
mecanismo para comunicar los cambios entre el nivel de presentación y el nivel intermedio. Depende del
diseñador del sistema decidir si tiene sentido hacer este pequeño trabajo adicional para obtener las
ventajas que proporciona LINQ a SQL en el nivel intermedio. Por otro lado, si la base de datos tiene
marcas de tiempo, no es necesario utilizar lógica personalizada para el seguimiento de los cambios.

8.3.2.6.7.4. Implementar Lógica Empresarial


El término "lógica empresarial" de este tema se refiere a cualquier regla personalizada o prueba de
validación que se aplica a los datos antes de insertarlos, actualizarlos o eliminarlos de la base de datos.
La lógica empresarial también se conoce a veces como "reglas de empresa" o "lógica del dominio". En
aplicaciones de n niveles, se diseña generalmente como una capa lógica para que se pueda modificar
independientemente de la capa de presentación o de la capa de acceso a datos. La capa de acceso a
datos puede invocar la lógica empresarial antes o después de cualquier actualización, inserción o
eliminación de datos en la base de datos.

La lógica empresarial puede ser tan simple como una validación del esquema para asegurarse de que el
tipo del campo es compatible con el tipo de la columna de la tabla. Por el contrario, también puede estar
compuesta por un conjunto de objetos que interactúan con diversos grados de complejidad. Las reglas se
pueden implementar como procedimientos almacenados en la base de datos o como objetos en
memoria. Independientemente de cómo se implemente la lógica empresarial, LINQ a SQL permite utilizar

MCT: Luis Dueñas Pag 327 de 388


Manual de LINQ

clases parciales y métodos parciales para separar la lógica empresarial del código de acceso a datos. El
siguiente diagrama muestra la relación de la lógica empresarial con las clases de LINQ a SQL:

Invocación de la lógica empresarial desde LINQ a SQL


Cuando se genera una clase de entidad en tiempo de diseño, ya sea manualmente o mediante el
Diseñador relacional de objetos o SQLMetal, se define como una clase parcial. Esto significa que puede
definir, en un archivo de código independiente, otra parte de la clase de entidad que contiene su lógica
empresarial personalizada. En tiempo de compilación, las dos partes se combinan en una única
clase. Pero si tiene que volver a generar sus clases de entidad con el Diseñador relacional de objetos o
SQLMetal, puede hacerlo; su parte de la clase no se modificará.

Las clases parciales que definen las entidades y el DataContext contienen métodos parciales. Éstos son
los puntos de extensibilidad que puede utilizar para aplicar su lógica empresarial antes y después de
cualquier actualización, inserción o eliminación para una entidad o propiedad de entidad. Los métodos
parciales se pueden ver como eventos de tiempo de compilación. El generador de código define una
firma de método y llama a los métodos de los descriptores de acceso get y set de la propiedad, al
constructor DataContext y, en algunos casos, en segundo plano, se llama a SubmitChanges. Sin
embargo, si no implementa un método parcial particular, entonces su definición y todas las referencias a
él se eliminan en tiempo de compilación.

En la definición de la implementación que escriba en su archivo de código independiente, puede ejecutar


cualquier lógica personalizada que requiera. Puede utilizar su clase parcial por sí misma como la capa de
dominio, o puede llamar desde la definición de implementación del método parcial a un objeto o varios
objetos independientes. De cualquier modo, su lógica empresarial se separa limpiamente de su código de
acceso a datos y de su código de la capa de presentación.

Examen más detallado de los puntos de extensibilidad


El ejemplo siguiente muestra parte del código generado por el Diseñador relacional de objetos para la
clase DataContext que tiene dos tablas: Customers y Orders. Observe que se definen métodos Insert,
Update y Delete para cada tabla de la clase.
Partial Public Class Northwnd
Inherits System.Data.Linq.DataContext
Private Shared mappingSource As _
System.Data.Linq.Mapping.MappingSource = New _
AttributeMappingSource
#Region "Extensibility Method Definitions"
Partial Private Sub OnCreated()
End Sub
Partial Private Sub InsertCustomer(instance As Customer)
End Sub

MCT: Luis Dueñas Pag 328 de 388


Manual de LINQ

Partial Private Sub UpdateCustomer(instance As Customer)


End Sub
Partial Private Sub DeleteCustomer(instance As Customer)
End Sub
Partial Private Sub InsertOrder(instance As [Order])
End Sub
Partial Private Sub UpdateOrder(instance As [Order])
End Sub
Partial Private Sub DeleteOrder(instance As [Order])
End Sub
#End Region
Si implementa los métodos Insert, Update y Delete en su clase parcial, el motor de ejecución de LINQ a
SQL los llamará en lugar de sus propios métodos predeterminados al llamar a SubmitChanges. Esto
permite invalidar el comportamiento predeterminado para las operaciones de crear, leer, actualizar y
eliminar datos.

Al método OnCreated se le llama en el constructor de la clase.


Public Sub New(ByVal connection As String)
MyBase.New(connection, mappingSource)
OnCreated()
End Sub
Las clases de entidad tienen tres métodos a los que llama el motor de ejecución de LINQ a SQL al crear,
cargar y validar la entidad (cuando se llama a SubmitChanges). Las clases de entidad también tienen
dos métodos parciales para cada propiedad, uno al que se llama antes de que se establezca la propiedad
y otro al que se llama después. En el ejemplo de código siguiente se muestran algunos de los métodos
generados por la clase Customer.
#Region "Extensibility Method Definitions"
Partial Private Sub OnLoaded()
End Sub
Partial Private Sub OnValidate(action As _
System.Data.Linq.ChangeAction)
End Sub
Partial Private Sub OnCreated()
End Sub
Partial Private Sub OnCustomerIDChanging(value As String)
End Sub
Partial Private Sub OnCustomerIDChanged()
End Sub
Partial Private Sub OnCompanyNameChanging(value As String)
End Sub
Partial Private Sub OnCompanyNameChanged()
End Sub
' ...Additional Changing/Changed methods for each property.
Se llama a los métodos en el descriptor de acceso set de la propiedad, como se muestra en el ejemplo
siguiente para la propiedad CustomerID:
Public Property CustomerID() As String
Set
If (String.Equals(Me._CustomerID, value) = False) Then
Me.OnCustomerIDChanging(value)
Me.SendPropertyChanging()

MCT: Luis Dueñas Pag 329 de 388


Manual de LINQ

Me._CustomerID = value
Me.SendPropertyChanged("CustomerID")
Me.OnCustomerIDChanged()
End If
End Set
End Property
En su parte de la clase, escriba una definición de implementación del método. En Visual Studio, después
de escribir partial, aparecerá información de IntelliSense para las definiciones de método de la otra parte
de la clase.
Partial Public Class Customer
Private Sub OnCustomerIDChanging(value As String)
' Perform custom validation logic here.
End Sub
End Class

8.3.2.6.8. Identidad de Objetos


Los objetos en tiempo de ejecución tienen identidades únicas. Dos variables que hacen referencia al
mismo objeto en realidad hacen referencia a la misma instancia del objeto. Por este motivo, los cambios
que se realizan a través de una variable están visibles inmediatamente a través de la otra.

Las filas de una tabla de base de datos relacional no tienen identidades únicas. Dado que cada fila tiene
una clave principal única, dos filas no comparten el mismo valor de clave. Sin embargo, este hecho
restringe sólo el contenido de la tabla de base de datos.

En realidad, la mayoría de las veces los datos se extraen de la base de datos y se colocan en un nivel
diferente, donde una aplicación trabaja con ellos. Éste es el modelo que LINQ a SQL admite. Cuando los
datos se extraen de la base de datos como filas, no se espera que dos filas que representan los mismos
datos realmente se correspondan con las mismas instancias de fila. Si consulta un cliente concreto dos
veces, obtiene dos filas de datos. Cada fila contiene la misma información.

En el caso de los objetos, se espera algo muy diferente. Se espera que, si se solicita la misma
información a DataContext varias veces, de hecho se obtendrá la misma instancia de objeto. Este
comportamiento es el esperado porque los objetos tienen un significado especial para una aplicación y se
espera que se comporten como objetos. Se han diseñado como jerarquías o gráficos. Lo que se espera es
que se recuperarán como tales y no que se recibirán múltiples instancias replicadas sólo porque se
solicitó lo mismo más de una vez.

En LINQ a SQL, DataContext administra la identidad de objeto. Siempre que se recupera una nueva fila
de la base de datos, la fila se registra en una tabla de identidad según su clave principal y se crea un
nuevo objeto. Siempre que se recupera esa misma fila, la instancia de objeto original se devuelve a la
aplicación. De esta manera, DataContext convierte el concepto de identidad desde el punto de vista de la
base de datos (es decir, claves principales) en el concepto de identidad desde el punto de vista del
lenguaje (es decir, instancias). La aplicación sólo ve el objeto en el estado en que se recuperó por
primera vez. Los nuevos datos, si son diferentes, se descartan.

LINQ a SQL utiliza este enfoque para administrar la integridad de los objetos locales de manera que se
admitan las actualizaciones optimistas. Dado que los únicos cambios que se producen después de crear
el objeto son los realizados por la aplicación, la intención de la aplicación está clara. Si en ese período de

MCT: Luis Dueñas Pag 330 de 388


Manual de LINQ

tiempo un tercero ha realizado cambios, se identifican en el momento en que se llama a


SubmitChanges().

Nota:

Si el objeto solicitado por la consulta se puede identificar con facilidad como ya recuperado, no se
ejecuta ninguna consulta. La tabla de identidad actúa como caché de todos los objetos previamente
recuperados.

Ejemplos
Primer ejemplo de almacenamiento de objetos en caché
En este ejemplo, si ejecuta la misma consulta dos veces, en cada ocasión recibe una referencia al mismo
objeto en memoria.
Dim cust1 As Customer = (From cust In db.Customers Where cust.CustomerID
= "BONAP" Select cust).First()
Dim cust2 As Customer = (From cust In db.Customers Where cust.CustomerID
= "BONAP" Select cust).First()
Segundo ejemplo de almacenamiento de objetos en caché
En este ejemplo, si ejecuta consultas diferentes que devuelven la misma fila de la base de datos, en cada
ocasión recibe una referencia al mismo objeto en memoria.
Dim cust1 As Customer = (From cust In db.Customers Where cust.CustomerID
= "BONAP" Select cust).First()
Dim cust2 As Customer = (From ord In db.Orders Where
ord.Customer.CustomerID = "BONAP" Select ord).First().Customer

8.3.2.6.9. Modelo de Objetos de LINQ a SQL


En LINQ a SQL, el modelo de datos de una base de datos relacional se asigna a un modelo de objetos
expresado en el lenguaje de programación del desarrollador. Así, las operaciones con los datos se
realizan según el modelo de objetos.

En este escenario, no ejecuta comandos de base de datos (como INSERT) en la base de datos. En su
lugar, cambia los valores y ejecuta los métodos de su modelo de objetos. Si desea consultar la base de
datos o enviar cambios, LINQ a SQL convierte sus solicitudes en los comandos SQL correctos y los envía
a la base de datos.

En la tabla siguiente se resumen los elementos más fundamentales del modelo de objetos de LINQ a SQL
y su relación con los elementos del modelo de datos relacionales:

Modelo de objetos de LINQ a SQL Modelo de datos relacionales

Clase de entidad Tabla

Miembro de clase Column

Asociación Relación de clave externa

MCT: Luis Dueñas Pag 331 de 388


Manual de LINQ

Method Procedimiento almacenado o función

Nota:

En las líneas siguientes se asume que tiene conocimientos básicos del modelo de datos relacionales y
sus reglas.

Tablas de base de datos y clases de entidad en LINQ a SQL


En LINQ a SQL, una tabla de base de datos se representa mediante una clase de entidad. Una clase de
entidad es como cualquier otra clase que se pueda crear, con la salvedad de que se anota utilizando
información especial que asocia la clase a una tabla de base de datos. Para realizar esta anotación, se
agrega un atributo personalizado (TableAttribute) a la declaración de clase, como en el ejemplo
siguiente:

Código:
<Table(Name:="Customers")> _
Public Class Customer
Public CustomerID As String
' ...
Public City As String
End Class
Sólo las instancias de clases declaradas como tablas (es decir, clases de entidad) pueden guardarse en la
base de datos.

Miembros de clase y columnas de base de datos en LINQ a SQL


Además de asociar clases a tablas, se designan campos o propiedades para representar columnas de
base de datos. Para este propósito, LINQ a SQL define el atributo ColumnAttribute, como en el ejemplo
siguiente:

Código:
<Table(Name:="Customers")> _
Public Class Customer
<Column(IsPrimaryKey:=True)> _
Public CustomerID As String
<Column()> _
Public City As String
End Class
Sólo los campos y las propiedades que estén asignados a columnas se conservan en la base de datos o
se recuperan de ella. Si no se han declarado como columnas, se consideran partes transitorias de la
lógica de aplicación.

El atributo ColumnAttribute tiene varias propiedades que se pueden utilizar para personalizar los
miembros que representan columnas (por ejemplo, para designar un miembro como representativo de
una columna de clave principal).

Asociaciones y relaciones de clave externa de base de datos en LINQ a SQL


En LINQ a SQL, las asociaciones de base de datos (como las relaciones de clave externa y clave
principal) se representan aplicando el atributo AssociationAttribute. En el segmento de código siguiente,
la clase Order contiene una propiedad Customer que tiene un atributo AssociationAttribute. Esta
propiedad y su atributo proporcionan a la clase Order una relación con la clase Customer.

MCT: Luis Dueñas Pag 332 de 388


Manual de LINQ

En el ejemplo de código siguiente se muestra la propiedad Customer de la clase Order.


Código:
<Association(Name:="FK_Orders_Customers", Storage:="_Customer",
ThisKey:="CustomerID", IsForeignKey:=true)> _
Public Property Customer() As Customer
Get
Return Me._Customer.Entity
End Get
Set
Dim previousValue As Customer = Me._Customer.Entity
If (((previousValue Is value) = false) _
OrElse (Me._Customer.HasLoadedOrAssignedValue = false)) Then
Me.SendPropertyChanging
If ((previousValue Is Nothing) = false) Then
Me._Customer.Entity = Nothing
previousValue.Orders.Remove(Me)
End If
Me._Customer.Entity = value
If ((value Is Nothing) = false) Then
value.Orders.Add(Me)
Me._CustomerID = value.CustomerID
Else
Me._CustomerID = CType(Nothing, String)
End If
Me.SendPropertyChanged("Customer")
End If
End Set
End Property
Métodos y procedimientos almacenados de base de datos en LINQ a SQL
LINQ a SQL admite los procedimientos almacenados y las funciones definidas por el usuario. En LINQ a
SQL, estas abstracciones definidas por la base de datos se asignan a objetos de cliente de tal forma que
se pueda tener acceso a ellos con establecimiento inflexible de tipos desde el código de cliente. Las
firmas de método guardan la máxima similitud con las firmas de los procedimientos y funciones que se
definen en la base de datos. Puede utilizar IntelliSense para detectar estos métodos.

Un conjunto de resultados devuelto por una llamada a un procedimiento asignado es una colección con
establecimiento inflexible de tipos.

LINQ a SQL asigna los procedimientos almacenados y las funciones a los métodos utilizando los atributos
FunctionAttribute y ParameterAttribute. Los métodos que representan procedimientos almacenados se
distinguen de los que representan funciones definidas por el usuario mediante la propiedad
IsComposable. Si esta propiedad se establece en false (valor predeterminado), el método representa un
procedimiento almacenado. Si se establece en true, el método representa una función de base de datos.

Nota:

Si utiliza Visual Studio, puede utilizar el Diseñador relacional de objetos para crear métodos
asignados a procedimientos almacenados y funciones definidas por el usuario.

Código:
' This is an example of a stored procedure in the Northwind

MCT: Luis Dueñas Pag 333 de 388


Manual de LINQ

' sample database. The IsComposable property defaults to false.


<FunctionAttribute(Name:="dbo.CustOrderHist")> _
Public Function CustOrderHist(<Parameter(Name:="CustomerID",
DbType:="NChar(5)")> ByVal customerID As String) As ISingleResult(Of
CustOrderHistResult)
Dim result As IExecuteResult = Me.ExecuteMethodCall(Me,
CType(MethodInfo.GetCurrentMethod, MethodInfo), customerID)
Return CType(result.ReturnValue, ISingleResult(Of
CustOrderHistResult))
End Function

8.3.2.6.10. Estados de Objeto y Control de Cambios


Los objetos de LINQ a SQL siempre participan en algún estado. Por ejemplo, cuando LINQ a SQL crea un
nuevo objeto, el objeto está en estado Unchanged. Un objeto nuevo que ha creado es desconocido para
DataContext y su estado es Untracked. Después de la ejecución correcta de SubmitChanges, todos los
objetos que LINQ a SQL reconoce están en estado Unchanged. (La única excepción son los objetos que
se han eliminado correctamente de la base de datos, que están en estado Deleted y no se pueden
utilizar en esa instancia de DataContext.)

Estados de objeto
La tabla siguiente enumera los posibles estados de los objetos de LINQ a SQL.

Estado Descripción

Untracked Objeto del que LINQ a SQL no realiza un seguimiento. Algunos ejemplos son:
Un objeto no consultado a través del DataContext actual (como un
objeto recientemente creado).
Un objeto creado a través de la deserialización.
Un objeto consultado a través de un DataContext diferente.

Unchanged Un objeto recuperado utilizando el DataContext actual y que se desconoce si se


ha modificado desde que se creó.

PossiblyModified Objeto asociado a un DataContext.

ToBeInserted Objeto no recuperado utilizando el DataContext actual. Esto produce una


operación INSERT en la base de datos durante la ejecución de
SubmitChanges.

ToBeUpdated Objeto que se sabe que se modificó desde que se recuperó. Esto produce una
operación UPDATE en la base de datos durante la ejecución de
SubmitChanges.

ToBeDeleted Objeto marcado para la eliminación, que produce una operación DELETE en la
base de datos durante la ejecución de SubmitChanges.

Deleted Objeto eliminado de la base de datos. Este estado es definitivo y no permite


transiciones adicionales.

Insertar objetos
Puede solicitar Inserts explícitamente mediante el método InsertOnSubmit. Como alternativa, LINQ a
SQL puede deducir los elementos Inserts mediante la búsqueda de los objetos conectados a uno de los
objetos conocidos que deben actualizarse. Por ejemplo, si agrega un objeto Untracked a EntitySet<(Of

MCT: Luis Dueñas Pag 334 de 388


Manual de LINQ

<(TEntity>)>) o establece EntityRef<(Of <(TEntity>)>) en un objeto Untracked, hace que se pueda


obtener acceso al objeto Untracked por medio de los objetos del gráfico de los que se realiza un
seguimiento. Durante el procesamiento de SubmitChanges, LINQ a SQL recorre los objetos de los que se
realiza un seguimiento y detecta cualquier objeto persistente accesible del que no se realiza un
seguimiento. Tales objetos son candidatos para la inserción en la base de datos.

Para las clases de una jerarquía de herencia, InsertOnSubmit(o) establece también el valor del miembro
designado como discriminador para que coincida con el tipo del objeto o. Si un tipo que coincide con el
valor de discriminador predeterminado, esta acción hace que dicho valor se sobrescriba con el valor
predeterminado.

Nota importante:

Un objeto agregado a Table no se encuentra en la memoria caché de identidad. La memoria caché


de identidad sólo refleja lo que se recupera de la base de datos. Después de una llamada a
InsertOnSubmit, la entidad agregada no aparece en las consultas en la base de datos hasta que se
complete SubmitChanges correctamente.

Eliminar objetos
Un objeto o del que se realiza un seguimiento se marca para la eliminación mediante una llamada a
DeleteOnSubmit(o) en el objeto Table<(Of <(TEntity>)>) adecuado. LINQ a SQL considera la eliminación
de un objeto de un EntitySet<(Of <(TEntity>)>) como una operación de actualización y el valor de clave
externa correspondiente se establece en null. El destino de la operación (o) no se elimina de su tabla.
Por ejemplo, cust.Orders.DeleteOnSubmit(ord) indica una actualización donde la relación entre cust y ord
se rompe estableciendo la clave externa ord.CustomerID en null. Esto no produce la eliminación de la fila
que corresponde a ord.
LINQ a SQL realiza el procesamiento siguiente cuando se elimina un objeto (DeleteOnSubmit) de su
tabla:

Cuando se llama a SubmitChanges, se realiza una operación DELETE para ese objeto.

La eliminación no se propaga a los objetos relacionados, estén cargados o no. Concretamente,


los objetos relacionados no se cargan para actualizar la propiedad de la relación.

Después de la ejecución correcta de SubmitChanges, los objetos se establecen en el estado


Deleted. Como resultado, no se puede utilizar el objeto ni su id en DataContext. La memoria
caché interna mantenida por una instancia de DataContext no elimina los objetos que se
recuperan o que se agregan como nuevos, incluso una vez eliminados de la base de datos.
Sólo se puede llamar a DeleteOnSubmit en un objeto del que DataContext realiza un seguimiento. Para
un objeto Untracked, se debe llamar a Attach antes de llamar a DeleteOnSubmit. Si se llama a
DeleteOnSubmit en un objeto Untracked, se inicia una excepción.

Nota:

Al quitar un objeto de una tabla, se indica a LINQ a SQL que genere un comando SQL DELETE
correspondiente en el momento de ejecutar el método SubmitChanges. Esta acción no quita el objeto
de la memoria caché ni propaga la eliminación a los objetos relacionados.
Para reclamar el id de un objeto eliminado, utilice una nueva instancia de DataContext. Para la
limpieza de los objetos relacionados, puede utilizar la característica de eliminación en cascada de la
base de datos, o eliminar los objetos relacionados manualmente.
Los objetos relacionados no tienen que eliminarse en ningún orden especial (a diferencia de lo que
sucede en la base de datos).

Actualizar objetos

MCT: Luis Dueñas Pag 335 de 388


Manual de LINQ

Las Updates se detectan observando las notificaciones de cambios. Las notificaciones se proporcionan a
través del evento PropertyChanging en los establecedores de propiedades. Cuando LINQ a SQL recibe
una notificación del primer cambio de un objeto, crea una copia del mismo y lo considera como candidato
para generar una instrucción Update.

Para los objetos que no implementan INotifyPropertyChanging, LINQ a SQL mantiene una copia de los
valores que tenían los objetos la primera vez que se materializaron. Al llamar a SubmitChanges, LINQ a
SQL compara los valores originales y actuales para decidir si se ha cambiado el objeto.

Para las actualizaciones de relaciones, la autoridad es la referencia del elemento secundario al elemento
primario (es decir, la referencia que corresponde a la clave externa). La referencia en dirección inversa
(es decir, del elemento primario al elemento secundario) es opcional. Las clases de relaciones
(EntitySet<(Of <(TEntity>)>) y EntityRef<(Of <(TEntity>)>)) garantizan que las referencias
bidireccionales son coherentes para las relaciones uno a varios y uno a uno. Si el modelo de objetos no
utiliza EntitySet<(Of <(TEntity>)>) o EntityRef<(Of <(TEntity>)>) y si la referencia inversa está
presente, es su responsabilidad mantenerla coherente con la referencia adelantada cuando se actualice
la relación.

Si actualiza la referencia requerida y la clave externa correspondiente, debe asegurarse de que


coinciden. Se producirá una excepción InvalidOperationException si ambas no están sincronizadas
cuando se llame a SubmitChanges. Aunque los cambios de los valores de clave externa son suficientes
para que se actualice la fila subyacente, debería cambiar la referencia para mantener la conectividad del
gráfico de objetos y la coherencia bidireccional de las relaciones.

8.3.2.6.11. Información General sobre la Simultaneidad


Optimista
LINQ a SQL admite el control de simultaneidad optimista. En la tabla siguiente se describen los términos
que se refieren a la simultaneidad optimista en la documentación de LINQ a SQL:

Términos Description

Simultaneidad Situación en la que dos o más usuarios intentan actualizar la misma fila de la
base de datos al mismo tiempo.

Conflicto de Situación en la cual dos o más usuarios intentan enviar valores incompatibles
simultaneidad a una o más columnas de una fila al mismo tiempo.

Control de Técnica utilizada para resolver los conflictos de simultaneidad.


simultaneidad

Control de Técnica que primero investiga si otras transacciones han cambiado los
simultaneidad valores de una fila antes de permitir que se envíen los cambios.
optimista Por otra parte, se encuentra el control de simultaneidad pesimista, que
bloquea el registro para evitar los conflictos de simultaneidad.
El control optimista se denomina así porque considera que son escasas las
posibilidades de que una transacción interfiera con otra.

Resolución de Proceso mediante el cual un elemento en conflicto se actualiza volviendo a


conflictos consultar la base de datos y resolviendo las diferencias.
Cuando un objeto se actualiza, el seguimiento de cambios de LINQ a SQL
conserva los datos siguientes:
Los valores tomados originalmente de la base de datos y utilizados

MCT: Luis Dueñas Pag 336 de 388


Manual de LINQ

para la comprobación de actualizaciones.


Los nuevos valores de la base de datos obtenidos en la consulta
posterior.
Después, LINQ a SQL determina si el objeto está en conflicto (es decir, si uno
o más de sus valores de miembro ha cambiado). Si el objeto está en
conflicto, LINQ a SQL determina cuál de sus miembros lo está.
Cualquier conflicto de miembro que LINQ a SQL detecta se agrega a una lista
de conflictos.

En el modelo de objetos de LINQ a SQL, se produce un conflicto de simultaneidad optimista cuando se


dan las dos condiciones siguientes:

El cliente intenta enviar cambios a la base de datos.

Uno o más valores de comprobación de actualizaciones se han actualizado en la base de datos


desde la última vez que el cliente los leyó.

Para resolver el conflicto, primero se detectan los miembros del objeto que están en conflicto y, después,
se decide qué hacer.

Nota:

Sólo los miembros asignados como Always o WhenChanged participan en las comprobaciones de
simultaneidad optimista. No se realiza ninguna comprobación para los miembros marcados como
Never.

Código:
Por ejemplo, en el escenario siguiente, User1 empieza a preparar una actualización consultando una fila
en la base de datos. User1 recibe una fila con los valores Alfreds, Maria y Sales.

User1 desea cambiar el valor de la columna Manager a Alfred y el valor de la columna Department a
Marketing. Antes de que User1 pudiera enviar los cambios, User2 ha enviado cambios a la base de datos.
Por lo tanto, ahora el valor de la columna Assistant ha cambiado a Mary y el valor de la columna
Department a Service.

Cuando User1 intenta enviar los cambios, se produce un error en el envío y se inicia una excepción
ChangeConflictException. Esto es consecuencia de que los valores de base de datos para la columna
Assistant y la columna Department no son los esperados. Los miembros que representan las columnas
Assistant y Department están en conflicto. En la tabla siguiente se resume la situación.

Administrador Assistant Department

Estado original Alfreds Maria Sales

User1 Alfred Marketing

User2 Mary Servicio

Lista de comprobación para detectar y resolver conflictos


Puede detectar y resolver los conflictos en cualquier nivel de detalle. Por una parte, puede resolver todos
los conflictos de una de tres maneras (vea RefreshMode) sin consideraciones adicionales. En el otro
extremo, puede designar una acción concreta para cada tipo de conflicto en cada miembro en conflicto.

Especifique o revise las opciones de UpdateCheck en su modelo de objetos.

MCT: Luis Dueñas Pag 337 de 388


Manual de LINQ

En el bloque la try/catch de la llamada a SubmitChanges, especifique en qué punto desea que se


inicien excepciones.

Determine cuántos detalles del conflicto desea recuperar e incluya el código correspondiente en
el bloque try/catch.

Indique en el código try/catch cómo desea resolver los distintos conflictos que detecte.

8.3.2.6.12. Conceptos de Consulta en LINQ a SQL


En esta sección se describen los conceptos fundamentales del diseño de consultas LINQ en LINQ a SQL.

8.3.2.6.12.1. Consultas en LINQ a SQL


Las consultas LINQ a SQL se definen utilizando la misma sintaxis que utilizaría en LINQ. La única
diferencia es que los objetos a los que se hace referencia en las consultas se asignan a elementos de una
base de datos.

LINQ a SQL convierte las consultas que se escriben en consultas SQL equivalentes y las envía al servidor
para su procesamiento. Más específicamente, la aplicación utiliza la API de LINQ a SQL para solicitar la
ejecución de la consulta. Después, el proveedor LINQ a SQL transforma la consulta en texto SQL y
delega la ejecución al proveedor ADO. El proveedor ADO devuelve los resultados de la consulta como
DataReader. El proveedor LINQ a SQL convierte los resultados de ADO en una colección IQueryable de
objetos de usuario.

En la siguiente ilustración se muestra este flujo general.

Diagrama de ejecución de la consulta

Nota:

La mayoría de los métodos y operadores de los tipos integrados de .NET Framework tienen
equivalentes directos en SQL. Los que LINQ no puede convertir generan excepciones en tiempo de
ejecución.

La tabla siguiente muestra las similitudes y diferencias entre los elementos de las consultas LINQ y LINQ
a SQL.

Item Consulta LINQ Consulta LINQ a SQL

Tipo de valor devuelto de la Interfaz genérica Interfaz genérica IQueryable


variable local que contiene la IEnumerable
consulta (para las consultas que
devuelven secuencias)

MCT: Luis Dueñas Pag 338 de 388


Manual de LINQ

Especificar el origen de datos Utiliza la cláusula From Igual


(Visual Basic) o from (C#)

Filtrar Utiliza la cláusula Igual


Where/where

Grupo Utiliza la cláusula Igual


Group…By/groupby

Seleccionar (proyectar) Utiliza la cláusula Igual


Select/select

Ejecución diferida frente a Vea Introducción a Igual


ejecución inmediata consultas con LINQ.

Implementar combinaciones Utiliza la cláusula Puede utilizar la cláusula


Join/join Join/join, pero utiliza el atributo
AssociationAttribute más
eficazmente.

Consultas con transmisión por No aplicable en un


frecuencias frente a consultas con escenario de memoria
almacenamiento en caché local

8.3.2.6.12.2. Realizar Consultas en Varias Relaciones


Las referencias a otros objetos o colecciones de otros objetos en sus definiciones de clase se
corresponden directamente con relaciones de clave externa en la base de datos. Puede utilizar estas
relaciones cuando realice consultas utilizando la notación de punto para tener acceso a las propiedades
de la relación y navegar entre los objetos. Estas operaciones de acceso se convierten en combinaciones
más complejas o subconsultas correlacionadas en el código SQL equivalente.

Por ejemplo, la consulta siguiente navega de pedidos a clientes como una manera de restringir los
resultados a sólo los pedidos de los clientes de Londres.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim londonOrderQuery = From ord In db.Orders Where ord.Customer.City =
"London" Select ord
Si no existieran propiedades de relación, tendría que escribirlas manualmente como combinaciones,
como lo haría en una consulta SQL y como muestra el código siguiente:
Dim db As New Northwnd("c:\northwnd.mdf")
Dim londOrderQuery = From cust In db.Customers Join ord In db.Orders On
cust.CustomerID Equals ord.CustomerID Select ord
Puede utilizar la propiedad de relación para definir esta relación concreta una vez. Después, puede
utilizar la sintaxis de punto más apropiada. Sin embargo, la existencia de las propiedades de relación es
más importante porque los modelos de objetos específicos del dominio se definen normalmente como
jerarquías o gráficos. Los objetos para los que programa tienen referencias a otros objetos. Es una mera
coincidencia que las relaciones de objeto a objeto se correspondan con tipos de relaciones de clave
externa en las bases de datos. Por ello, el acceso de propiedad proporciona una manera apropiada de
escribir combinaciones.

A este respecto, las propiedades de relación son más importantes en el lado de los resultados de una
consulta que como parte de la propia consulta. Una vez que la consulta ha recuperado los datos sobre un
cliente determinado, la definición de clase indica que los clientes tienen pedidos. En otras palabras, se

MCT: Luis Dueñas Pag 339 de 388


Manual de LINQ

espera que la propiedad Orders de un cliente determinado sea una colección que se llena con todos los
pedidos de ese cliente. De hecho, esa era su intención cuando definió las clases de esta manera. Espera
ver allí los pedidos aun cuando la consulta no los ha solicitado. Espera que su modelo de objetos refleje
lo que en realidad se encuentra en una extensión en memoria de la base de datos, con los objetos
relacionados inmediatamente disponibles.

Ahora que tiene las relaciones, puede escribir consultas haciendo referencia a las propiedades de relación
definidas en las clases. Estas referencias de relación se corresponden con las relaciones de clave externa
de la base de datos. Las operaciones que utilizan estas relaciones se convierten en combinaciones más
complejas en el código SQL equivalente. Siempre y cuando haya definido una relación (mediante el
atributo AssociationAttribute ), no tiene que codificar una combinación explícita en LINQ a SQL.

Para ayudar a mantener esta ilusión, LINQ a SQL implementa una técnica denominada carga diferida.
Considere la consulta SQL siguiente para proyectar una lista de pares CustomerID-OrderID:
SELECT t0.CustomerID, t1.OrderID
FROM Customers AS t0 INNER JOIN
Orders AS t1 ON t0.CustomerID = t1.CustomerID
WHERE (t0.City = @p0)
La ilustración siguiente muestra gráficamente la relación entre las tablas.

Para obtener los mismos resultados en LINQ a SQL, utilice la referencia de propiedad Orders que ya
existe en la clase Customer. La referencia Orders proporciona la información necesaria para ejecutar la
consulta y proyectar los pares CustomerID-OrderID, como en el código siguiente:
Dim db As New Northwnd("c:\northwnd.mdf")
Dim idQuery = From cust In db.Customers, ord In cust.Orders Where
cust.City = "London" Select cust.CustomerID, ord.OrderID
También puede hacer lo contrario. Es decir, puede consultar Orders y utilizar su referencia de relación
Customer para tener acceso a información sobre el objeto Customer asociado. El código siguiente
proyecta los mismos pares CustomerID-OrderID que antes, pero esta vez se consulta Orders en lugar de
Customers.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim idQuery = From ord In db.Orders Where ord.Customer.City = "London"
Select ord.CustomerID, ord.OrderID

8.3.2.6.12.3. Comparación Entre la Ejecución Remota y


Local de Consultas
Puede decidir ejecutar las consultas de manera remota (es decir, el motor de base de datos ejecuta la
consulta en la base de datos) o localmente (LINQ a SQL ejecuta la consulta en una memoria caché local).

Ejecución remota

MCT: Luis Dueñas Pag 340 de 388


Manual de LINQ

Considere la siguiente consulta:


Dim db As New Northwnd("c:\northwnd.mdf")
Dim c As Customer = (From cust In db.Customers Where cust.CustomerID =
19283).First()
Dim orders = From ord In c.Orders Where ord.ShippedDate.Value.Year = 1998
For Each nextOrder In orders
' Do something.
Next
Si su base de datos tuviese miles de filas de pedidos, no desearía recuperarlos todos para procesar un
subconjunto pequeño. En LINQ a SQL, la clase EntitySet<(Of <(TEntity>)>) implementa la interfaz
IQueryable. Este enfoque garantiza que ese tipo de consultas se puedan ejecutar de manera remota. De
esta técnica se derivan dos ventajas principales:

No se recuperan datos innecesarios.

Una consulta ejecutada por el motor de base de datos suele ser más eficaz debido a los índices
de la base de datos.

Ejecución local
En otras situaciones, podría desear tener el conjunto completo de entidades relacionadas en la memoria
caché local. Para este propósito, EntitySet<(Of <(TEntity>)>) proporciona el método Load para cargar
explícitamente todos los miembros de EntitySet<(Of <(TEntity>)>).

Si EntitySet<(Of <(TEntity>)>) ya está cargado, las consultas posteriores se ejecutan localmente. Este
enfoque ayuda en dos sentidos:

Si se debe utilizar el conjunto completo localmente o varias veces, puede evitar las consultas
remotas y la latencia asociada a las mismas.

La entidad se puede serializar como una entidad completa.

El fragmento de código siguiente muestra cómo se puede obtener la ejecución local:


Dim db As New Northwnd("c:\northwnd.mdf")
Dim c As Customer = (From cust In db.Customers Where cust.CustomerID =
19283).First
c.Orders.Load()
Dim orders = From ord In c.Orders _
Where ord.ShippedDate.Value.Year = 1998
For Each nextOrder In orders
' Do something.
Next
Comparación
Estas dos funciones proporcionan una combinación eficaz de opciones: ejecución remota para las
colecciones grandes y ejecución local para las colecciones pequeñas o si se necesita la colección
completa. La ejecución remota se implementa a través de IQueryable y la ejecución local en una
colección IEnumerable<(Of <(T>)>) en memoria.

Consultas en conjuntos no ordenados


Observe la importante diferencia que existe entre una colección local que implementa List<(Of <(T>)>)
y una colección que proporciona consultas remotas ejecutadas en conjuntos no ordenados en una base
de datos relacional. Los métodos List<(Of <(T>)>), como los que utilizan valores de índice, requieren

MCT: Luis Dueñas Pag 341 de 388


Manual de LINQ

semántica de lista, que normalmente no se puede obtener a través de una consulta remota en un
conjunto no ordenado. Por esta razón, tales métodos cargan implícitamente EntitySet<(Of <(TEntity>)>)
para permitir la ejecución local.

8.3.2.6.12.4. Comparación Entre Carga Aplazada y Carga


Inmediata
Al consultar un objeto, en realidad se recupera únicamente el objeto solicitado. No se capturan los
objetos relacionados al mismo tiempo automáticamente. No hay forma de saber si los objetos
relacionados están ya cargados, porque, si se intenta tener acceso a ellos, se genera una solicitud que
los recupera.

Por ejemplo, quizás desee consultar un conjunto determinado de pedidos y después, sólo en algunas
ocasiones, enviar una notificación por correo electrónico a determinados clientes. Inicialmente, no está
obligado a recuperar todos los datos de clientes con todos los pedidos. Puede utilizar la carga diferida
para aplazar la recuperación de información adicional hasta que ésta no sea realmente necesaria.
Considere el ejemplo siguiente:
Dim db As New Northwnd("c:\northwnd.mdf")
Dim notificationQuery = From ord In db.Orders Where ord.ShipVia = 3
Select ord
For Each ordObj As Order In notificationQuery
If ordObj.Freight > 200 Then
SendCustomerNotification(ordObj.Customer)
ProcessOrder(ordObj)
End If
Next
También podría darse el caso contrario. Es posible que una aplicación necesite ver al mismo tiempo los
datos de los clientes y los pedidos. Sabe que necesita ambos conjuntos de datos. Sabe que su aplicación
necesita información de los pedidos de cada cliente en cuanto obtiene los resultados. Seguramente no
deseará enviar consultas individuales para los pedidos de cada cliente. Lo que realmente desea es
recuperar los datos de pedidos junto con los clientes.
Dim db As New Northwnd("c:\northwnd.mdf")
db.DeferredLoadingEnabled = False
Dim custQuery = From cust In db.Customers Where cust.City = "London"
Select cust
For Each custObj As Customer In custQuery
For Each ordObj As Order In custObj.Orders
ProcessCustomerOrder(ordObj)
Next
Next
También puede combinar clientes y pedidos en una consulta si forma el producto cruzado y recupera
todos los elementos de datos relativos como una gran proyección. Sin embargo, estos resultados no son
entidades. Las entidades son objetos que tienen identidad y que se pueden modificar, mientras que estos
resultados serían proyecciones que no se pueden cambiar ni conservar. Lo que es peor, recuperaría una
gran cantidad de datos redundantes, ya que cada cliente se repite para cada pedido en el resultado
simplificado de la combinación.

Lo que realmente necesita es una manera de recuperar al mismo tiempo un conjunto de objetos
relacionados. El conjunto es una sección delineada de un gráfico, de tal forma que nunca recuperaría

MCT: Luis Dueñas Pag 342 de 388


Manual de LINQ

más o menos de lo necesario para el uso previsto. Con este fin, LINQ a SQL proporciona
DataLoadOptions para la carga inmediata de una región de un modelo de objetos. Entre los métodos, se
incluyen:

El método LoadWith, para cargar inmediatamente los datos relacionados con el destino principal.

El método AssociateWith, para filtrar los objetos recuperados para una relación determinada.

8.3.2.6.13. Seguridad en LINQ a SQL


Siempre existen riesgos de seguridad en la conexión a una base de datos, sobre todo si la contraseña se
puede leer con claridad en la cadena de conexión.

Para minimizar tales riesgos, utilice la seguridad integrada para realizar una conexión confiable con SQL
Server. Con este enfoque no es necesario almacenar una contraseña en la cadena de conexión.

8.3.2.6.14. Serialización
En este tema se describe la funcionalidad de serialización de LINQ a SQL. En los párrafos siguientes se
proporciona información sobre cómo agregar la característica de serialización durante la generación de
código en tiempo de diseño y el comportamiento de serialización en tiempo de ejecución de las clases
LINQ a SQL.

Puede agregar código de serialización en tiempo de diseño mediante cualquiera de estos métodos:

En el Diseñador relacional de objetos, cambie la propiedad Modo de serialización a


Unidirectional.

En la línea de comandos de SQLMetal, agregue la opción /serialization.

Información general
El código generado por LINQ a SQL proporciona funcionalidad de carga diferida de forma
predeterminada. La carga diferida es muy apropiada en el nivel intermedio, para una carga de datos
transparente a petición. Sin embargo, da problemas para la serialización, porque el serializador activa la
carga diferida esté prevista o no. De hecho, cuando se serializa un objeto, se serializa su cierre transitivo
en todas las referencias con carga diferida salientes.

La característica de serialización de LINQ a SQL resuelve este problema, principalmente a través de dos
mecanismos:

Un modo DataContext para desactivar la carga diferida (ObjectTrackingEnabled).

Un modificador de generación de código para generar los atributos


System.Runtime.Serialization..::.DataContractAttribute y
System.Runtime.Serialization..::.DataMemberAttribute en las entidades generadas. Este
aspecto, que incluye el comportamiento de la carga diferida de las clases en la serialización, es
el asunto principal de este tema.

Definitions
Serializador DataContract: serializador predeterminado utilizado por el componente WCF de .NET
Framework 3.0 o versiones posteriores.

MCT: Luis Dueñas Pag 343 de 388


Manual de LINQ

Serialización unidireccional: versión serializada de una clase que contiene sólo una propiedad de
asociación unidireccional (para evitar un ciclo). Por convención, se marca para la serialización la
propiedad que se encuentra en el lado primario de una relación de clave externa y clave
principal. No se serializa el otro lado en una asociación bidireccional.

La serialización unidireccional es el único tipo de serialización que LINQ a SQL admite.

Ejemplo de código
El código siguiente utiliza las clases Customer y Order tradicionales de la base de datos de ejemplo
Northwind y muestra cómo estas clases se decoran con atributos de serialización.
' The class is decorated with the DataContract attribute.
<Table(Name:="dbo.Customers"), DataContract()> _
Partial Public Class Customer
Implements System.ComponentModel.INotifyPropertyChanging,
System.ComponentModel.INotifyPropertyChanged
' Private fields are not decorated with any attributes,
' and are elided.
Private _CustomerID As String
' Public properties are decorated with the DataMember
' attribute and the Order property specifying the
' serial number. See the Order class later in this topic
' for exceptions
<Column(Storage:="_CustomerID", DbType:="NChar(5) NOT NULL",
CanBeNull:=false, IsPrimaryKey:=true),DataMember(Order:=1)> _
Public Property CustomerID() As String
Get
Return Me._CustomerID
End Get
Set
If ((Me._CustomerID = value) = false) Then
Me.OnCustomerIDChanging(value)
Me.SendPropertyChanging
Me._CustomerID = value
Me.SendPropertyChanged("CustomerID")
Me.OnCustomerIDChanged
End If
End Set
End Property
' The following Association property is decorated with
' DataMember because it is the parent side of the
' relationship. The reverse property in the Order
' class does not have a DataMember attribute. This
' factor prevents a 'cycle.'
<Association(Name:="FK_Orders_Customers", Storage:="_Orders",
OtherKey:="CustomerID", DeleteRule:="NO ACTION"), _
DataMember(Order:=13)> _
Public Property Orders() As EntitySet(Of [Order])
Get
Return Me._Orders
End Get

MCT: Luis Dueñas Pag 344 de 388


Manual de LINQ

Set(ByVal value As EntitySet(Of [Order]))


Me._Orders.Assign(Value)
End Set
End Property
Para la clase Order del ejemplo siguiente, por brevedad sólo se muestra la propiedad de asociación
inversa que corresponde a la clase Customer. No tiene un atributo DataMember para evitar un ciclo.
' The class for the Orders table is also decorated with the
' DataContract attribute.
<Table(Name:="dbo.Orders"), DataContract()> _
Partial Public Class [Order]
Implements System.ComponentModel.INotifyPropertyChanging,
System.ComponentModel.INotifyPropertyChanged
' Private fields for the Orders table are not decorated with
' any attributes, and are elided.
Private _CustomerID As String
' Public properties are decorated with the DataMember
' attribute.
' The reverse Association property on the side of the
' foreign key does not have the DataMember attribute.
<Association(Name:="FK_Orders_Customers", Storage:="_Customer",
ThisKey:="CustomerID", IsForeignKey:=true)> _
Public Property Customer() As Customer
Cómo serializar entidades
Puede serializar las entidades del código que se incluía en la sección anterior de la manera siguiente:
Dim db As New Northwnd("...")
Dim cust = (From c In db.Customers Where c.CustomerID = "ALFKI").Single
Dim dcs As New DataContractSerializer(GetType(Customer))
Dim sb As StringBuilder = New StringBuilder()
Dim writer As XmlWriter = XmlWriter.Create(sb)
dcs.WriteObject(writer, cust)
writer.Close()
Dim xml As String = sb.ToString()
Relaciones auto-recursivas
Las relaciones auto-recursivas siguen el mismo modelo. La propiedad de asociación que corresponde a la
clave externa no tiene un atributo DataMember, mientras que la propiedad primaria sí lo tiene.

Considere la clase siguiente, que tiene dos relaciones auto-recursivas: Employee.Manager/Reports y


Employee.Mentor/Mentees.
' No DataMember attribute
Public Manager As Employee
<DataMember(Order:=3)> _
Public Reports As EntitySet(Of Employee)
' No DataMember attribute
Public Mentor As Employee
<DataMember(Order:=5)> _
Public Mentees As EntitySet(Of Employee)

8.3.2.6.15. Procedimientos Almacenados

MCT: Luis Dueñas Pag 345 de 388


Manual de LINQ

LINQ a SQL utiliza métodos del modelo de objetos para representar procedimientos almacenados en la
base de datos. Los métodos se designan como procedimientos almacenados aplicando el atributo
FunctionAttribute y, si es necesario, el atributo ParameterAttribute.

Los programadores de Visual Studio normalmente usarán el Diseñador relacional de objetos para asignar
procedimientos almacenados. Los temas de esta sección muestran cómo formar estos métodos y cómo
llamarlos en una aplicación si es usted quien escribe el código.

8.3.2.6.15.1. Cómo: Usar Procedimientos Almacenados


para Devolver Conjuntos de Filas
Este ejemplo devuelve un conjunto de filas de la base de datos e incluye un parámetro de entrada para
filtrar el resultado.

Al ejecutar un procedimiento almacenado que devuelve un conjunto de filas, se utiliza una clase de
resultado que almacena los resultados devueltos del procedimiento almacenado.

Ejemplo
El ejemplo siguiente representa un procedimiento almacenado que devuelve filas de clientes y utiliza un
parámetro de entrada para devolver sólo aquellas filas en las que figura "London" como la ciudad del
cliente. En el ejemplo se supone que hay una clase CustomersByCityResult enumerable.
CREATE PROCEDURE [dbo].[Customers By City]
(@param1 NVARCHAR(20))
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SELECT CustomerID, ContactName, CompanyName, City from Customers
as c where c.City=@param1
END

<FunctionAttribute(Name:="dbo.Customers By City")> _
Public Function CustomersByCity(<Parameter(DbType:= "NVarChar(20)")>
ByVal param1 As String) As ISingleResult(Of CustomersByCityResult)
Dim result As IExecuteResult = Me.ExecuteMethodCall(Me,
CType(MethodInfo.GetCurrentMethod, MethodInfo), param1)
Return CType(result.ReturnValue, ISingleResult(Of
CustomersByCityResult))
End Function
Sub ReturnRowset()
' Call the stored procedure.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim result As IEnumerable(Of CustomersByCityResult) = _
db.CustomersByCity("London")
For Each cust As CustomersByCityResult In result
Console.WriteLine("CustID={0};City={1}",cust.CustomerID,cust.City)
Next
End Sub

MCT: Luis Dueñas Pag 346 de 388


Manual de LINQ

8.3.2.6.15.2. Cómo: Usar Procedimientos Almacenados que


Toman Parámetros
LINQ a SQL asigna los parámetros de salida a parámetros de referencia y, para los tipos de valor,
declara el parámetro como que acepta valores NULL.

Ejemplo
En el ejemplo siguiente se utiliza un parámetro de entrada único (el identificador de cliente) y se
devuelve un parámetro de salida (las ventas totales para ese cliente).
CREATE PROCEDURE [dbo].[CustOrderTotal]
@CustomerID nchar(5),
@TotalSales money OUTPUT
AS
SELECT @TotalSales = SUM(OD.UNITPRICE*(1-OD.DISCOUNT) * OD.QUANTITY)
FROM ORDERS O, "ORDER DETAILS" OD
where O.CUSTOMERID = @CustomerID AND O.ORDERID = OD.ORDERID

<FunctionAttribute(Name:="dbo.CustOrderTotal")> _
Public Function CustOrderTotal(<Parameter(Name:="CustomerID",
DbType:="NChar(5)")> ByVal customerID As String,
<Parameter(Name:="TotalSales", DbType:="Money")> ByRef totalSales As
System.Nullable(Of Decimal)) As <Parameter(DbType:="Int")> Integer
Dim result As IExecuteResult = Me.ExecuteMethodCall(Me,
CType(MethodInfo.GetCurrentMethod, MethodInfo), customerID, totalSales)
totalSales = CType(result.GetParameterValue(1), System.Nullable(Of
Decimal))
Return CType(result.ReturnValue, Integer)
End Function
La llamada a este procedimiento almacenado sería como sigue:
Dim db As New Northwnd("C:\...\northwnd.mdf")
Dim totalSales As Decimal? = 0
db.CustOrderTotal("alfki", totalSales)
Console.WriteLine(totalSales)

8.3.2.6.15.3. Cómo: Usar Procedimientos Almacenados


Asignados para Formas de Resultados Múltiples
Cuando un procedimiento almacenado puede devolver varias formas de resultados, el tipo de valor
devuelto no puede tener un establecimiento inflexible de tipos para una forma de proyección única.
Aunque LINQ a SQL pueden generar todos los tipos de proyección posibles, no tiene forma de saber el
orden en el que se devolverán.

Compare este escenario con los procedimientos almacenados que generan varias formas de resultados
secuencialmente.

El atributo ResultTypeAttribute se aplica a los procedimientos almacenados que devuelven varios tipos de
resultados para especificar el conjunto de tipos que el procedimiento puede devolver.

MCT: Luis Dueñas Pag 347 de 388


Manual de LINQ

Ejemplo
En el siguiente ejemplo de código SQL, la forma del resultado depende de la entrada (shape =1 o shape
= 2). No se sabe qué proyección se devolverá primero.
CREATE PROCEDURE VariableResultShapes(@shape int)
AS
if(@shape = 1)
select CustomerID, ContactTitle, CompanyName from customers
else if(@shape = 2)
select OrderID, ShipName from orders

<FunctionAttribute(Name:="dbo.VariableResultShapes"), _
ResultType(GetType(VariableResultShapesResult1)), _
ResultType(GetType(VariableResultShapesResult2))> _
Public Function VariableResultShapes(<Parameter(DbType:="Int")> ByVal
shape As System.Nullable(Of Integer)) As IMultipleResults
Dim result As IExecuteResult = Me.ExecuteMethodCall(Me,
CType(MethodInfo.GetCurrentMethod, MethodInfo), shape)
Return CType(result.ReturnValue, IMultipleResults)
End Function
Usaría código similar al siguiente para ejecutar este procedimiento almacenado.

Nota:

Debe utilizar el modelo GetResult<(Of <(TElement>)>) para obtener un enumerador del tipo
correcto, basado en su conocimiento del procedimiento almacenado.

Dim db As New Northwnd("c:\northwnd.mdf")


' Assign the results of the procedure with an argument
' of (1) to local variable 'result'.
Dim result As IMultipleResults = db.VariableResultShapes(1)
' Iterate through the list and write results (the company name)
' to the console.
For Each compName As VariableResultShapesResult1 _
In result.GetResult(Of VariableResultShapesResult1)()
Console.WriteLine(compName.CompanyName)
Next
' Pause to view company names; press Enter to continue.
Console.ReadLine()
' Assign the results of the procedure with an argument
' of (2) to local variable 'result.'
Dim result2 As IMultipleResults = db.VariableResultShapes(2)
' Iterate through the list and write results (the order IDs)
' to the console.
For Each ord As VariableResultShapesResult2 _
In result2.GetResult(Of VariableResultShapesResult2)()
Console.WriteLine(ord.OrderID)
Next

8.3.2.6.15.4. Cómo: Usar Procedimientos Almacenados


Asignados para Formas de Resultados Secuenciales

MCT: Luis Dueñas Pag 348 de 388


Manual de LINQ

Este tipo de procedimiento almacenado puede generar más de una forma de resultado, pero se sabe en
qué orden se devuelven los resultados. Compare este escenario con el escenario donde no se sabe el
orden de los resultados devueltos.

Ejemplo
A continuación se muestra el código T-SQL de un procedimiento almacenado que devuelve varias formas
de resultados secuencialmente:
CREATE PROCEDURE MultipleResultTypesSequentially
AS
select * from products
select * from customers

<FunctionAttribute(Name:="dbo.MultipleResultTypesSequentially"), _
ResultType(GetType(MultipleResultTypesSequentiallyResult1)), _
ResultType(GetType(MultipleResultTypesSequentiallyResult2))> _
Public Function MultipleResultTypesSequentially() As IMultipleResults
Dim result As IExecuteResult = Me.ExecuteMethodCall(Me,
CType(MethodInfo.GetCurrentMethod, MethodInfo))
Return CType(result.ReturnValue, IMultipleResults)
End Function
Usaría código similar al siguiente para ejecutar este procedimiento almacenado.
Dim db As New Northwnd("c:\northwnd.mdf")
Dim sprocResults As IMultipleResults = db.MultipleResultTypesSequentially
' First read products.
For Each prod As Product In sprocResults.GetResult(Of Product)()
Console.WriteLine(prod.ProductID)
Next
' Next read customers.
For Each cust As Customer In sprocResults.GetResult(Of Customer)()
Console.WriteLine(cust.CustomerID)
Next

8.3.2.6.15.5. Personalizar las Operaciones Mediante


Procedimientos Almacenados
Los procedimientos almacenados representan un enfoque común para invalidar el comportamiento
predeterminado. Los ejemplos de este tema muestran cómo utilizar los contenedores de método
generados para los procedimientos almacenados, y cómo se puede llamar directamente a los
procedimientos almacenados.

Si utiliza Visual Studio, puede usar el Diseñador relacional de objetos para asignar los procedimientos
almacenados para realizar operaciones de inserción, actualización y eliminación.

Nota:

Para volver a leer los valores generados por la base de datos, utilice parámetros de salida en los
procedimientos almacenados. Si no puede usar parámetros de salida, escriba una implementación de
método parcial en lugar de confiar en las invalidaciones generadas por el Diseñador relacional de
objetos. Los miembros asignados a los valores generados por la base de datos deben establecerse en
los valores adecuados una vez que se ha completado correctamente una operación INSERT o
UPDATE.

MCT: Luis Dueñas Pag 349 de 388


Manual de LINQ

Código:
Description
En el ejemplo siguiente se da por hecho que la clase Northwind contiene dos métodos para llamar a los
procedimientos almacenados que se utilizan para las invalidaciones en una clase derivada.

Código
<[Function]()> _
Public Function CustomerOrders(<Parameter(Name:="CustomerID",
DbType:="NChar(5)")> ByVal customerID As String) As IEnumerable(Of Order)
Dim result As IExecuteResult = Me.ExecuteMethodCall(Me, _
(CType(MethodInfo.GetCurrentMethod(), MethodInfo)), _
customerID)
Return CType(result.ReturnValue, IEnumerable(Of Order))
End Function
<[Function]()> _
Public Function CustomerById(<Parameter(Name:="CustomerID",
DbType:="NChar(5)")> ByVal customerID As String) As IEnumerable(Of
Customer)
Dim result As IExecuteResult = Me.ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID)
Return CType(result.ReturnValue, IEnumerable(Of Customer))
End Function
Código:
Description
La clase siguiente utiliza estos métodos para la invalidación.

Código
Public Class NorthwindThroughSprocs : Inherits Northwnd
Sub New()
MyBase.New("")
End Sub
' Override loading of Customer.Orders by using method wrapper.
Private Function LoadOrders(ByVal customer As Customer) As _
IEnumerable(Of Order)
Return Me.CustomerOrders(customer.CustomerID)
End Function
' Override loading of Order.Customer by using method wrapper.
Private Function LoadCustomer(ByVal order As Order) As Customer
Return Me.CustomerById(order.CustomerID).Single()
End Function
' Override INSERT operation on Customer by calling the
' stored procedure directly.
Private Sub InsertCustomer(ByVal customer As Customer)
' Call the INSERT stored procedure directly.
Me.ExecuteCommand("exec sp_insert_customer …")
End Sub
' The UPDATE override works similarly, that is, by
' calling the stored procedure directly.
Private Sub UpdateCustomer(ByVal original As Customer, ByVal _
current As Customer)

MCT: Luis Dueñas Pag 350 de 388


Manual de LINQ

' Call the UPDATE stored procedure by using current


' and original values.
Me.ExecuteCommand("exec sp_update_customer …")
End Sub
' The DELETE override works similarly.
Private Sub DeleteCustomer(ByVal customer As Customer)
' Call the DELETE stored procedure directly.
Me.ExecuteCommand("exec sp_delete_customer …")
End Sub
End Class
Código:
Description
Puede utilizar NorthwindThroughSprocs de la misma forma que utilizaría Northwnd.

Código
Dim db As New NorthwindThroughSprocs()
Dim custQuery = From cust In db.Customers Where cust.City = "London"
Select cust
For Each custObj In custQuery
' Deferred loading of cust.Orders uses the override LoadOrders.
For Each ord In custObj.Orders
' ...
' Make some changes to customers/orders.
' Overrides for Customer are called during the execution
' of the following:
db.SubmitChanges()
Next
Next

8.3.2.6.15.6. Personalizar las Operaciones Mediante el Uso


Exclusivo de Procedimientos Almacenados
Un escenario común es obtener acceso a los datos utilizando únicamente procedimientos almacenados.

Código:
Description
Puede modificar el ejemplo proporcionado en Personalizar las operaciones mediante procedimientos
almacenados (LINQ a SQL) reemplazando incluso la primera consulta (que provoca la ejecución de SQL
dinámico) con una llamada a método que incluya un procedimiento almacenado.

Supongamos que CustomersByCity es el método, como en el siguiente ejemplo.

Código
<[Function]()> _
Public Function CustomersByCity(<Parameter(Name:="City",
DbType:="NVarChar(15)")> ByVal city As String) As IEnumerable(Of
Customer)
Dim result = Me.ExecuteMethodCall(Me, _
(CType(MethodInfo.GetCurrentMethod(), IEnumerable(Of _
Customer))), city)

MCT: Luis Dueñas Pag 351 de 388


Manual de LINQ

Return CType(result.ReturnValue, IEnumerable(Of Customer))


End Function
El código siguiente se ejecuta sin SQL dinámico.
Dim db As New Northwnd("...")
' Use a method call (stored procedure wrapper) instead of
' a LINQ query against the database.
Dim custQuery = db.CustomersByCity("London")
For Each custObj In custQuery
' Deferred loading of custObj.Orders uses the override
' LoadOrders. There is no dynamic SQL.
For Each ord In custObj.Orders
' Make some changes to customers/orders.
' Overrides for Customer are called during the execution
' of the following:
db.SubmitChanges()
Next
Next

8.3.2.6.16. Transacciones
LINQ a SQL admite tres modelos de transacción distintos. A continuación se enumeran estos modelos por
orden de comprobaciones realizadas.

Transacción local explícita


Cuando se llama a SubmitChanges, si la propiedad Transaction se establece en una transacción
(IDbTransaction), la llamada a SubmitChanges se ejecuta en el contexto de la misma transacción.

Es su responsabilidad confirmar o revertir la transacción después de su correcta ejecución. La conexión


que corresponde a la transacción debe coincidir con la conexión utilizada para construir DataContext. Si
se utiliza una conexión diferente, se inicia una excepción.

Transacción distribuible explícita


Puede llamar a las API de LINQ a SQL (incluidas, entre otras, SubmitChanges) en el ámbito de una
Transaction activa. LINQ a SQL detecta que la llamada se encuentra en el ámbito de una transacción y
no crea una transacción nueva. LINQ a SQL también evita que se cierre la conexión en este caso. Puede
ejecutar consultas y el método SubmitChanges en el contexto de este tipo de transacción.

Transacción implícita
Al llamar a SubmitChanges, LINQ a SQL comprueba si la llamada se encuentra en el ámbito de una
Transaction o si la propiedad Transaction (IDbTransaction) está establecida en una transacción local
iniciada por el usuario. Si no encuentra ninguna de estas transacciones, LINQ a SQL inicia una
transacción local (IDbTransaction) y la usa para ejecutar los comandos SQL generados. Cuando se han
completado todos los comandos SQL correctamente, LINQ a SQL confirma la transacción local y devuelve
un valor.

8.3.2.6.17. Discordancias de Sistemas de Tipos


LINQ a SQL automatiza la mayor parte de la conversión entre un modelo de objetos y el sistema de
administración de bases de datos (DBMS) SQL. No obstante, algunas situaciones impiden la exactitud de
la conversión.

MCT: Luis Dueñas Pag 352 de 388


Manual de LINQ

El propósito de este tema es resumir las discordancias respecto al sistema de tipos de clave entre los
tipos CTS (Common Type System) y los tipos de base de datos.

Sistemas de tipos
Los dos sistemas de tipos que se analizarán son:

El sistema de tipos del modelo de objetos.

Este sistema de tipos se define mediante una combinación de una versión de un lenguaje CLS
(Common Language Specification), como Visual Basic o C#, que se implementa en Common
Language Runtime (CLR), y los tipos principales que se definen en el espacio de nombres
System de .NET Framework. Entre los primeros, se incluyen Int32, Int64 y String; entre los
últimos, se incluyen tipos derivados como DateTime, TimeSpan, etc.

El sistema de tipos definido por una implementación de SQL.

Para el tema que nos ocupa, es el sistema de tipos implementado por la implementación de T-
SQL de SQL Server en SQL Server 2000 y SQL Server 2005.

Tipos de datos
Para la conversión de valores deben tenerse en cuenta las diferencias básicas de los tipos de datos. La
materialización de la proyección en una consulta hace que se conviertan los valores de los tipos de base
de datos a los tipos CTS correspondientes especificados por el programador de la consulta. De igual
forma, el uso de parámetros requiere una conversión en la otra dirección.

Por ejemplo, la consulta siguiente requiere la conversión de dos valores:


Select DateOfBirth From Customer Where CustomerId = @id
El valor DateOfBirth de T-SQL se debe convertir en el valor CTS correspondiente (quizás DateTime de T-
SQL a System.DateTime de CTS) y el valor id de CTS se debe convertir en el valor de parámetro
correspondiente de un tipo T-SQL (quizás System.Int32 de CTS a integer de T-SQL).

Ausencia de tipos equivalentes


Los tipos siguientes no tienen equivalencias razonables.

Tipos System.* de CTS

Nota:

Los tipos de .NET Framework que son específicos de SQL Server en el espacio de nombres
System.Data.SqlTypes no se incluyen en la comparación.

Enteros sin signo. Estos tipos suelen asignarse a sus equivalentes con signo de mayor
tamaño para evitar el desbordamiento. Los literales se pueden convertir a un tipo numérico
con signo del mismo tamaño o de un tamaño inferior, según su valor.

Boolean. Estos tipos pueden asignarse a un valor numérico en bits o mayor, o a una
cadena. Un literal se puede asignar a una expresión que se evalúe en el mismo valor (por
ejemplo, 1=1 en SQL para True en CLS).

TimeSpan. Este tipo representa la diferencia entre dos valores DateTime y no se


corresponde con timestamp en SQL Server.

Tipos de SQL Server

MCT: Luis Dueñas Pag 353 de 388


Manual de LINQ

Tipos de caracteres de longitud fija. T-SQL distingue entre la categoría Unicode y no


Unicode, y tiene tres tipos distintos en cada categoría: nchar/char de longitud fija,
nvarchar/varchar de longitud variable limitada a 8k caracteres y ntext/text de mayor tamaño
(hasta 230 -1). Los tipos de caracteres de longitud fija podrían asignarse a char[] de CTS
para recuperar los caracteres, pero realmente no se corresponden con el mismo tipo en
cuanto a conversión y comportamiento.

Bit. Aunque el dominio bit tiene el mismo número de valores que Nullable<Boolean>,
ambos son tipos diferentes. Bit usa los valores 1 y 0 en lugar de true/false y no se puede
utilizar como un equivalente a las expresiones booleanas.

Timestamp. A diferencia de TimeSpan de CTS, timestamp de T-SQL representa un


número de 8 bytes generado por la base de datos que es único para cada actualización y no
se basa en la diferencia entre los valores DateTime.

Money y SmallMoney. Estos tipos pueden asignarse a Decimal, pero básicamente son
tipos diferentes y son tratados como tales en las conversiones y por las funciones basadas
en servidor.

Asignaciones múltiples
Los tipos siguientes tienen asignaciones múltiples. Esta cuestión puede considerarse una generalización
del problema de la ausencia de tipos equivalentes, ya que cada tipo que no tiene ningún equivalente
puede asignarse a uno o más tipos en el otro sistema de tipos, con valores y semántica de expresión
diferentes. En todas las conversiones de valores se debe considerar la asignación que se utiliza y la
posible necesidad de viajes de ida y vuelta. En todas las conversiones de expresiones se debe considerar
la semántica específica del tipo en el dominio asignado.

Tipos CTS

String. Se puede asignar a uno de seis tipos de caracteres diferentes que utilizan
codificaciones (ASCII o Unicode), tamaños (8k o más) y relleno (fijo o de longitud variable)
diferentes.

DateTime. Se puede asignar a DateTime o SmallDateTime.

Tipos de base de datos

Decimal. Se puede asignar a Decimal o Double, dependiendo de la escala, precisión y


restricciones CHECK que restringen el intervalo de valores.

Integer. Los tipos enteros se pueden asignar (en función de las restricciones CHECK que
limitan el intervalo de valores) a tipos integrales de CTS con o sin signo. El tipo sin signo
puede ser del mismo tamaño o del siguiente tamaño más bajo (por ejemplo, int se puede
asignar a Int32 o UInt32 o UInt16).

Diferencias de precisión y escala


Entre los ejemplos se incluyen los siguientes:

Decimal. Decimal permite 29 dígitos como máximo, mientras que el tipo decimal de T-SQL
puede tener hasta 38 dígitos. Para conservar la magnitud, el tipo decimal de T-SQL se debe
asignar a Double. En este tipo de asignación se pierde precisión, porque Double no tiene una

MCT: Luis Dueñas Pag 354 de 388


Manual de LINQ

precisión equivalente. Por otro lado, sin la restricción de precisión del tipo decimal de T-SQL, una
conversión a Decimal podría producir una pérdida de magnitud, que no es aceptable.

DateTime/SmallDateTime. Los intervalos de valores y la especificidad de los tres tipos (DateTime


de T-SQL, SmallDateTime de T-SQL y System.DateTime de CTS) son diferentes. Cualquier
conversión de valores puede generar un error o pérdida de precisión.

Números de punto flotante. Tienen una semántica peculiarmente diferente. Los números de
punto flotante de CTS admiten valores de cero positivo y negativo, infinito positivo y negativo y
NaN (no es un número). No sucede lo mismo con real y float de SQL. SQL también admite
números de punto flotante de precisión variable (dígitos de mantisa), mientras que CTS utiliza
una precisión preestablecida (mantisa).

Asignación no predeterminada
Algunas asignaciones son interesantes debido a los esquemas de base de datos heredados que utilizan
tipos que no se corresponden con el uso previsto en un programa CLS. Considere el lenguaje de
definición de datos (DDL) siguiente y las clases correspondientes en Visual Basic :
create table T1 (
Col1 nchar(10),
Col2 nchar(10))

Class C1
Dim x As Integer ' Map to T1.Col1
Dim y As Integer ' Map to T1.Col2
End Class
En el ejemplo anterior, los miembros C1 de la clase se asignan a un tipo bastante diferente (entero a
cadena de caracteres). Hay diferencias obvias en cuanto al dominio de los valores. Los enteros mayores
que 231-1 producirán un desbordamiento durante la lectura. Los enteros negativos de 10 dígitos no se
pueden representar si se utiliza la representación decimal. Lo que es aún más interesante, debe
considerarse la asignación al convertir las expresiones, porque la asignación afecta a la cláusula where,
no sólo a la proyección.

Otro ejemplo común es asignar una enumeración definida por el usuario a una columna de tipo integral.
Para los datos y esquemas heredados en los que no se usa una enumeración definida por el usuario en la
base de datos, esta asignación no predeterminada requiere una validación adicional.

Tipos definidos por el usuario


Los tipos definidos por el usuario (en lenguaje de CLS) están pensados para ayudar a eliminar las
lagunas del sistema de tipos. No obstante, revelan problemas interesantes con el control de versiones de
los tipos. Un cambio de versión en el cliente podría no corresponderse con un cambio en el tipo
almacenado en el servidor de bases de datos. Esto generaría otra discordancia de tipos según la cual no
coincidiría la semántica de tipos y es probable que saltase a la vista la diferencia entre las versiones.
Esto se complica todavía más cuando las jerarquías de herencia se refactorizan en sucesivas versiones.

Semántica de expresión
Además de la discordancia en pares entre los tipos de CTS y los tipos de base de datos, las expresiones
agregan complejidad. Deben considerarse las discordancias en la semántica de operador, semántica de
función, conversión de tipos implícita y reglas de prioridad.

Las subsecciones siguientes muestran la discordancia entre expresiones aparentemente similares. Quizás
podrían generarse expresiones SQL semánticamente equivalentes a una expresión CLS dada. Sin

MCT: Luis Dueñas Pag 355 de 388


Manual de LINQ

embargo, no está claro si las diferencias semánticas entre expresiones aparentemente similares son
evidentes para un usuario de CLS y, por tanto, si se han previsto o no los cambios requeridos para la
equivalencia semántica. Éste es un problema especialmente crítico cuando una expresión se evalúa para
un conjunto de valores. La visibilidad de la diferencia podría depender de los datos y ser difícil de
identificar durante la codificación y depuración.

Semántica de valores null


Las expresiones SQL proporcionan lógica de tres valores para las expresiones booleanas. El resultado
puede ser True, False o Null. En cambio, CLS especifica un resultado booleano de dos valores para las
comparaciones que implican valores nulos. Observe el código siguiente:
Dim i? As Integer = Nothing
Dim j? As Integer = Nothing
If i = j Then
' This branch is executed.
End If

-- Assume col1 and col2 are integer columns with null values.
-- Assume that ANSI null behavior has not been explicitly
-- turned off.
Select …
From …
Where col1 = col2
-- Evaluates to null, not true and the corresponding row is not
-- selected.
-- To obtain matching behavior (i -> col1, j -> col2) change
-- the query to the following:
Select …
From …
Where
col1 = col2
or (col1 is null and col2 is null)
-- (Visual Basic 'Nothing'.)
Un problema similar se da cuando se asumen resultados de dos valores.
If (i = j) Or (i <> j) Then ' Redundant condition.
' ...
End If

-- Assume col1 and col2 are nullable columns.


-- Assume that ANSI null behavior has not been explicitly
-- turned off.
Select …
From …
Where
col1 = col2
or col1 != col2
-- Visual Basic: col1 <> col2.
-- Excludes the case where the boolean expression evaluates
-- to null. Therefore the where clause does not always
-- evaluate to true.

MCT: Luis Dueñas Pag 356 de 388


Manual de LINQ

En el caso anterior, puede obtener un comportamiento equivalente para generar SQL, pero la conversión
podría no reflejar su intención de manera precisa.

LINQ a SQL no impone la semántica de comparación de valores null en C# o nothing en Visual Basic
para SQL. Los operadores de comparación se convierten sintácticamente en sus equivalentes SQL. La
semántica refleja la semántica de SQL tal como la define la configuración del servidor o de la conexión.
Dos valores nulos se consideran distintos según la configuración predeterminada de SQL Server (aunque
se puede cambiar la configuración para cambiar la semántica). Sea como fuere, LINQ a SQL no considera
la configuración del servidor en la conversión de consultas.

Una comparación con el literal null (nothing) se convierte a la versión de SQL correcta (is null o is not
null).

SQL Server define el valor null (nothing) en la intercalación; LINQ a SQL no cambia la intercalación.

Conversión y promoción de tipos


SQL admite un completo conjunto de conversiones implícitas en las expresiones. Las mismas expresiones
en C# requerirían una conversión de tipos explícita. Por ejemplo:

Los tipos Nvarchar y DateTime se pueden comparar en SQL sin ninguna conversión de tipos
explícita; C# requiere la conversión explícita.

Decimal se convierte implícitamente a DateTime en SQL. C# no permite una conversión


implícita.

De igual forma, la prioridad de los tipos en T-SQL difiere de la prioridad de los tipos en C#, ya que el
conjunto de tipos subyacente es diferente. De hecho, no hay una relación clara de
subconjuntos/supraconjuntos entre las listas de prioridades. Por ejemplo, al comparar nvarchar con
varchar, se produce la conversión implícita de la expresión varchar a nvarchar. CTS no proporciona
ninguna promoción equivalente.

En los casos más sencillos, estas diferencias generan expresiones CLS con conversiones de tipos que son
redundantes para la expresión SQL correspondiente. Lo que es más importante, los resultados
intermedios de una expresión SQL podrían promoverse implícitamente a un tipo que no tiene ningún
homólogo preciso en C#, y lo mismo a la inversa. En general, las pruebas, la depuración y la validación
de tales expresiones representan una carga adicional importante para el usuario.

Collation
T-SQL admite las intercalaciones explícitas como anotaciones en tipos de cadena de caracteres. Estas
intercalaciones determinan la validez de ciertas comparaciones. Por ejemplo, al comparar dos columnas
con intercalaciones explícitas diferentes, se genera un error. Si se utiliza el tipo de cadena CTS, más
simplificado, no se producen tales errores. Considere el ejemplo siguiente:
create table T2 (
Col1 nvarchar(10),
Col2 nvarchar(10) collate Latin_general_ci_as)

Class C
Dim s1 As String ' Map to T2.Col1.
Dim s2 As String ' Map to T2.Col2.
Sub Compare()
If s1 = s2 Then ' This is correct.

MCT: Luis Dueñas Pag 357 de 388


Manual de LINQ

' ...
End If
End Sub
End Class

Select …
From …
Where Col1 = Col2
-- Error, collation conflict.
De hecho, la subcláusula de intercalación crea un tipo restringido que no es sustituible.

De forma similar, el criterio de ordenación puede ser bastante diferente entre los sistemas de tipos. Esta
diferencia afecta a la ordenación de los resultados. Guid se ordena en los 16 bytes por orden
lexicográfico (IComparable()), mientras que T-SQL compara los GUID en el orden siguiente: node(10-
15), clock-seq(8-9), time-high(6-7), time-mid(4-5), time-low(0-3). Esta ordenación era la habitual en
SQL 7.0, cuando los GUID generados por NT tenían este orden de octetos. Este enfoque garantizaba que
los GUID generados en el mismo clúster de nodos se obtenían juntos y ordenados secuencialmente,
según la marca de tiempo. También era útil para generar índices (las inserciones se convertían en
anexos en lugar de E/S aleatorias). Posteriormente, el orden se codificó en Windows por cuestiones de
privacidad, pero SQL debe mantener la compatibilidad. Una solución alternativa es utilizar SqlGuid en
lugar de Guid.

Diferencias entre operadores y funciones


Los operadores y las funciones que son esencialmente comparables presentan una semántica
ligeramente diferente. Por ejemplo:

C# especifica una semántica de cortocircuito basada en el orden léxico de los operandos para los
operadores lógicos && y ||. Por otro lado, SQL está orientado a consultas basadas en conjuntos
y, por consiguiente, proporciona más libertad al optimizador para que decida el orden de
ejecución. Algunas de las implicaciones se exponen a continuación:

Una conversión semánticamente equivalente requeriría la construcción “CASE … WHEN


… THEN” en SQL para evitar la reordenación en la ejecución de los operandos.

Una conversión libre a AND / OR podría producir errores inesperados si la expresión de


C# confía en que la evaluación del segundo operando se basa en el resultado de la
evaluación del primer operando.

El desbordamiento siempre se comprueba en SQL, pero tiene que especificarse explícitamente


en C# (no en Visual Basic) para evitar el salto. Dadas las columnas de enteros C1, C2 y C3, si
C1+C2 se almacena en C3 (Update T Set C3 = C1 + C2):
create table T3 (
Col1 integer,
Col2 integer)
insert into T3 (col1, col2) values (2147483647, 5)
-- Valid values: max integer value and 5.
select * from T3 where col1 + col2 < 0
-- Produces arithmetic overflow error.

' Does not apply.


' Visual Basic overflow in absence of implicit check

MCT: Luis Dueñas Pag 358 de 388


Manual de LINQ

' (turn off overflow checks in compiler options)


Dim I As Integer = Int32.MaxValue
Dim j As Integer = 5
If I + j < 0 Then
' This code prints the overflow message.
Console.WriteLine("Overflow!")
End If
SQL realiza un redondeo aritmético simétrico mientras que .NET Framework utiliza el redondeo
bancario. Para obtener información detallada, vea el artículo 196652 de Microsoft Knowledge
Base.

De forma predeterminada, para las configuraciones regionales comunes, en las comparaciones


de cadenas de caracteres no se hace distinción entre mayúsculas y minúsculas en SQL. En Visual
Basic y en C#, se distingue entre mayúsculas y minúsculas. Por ejemplo, s == "Food" (s =
"Food" en Visual Basic) y s == "Food" pueden producir resultados diferentes si s es food.
-- Assume default US-English locale (case insensitive).
create table T4 (
Col1 nvarchar (256)
)
insert into T4 values („Food‟)
insert into T4 values („FOOD‟)
select * from T4 where Col1 = „food‟
-- Both the rows are returned because of case-insensitive matching.

' Visual Basic equivalent on collections of Strings in place of


' nvarchars.
Dim strings() As String = {"food", "FOOD"}
For Each s As String In strings
If s = "food" Then
Console.WriteLine(s)
End If
Next
' Only "food" is returned.
El operador Like admite eficazmente las sobrecargas automáticas basadas en las conversiones
implícitas. Aunque el operador Like por definición funciona en tipos de cadena de caracteres, la
conversión implícita de tipos numéricos o tipos DateTime permite utilizar igualmente tipos que
no son de cadena con Like. En CTS, no existe una conversión implícita comparable. Por lo tanto,
se requieren sobrecargas adicionales.

Nota:

Este comportamiento del operador Like sólo se aplica en C#; la palabra clave de Visual Basic
Like se mantiene invariable.

Los operadores o las funciones aplicados a argumentos de tipo de caracteres de longitud fija en
SQL tienen una semántica significativamente diferente a la de las mismas funciones y
operadores aplicados a una cadena de CTS. Esto también podría interpretarse como una
implicación más del problema de la ausencia de tipos equivalentes que se describió en la sección
relativa a los tipos.
create table T4 (
Col1 nchar(4)

MCT: Luis Dueñas Pag 359 de 388


Manual de LINQ

)
Insert into T5(Col1) values ('21');
Insert into T5(Col1) values ('1021');
Select * from T5 where Col1 like '%1'
-- Only the second row with Col1 = '1021' is returned.
-- Not the first row!

' Assume Like(String, String) method.


Dim s As String ' Map to T4.Col1.
If s Like (System.Data.Linq.SqlClient.SqlMethods.Like(s, "%1"))
Then
Console.WriteLine(s)
End If
' Expected to return true for both "21" and "1021".
Un problema similar se produce con la concatenación de cadenas.
create table T6 (
Col1 nchar(4)
Col2 nchar(4)
)
Insert into T6 values ('a', 'b');
Select Col1+Col2 from T6
-- Returns concatenation of padded strings "a b " and not "ab".
La función Round() tiene una semántica diferente en .NET Framework y en T-SQL.

El índice de inicio de las cadenas es 0 en CTS, pero 1 en SQL. Por consiguiente, cualquier función
que tenga un índice, requiere la conversión del índice.

CTS admite el operador de módulo ('%') para los números de punto flotante, pero SQL no.

Para resumir, podría requerirse una conversión compleja para las expresiones de CLS, así como
operadores o funciones adicionales para exponer la funcionalidad de SQL.

Conversión de tipos
En C# y en SQL, los usuarios pueden invalidar la semántica predeterminada de las expresiones utilizando
conversiones de tipos explícitas (Cast y Convert). Sin embargo, exponer esta funcionalidad entre los
límites del sistema de tipos puede plantear un dilema. Una conversión de tipos de SQL que proporciona
la semántica deseada no se puede convertir con facilidad a la conversión de tipos de C# correspondiente.
Por otro lado, una conversión de tipos de C# no se puede convertir directamente en una conversión de
tipos de SQL equivalente debido a las discordancias de los tipos, la ausencia de tipos equivalentes y las
diferencias en las jerarquías de prioridades de tipos. Es necesario elegir entre exponer la discordancia
con el sistema de tipos y perder una importante capacidad de expresión.

En otros casos, puede que la conversión de tipos no sea necesaria para validar una expresión en
cualquiera de los dos dominios, pero podría serlo para garantizar que una asignación no predeterminada
se aplica correctamente a la expresión.
-- Example from “Non-default Mapping” section extended
create table T5 (
Col1 nvarchar(10),
Col2 nvarchar(10)
)
Insert into T5(col1, col2) values („3‟, „2‟);

MCT: Luis Dueñas Pag 360 de 388


Manual de LINQ

Class C
Dim x As Integer ' Map to T5.Col1.
Dim y As Integer ' Map to T5.Col2.
Sub Casting()
' Intended predicate.
If (x + y) > 4 Then
' Valid for the data above.
End If
End Sub
End Class

Select * From T5 Where Col1 + Col2 > 4


-- "Col1 + Col2" expr evaluates to '32'
Forma en que se manifiestan las diferencias
Las diferencias del sistema de tipos podrían evitarse al menos parcialmente utilizando tipos definidos por
el usuario en el servidor y tipos SQL en el cliente. Sin embargo, ésta no es una solución válida para los
datos y esquemas heredados y la pretensión de usar tipos nativos de CTS para la programación. Por
consiguiente, los programadores que trabajan con tipos de sistemas diferentes suelen apreciar las
diferencias con claridad. Las diferencias se manifiestan como pérdida de datos, resultados inesperados,
restricciones de asignación o problemas de rendimiento.

Para complicar el problema, algunos síntomas pueden depender de los datos. Si no se incluyen en las
pruebas valores de datos concretos, la discordancia puede aparecer por sorpresa en tiempo de ejecución
o, lo que sería peor, pasar desapercibida y dañar los datos.

Conversión de valores con pérdida de información


La conversión con pérdida de información se produce si no hay una correspondencia exacta entre los
valores. Éste es un caso común: aunque System..::.Double no pierde la magnitud de un decimal de base
de datos, el cambio de número de punto fijo a número de punto flotante a menudo es inoportuno y
puede llevar a pérdidas de precisión. Aunque la opción de System..::.Decimal es similar en cuanto a
aritmética y formato, podría conducir a una pérdida de magnitud irrecuperable.

Diferencias semánticas en los resultados de consulta


Normalmente, la conversión con pérdida de información se puede comprobar examinando la conversión
en ambas direcciones para la proyección y los parámetros. Sin embargo, las diferencias en la semántica
de expresión, ya sea causada por la conversión implícita de los tipos o por las diferencias entre
operadores o funciones, puede ser mucho más difícil de diagnosticar. Considere los ejemplos siguientes,
que se incluyeron en secciones anteriores. En cada uno se asume que el usuario de la API espera la
semántica de CTS y la capa de conversión relacional de objetos genera SQL sin ajustes semánticos
adicionales.

Se pierde el "tercer" valor (null o Visual BasicNothing ) en las expresiones booleanas.

Las expresiones CTS con dos valores se deben convertir en una expresión equivalente de tres
valores. Consulte la sección 4.1.

Asignación no estándar

La conversión automática de tipos numéricos en cadenas y a la inversa (a tipos de SQL) puede


producir una expresión válida pero semánticamente diferente. Consulte la sección 4.5.

MCT: Luis Dueñas Pag 361 de 388


Manual de LINQ

Diferencias de intercalación

La ordenación depende de los valores predeterminados y la configuración de intercalación.


Consulte la sección 4.3.

Expresiones CTS válidas pueden generar errores en tiempo de ejecución porque se asume una
semántica de operador específica.

Por ejemplo, el segundo predicado de una expresión conjuntiva (AND lógica) podría producir un
error a menos que la expresión se convierta a SQL con la semántica de cortocircuito. Consulte el
ejemplo 1 de la sección 4.4.

Limitaciones de la asignación
Es posible restringir los casos sujetos a discordancia de tipos si se limita el conjunto de tipos CTS que se
asignan y limitando además los tipos correspondientes para cada tipo CTS determinado en el conjunto
restringido. Sin embargo, esto deja al usuario como responsable de la tarea de conversión. Por ejemplo,
si no se permite la asignación de nvarchar a System..::.String en el ejemplo de la sección 4.5, el
programador de la consulta tiene que realizar la conversión de los valores y expresiones de la consulta
para lograr las mismas tareas.

Problemas de rendimiento
Además de las diferencias semánticas enumeradas anteriormente, un problema práctico común es la
significativa reducción del rendimiento. Entre los ejemplos se incluyen los siguientes:

Orden forzado de evaluación de los operadores lógicos AND/OR.

La generación de SQL para aplicar el orden de evaluación del predicado limita la funcionalidad
del optimizador de SQL.

Las conversiones de tipos, ya sean originadas por un compilador de CLS o por una
implementación de consulta relacional de objetos, pueden reducir el uso de los índices.

Por ejemplo:
-- Table DDL
create table T5 (
Col1 varchar(100)
)

Class C5
Dim s As String ' Map to T5.Col1.
End Class
Considere la conversión de la expresión (s = SOME_STRING_CONSTANT).
-- Corresponding part of SQL where clause
Where …
Col1 = SOME_STRING_CONSTANT
-- This expression is of the form <varchar> = <nvarchar>.
-- Hence SQL introduces a conversion from varchar to nvarchar,
-- resulting in
Where …
Convert(nvarchar(100), Col1) = SOME_STRING_CONSTANT
-- Cannot use the index for column Col1 for some implementations.

MCT: Luis Dueñas Pag 362 de 388


Manual de LINQ

Para los conjuntos de datos grandes, estos problemas de rendimiento pueden determinar si una
aplicación se puede implementar o no. Además de las diferencias semánticas, los problemas de
rendimiento también pasan a ser críticos.

8.3.2.6.18. Funciones Definidas por el Usuario


LINQ a SQL utiliza los métodos de un modelo de objetos para representar las funciones definidas por el
usuario. Los métodos se designan como funciones aplicando el atributo FunctionAttribute y, si es
necesario, el atributo ParameterAttribute.

Para evitar que se inicie InvalidOperationException, las funciones definidas por el usuario en LINQ a SQL
deben presentarse de una de las formas siguientes:

Una función ajustada como llamada a método que tiene los atributos de asignación correctos.

Un método SQL estático específico de LINQ a SQL.

Una función admitida por un método .NET Framework.

Los temas de esta sección muestran cómo formar estos métodos y cómo llamarlos en una aplicación si
es usted quien escribe el código. Los programadores de Visual Studio normalmente usarían el Diseñador
relacional de objetos para asignar las funciones definidas por el usuario.

8.3.2.6.18.1. Cómo: Usar Funciones Definidas por el


Usuario con Valores Escalares
Puede asignar un método de cliente definido en una clase a una función definida por el usuario utilizando
el atributo FunctionAttribute. Observe que el cuerpo del método construye una expresión que captura el
intento de llamada al método y pasa esa expresión a DataContext para su conversión y ejecución.

Nota:

La ejecución directa sólo se produce si se llama a la función fuera de una consulta.

Ejemplo
El código de SQL siguiente presenta una función ReverseCustName() definida por el usuario con valores
escalares.
CREATE FUNCTION ReverseCustName(@string varchar(100))
RETURNS varchar(100)
AS
BEGIN
DECLARE @custName varchar(100)
-- Implementation left as exercise for users.
RETURN @custName
END
Para este código, asignaría un método de cliente como el siguiente:
<FunctionAttribute(Name:="dbo.ReverseCustName", IsComposable:=True)> _
Public Function ReverseCustName(<Parameter(Name:="string", _
DbType:="VarChar(100)")> ByVal [string] As String) As _
<Parameter(DbType:="VarChar(100)")> String
Return CType(Me.ExecuteMethodCall(Me, _

MCT: Luis Dueñas Pag 363 de 388


Manual de LINQ

CType(MethodInfo.GetCurrentMethod, MethodInfo), _
[string]).ReturnValue, String)
End Function

8.3.2.6.18.2. Cómo: Usar Funciones Definidas por el


Usuario con Valores de Tabla
Una función con valores de tabla devuelve un conjunto de filas único (a diferencia de los procedimientos
almacenados, que pueden devolver varias formas de resultados). Dado que el tipo devuelto de una
función con valores de tabla es Table, una función con valores de tabla se puede usar en cualquier lugar
de SQL donde se pueda usar una tabla. La función con valores de tabla se puede tratar como se trataría
una tabla.

Ejemplo
La función de SQL siguiente declara explícitamente que devuelve TABLE. Por lo tanto, la estructura de
conjunto de filas devuelta se define implícitamente.
CREATE FUNCTION ProductsCostingMoreThan(@cost money)
RETURNS TABLE
AS
RETURN
SELECT ProductID, UnitPrice
FROM Products
WHERE UnitPrice > @cost
LINQ a SQL asigna la función de la manera siguiente:
<FunctionAttribute(Name:="dbo.ProductsCostingMoreThan",
IsComposable:=True)> _
Public Function ProductsCostingMoreThan(<Parameter(DbType:="Money")>
ByVal cost As System.Nullable(Of Decimal)) As IQueryable(Of
ProductsCostingMoreThanResult)
Return Me.CreateMethodCallQuery(Of ProductsCostingMoreThanResult)
(Me, CType(MethodInfo.GetCurrentMethod, MethodInfo), cost)
End Function
El código de SQL siguiente muestra cómo se puede unir a la tabla devuelta por la función y, si no,
tratarla como lo haría con cualquier otra tabla:
SELECT p2.ProductName, p1.UnitPrice
FROM dbo.ProductsCostingMoreThan(80.50)
AS p1 INNER JOIN Products AS p2 ON p1.ProductID = p2.ProductID
En LINQ a SQL, la consulta se representaría de la siguiente manera:
Dim q = From p In db.ProductsCostingMoreThan(80.5), p1 In db.Products
Where p.ProductID = p1.ProductID Select p.ProductID, p1.UnitPrice

8.3.2.6.18.3. Cómo: Llamar a Funciones Inline Definidas por


el Usuario
Aunque se puede llamar a funciones inline definidas por el usuario, las funciones que se incluyen en una
consulta cuya ejecución está diferida no se ejecutan hasta que se ejecute la consulta.

MCT: Luis Dueñas Pag 364 de 388


Manual de LINQ

Al llamar a la misma función fuera de una consulta, LINQ a SQL crea una consulta simple a partir de la
expresión de llamada al método. A continuación se muestra la sintaxis de SQL (el parámetro @p0 se
enlaza a la constante pasada):
SELECT dbo.ReverseCustName(@p0)
LINQ a SQL crea lo siguiente:
Dim str As String = db.ReverseCustName("LINQ to SQL")
Ejemplo
En la siguiente consulta LINQ a SQL se puede ver una llamada inline al método de la función definida por
el usuario ReverseCustName que se ha generado. La función no se ejecuta inmediatamente, porque la
ejecución de la consulta está diferida. El código SQL generado para esta consulta realiza la conversión a
una llamada a la función definida por el usuario en la base de datos (vea el código SQL que se encuentra
después de la consulta).
Dim custQuery = From cust In db.Customers Select cust.ContactName, Title
= db.ReverseCustName(cust.ContactTitle)

SELECT [t0].[ContactName],
dbo.ReverseCustName([t0].[ContactTitle]) AS [Title]
FROM [Customers] AS [t0]

8.3.3. Referencia
En esta sección se proporciona información de referencia para programadores de LINQ a SQL.

8.3.3.1. Tipos de Datos y Funciones


Las construcciones de Common Language Runtime (CLR) tienen expresiones correspondientes en SQL
sólo si LINQ a SQL ha proporcionado explícitamente una equivalencia en el motor de conversión. En otras
palabras, cualquier funcionalidad de las clases de .NET Framework que no se catalogue como admitida en
los temas enumerados en la tabla siguiente no es compatible para su conversión a SQL. Esta limitación
también se aplica a las propiedades, conversiones de tipos y métodos definidos por el usuario.

Sin embargo, puede utilizar los métodos incompatibles en las consultas si se da alguna de estas
condiciones:

El método se puede evaluar como valor convertible antes de que tenga lugar el proceso de
conversión.

En otras palabras, el método no debe depender de variables lambda que no se enlazan hasta
que se produce la ejecución de la consulta.

El método se puede aplicar a los resultados una vez recuperado de la base de datos.

En otras palabras, no se puede realizar ninguna solicitud de información de base de datos


adicional una vez aplicado el método.

8.3.3.1.1. Correspondencia de Tipos SQL-CLR


Una asignación de tipos es la correspondencia entre un tipo de Common Language Runtime (CLR) de un
campo o una propiedad de un objeto y el tipo de SQL Server de un campo de tabla.

MCT: Luis Dueñas Pag 365 de 388


Manual de LINQ

En este tema se proporciona una matriz de asignaciones completa y alguna información específica sobre
lo siguiente:
Asignación de Enum, DateTime y XML.
Tipos de moneda de SQL Server y conversiones en CLR.
Tipos de punto flotante.
Serialización binaria y de cadena.
Asignación de enumeración
LINQ a SQL admite la asignación de tipos Enum de CLR de dos maneras:

Asignación a tipos SQL numéricos (TINYINT, SMALLINT, INT, BIGINT)

Al asignar un tipo Enum de CLR a un tipo numérico de SQL, el valor entero subyacente del tipo
Enum de CLR se asigna al valor del campo de la base de datos de SQL. El valor del campo en
SQL se recupera directamente como el valor integral subyacente del tipo Enum. Al cambiar el
valor de Enum y guardar los datos en la base de datos, el valor integral subyacente de Enum se
almacena en el campo de la base de datos.

Asignación a tipos SQL de texto (CHAR, NCHAR, VARCHAR, NVARCHAR)

Al asignar un tipo Enum de CLR a un tipo de texto SQL, el valor de SQL de la base de datos se
asigna a los nombres de los miembros Enum de CLR. Por ejemplo, si un tipo Enum denominado
DaysOfWeek contiene un miembro denominado Tue, ese miembro se asigna a un valor de base
de datos de Tue. Puede lograr esta asignación utilizando la reflexión en el tipo Enum.

La asignación predeterminada de SQL para un tipo Enum de CLR es el equivalente de SQL de su tipo
integral subyacente.

Asignación de fecha y hora


Los valores DateTime se guardan tal cual en la base de datos, sin conversión TimeZone,
independientemente de la información original de DateTimeKind. Cuando los valores DateTime se
recuperan de la base de datos, su valor se carga tal cual en DateTime, con un valor DateTimeKind de
Unspecified.

Asignaciones de tipos XML


Puede asignar el tipo de datos XML de SQL Server 2005 a XElement (valor predeterminado) o String. Si
la columna almacena fragmentos XML que no se pueden leer en XElement, dicha columna debe asignarse
a String para evitar errores en tiempo de ejecución. Éstos son algunos ejemplos de fragmentos XML que
se deben asignar a String:
Una secuencia de elementos XML.
Atributos, PI, comentarios.

Nota:

XDocument no puede utilizarse para asignar tipos de datos XML porque esta clase no es serializable
(no implementa la interfaz IXmlSerializable).

Tipos Decimal y Money


Existen las siguientes diferencias entre los tipos DECIMAL/MONEY/SMALLMONEY de SQL Server y los
tipos Decimal / Double de CLR:

SQL Server
DECIMAL(precision,scale)

MCT: Luis Dueñas Pag 366 de 388


Manual de LINQ

Precisión de hasta 38 dígitos.


Intervalo (con todos los dígitos a la izquierda del separador decimal): -1038 + 1 a 1038 –
1.
Puede representar todos los posibles números de 0-38 dígitos.
MONEY
Precisión de hasta 18-19 dígitos, pero siempre con exactamente 4 dígitos a la derecha
del separador decimal.
Intervalo: -263/1000 a (263 – 1)/1000.
Puede representar todos los posibles números de 0-18 dígitos y algunos, pero no todos,
los números de 19 dígitos.
SMALLMONEY
Precisión de hasta 5-6 dígitos, pero siempre con exactamente 4 dígitos a la derecha del
separador decimal.
Intervalo: -231/1000 a (231 – 1)/1000.
Puede representar todos los posibles números de 0-5 dígitos y algunos, pero no todos,
los números de 6 dígitos.
CLR
Decimal
Precisión de hasta 28-29 dígitos.
Intervalo (con todos los dígitos a la izquierda del separador decimal): -296 + 1 a 296 – 1.
Puede representar todos los posibles números de 0-28 dígitos y algunos, pero no todos,
los números de 29 dígitos.
Double
Intervalo: de ±4,94065645841246544E-324 a 1,79769313486231570E+308.
Admite una magnitud muy superior a Decimal, pero tiene menos precisión.
Todos los valores decimales se pueden convertir en Double sin desbordamiento, pero se
puede perder precisión.
Tipos de punto flotante
SQL Server admite el tipo de punto flotante de tamaño variable, especificado como
FLOAT(mantissaBits). El tipo Single de CLR es equivalente a REAL, sinónimo de FLOAT(24). El tipo
Double de CLR es equivalente a FLOAT, que es de forma predeterminada FLOAT(53). LINQ a SQL
asigna los tipos FLOAT que son FLOAT(24) o menores a Single, y los tipos flotantes mayores a Double.

Nota:

Técnicamente, SQL Server puede almacenar valores NaN, infinitos positivos/negativos y cero
positivo/negativo en las consultas. Estos valores no siempre se comportan como se espera, incluso
en las consultas SQL directas.

Serialización binaria y de cadena


LINQ a SQL admite dos tipos de serialización de las clases de .NET Framework y clases de usuario.

Serialización de cadena (.Parse())

Si una clase implementa .Parse() (con una firma similar a DateTime, por ejemplo), puede
serializarlo en cualquier campo de texto SQL (CHAR, NCHAR, VARCHAR, NVARCHAR, TEXT,
NTEXT, XML). Si serializa el objeto en una cadena, el valor devuelto de ToString() se guarda en
la base de datos. Si deserializa una cadena almacenada, .Parse() se invoca en la cadena para
devolver el objeto construido.

Serialización binaria (ISerializable)

MCT: Luis Dueñas Pag 367 de 388


Manual de LINQ

Si una clase implementa ISerializable, se puede serializar en cualquier campo binario de SQL
(BINARY, VARBINARY, IMAGE). Para serializar y deserializar el objeto, se sigue el
comportamiento estándar de ISerializable.

IXmlSerializable

LINQ a SQL no admite la serialización mediante IXmlSerializable.

8.3.3.1.2. Tipos de Datos Básicos


Encontrará una matriz de las conversiones válidas integradas en SQL Server 2005 en el tema de SQL
Server CAST y CONVERT (Transact-SQL).

LINQ a SQL admite los operadores siguientes.

Visual Basic = y <> (Operadores de comparación (Visual Basic)); operadores de comparación de


C# == (Operador == (Referencia de C#)) y != (Operador != (Referencia de C#)). Se admiten
la igualdad y la desigualdad para tipos numéricos, Boolean, DateTime y TimeSpan.

Operador Is

El operador Is (Visual Basic)/is (C#) tiene un equivalente compatible cuando se utiliza la


asignación de herencia. Se puede utilizar en lugar de probar directamente la columna
discriminadora para determinar si un objeto es de un tipo de entidad concreto, y se convierte en
una marca de verificación en la columna discriminadora.

Conversiones de tipo

Se habilitan las conversiones implícitas o explícitas de tipos CLR de origen a tipos CLR de destino
si hay una conversión similar válida en SQL Server. Después de la conversión, las conversiones
de tipos cambian el comportamiento de las operaciones realizadas en una expresión CLR para
coincidir con el comportamiento de otras expresiones CLR que tienen una correspondencia
natural con el tipo de destino. Las conversiones de tipos también se pueden convertir en el
contexto de la asignación de herencia. Los tipos de los objetos se pueden convertir a subtipos de
entidad más específicos de forma que se pueda tener acceso a los datos específicos de su
subtipo.

8.3.3.1.3. Tipos de Datos Booleanos


Los operadores booleanos funcionan tal como cabía esperar en Common Language Runtime (CLR), con la
salvedad de que no hay equivalencia para el comportamiento de cortocircuito. Por ejemplo, el operador
AndAlso de Visual Basic se comporta como el operador And. El operador && de C# se comporta como
el operador &.

LINQ a SQL admite los operadores siguientes.

Visual Basic C#

And (Operador, Visual Basic) Operador & (Referencia de C#)

AndAlso (Operador) Operador && (Referencia de C#)

MCT: Luis Dueñas Pag 368 de 388


Manual de LINQ

Or (Operador, Visual Basic) Operador | (Referencia de C#)

OrElse (Operador) Operador || (Referencia de C#)

Xor (Operador, Visual Basic) Operador ^ (Referencia de C#)

Not (Operador, Visual Basic) Operador ! (Referencia de C#)

8.3.3.1.4. Descripción del Concepto Null


La tabla siguiente proporciona vínculos a varias partes de la documentación de LINQ a SQL donde se
analizan los problemas de valores null (Nothing en Visual Basic).

Topico Descripción

Discordancias de La sección relativa a la semántica de valores Null de este tema analiza


sistemas de tipos el valor booleano SQL de tres estados frente al objeto Boolean de dos
estados de Common Language Runtime (CLR), los valores literales
Nothing (Visual Basic) y null (C#), y otros problemas similares.

traducción de operadores La sección relativa a la semántica de valores Null de este tema describe
de consulta estándar la semántica de comparación de valores Null en LINQ a SQL.

System.String (métodos - La sección "Diferencias respecto a .NET" de este tema describe cómo un
LINQ a SQL) valor 0 devuelto de LastIndexOf podría significar que la cadena es nula
o que la posición encontrada es 0.

Cómo: Calcular la suma Describe cómo el operador Sum se evalúa como null (Nothing en
de los valores de una Visual Basic) en lugar de 0 para una secuencia que sólo contiene valores
secuencia numérica nulos o para una secuencia vacía.

8.3.3.1.5. Operadores Numéricos y de Comparación


Los operadores aritméticos y de comparación funcionan como cabía esperar en Common Language
Runtime (CLR), con las siguientes excepciones:

SQL no admite al operador de módulo en números de punto flotante.

SQL no admite la aritmética no comprobada.

Los operadores de incremento y decremento producen efectos no deseados cuando se


utilizan en expresiones que no se pueden replicar en SQL y son, por tanto, incompatibles.

Operadores compatibles
LINQ a SQL admite los operadores siguientes.
Operadores aritméticos básicos:
+
- (resta)
*
/
División de enteros en Visual Basic (\)
% (Visual Basic Mod)
<<
>>

MCT: Luis Dueñas Pag 369 de 388


Manual de LINQ

- (negación unaria)
Operadores de comparación básicos:
= en Visual Basic y == en C#
<> en Visual Basic y != en C#
Is/IsNot en Visual Basic
<
<=
>
>=

8.3.3.1.6. Operadores de Secuencia


Generalmente hablando, LINQ a SQL no admite los operadores de secuencia que tienen una o más de las
características siguientes:
Aceptan una expresión lambda con un parámetro de índice.
Se basan en las propiedades de filas secuenciales, como TakeWhile.
Se basan en una implementación de CLR arbitraria, como IComparer<(Of <(T>)>).
Diferencias respecto a .NET
Todos los operadores de secuencia admitidos funcionan como es de esperar en Common Language
Runtime (CLR), salvo Average. Average devuelve un valor del mismo tipo que el tipo para el que se
calcula el promedio, mientras que en CLR Average siempre devuelve Double o Decimal. Si el argumento
de origen se convierte explícitamente en double o decimal o el selector se convierte en double o decimal,
el código SQL resultante también tendrá este tipo de conversión y el resultado será el esperado.

8.3.3.1.7. System.Convert
LINQ a SQL no admite los siguientes métodos Convert.
Versiones con un parámetro IFormatProvider.
Métodos que incluyen matrices de caracteres o matrices de bytes:
FromBase64CharArray
ToBase64CharArray
FromBase64String
ToBase64String
Los métodos siguientes:
public static <Type2> To <Type2>(<Type1> value); donde
Type1 y Type2 son sbyte, uint, ulong o ushort.
C#:
int To<int type>(string value, int fromBase),
ToString(... value, int toBase)
Visual Basic:
Function To(Of [Numeric])(value as String, fromBase As Integer)
As [Numeric], ToString( value As …, toBase As Integer)
IsDBNull
GetTypeCode
ChangeType

8.3.3.1.8. System.DateTime
LINQ a SQL no admite los métodos DateTime siguientes.

MCT: Luis Dueñas Pag 370 de 388


Manual de LINQ

IsDaylightSavingTime IsLeapYear

DaysInMonth ToBinary

ToFileTime ToFileTimeUtc

ToLongDateString ToLongTimeString

ToOADate ToShortDateString

ToShortTimeString ToUniversalTime

FromBinary UtcNow

FromFileTime FromFileTimeUtc

FromOADate GetDateTimeFormats

Diferencias respecto a .NET


Los tipos DateTime de SQL Server y Common Language Runtime (CLR) presentan diferencias en cuanto
a intervalo y precisión de paso, como se describe en la tabla siguiente.

Type Valor mínimo Valor máximo Paso

System.DateTime 1 de enero de 31 de diciembre de 100 nanosegundos (0,0000001


0001 9999 segundos)

Datetime de T-SQL 1 de enero de 31 de diciembre de 3,33 milisegundos


1753 9999 (0,0033333 segundos)

SmallDateTime de T- 1 de enero de 6 de junio de 2079 1 minuto (60 segundos)


SQL 1900

El intervalo y la precisión del tipo DateTime de CLR son mayores que el intervalo y la precisión
de los tipos de SQL Server. Así, los datos de SQL Server nunca pierden magnitud o precisión
cuando se expresan con un tipo de CLR. Sin embargo, a la inversa, el intervalo y la precisión
pueden estar comprometidos.

Las fechas de SQL Server no reconocen el concepto de TimeZone, una característica totalmente
admitida en CLR.

Los usuarios deben decidir cómo desean almacenar las fechas en sus bases de datos (como hora local,
hora UTC o invariable) y realizar las conversiones necesarias antes y después de las consultas LINQ a
SQL.

Modelo parcial de resta de System.DateTime


Por ejemplo, considere el siguiente modelo:

(dateTime1 – dateTime2).{Days, Hours, Milliseconds, Minutes, Months, Seconds, Years}

Cuando se reconoce, se convierte en una llamada directa a DATEDIFF, de la manera siguiente:

DATEDIFF({DatePart}, @dateTime1, @dateTime2)

8.3.3.1.9. Métodos System.Math

MCT: Luis Dueñas Pag 371 de 388


Manual de LINQ

LINQ a SQL no admite los métodos Math siguientes.


Math..::.DivRem(Int32, Int32, Int32%)
Math..::.DivRem(Int64, Int64, Int64%)
Math..::.IEEERemainder(Double, Double)
Diferencias respecto a .NET
.NET Framework tiene una semántica de redondeo diferente a la de SQL Server. El método Round de
.NET Framework realiza un redondeo bancario, según el cual los números que terminan en ,5 se
redondean al dígito par más cercano en lugar de al siguiente dígito superior. Por ejemplo, 2,5 se
redondea a 2, mientras que 3,5 se redondea a 4. (Esta técnica ayuda a evitar la desviación sistemática
hacia los valores más altos en transacciones de datos grandes.)

Por el contrario, en SQL, la función ROUND siempre aplica un redondeo desde 0. Por consiguiente, 2,5
se redondea a 3, mientras que se redondea a 2 en .NET Framework.

LINQ a SQL admite directamente la semántica de ROUND en SQL y no intenta implementar el redondeo
bancario.

8.3.3.1.10. System.Object
LINQ a SQL admite los siguientes métodos Object.

Object..::.Equals(Object) Object..::.Equals(Object, Object)

Object..::.ToString()()()

LINQ a SQL no admite los siguientes métodos Object.

Object..::.GetHashCode()()() Object..::.ReferenceEquals(Object,
Object)

Object..::.MemberwiseClone()()() Object..::.GetType()()()

Object..::.ToString()()() para los tipos binarios como


BINARY, VARBINARY, IMAGE y TIMESTAMP.

Diferencias respecto a .NET


El resultado de Object..::.ToString()()() para double utiliza SQL CONVERT(NVARCHAR(30), @x, 2) en
SQL. En este caso, SQL siempre utiliza 16 dígitos y notación científica (por ejemplo, ,
"0.000000000000000e+000" para 0). En consecuencia, la conversión de Object..::.ToString()()() no
genera la misma cadena que Convert..::.ToString en .NET Framework.

8.3.3.1.11. System.String
LINQ a SQL no admite los métodos String siguientes.

Métodos System.String no admitidos en general


Métodos String no admitidos en general:

Sobrecargas que reconocen la referencia cultural (métodos que utilizan CultureInfo /


StringComparison / IFormatProvider).

Métodos que utilizan o generan una matriz char.

Métodos System.String estáticos no admitidos

MCT: Luis Dueñas Pag 372 de 388


Manual de LINQ

Métodos System.String estáticos no admitidos

String..::.Copy(String)

String..::.Compare(String, String, Boolean)

String..::.Compare(String, String, Boolean, CultureInfo)

String..::.Compare(String, Int32, String, Int32, Int32)

String..::.Compare(String, Int32, String, Int32, Int32, Boolean)

String..::.Compare(String, Int32, String, Int32, Int32, Boolean, CultureInfo)

String..::.CompareOrdinal(String, String)

String..::.CompareOrdinal(String, Int32, String, Int32, Int32)

String..::.Format

String..::.Join

Métodos System.String no estáticos no admitidos

Métodos System.String no estáticos no admitidos

String..::.IndexOfAny(array<Char>[]()[])

String..::.Split

String..::.ToCharArray()()()

String..::.ToUpper(CultureInfo)

String..::.TrimEnd(array<Char>[]()[])

String..::.TrimStart(array<Char>[]()[])

Diferencias respecto a .NET


Las consultas no tienen en cuenta las intercalaciones de SQL Server que podrían aplicarse en el
servidor y, por lo tanto, proporcionan de forma predeterminada comparaciones dependientes de
la referencia cultural, sin distinción entre mayúsculas y minúsculas. Este comportamiento difiere
del predeterminado, la semántica con distinción entre mayúsculas y minúsculas de .NET
Framework.

Cuando LastIndexOf devuelve 0, la cadena es NULL o la posición encontrada es 0.

Pueden obtenerse resultados inesperados en operaciones de concatenación u otras operaciones


con cadenas de longitud fija (CHAR, NCHAR), porque a estos tipos se les aplica relleno
automáticamente en la base de datos.

Dado que muchos métodos, como Replace, ToLower, ToUpper, y el indizador de carácter, no
tienen ningún equivalente válido para las columnas TEXT o NTEXT y XML, se producirán
SqlExceptions si se convierten de la forma habitual. Este comportamiento se considera que es
aceptable para estos tipos. Sin embargo, todas las operaciones de cadena deben coincidir con la

MCT: Luis Dueñas Pag 373 de 388


Manual de LINQ

semántica de Common Language Runtime (CLR) para VARCHAR, NVARCHAR,


VARCHAR(max) y NVARCHAR(max).

8.3.3.1.12. System.TimeSpan
En LINQ a SQL, no puede asignar campos de base de datos a TimeSpan. Sin embargo, se admiten las
operaciones en TimeSpan porque se pueden devolver valores TimeSpan a partir de la sustracción de
DateTime o se pueden incluir en una expresión como literales o variables enlazadas.

8.3.3.1.13. Funciones Incompatibles


La siguiente funcionalidad de SQL no se puede exponer a través de la conversión de las construcciones
existentes de Common Language Runtime (CLR) y .NET Framework:
LIKE
STDDEV
Se admite lo siguiente, con diferencias:
DATEDIFF
ROUND

8.3.3.2. Asignación Basada en Atributos


LINQ a SQL asigna una base de datos de SQL Server a un modelo de objetos LINQ a SQL aplicando
atributos o utilizando un archivo de asignación externo. En este tema se describe el enfoque basado en
atributos.

En su forma más elemental, LINQ a SQL asigna una base de datos a un DataContext, una tabla a una
clase y las columnas y relaciones a las propiedades de esas clases. También puede utilizar atributos para
asignar una jerarquía de herencia en su modelo de objetos.

Los programadores que utilizan Visual Studio normalmente realizan la asignación basada en atributos
mediante el Diseñador relacional de objetos. También puede utilizar la herramienta de línea de comandos
SQLMetal o puede incluir los atributos en el código usted mismo.

Nota:

También puede realizar la asignación utilizando un archivo XML externo.

En las secciones siguientes se describe con más detalle la asignación basada en atributos.

Atributo DatabaseAttribute
Utilice este atributo para especificar el nombre predeterminado de la base de datos cuando la conexión
no proporciona ningún nombre. Este atributo es opcional, pero, si lo utiliza, debe aplicar la propiedad
Name, como se indica en la tabla siguiente.

Property Type Default Description

Name Cadena Vea Cuando se usa con su propiedad Name, especifica el nombre de
Name. la base de datos.

Atributo TableAttribute
Utilice este atributo para designar una clase como una clase de entidad que está asociada a una tabla o
vista de base de datos. LINQ a SQL trata las clases que tienen este atributo como clases persistentes. En
la tabla siguiente se describe la propiedad Name.

MCT: Luis Dueñas Pag 374 de 388


Manual de LINQ

Property Type Default Description

Name Cadena La misma cadena que Designa una clase como una clase de entidad que
el nombre de clase está asociada a una tabla de base de datos.

Atributo ColumnAttribute
Utilice este atributo para designar un miembro de una clase de entidad para que represente una columna
de una tabla de base de datos. Este atributo se puede aplicar cualquier campo o propiedad.

Sólo los miembros que identifique como columnas se recuperarán y conservarán cuando LINQ a SQL
guarde los cambios en la base de datos. Se supone que los miembros que no tienen este atributo no son
persistentes y no se envían para operaciones de inserción o actualización.

En la tabla siguiente se describen las propiedades de este atributo.

Property Type Default Description

AutoSync AutoSync Nunca Indica a Common Language Runtime


(CLR) que recupere el valor después de
una operación de inserción o
actualización.
Opciones: Always, Never, OnUpdate,
OnInsert.

CanBeNull Boolean true Indica que una columna puede contener


valores nulos.

DbType Cadena Tipo de columna de Utiliza tipos de base de datos y


base de datos modificadores para especificar el tipo de
deducido la columna de base de datos.

Expression Cadena Vacío Define una columna calculada en una


base de datos.

IsDbGenerated Boolean false Indica que una columna contiene valores


que la base de datos genera
automáticamente.

IsDiscriminator Boolean false Indica que la columna contiene un valor


de discriminador para una jerarquía de
herencia de LINQ a SQL.

IsPrimaryKey Boolean false Especifica que este miembro de clase


representa una columna que es o forma
parte de las claves principales de la tabla.

IsVersion Boolean false Identifica el tipo de columna del miembro


como una marca de tiempo o número de
versión de la base de datos.

UpdateCheck UpdateCheck Always, a menos Especifica cómo se plantea LINQ a SQL la


que IsVersion sea detección de conflictos de simultaneidad
true para un optimista.
miembro

Atributo AssociationAttribute
Utilice este atributo para designar una propiedad que represente una asociación en la base de datos,
como una relación entre clave externa y clave principal.

En la tabla siguiente se describen las propiedades de este atributo.

MCT: Luis Dueñas Pag 375 de 388


Manual de LINQ

Property Type Default Description

DeleteOnNull Boolean false Cuando se coloca en una asociación cuyos


miembros de clave externa no aceptan valores Null,
elimina el objeto cuando la asociación está
establecida en null.

DeleteRule Cadena Ninguna Agrega comportamiento de eliminación a una


asociación.

IsForeignKey Boolean false Si es verdadero, designa el miembro como la clave


externa de una asociación que representa una
relación de base de datos.

IsUnique Boolean false Si es verdadero, indica una restricción de unicidad


en la clave externa.

OtherKey Cadena Identificador de la Designa uno o más miembros de la clase de entidad


clase relacionada. de destino como valores de clave en el otro lado de
la asociación.

ThisKey Cadena Identificador de la Designa miembros de esta clase de entidad para


clase contenedora que representen los valores de clave en este lado
de la asociación.

Atributo InheritanceMappingAttribute
Utilice este atributo para asignar una jerarquía de herencia.

En la tabla siguiente se describen las propiedades de este atributo.

Property Type Default Description

Code Cadena Ninguno El valor Especifica el valor de código del discriminador.


debe suministrarse.

IsDefault Boolean false Si es verdadero, crea instancias de un objeto de este


tipo cuando ningún valor de discriminador del
almacén coincide con ninguno de los valores
especificados.

Type Type Ninguno El valor Especifica el tipo de la clase en la jerarquía.


debe suministrarse.

Atributo FunctionAttribute
Utilice este atributo para designar un método para representar un procedimiento almacenado o una
función definida por el usuario en la base de datos.

En la tabla siguiente se describen las propiedades de este atributo.

Property Type Default Description

IsComposable Boolean false Si es falso, indica la asignación a un


procedimiento almacenado. Si es verdadero,
indica la asignación a una función definida por el
usuario.

Name Cadena La misma cadena Especifica el nombre del procedimiento


que el nombre en la almacenado o la función definida por el usuario.
base de datos

Atributo ParameterAttribute

MCT: Luis Dueñas Pag 376 de 388


Manual de LINQ

Utilice este atributo para asignar parámetros de entrada en métodos de procedimiento almacenado.

En la tabla siguiente se describen las propiedades de este atributo.

Property Type Default Description

DbType Cadena Ninguna Especifica el tipo de base de


datos.

Name Cadena La misma cadena que el nombre del Especifica un nombre para el
parámetro en la base de datos parámetro.

Atributo ResultTypeAttribute
Utilice este atributo para especificar un tipo de resultado.

En la tabla siguiente se describen las propiedades de este atributo.

Property Type Default Description

Type Type (Ninguno) Se utiliza en los métodos asignados a los procedimientos


almacenados que devuelven IMultipleResults. Declara las
asignaciones de tipos válidas o esperadas para el procedimiento
almacenado.

Atributo DataAttribute
Utilice este atributo para especificar nombres y campos de almacenamiento privados.

En la tabla siguiente se describen las propiedades de este atributo.

Property Type Default Description

Name Cadena Igual que el nombre en la Especifica el nombre de la tabla, columna,


base de datos etc.

Storage Cadena Descriptores de acceso Especifica el nombre del campo de


públicos almacenamiento subyacente.

8.3.3.3. Generación de Código en LINQ a SQL


Puede generar código para representar una base de datos utilizando el Diseñador relacional de objetos o
la herramienta de línea de comandos SQLMetal. En cualquier caso, la generación de código de un
extremo a otro se produce en tres etapas:

1. El Extractor de DBML extrae información del esquema de la base de datos y vuelve a ensamblar
la información en un archivo DBML con formato XML.

2. El Validador de DBML examina el archivo DBML en busca de errores.

3. Si no aparece ningún error de validación, el archivo se pasa al Generador de código.

Extractor de DBML
El Extractor de DBML es un componente de LINQ a SQL que acepta metadatos de una base de datos
como entrada y genera un archivo DBML como resultado. La siguiente ilustración muestra la secuencia
de operaciones.

MCT: Luis Dueñas Pag 377 de 388


Manual de LINQ

Generador de código
El Generador de código es un componente de LINQ a SQL que traduce archivos DBML a Visual Basic, C#
o archivos de asignación XML. La siguiente ilustración muestra la secuencia de operaciones.

Archivo de definición de esquema XML


El archivo DBML debe ser válido, según la siguiente definición de esquema, como archivo XSD.

Distinga este archivo de definición de esquema del archivo de definición de esquema que se utiliza para
validar un archivo de asignación externa.

Nota:

Los usuarios de Visual Studio también encontrarán este archivo XSD en el cuadro de diálogo de
Esquemas XML como "DbmlSchema.xsd".

8.3.3.4. Referencia de Asignación Externa


LINQ a SQL admite asignación externa, un proceso por el cual se utiliza un archivo XML independiente
para especificar la asignación entre el modelo de datos de la base de datos y el modelo de objetos. Las
ventajas de utilizar una archivo de asignación externa son las siguientes:

Puede separar el código de la asignación del código de la aplicación. Este enfoque reduce el
desorden en el código de la aplicación.

Puede tratar un archivo de asignación externo de forma similar a un archivo de configuración.


Por ejemplo, puede actualizar cómo se comportará su aplicación después de distribuir los
binarios simplemente cambiando el archivo de asignación externo.

Requisitos
El archivo de asignación debe ser un archivo XML, y se debe validar contra un archivo de definición de
esquema (.xsd) de LINQ a SQL.

Se aplican las siguientes reglas:

El archivo de asignación debe ser un archivo XML.

El archivo de asignación XML debe ser válido según el archivo de definición de esquema XML.

MCT: Luis Dueñas Pag 378 de 388


Manual de LINQ

La asignación externa invalida la asignación basada en atributos. En otras palabras, al utilizar un


origen de asignación externo para crear un DataContext, el DataContext omite todos los
atributos de asignación que se han creado en las clases. Este comportamiento es cierto si la
clase está incluida en el archivo de asignación externo.

LINQ a SQL no admite el uso híbrido de los dos enfoques de asignación (basado en atributos y
externo).

La asignación externa puede ser específica de un proveedor de base de datos. Puede asignar la
misma clase utilizando asignaciones externas independientes para proveedores independientes.
Ésta es una característica que la asignación basada en atributos no admite.

Archivo de definición de esquema XML


La asignación externa en LINQ a SQL debe ser válida según la siguiente definición de esquema XML.

Distinga este archivo de definición de esquema del archivo de definición de esquema que se utiliza para
validar un archivo DBML.

Nota:

Los usuarios de Visual Studio también encontrarán este archivo XSD en el cuadro de diálogo
Esquemas XML como "LinqToSqlMapping.xsd".

8.3.3.5. Preguntas Más Frecuentes


Las siguientes secciones dan respuesta a algunos problemas comunes que podría encontrar al
implementar LINQ.

No se puede conectar
P. No puedo conectarme a mi base de datos.

R. Asegúrese de que su cadena de conexión es correcta y que su instancia de SQL Server se está
ejecutando. También tenga en cuenta que LINQ a SQL requiere que el protocolo Canalizaciones con
nombre esté habilitado.

La base de datos pierde los cambios realizados


P. Realicé un cambio en los datos de la base de datos, pero, cuando volví a ejecutar mi aplicación, el
cambio ya no estaba.

R. Asegúrese de que llama a SubmitChanges para guardar los resultados en la base de datos.

Conexión a bases de datos: ¿cuánto tiempo permanece abierta?


P. ¿Cuánto tiempo permanece abierta mi conexión a una base de datos?

R. Normalmente, una conexión permanece abierta hasta que se utilizan los resultados de la consulta. Si
espera que los resultados tarden tiempo en procesarse, y no se opone a que se almacenen en memoria
caché, aplique ToList<(Of <(TSource>)>) a la consulta. En escenarios habituales donde cada objeto se
procesa sólo una vez, el modelo de transmisión por secuencias es superior tanto en DataReader como
en LINQ a SQL.

Los detalles exactos de uso de la conexión dependen de lo siguiente:

MCT: Luis Dueñas Pag 379 de 388


Manual de LINQ

Estado de la conexión si el DataContext se construye con un objeto de conexión.

Opciones de la cadena de conexión; por ejemplo, habilitar conjuntos de resultados activos


múltiples (MARS).

Actualizaciones sin consultas


P. ¿Puedo actualizar los datos de la tabla sin consultar primero la base de datos?

R. Aunque LINQ a SQL no posee comandos de actualización basados en conjuntos, puede utilizar
cualquiera de las técnicas siguientes para actualizar sin consultar primero:

Utilice ExecuteCommand para enviar código SQL.

Cree una nueva instancia del objeto e inicialice todos los valores (campos) actuales que afectan
a la actualización. A continuación, asocie el objeto al DataContext utilizando Attach y modifique
el campo que desee cambiar.

Resultados inesperados en la consulta


P. Mi consulta devuelve resultados inesperados. ¿Cómo puedo inspeccionar lo que está ocurriendo?

R. LINQ a SQL proporciona varias herramientas para inspeccionar el código SQL que genera. Una de las
más importantes es Log.

Resultados inesperados del procedimiento almacenado


P. Tengo un procedimiento almacenado cuyo valor devuelto se calcula mediante MAX(). Cuando arrastro
el procedimiento almacenado hasta la superficie de Diseñador relacional de objetos, el valor devuelto no
es correcto.

R. LINQ a SQL proporciona dos maneras de devolver los valores generados por la base de datos a través
de procedimientos almacenados:

Asignando un nombre al resultado de salida.

Especificando explícitamente un parámetro de salida.

El siguiente es un ejemplo de resultado incorrecto. Dado que LINQ a SQL no puede asignar los
resultados, siempre devuelve 0:

create procedure proc2


as
begin
select max(i) from t where name like 'hello'
end
El siguiente es un ejemplo de resultado correcto que utiliza un parámetro de salida:

create procedure proc2


@result int OUTPUT
as
select @result = MAX(i) from t where name like 'hello'
go
El siguiente es un ejemplo de resultado correcto que asigna un nombre al resultado de salida:

MCT: Luis Dueñas Pag 380 de 388


Manual de LINQ

create procedure proc2


as
begin
select nax(i) AS MaxResult from t where name like 'hello'
end
Errores de serialización
P. Cuando intento serializar, obtengo el siguiente error: "El tipo
'System.Data.Linq.ChangeTracker+StandardChangeTracker'... no está marcado como serializable".

R. La generación de código en LINQ a SQL admite serialización DataContractSerializer. No admite


XmlObjectSerializer o BinaryFormatter.

Múltiples archivos DBML


P. Cuando tengo varios archivos DBML que comparten algunas tablas, obtengo un error del compilador.

R. Establezca las propiedades Espacio de nombres del contexto y Espacio de nombres de la


entidad de Diseñador relacional de objetos en un valor distinto para cada archivo DBML. Este enfoque
elimina la colisión entre nombres o espacios de nombres.

Evitar el establecimiento explícito de valores generados por la base de datos al insertar o actualizar
P. Tengo una tabla de base de datos con una columna DateCreated que tiene como valor predeterminado
Getdate() de SQL. Cuando intento insertar un nuevo registro utilizando LINQ a SQL, el valor queda
establecido en NULL. Lo que esperaba es que tomara el valor predeterminado de la base de datos.

P. LINQ a SQL administra automáticamente esta situación para la identidad (incremento automático) y
rowguidcol (GUID generado por base de datos) y para las columnas con marca de tiempo. En otros
casos, debería establecer manualmente IsDbGenerated=true y AutoSync=Always/OnInsert/OnUpdate.

Múltiples DataLoadOptions
P. ¿Puedo especificar opciones de carga adicionales sin sobrescribir la primera?

R. Sí. La primera no se sobrescribe, como se muestra en el ejemplo siguiente:


Dim dlo As New DataLoadOptions()
dlo.LoadWith(Of Order)(Function(o As Order) o.Customer)
dlo.LoadWith(Of Order)(Function(o As Order) o.OrderDetails)
Errores en el uso de SQL Compact 3.5
P. Obtengo un error cuando arrastro tablas fuera de una base de datos SQL Server Compact 3.5.

R. El Diseñador relacional de objetos no admite SQL Server Compact 3.5, aunque el motor de ejecución
de LINQ a SQL sí lo hace. En esta situación, debe crear sus propias clases de entidad y agregar los
atributos adecuados.

Errores en relaciones de herencia


P. Utilizo la herramienta de herencia incluida en el cuadro de herramientas del Diseñador relacional de
objetos para conectar dos entidades, pero obtengo errores.

R. Crear una relación no es suficiente. Debe proporcionar información tal como la columna de
discriminador, el valor de discriminador de la clase base y el valor de discriminador de la clase derivada.

Modelo de proveedor
P. ¿Existe un modelo de proveedor público disponible?

MCT: Luis Dueñas Pag 381 de 388


Manual de LINQ

R. No existe ningún modelo de proveedor público disponible. En este momento, LINQ a SQL sólo admite
SQL Server y SQL Server Compact 3.5.

Ataques mediante inserción de SQL


P. ¿Cómo se protege LINQ a SQL de ataques de inserción de SQL?

R. La inserción de SQL ha sido un riesgo significativo para las consultas SQL tradicionales formadas
mediante concatenación de los datos proporcionados por el usuario. LINQ a SQL evita esa inserción
mediante el uso de SqlParameter en las consultas. Los datos proporcionados por el usuario se convierten
en valores de parámetro. Este enfoque impide que se utilicen comandos malintencionados en los datos
proporcionados por el cliente.

Cambiar el marcador de sólo lectura en archivos DBML


P. ¿Cómo elimino los establecedores procedentes de algunas propiedades cuándo creo un modelo de
objetos a partir de un archivo DBML?

R. Siga estos pasos para este escenario avanzado:

1. En el archivo .dbml, modifique la propiedad cambiando el marcador IsReadOnly a True.

2. Agregue una clase parcial. Cree un constructor con parámetros para los miembros de sólo
lectura.

3. Revise el valor predeterminado de UpdateCheck (Never) para determinar si ése es el valor


correcto para su aplicación.

Precaución:

Si está utilizando el Diseñador relacional de objetos en Visual Studio, sus cambios podrían resultar
sobrescritos.

APTCA
P. ¿Está System.Data.Linq marcado para que el código de confianza parcial pueda utilizarlo?

A. Sí, el ensamblado System.Data.Linq.dll se encuentra entre los ensamblados de .NET Framework


marcados con el atributo AllowPartiallyTrustedCallersAttribute. Sin esta señal, los ensamblados incluidos
en .NET Framework están destinados para su uso en código de plena confianza.

El principal escenario de LINQ a SQL para permitir llamadores que no son de plena confianza es hacer
que se pueda obtener acceso al ensamblado LINQ a SQL desde aplicaciones web, cuya configuración de
confianza es Media.

Asignación de datos procedentes de varias tablas


P. Los datos de mi entidad proceden de varias tablas. ¿Cómo realizo la asignación?

R. Puede crear una vista en una base de datos y asignar la entidad a la vista. LINQ a SQL genera el
mismo código SQL para vistas que para tablas.

Nota:

El uso de vistas en este escenario presenta limitaciones. Este enfoque funciona de forma más segura
cuando las operaciones realizadas sobre Table<(Of <(TEntity>)>) se admiten en la vista subyacente.
Sólo usted puede saber qué operaciones son las deseadas. Por ejemplo, la mayoría de las
aplicaciones son de sólo lectura, y otro número considerable realiza operaciones

MCT: Luis Dueñas Pag 382 de 388


Manual de LINQ

Create/Update/Delete sólo mediante procedimientos almacenados ejecutados contra vistas.

Agrupar conexiones
P. ¿Existe una construcción que pueda ayudar con el agrupamiento de DataContext?

R. No intente reutilizar instancias de DataContext. Cada DataContext mantiene el estado (incluida una
caché de identidad) para una sesión de edición o consulta particular. Para obtener nuevas instancias
según el estado actual de la base de datos, utilice un nuevo DataContext.

Puede seguir utilizando agrupamiento de conexiones ADO.NET subyacente.

El segundo DataContext no resulta actualizado


P. Utilizo una instancia de DataContext para almacenar valores en la base de datos. Sin embargo, un
segundo DataContext en la misma base de datos no refleja los valores actualizados. La segunda
instancia de DataContext parece devolver valores almacenados en memoria caché.

R. Este comportamiento está diseñado así. LINQ a SQL continúa devolviendo los mismos valores o
instancias que aparecen en la primera instancia. Cuando se realizan actualizaciones, se utiliza
simultaneidad optimista. Los datos originales se utilizan para realizar una comprobación contra el estado
de la base de datos actual a fin de comprobar que, de hecho, permanecen sin modificar. Si han
cambiado, se produce un conflicto, y su aplicación deberá resolverlo. Una opción para su aplicación es
restablecer el estado original al estado actual de la base de datos e intentar de nuevo la actualización.

También puede establecer ObjectTrackingEnabled como falso, lo cual desactiva el almacenamiento en


memoria caché y el seguimiento de cambios. A continuación, puede recuperar los últimos valores cada
vez que realiza una consulta.

No se puede llamar a SubmitChanges en modo de sólo lectura


P. Cuando intento llamar a SubmitChanges en modo de sólo lectura, obtengo un error.

R. El modo de sólo lectura desactiva la capacidad del contexto de realizar seguimiento de cambios.

8.3.3.6. SQL Server Compact 3.5 y LINQ a SQL


SQL Server Compact 3.5 es la base de datos predeterminada que se instala con Visual Studio 2008.

En este tema se describen las diferencias fundamentales en cuanto a uso, configuración, conjuntos de
características y ámbito de compatibilidad con LINQ a SQL.

Características de SQL Server Compact 3.5 en relación con LINQ a SQL


De forma predeterminada, SQL Server Compact 3.5 se instala para todas las ediciones de Visual Studio
y, por lo tanto, está disponible en el equipo de desarrollo para su uso con LINQ a SQL. Sin embargo, la
implementación de una aplicación que utiliza SQL Server Compact 3.5 y LINQ a SQL difiere de la de una
aplicación SQL Server. SQL Server Compact 3.5 no forma parte de .NET Framework y, por lo tanto, se
debe incluir con la aplicación o descargar específicamente desde el sitio de Microsoft.

Observe las siguientes características:

SQL Server Compact 3.5 se empaqueta como una DLL que se puede utilizar directamente en
archivos de base de datos (extensión .sdf).

MCT: Luis Dueñas Pag 383 de 388


Manual de LINQ

SQL Server Compact 3.5 se ejecuta en el mismo proceso que la aplicación cliente. Por lo tanto,
la eficacia de la comunicación con SQL Server Compact 3.5 puede ser significativamente mayor
que con SQL Server. Por otro lado, SQL Server Compact 3.5 requiere interoperabilidad entre el
código administrado y no administrado, con el costo que ello implica.

El tamaño de la DLL de SQL Server Compact 3.5 es pequeño. Esta característica reduce el
tamaño total de la aplicación.

El motor de tiempo de ejecución de LINQ a SQL y la herramienta de línea de comandos


SQLMetal admiten SQL Server Compact 3.5.

El Diseñador relacional de objetos no admite SQL Server Compact 3.5.

Conjunto de características
El conjunto de características de SQL Server Compact 3.5 es mucho más sencillo que el de SQL Server
en los aspectos siguientes, que pueden afectar a las aplicaciones LINQ a SQL:

SQL Server Compact 3.5 no admite las vistas ni los procedimientos almacenados.

SQL Server Compact 3.5 admite sólo un subconjunto de tipos de datos y funciones de SQL.

SQL Server Compact 3.5 admite sólo un subconjunto de construcciones de SQL.

SQL Server Compact 3.5 proporciona sólo un optimizador mínimo. Es posible que algunas
consultas agoten el tiempo de espera.

SQL Server Compact 3.5 no admite la confianza parcial.

8.3.3.7. Traducción de Operadores de Consulta Estándar


LINQ a SQL convierte los operadores de consulta estándar en comandos SQL. El procesador de consultas
de la base de datos determina la semántica de ejecución de la conversión a SQL.

Los operadores de consulta estándar se definen en secuencias. Una secuencia se ordena y se basa en la
identidad de referencia de cada elemento de la secuencia.

SQL trata principalmente con conjuntos de valores no ordenados. La ordenación es normalmente una
operación de procesamiento posterior declarada de forma explícita que se aplica al resultado final de una
consulta en lugar de a los resultados intermedios. La identidad se define con valores. Por esta razón se
entiende que las consultas SQL tratan con conjuntos múltiples (bolsas) en lugar de conjuntos.

En los párrafos siguientes se describen las diferencias entre los operadores de consulta estándar y su
conversión a SQL para el proveedor de SQL Server de LINQ a SQL.

Compatibilidad de los operadores


Concat
El método Concat<(Of <(TSource>)>) se define para los conjuntos múltiples ordenados cuando el orden
del receptor y el orden del argumento son iguales. Concat<(Of <(TSource>)>) funciona como UNION
ALL sobre los conjuntos múltiples después del orden común.

El último paso es ordenar en SQL antes de que se generen los resultados. Concat<(Of <(TSource>)>) no
conserva el orden de sus argumentos. Para garantizar una ordenación correcta, debe ordenar
explícitamente los resultados de Concat<(Of <(TSource>)>).

MCT: Luis Dueñas Pag 384 de 388


Manual de LINQ

Intersect, Except, Union


Los métodos Intersect y Except se definen bien sólo en los conjuntos. La semántica de conjuntos
múltiples no está definida.

El método Union se define para los conjuntos múltiples como la concatenación no ordenada de los
conjuntos múltiples (de hecho, el resultado de la cláusula UNION ALL en SQL).

Take, Skip
Los métodos Take<(Of <(TSource>)>) y Skip<(Of <(TSource>)>) están bien definidos sólo en
conjuntos ordenados. La semántica para los conjuntos no ordenados o conjuntos múltiples no está
definida.

Nota:

Take<(Of <(TSource>)>) y Skip<(Of <(TSource>)>) tienen ciertas limitaciones cuando se utilizan


en consultas en SQL Server 2000.

Debido a las limitaciones de la ordenación en SQL, LINQ a SQL intenta trasladar la ordenación del
argumento de estos métodos al resultado del método. Por ejemplo, considere la siguiente consulta LINQ
a SQL:
Dim custQuery = From cust In db.Customers Where cust.City = "London"
Order By cust.CustomerID Select cust Skip 1 Take 1
El SQL generado para este código traslada la ordenación al final, como se observa a continuación:
SELECT TOP 1 [t0].[CustomerID], [t0].[CompanyName],
FROM [Customers] AS [t0]
WHERE (NOT (EXISTS(
SELECT NULL AS [EMPTY]
FROM (
SELECT TOP 1 [t1].[CustomerID]
FROM [Customers] AS [t1]
WHERE [t1].[City] = @p0
ORDER BY [t1].[CustomerID]
) AS [t2]
WHERE [t0].[CustomerID] = [t2].[CustomerID]
))) AND ([t0].[City] = @p1)
ORDER BY [t0].[CustomerID]
Parece obvio que toda la ordenación especificada debe ser coherente cuando se encadenan Take<(Of
<(TSource>)>) y Skip<(Of <(TSource>)>). De lo contrario, los resultados no están definidos.

Tanto Take<(Of <(TSource>)>) como Skip<(Of <(TSource>)>) están bien definidos para los
argumentos integrales de constante no negativos basados en la especificación de operadores de consulta
estándar.

Operadores sin conversión


LINQ a SQL no convierte los métodos siguientes. La razón más común es la diferencia entre los
conjuntos múltiples no ordenados y las secuencias.

Operadores Razonamiento

TakeWhile, SkipWhile Las consultas SQL funcionan con conjuntos múltiples, no con
secuencias. ORDER BY debe ser la última cláusula aplicada a
los resultados. Por esta razón, no hay ninguna conversión
general para estos dos métodos.

MCT: Luis Dueñas Pag 385 de 388


Manual de LINQ

Reverse<(Of <(TSource>)>) La conversión de este método es posible para un conjunto


ordenado pero LINQ a SQL no la realiza actualmente.

Last, LastOrDefault La conversión de estos métodos es posible para un conjunto


ordenado pero LINQ a SQL no la realiza actualmente.

ElementAt<(Of <(TSource>)>), Las consultas SQL funcionan en conjuntos múltiples, no en


ElementAtOrDefault<(Of secuencias indizables.
<(TSource>)>)

DefaultIfEmpty (sobrecarga con En general, no se puede especificar un valor predeterminado


argumento predeterminado) para una tupla arbitraria. Los valores nulos para las tuplas
son posibles en algunos casos a través de combinaciones
externas.

Conversión de expresiones
Semántica de valores null
LINQ a SQL no impone las semántica de comparación de valores null en SQL. Los operadores de
comparación se convierten sintácticamente en sus equivalentes SQL. Por esta razón, la semántica refleja
la semántica de SQL definida según la configuración del servidor o la conexión. Por ejemplo, dos valores
nulos se consideran distintos según la configuración predeterminada de SQL Server, pero puede cambiar
la configuración para cambiar la semántica. LINQ a SQL no tiene en cuenta la configuración del servidor
cuando convierte las consultas.

Una comparación con el literal null se convierte a la versión de SQL correcta (is null o is not null).

SQL Server define el valor null en la intercalación. LINQ a SQL no cambia la intercalación.

Agregados
El método de agregado del operador de consulta estándar Sum se evalúa como cero para una secuencia
vacía o para una secuencia que sólo contiene valores nulos. En LINQ a SQL, la semántica de SQL se
mantiene invariable y Sum se evalúa como null en lugar de cero para una secuencia vacía o para una
secuencia que sólo contiene valores nulos.

En LINQ a SQL se aplican las restricciones de SQL para los agregados en los resultados intermedios. Sum
para cantidades enteras de 32 bits no se calcula utilizando los resultados de 64 bits. Puede producirse un
desbordamiento en la conversión de Sum que realiza LINQ a SQL aun cuando la implementación del
operador de consulta estándar no produce un desbordamiento para la secuencia en memoria
correspondiente.

De igual forma, la conversión que realiza LINQ a SQL de Average de valores enteros se calcula como
integer, no como double.

Argumentos de entidad
LINQ a SQL permite utilizar los tipos de entidad en los métodos GroupBy y OrderBy. En la conversión de
estos operadores, el uso de un argumento de un tipo se considera equivalente a especificar todos los
miembros de ese tipo. Por ejemplo, el código siguiente es equivalente:
db.Customers.GroupBy(Function(c) c)
db.Customers.GroupBy(Function(c) New With {c.CustomerID, _
c.ContactName})
Argumentos equivalentes o comparables
La igualdad de los argumentos se requiere en la implementación de los métodos siguientes:
Contains

MCT: Luis Dueñas Pag 386 de 388


Manual de LINQ

Skip<(Of <(TSource>)>)
Union
Intersect
Except
LINQ a SQL admite la igualdad y comparación para los argumentos planos, pero no para los argumentos
que son secuencias o las contienen. Un argumento plano es un tipo que asignarse a una fila de SQL. Una
proyección de uno o más tipos de entidad que se pueden determinar estáticamente que no contiene una
secuencia se considera un argumento plano.

A continuación se ofrecen ejemplos de argumentos planos:


db.Customers.Select(Function(c) c)
db.Customers.Select(Function(c) New With {c.CustomerID, c.City})
db.Orders.Select(Function(o) New With {o.OrderID, o.Customer.City})
db.Orders.Select(Function(o) New With {o.OrderID, o.Customer})
A continuación se ofrecen ejemplos de argumentos no planos (jerárquicos).
' In the following line, c.Orders is a sequence.
db.Customers.Select(Function(c) New With {c.CustomerID, c.Orders})
' In the following line, the result has a sequence.
db.Customers.GroupBy(Function(c) c.City)
Conversión de funciones de Visual Basic
Las siguientes funciones auxiliares que utiliza el compilador de Visual Basic se convierten a las funciones
y operadores de SQL correspondientes:
CompareString
DateTime.Compare
Decimal.Compare
IIf (in Microsoft.VisualBasic.Interaction)
Métodos de conversión:

ToBoolean ToSByte ToByte ToChar

ToCharArrayRankOne ToDate ToDecimal ToDouble

ToInteger ToUInteger ToLong ToULong

ToShort ToUShort ToSingle ToString

Compatibilidad de la herencia
Herencia en consultas
Las conversión de tipos de C# sólo se admite en la proyección. Las conversiones de tipos que se utilizan
en otra parte no se convierten y se omiten. Además de los nombres de función de SQL, SQL realmente
sólo realiza la operación equivalente a Convert de Common Language Runtime (CLR). Es decir, SQL
puede cambiar el valor de un tipo a otro. No hay ningún equivalente de conversión de tipos de CLR
porque no existe el concepto de reinterpretar los mismos bits que los de otro tipo. Por esa razón una
conversión de tipos de C# sólo funciona localmente. No es remota.

Los operadores, is y as, y el método GetType no están limitados al operador Select. También se pueden
utilizar en otros operadores de consulta.

Compatibilidad con SQL Server 2005


LINQ a SQL no admite las características de SQL Server 2005 siguientes:
Procedimientos almacenados escritos para SQL CLR.

MCT: Luis Dueñas Pag 387 de 388


Manual de LINQ

Tipo definido por el usuario.


Características de consulta XML.

Compatibilidad con SQL Server 2000


Las siguientes limitaciones de SQL Server 2000 (frente a Microsoft SQL Server 2005) afectan a la
compatibilidad con LINQ a SQL.

Operadores Cross Apply y Outer Apply


Estos operadores no están disponibles en SQL Server 2000. LINQ a SQL intenta una serie de operaciones
de reescritura para sustituirlos por las combinaciones adecuadas.

Cross Apply y Outer Apply se generan para la navegación de relaciones. El conjunto de consultas para
el que son posibles tales operaciones de reescritura no está bien definido. Por esta razón, el conjunto
mínimo de consultas que se admite para SQL Server 2000 es el conjunto que no implica la navegación de
relaciones.

text / ntext
Los tipos de datos text / ntext no se pueden utilizar en ciertas operaciones de consulta con
varchar(max) / nvarchar(max), que son admitidos por Microsoft SQL Server 2005.

Esta limitación no tiene ninguna resolución. Concretamente, no puede utilizar Distinct() en ningún
resultado que contenga miembros que se asignen a columnas text o ntext.

Comportamiento desencadenado por las consultas anidadas


El enlazador de SQL Server 2000 (hasta SP4) tiene algunas peculiaridades que son desencadenadas por
las consultas anidadas. El conjunto de consultas SQL que las desencadenan no está bien definido. Por
esta razón, no se puede definir el conjunto de consultas LINQ a SQL que podrían producir excepciones de
SQL Server.

Operadores Skip y Take


Take<(Of <(TSource>)>) y Skip<(Of <(TSource>)>) tienen ciertas limitaciones cuando se utilizan en
consultas en SQL Server 2000.

Materialización de objetos
La materialización crea objetos CLR a partir de las filas devueltas por una o más consultas SQL.
Las llamadas siguientes se ejecutan localmente como parte de la materialización:
Constructores
Métodos ToString en las proyecciones
Conversiones de tipos en las proyecciones

Los métodos que siguen al método AsEnumerable<(Of <(TSource>)>) se ejecutan localmente.


Este método no produce la ejecución inmediata.

Puede utilizar struct como tipo de valor devuelto en el resultado de una consulta o como un
miembro del tipo de resultado. Las entidades deben ser clases. Los tipos anónimos se
materializan como instancias de clase, pero las estructuras con nombre (no entidades) se
pueden utilizar en la proyección.

Un miembro del tipo de valor devuelto en el resultado de una consulta puede ser de tipo
IQueryable<(Of <(T>)>). Se materializa como una colección local.

MCT: Luis Dueñas Pag 388 de 388

También podría gustarte