sábado, 27 de agosto de 2011

Sintaxis de método en LINQ

Como ya vimos en el post Distintas sintaxis de Linq, cualquier expresión de consulta  expresada con la sintaxis de consulta (query syntax) es finalmente traducida en tiempo de ejecución a una expresión de consulta con sintaxis de método (method syntax)

Esto significa que la siguiente expresión de consulta con sintaxis de consulta:

                From r In Clientes

                Where r.Nombre.StartsWith("C")

                Order By r.IdCliente Descending

                Select r


Terminará siendo una expresión de consulta con la siguiente sintaxis de método:

                Clientes.

                Where(Function(r) r.Nombre.StartsWith("C")).

                OrderByDescending(Function(r) r.IdCliente).

                Select(Function(r) r)

 

Asumimos que Clientes es una variable del tipo List(Of Cliente) y que la clase Cliente tiene una propiedad IdCliente.

 

Antes de continuar, te diré que hay 2 programas que me parecen imprescindible (sobre todo sin aún no somos maestros de Linq, que es mi caso) que son:

LINQPad. Gratuita. Response a preguntas como ¿Qué sintaxis de método generará esta expresión de consulta con sintaxis de consulta? ¿Qué consulta SQL generará esta expresión de consulta LINQ?

Linquer. Es de pago, pero 60$ me parecen un dinero muy bien invertido. Convierte código T-SQL a expresiones de consulta LINQ. Imprescindible para contestar preguntas del estilo ¿Cómo hago en LINQ esta consulta que SÍ se hacer en T-SQL? ¿Qué consulta SQL generará esta expresión de consulta LINQ?

En realidad ambos programas hacen muchas más cosas de lo que te he dicho y es por eso que te recomiendo encarecidamente que los agregues a tu Toolbelt y ¡no salgas de casa sin ellos!

Volviendo al tema de sintaxis de consulta vs sintaxis de método, lo que realmente quiero explicar en este post es ¿De dónde sale LINQ?

Ya tenemos claro que la sintaxis de consulta (aquella que nos recuerda al SQL) no es más que una sintaxis amigable para llegar finalmente a la sintaxis de método (aquella que NO nos recuerda a SQL y además ¡no nos gusta!). El problema es que, justamente conociendo a esta última sintaxis será como entendamos cómo funciona LINQ y de donde sale… que no tiene nada que ver con que finalmente utilicemos la sintaxis de consulta al estilo SQL que cierto es, parece más sencilla y agradecida.

La sintaxis de método (recuerda que es la que finalmente siempre se ejecuta) espera encontrar una serie de métodos encadenados (se encadenan porque normalmente reciben una secuencia y devuelven otra secuencia para poder llevar a cabo una cascada de métodos), en los que cada uno de ellos tiene una firma específica que normalmente reciben parámetros de tipo delegados genéricos (por ello podemos utilizar expresiones lambda, recuerda que “donde se espera un delegado se puede utilizar una expresión lambda”). Todos estos métodos que intervienen en la expresión de consulta a través de sintaxis de método son llamadas “operadores de consulta” (query operators). Es decir, los métodos Where, OrderByDescending y Select (algunos de los métodos a los que se traduce finalmente cualquier expresión de consulta) son operadores de consulta (y así verás que la gente se refiere a ellos en Internet).

Si hablamos de LINQ to Objects, todos estos operadores de consulta están definidos en el ensamblado System.Core, y en la clase System.Linq.Enumerable.

Si nos fijamos en la descripción de la clase en el examinador de objetos, dice “Proporciona un conjunto de métodos static (Shared en Visual Basic) para consultar objetos que implementan System.Collections.Generic.IEnumerable(Of T).”, blanco y en botella, ¿no? Todo tipo que implemente IEnumerable(Of T) tendrá automáticamente disponibles estos métodos u operadores de consulta. Y en nuestro caso, recuerda que Clientes es del tipo List(Of T) y por ende del tipo IEnumerable(Of T), y es por eso que podemos utilizar LINQ sobre nuestra variable Clientes.

Por otro lado, si estamos utilizando LINQ to Entities, también en el ensamblado System.Core tenemos la clase System.Linq.Queryable, cuya descripción es “Proporciona un conjunto de métodos static (Shared en Visual Basic) para consultar estructuras de datos que implementan System.Linq.IQueryable(Of T)”.

Está claro que los operadores de consulta para LINQ to Objects y LINQ to Entities están en el ensamblado System.Core. Pero ¿Y por ejemplo LINQ to XML? Pues está en el ensamblado System.Xml.Linq y en la clase System.Xml.Linq.ExtensionsContiene los métodos de extensión de LINQ to XML.”.

Lo que quiero decir, es que cualquier proveedor de LINQ necesita una clase que defina métodos de extensión para los operadores de consulta, sino no hay LINQ.

De hecho, y para confirmar que LINQ es extensible (esto es que cualquiera puede meterle mano), veamos un ejemplo en el que agregaremos un método extensor a IEnumerable(Of Clientes) y así podremos utilizar este método en la “cascada de métodos” de una expresión de consulta LINQ.

Public Class GestorClientes

    Public Function GetClientes() As IEnumerable(Of Clientes)

        Using ctx As New EFEntities

            Dim t =

                (From r In ctx.Clientes

                Order By r.IdCliente Descending).ToList.SoloClientesQueComiencenPorC

            Return t

        End Using

    End Function

End Class

 

Public Module MetodosExtensionParaClientes

    <System.Runtime.CompilerServices.Extension()> _

    Public Function SoloClientesQueComiencenPorC(ByVal t As IEnumerable(Of Clientes)) As IEnumerable(Of Clientes)

        Return From r In t Where r.Nombre.StartsWith("C")

    End Function

 

End Module


A partir de aquí y sabiendo ya de donde carajo sale LINQ, nuestra obligación es conocer la lista de operadores de consulta que hay disponible.

 

Un saludo!

jueves, 25 de agosto de 2011

Equivalencia de tipos SQL Server Compact Edition 4.0 en Entity Framework


En este
otro post, vimos la equivalencia entre tipos de datos SQL Server 2008 R2 y tipos de .NET en el modelo de Entity Framework.

Ahora es el turno de ver la equivalencia con los tipos de datos de SQL Server Compact Edition 4.0.

Siguiendo el mismo guión que en el primer post de equivalencias, aquí va el script para crear una tabla en SQL Server CE 4.0 con los todos los tipos disponibles.

CREATE TABLE [development_DataTypes] (

  [Cbiginit] bigint NOT NULL

, [Cbinary] binary(100) NULL

, [Cbit] bit NULL

, [Cdatetime] datetime NULL

, [Cfloat] float NULL

, [Cimage] image NULL

, [Cint] int NULL

, [Cmoney] money NULL

, [Cnchar] nchar(100) NULL

, [Cntext] ntext NULL

, [Cnumeric] numeric(18,0) NULL

, [Cnvarchar] nvarchar(100) NULL

, [Creal] real NULL

, [Crowversion] rowversion NULL

, [Csmallint] smallint NULL

, [Ctinyint] tinyint NULL

, [Cuniqueidentifier] uniqueidentifier NULL

, [Cvarbinary] varbinary(100) NULL

);

GO

ALTER TABLE [development_DataTypes] ADD CONSTRAINT [PK_development_DataTypes] PRIMARY KEY ([Cbiginit]);

GO


Y ahora la equivalencia prometida:

SQL Server Compact Edition 4.0

.NET

biginit

Int64

binary

Binary

bit

Boolean

datetime

DateTime

float

Double

image

Binary

int

Int32

money

Decimal

nchar

String

ntext

String

numeric

Decimal

nvarchar

String

real

Single

rowversion

Binary

smallint

Int16

tinyint

Byte

uniqueidentifier

Guid

varbinary

Binary

 

Un saludo!.

 

Soporte del diseñador de Entity Framework para SQL Server Compact Edition 4.0


Aunque en mi oficina echan pestes de SQL Server Compact Edition 4.0, yo sigo queriendo darle una oportunidad y quiero ver cómo trabajar con esta base de datos y ADO.NET Entity Framework.

El problema está en que para todos los ejemplos que hago siempre creo un nuevo proyecto de consola, y aunque puedo agregar un fichero de base de datos de SQL Server CE 4.0, cuando después agrego un modelo de datos de entidades ADO.NET, en el asistente nunca me aparece disponible la conexión a mi recién creado fichero .sdf y tampoco puede agregar una nueva conexión a SQL Server CE 4.0 (sólo aparece 3.5).

Entonces ¿Cómo trabajo con SQL Server CE 4.0 y ADO.NET Entity Framework?

Pues después de googlear un poco he descubierto que para SQL Server CE 4.0, Visual Studio 2010 sólo da soporte para el diseñador visual del modelo EDM para proyectos de tipo web (tanto un sitio web como una aplicación web).

Parece que hay posibles soluciones si estás trabajando en un proyecto de Windows Forms o de consola, pero lo cierto es por ahora no te estoy interesado en ver si la solución propuesta es o no válida.

Lo cierto es que sigo pensando que la gente de Microsoft ha hecho un gran trabajo con SQL Server CE 4.0… pero también un trabajo incompleto… y juro y perjuro que estoy intentando darle cancha a pesar de todas las incomodidades que presenta.

El enlace a la pregunta y respuesta en stackoverflow es este.

Un saludo!.

lunes, 22 de agosto de 2011

AsEnumerable en LINQ

Como ya sabrás, en LINQ se distinguen dos tipos distintos de proveedores:
los proveedores locales y proveedores remotos.

Los proveedores locales trabajan sobre datos en memoria y en esta categoría podemos encontrar a LINQ to Objects, LINQ to XML o LINQ to DataSets.

Por otro lado, los proveedores remotos son aquellos que necesitan traducir las expresiones de consulta de LINQ en llamadas específicas a la API del origen de datos. Por ejemplo, los proveedores LINQ to SQL o LINQ to Entities tienen que traducir las expresiones de consulta LINQ a T-SQL para ejecutarlas sobre el servidor de base de datos.

Siendo así, es responsabilidad del proveedor remoto intentar abarcar el mayor número de traducciones posibles desde nuestro código VB o C# al lenguaje nativo del origen de datos, pero por supuesto no todo el monte es orégano.

Si nos centramos en LINQ to Entities podemos observar como SÍ es capaz de traducir CType(<Campo> As Integer), pero NO Convert.ToInt32(<Campo>) y lanza un error del tipo NotSupportedException (lo que viene siendo que no es capaz de encontrar una equivalencia entre Convert.ToInt32 y una función de T-SQL).

Dim t =

From r In c.Clientes

Where Convert.ToInt32(r.IdCliente) > 1

image

Sin embargo, este otro código sí es perfectamente válido:

Dim t =

From r In c.Clientes

Where CType(r.IdCliente, Integer) > 1


Y generará la siguiente instrucción SQL:

SELECT

[Extent1].[IdCliente] AS [IdCliente],

[Extent1].[Nombre] AS [Nombre]

FROM [dbo].[Clientes] AS [Extent1]

WHERE CAST( [Extent1].[IdCliente] AS int) > 1

 

Partiendo de esta base, está claro que habrá métodos de VB o C# que no serán soportados por el proveedor de LINQ y lanzarán excepciones en tiempo de ejecución, y además me parece a mí que la solución pasa por el método ensayo-error para saber qué métodos .NET soporta y cuales no (tampoco le vamos a pedir las estrellas a LINQ, ¿no?). También ten en cuenta que la excepción sucederá en tiempo de ejecución porque en tiempo de compilación no hay problema ya que aún no se ha intentado traducir la consulta LINQ al origen de datos remoto, ¡cuidado con esto!).

Si llegamos a la situación en que tenemos que llamar a ciertos métodos que no están soportados por nuestro proveedor de LINQ, sólo tenemos la solución de utilizar LINQ to Objects. Es decir, aunque estemos utilizando LINQ to SQL o LINQ to Entities, eso no significa que no podamos utilizar también LINQ to Objects. De hecho, lo más habitual es mezclar ambos para conseguir tener lo mejor de ambos proveedores.

Por un lado, está claro que utilizar el proveedor de LINQ para SQL supondrá una mejora en el rendimiento de nuestro código, puesto que atacamos directamente a la base de datos y sólo recuperamos en memoria la información necesaria. Pero por otro lado, cuando hayamos recuperado esa información y ya esté cargada en memoria nadie nos impide utilizar LINQ to Objects para transformar o manipular aún más esa información.

Siendo así, para pasar del proveedor de LINQ remoto de turno a LINQ to Objects, podemos utilizar los siguientes métodos (entre otros):

  • ToArray
  • ToList
  • ToDictionary
  • AsEnumerable

La gran diferencia entre estos métodos es que los 3 primeros fuerzan la ejecución inmediata de la consulta, mientras que AsEnumerable NO fuerza esa ejecución.

Antes de continuar, permíteme que hablemos un poco sobre IQueryable(Of T).

LINQ to SQL y LINQ to Entities trabajan con la interfaz IQueryable(Of T) que hereda de IEnumerable(Of T).

Mientras que IEnumerable(Of T) trabaja directamente sobre la secuencia en memoria a través de LINQ to Objects, IQueryable(Of T) produce árboles de expresión (expression tree) que el proveedor de LINQ remoto examinará para intentar traducir la consulta al lenguaje nativo del origen de datos.

Es importante entender (al menos conceptualmente) que es exactamente una árbol de expresión y porqué lo utiliza IQueryable(Of T).

“Un árbol de expresión contiene la definición de una serie de instrucciones que servirán para traducirlas, posteriormente, en el lenguaje nativo del origen de datos“.

Uff, espero haberme explicado bien porque ha sido una interpretación libre y quizás, no muy científica.

Siendo así, podemos ver como la interfaz IEnumerable(Of T) recibe delegados genéricos de tipo Func, mientras que IQueryable(Of T) recibe árboles de expresión del tipo Expression. En lenguaje llano, IEnumerable(Of T) recibe una operación sobre la secuencia, mientras que IQueryable(Of T) recibe que se debería hacer sobre la secuencia (pero no se hace en este momento). Así después y cuando finalmente IQueryable(Of T) quiera ejecutar la expresión de consulta, podrá recorrer todo el árbol de expresión almacenado y traducirlo a una instrucción T-SQL (por cierto, que la traducción desde árboles de expresión a código nativo del origen de datos, es llevaba a cabo por clases auxiliares denominadas “proveedores de consultas” o “query providers”, ahí queda eso como cultura popular…)

Llegados a este punto y retornando a el motivo original del post, cuando llamamos por ejemplo a ToList, estamos forzando la ejecución inmediata de la consulta y convirtiendo un objeto del tipo IQueryable(Of T) en un List(Of T). Mientras que si llamamos a AsEnumerable, simplemente estamos convirtiendo un objeto del tipo IQueryable(Of T) a IEnumerable(Of T), pero NO estamos forzando la ejecución inmediata de la consulta de LINQ.

Además, es una buena convención el utilizar AsEnumerable en nuestro código para mostrar que estamos pasando del mundo del proveedor de LINQ “de turno” a LINQ to Objects porque necesitamos a este último para implementar ciertas consultas.

Por otro lado, AsEnumerable tiene su método opuesto que es AsQueryable, que justamente hace lo contrario, es decir, convertir desde IEnumerable(Of T) a IQueryable(Of T), es decir, volver desde LINQ to Objects a LINQ to SQL o LINQ to Entities.

AsQueryable podría parecer absurdo, pero visita el siguiente enlace y verás que tiene su utilidad.

Finalmente, la recomendación para obtener lo mejor de ambos mundos (proveedor de LINQ remoto y LINQ to Objects) es:

  • Primero intentar llevar a cabo el máximo de consulta en el proveedor de LINQ remoto que estemos utilizando.
  • Después llamar a AsEnumerable.
  • Por último, utilizar LINQ to Objects para incluir el resto de condiciones en la consulta.

De este modo, el error de Convert.ToInt32 de comienzo del post podríamos solucionar con la siguiente expresión de consulta:

Dim t =
(

  From r In c.Clientes
  Where r.Nombre.StartsWith("C")
).AsEnumerable.Where(
Function(p) Convert.ToInt32(p.IdCliente) > 1)


Un saludo!

jueves, 18 de agosto de 2011

Distintas sintaxis en LINQ

En LINQ puedes escribir tus consultas de 2 distintas formas.

La primera es a través de la sintaxis de consulta que permite escribir expresiones de consulta al estilo de sentencias SQL. Sin embargo, en tiempo de ejecución estas expresiones de consulta son traducidas a llamadas a métodos.  De este modo, si quieres también puedes escribir consultas LINQ a través de la sintaxis de método (o también llamada sintaxis lambda).

En Internet normalmente se habla de Query Sintax y Method Sintax (es por ello que he traducido literalmente aunque reconozco que los nombres no son muy amigables).

Por ejemplo, las siguientes 2 consultas son idénticas en su resultado, pero la primera utiliza sintaxis de consulta y la segunda sintaxis de método.

Dim consulta =

From p In personas

Where p.Nombre.IndexOf("a") <> -1

Order By p.Nombre Descending

Select p

 

Dim consulta = personas.Where(Function(p) p.Nombre.IndexOf("a") <> -1).OrderByDescending(Function(p) p.Nombre)

 
El propio Microsoft recomienda la sintaxis de consulta por ser más legible y sencilla. Lo cierto es que a mí (que estoy empezando con esto),  también me resulta más sencilla la primera expresión que la segunda.

De hecho, incluso puedes mezclar ambos tipos de sintaxis, por ejemplo:

Dim numero =

(From p In personas

            Where p.Nombre.IndexOf("a") <> -1

            Select p).Count()


Lo que está entre paréntesis es sintaxis de consulta y el método Count() es sintaxis de método.

Además y al hilo de este último ejemplo, también podemos decir que hay ciertos métodos que no tiene equivalente en la sintaxis de consulta, por lo que habrá que utilizar con obligatoriedad la sintaxis de método (por ejemplo Count).

Por otro lado, llevo muchas semanas preparando para este post. Digo esto porque es ahora cuando encajan los métodos extensores y las expresiones lambda. Es decir, la variable “consulta” es del tipo IEnumerable(Of Persona) (recordar que en LINQ to Objects todo es IEnumerable o IEnumerable(Of T)). Siendo así ¿De dónde salen el método Where, OrderByDescending, etc.?  Pues estos métodos son métodos extensores (por fin los encajo en algún sitio) de IEnumerable(Of T) y por supuesto y para terminar de hacerme feliz, reciben un parámetro que es un delegado genérico Func (ya hablamos sobre ello, ¿Recuerdas?) que puede ser reemplazado por una expresión lambda.

Ahora ya sabemos un poquito más de Linq, pero queda un montón!

Un saludo!

Ejecución diferida en LINQ

Ahora que me estoy metiendo de lleno con LINQ, he visto que hay un concepto que es muy importante de entender y asimilar que es la ejecución aplazada o diferida (en inglés deferred).

Para percatarnos de la importancia de este asunto, empezaremos la casa por el tejado y así veremos cuál es el resultado de una ejecución aplazada en una consulta de LINQ.

Module Module1

 

    Sub Main()

 

        Dim personas As New List(Of Persona)

        personas.Add(New Persona With {.Nombre = "Sergio"})

        personas.Add(New Persona With {.Nombre = "Antonio"})

        personas.Add(New Persona With {.Nombre = "Dani"})

        personas.Add(New Persona With {.Nombre = "Carmen"})

        personas.Add(New Persona With {.Nombre = "Jimena"})

 

        Dim consulta = From p In personas Where p.Nombre.IndexOf("a") <> -1 Select p

 

        For Each persona In consulta

            Console.WriteLine(persona.Nombre)

        Next

 

        personas.Add(New Persona With {.Nombre = "Sonia"})

 

        personas.Remove(personas.Find(Function(p) p.Nombre = "Dani"))

 

        For Each persona In consulta

            Console.WriteLine(persona.Nombre)

        Next

 

        Console.Read()

 

    End Sub

 

End Module

 

Public Class Persona

    Public Property Nombre As String

End Class

 

Este código realiza las siguientes acciones:

  • Crear una lista de objetos del tipo Persona.
  • Agregar 5 personas.
  • Crear una consulta LINQ, cuya condición es válida para 3 personas (Dani, Carmen y Jimena).
  • Imprimir los resultados de la consulta.
  • Añadir a nueva persona que también cumplirá la condición de la consulta (Sonia).
  • Eliminar una persona que cumple la condición de la consulta (Dani).
  • Volver a imprimir los resultados de la consulta.

Siendo así, los resultados son:

Primera impresión

Segunda impresión

Dani

 

Carmen

Carmen

Jimena

Jimena

 

Sonia

 

Gracias (o debido a… eso ahora lo discutiremos), las dos resultados son distintos.

Es decir, hemos cambiado la lista de Personas y la consulta LINQ parece que ha sabido actualizar los cambios. ¿Es esto lo que querías? ¿Hubieras preferido que los cambios no se hubieran reflejado y seguir trabajando con los datos originales? Pues si quieres resolver estas dudas tienes que entender que es la ejecución aplazada y la ejecución inmediata y después tomar la decisión que creas oportuna según tus necesidades.

Cuando declaramos una expresión de consulta LINQ y la guardamos en una variable, por ejemplo: Dim consulta = From p In personas Where p.Nombre.IndexOf("a") <> -1 Select p, en realidad lo que estamos guardando en la variable “consulta” no son los resultados de la ejecución de la consulta LINQ sino que estamos guardando la definición de la consulta y preparando la variable para almacenar los resultados, estamos guardando lo que en LINQ se conoce como un árbol de expresión (expression tree). De hecho, si nos fijamos en el tipo de la variable “consulta” veremos que es del tipo IEnumerable(Of Persona), es decir, se ha preparado para almacenar una lista de personas.

clip_image001

Siendo así, ¿Cuándo se ejecuta entonces la consulta? Pues se ejecuta cuando accedamos a los datos por primera vez (esto puede ser por un bucle For, por la ejecución de una función de agregado, porque llamamos al método ToList o ToArray, etc.)

En nuestro caso, la consulta se ejecuta en bucle For.

Sin embargo más tarde manipulamos la lista (agregando y eliminando elementos) y en el siguiente bucle For se vuelve a ejecutar la consulta y es por ello que los resultados coinciden con el estado actual de la lista.

Lo positivo de este comportamiento es que con una sola expresión de consulta (la que guardamos en la variable “consulta”) podemos consultar varias veces la misma lista y no tener que crear una nueva consulta cada vez que cambien los datos de la lista, siempre tendremos los datos actualizados.

Lo negativo es que si no sabes que el comportamiento predeterminado de una consulta LINQ es la ejecución diferida, puedes llegar a romperte los sesos pensando de donde salen esos nuevos elementos de la lista, y peor aún, imagina que en vez de LINQ to Objects estás trabajando con LINQ to Entities (o LINQ to SQL) y entonces cada nueva ejecución de la consulta significará una nueva consulta ejecutada en el motor de base de datos subyacente… vamos un application killer si no sabes manejarlo.

Veamos cómo se comporta el depurador para seguir afianzando este tema:

clip_image002

Sólo cuando el bucle For comienza a solicitar elementos sobre los que iterar es cuando la consulta es ejecutada y el con el depurador y paso a paso por instrucciones (F11), podemos ver como la consulta es ejecutada.

Igualmente, si antes de entrar en el bucle For hubiéramos inspeccionado la variable “consulta”, veríamos que se nos invita a “Expandir para procesar la colección”. De este modo y si expandimos, se ejecutaría la consulta puesto que estamos queriendo ver resultados y para mostrarlos, lógicamente se necesita ejecutar la consulta. Pero no te engañes, en la siguiente instrucción (el bucle For) se volvería a ejecutar.

En definitiva, cada vez que cualquier instrucción, método, etc., necesite iterar sobre el resultado, se ejecutará de nuevo la consulta.

clip_image003

Si trabajamos con datos en memoria (y sobre todo si las estructuras de datos no tiene un gran número de elementos), podemos hacernos los “suecos” con este tema porque que una consulta se ejecute varias veces no supondrá una merma importante de rendimiento. Sin embargo, en un escenario de base de datos (ya sea con Linq2Sql o Linq2Entities), una consulta que se ejecuta varias veces puede suponer un cuello de botella. Además, si la aplicación es multiusuario (ya me dirás tú que aplicación con base de datos no lo es), imagina filas “nuevas” apareciendo por arte de magia en tu maravillosa colección y dando al traste con tu código. ¿Cómo solucionar esto y operar sólo sobre las filas originales?

La solución pasa por “forzar” la ejecución inmediata de la consulta. De este modo la solución pasa por llamar a los métodos ToList o ToArray, que al querer devolvernos una lista o un array tendrán que ejecutar la consulta asociada y además la variable “consulta” ya sólo almacenará los resultados (no la expresión de consulta).

        Dim consulta =

            (From p In personas

            Where p.Nombre.IndexOf("a") <> -1

            Select p).ToList()

 

A partir de aquí, cualquier cambio en la colección “personas” no será reflejado en los resultados que guarda la variable “consulta”.

En cualquier caso, la solución para lidiar con la ejecución diferida es simple y llanamente entender que ocurre y obrar en consecuencia en función de los requerimientos de nuestro código (es decir, ToList está bien pero no es la panacea).

Espero que al menos hayamos entendido lo importante que es este tema, y que si no sabemos lo que es la ejecución diferida, tarde o temprano tendremos problemas con LINQ.

Un saludo!