¿Cuál es la diferencia entre .ToList(), .AsEnumerable(), AsQueryable()?
Conozco algunas diferencias de LINQ to Entities y LINQ to Objects que implementa el primero IQueryable
y el segundo IEnumerable
, y el alcance de mi pregunta está dentro de EF 5.
Mi pregunta es ¿cuál es la diferencia técnica de esos 3 métodos? Veo que en muchas situaciones todos funcionan. También veo el uso de combinaciones de ellos como .ToList().AsQueryable()
.
¿Qué significan exactamente esos métodos?
¿Existe algún problema de rendimiento o algo que pueda llevar al uso de uno sobre el otro?
¿Por qué se usaría, por ejemplo,
.ToList().AsQueryable()
en lugar de.AsQueryable()
?
Hay mucho que decir sobre esto. Permítanme centrarme AsEnumerable
en AsQueryable
y mencionar ToList()
a lo largo del camino.
¿Qué hacen estos métodos?
AsEnumerable
y AsQueryable
emitir o convertir a IEnumerable
o IQueryable
, respectivamente. Digo emitir o convertir con una razón:
Cuando el objeto de origen ya implementa la interfaz de destino, el objeto de origen en sí se devuelve pero se transmite a la interfaz de destino. En otras palabras: el tipo no se cambia, pero sí el tipo en tiempo de compilación.
Cuando el objeto de origen no implementa la interfaz de destino, el objeto de origen se convierte en un objeto que implementa la interfaz de destino. Por lo tanto, se cambian tanto el tipo como el tipo en tiempo de compilación.
Permítanme mostrar esto con algunos ejemplos. Tengo este pequeño método que informa el tipo en tiempo de compilación y el tipo real de un objeto ( cortesía de Jon Skeet ):
void ReportTypeProperties<T>(T obj)
{
Console.WriteLine("Compile-time type: {0}", typeof(T).Name);
Console.WriteLine("Actual type: {0}", obj.GetType().Name);
}
Probemos un linq-to-sql arbitrario Table<T>
, que implemente IQueryable
:
ReportTypeProperties(context.Observations);
ReportTypeProperties(context.Observations.AsEnumerable());
ReportTypeProperties(context.Observations.AsQueryable());
El resultado:
Compile-time type: Table`1
Actual type: Table`1
Compile-time type: IEnumerable`1
Actual type: Table`1
Compile-time type: IQueryable`1
Actual type: Table`1
Verá que siempre se devuelve la clase de tabla en sí, pero su representación cambia.
Ahora un objeto que implementa IEnumerable
, no IQueryable
:
var ints = new[] { 1, 2 };
ReportTypeProperties(ints);
ReportTypeProperties(ints.AsEnumerable());
ReportTypeProperties(ints.AsQueryable());
Los resultados:
Compile-time type: Int32[]
Actual type: Int32[]
Compile-time type: IEnumerable`1
Actual type: Int32[]
Compile-time type: IQueryable`1
Actual type: EnumerableQuery`1
Ahí está. AsQueryable()
ha convertido la matriz en un EnumerableQuery
, que "representa una IEnumerable<T>
colección como IQueryable<T>
fuente de datos". (MSDN).
¿Cual es el uso?
AsEnumerable
se usa con frecuencia para cambiar de cualquier IQueryable
implementación a LINQ a objetos (L2O), principalmente porque la primera no admite funciones que tiene L2O. Para obtener más detalles, consulte ¿Cuál es el efecto de AsEnumerable() en una entidad LINQ? .
Por ejemplo, en una consulta de Entity Framework solo podemos usar un número restringido de métodos. Entonces, si, por ejemplo, necesitamos usar uno de nuestros propios métodos en una consulta, normalmente escribiríamos algo como
var query = context.Observations.Select(o => o.Id)
.AsEnumerable().Select(x => MySuperSmartMethod(x))
ToList
– que convierte an IEnumerable<T>
en a List<T>
– también se utiliza a menudo para este propósito. La ventaja de usar AsEnumerable
vs. ToList
es que AsEnumerable
no ejecuta la consulta. AsEnumerable
preserva la ejecución diferida y no crea una lista intermedia a menudo inútil.
Por otro lado, cuando se desea la ejecución forzada de una consulta LINQ, ToList
puede haber una forma de hacerlo.
AsQueryable
se puede utilizar para hacer que una colección enumerable acepte expresiones en declaraciones LINQ. Consulte aquí para obtener más detalles: ¿Realmente necesito usar AsQueryable() en la colección? .
¡Nota sobre el abuso de sustancias!
AsEnumerable
funciona como una droga. Es una solución rápida, pero tiene un costo y no aborda el problema subyacente.
En muchas respuestas de Stack Overflow, veo personas que solicitan AsEnumerable
solucionar casi cualquier problema con métodos no admitidos en expresiones LINQ. Pero el precio no siempre está claro. Por ejemplo, si haces esto:
context.MyLongWideTable // A table with many records and columns
.Where(x => x.Type == "type")
.Select(x => new { x.Name, x.CreateDate })
...todo se traduce claramente en una declaración SQL que filtra ( Where
) y proyecta ( Select
). Es decir, se reducen tanto la longitud como la anchura, respectivamente, del conjunto de resultados de SQL.
Ahora supongamos que los usuarios solo quieren ver la parte de la fecha CreateDate
. En Entity Framework descubrirás rápidamente que...
.Select(x => new { x.Name, x.CreateDate.Date })
...no es compatible (en el momento de escribir este artículo). Ah, afortunadamente existe la AsEnumerable
solución:
context.MyLongWideTable.AsEnumerable()
.Where(x => x.Type == "type")
.Select(x => new { x.Name, x.CreateDate.Date })
Claro, probablemente funcione. Pero guarda toda la tabla en la memoria y luego aplica el filtro y las proyecciones. Bueno, la mayoría de la gente es lo suficientemente inteligente como para hacer lo Where
primero:
context.MyLongWideTable
.Where(x => x.Type == "type").AsEnumerable()
.Select(x => new { x.Name, x.CreateDate.Date })
Pero aún así se recuperan primero todas las columnas y la proyección se realiza en la memoria.
La verdadera solución es:
context.MyLongWideTable
.Where(x => x.Type == "type")
.Select(x => new { x.Name, DbFunctions.TruncateTime(x.CreateDate) })
(Pero eso requiere sólo un poco más de conocimiento...)
¿Qué NO hacen estos métodos?
Restaurar capacidades de IQueryable
Ahora una advertencia importante. Cuando tu lo hagas
context.Observations.AsEnumerable()
.AsQueryable()
terminará con el objeto fuente representado como IQueryable
. (Porque ambos métodos solo emiten y no convierten).
Pero cuando lo haces
context.Observations.AsEnumerable().Select(x => x)
.AsQueryable()
¿Cuál será el resultado?
El Select
produce un WhereSelectEnumerableIterator
. Esta es una clase interna de .Net que implementa IEnumerable
, noIQueryable
. Entonces se ha producido una conversión a otro tipo y la subsiguiente AsQueryable
ya no puede devolver la fuente original.
La implicación de esto es que el uso no AsQueryable
es una forma de inyectar mágicamente un proveedor de consultas con sus características específicas en un enumerable. Supongamos que lo haces
var query = context.Observations.Select(o => o.Id)
.AsEnumerable().Select(x => x.ToString())
.AsQueryable()
.Where(...)
La condición donde nunca se traducirá a SQL. AsEnumerable()
seguido de declaraciones LINQ corta definitivamente la conexión con el proveedor de consultas del marco de la entidad.
Muestro este ejemplo deliberadamente porque he visto preguntas aquí en las que la gente, por ejemplo, intenta "inyectar" Include
capacidades en una colección llamando a AsQueryable
. Compila y ejecuta, pero no hace nada porque el objeto subyacente Include
ya no tiene implementación.
Ejecutar
Ambos AsQueryable
y AsEnumerable
no ejecutan (ni enumeran ) el objeto fuente. Sólo cambian su tipo o representación. Ambas interfaces involucradas, IQueryable
y IEnumerable
, no son más que "una enumeración esperando a suceder". No se ejecutan antes de que se les obligue a hacerlo, por ejemplo, como se mencionó anteriormente, llamando a ToList()
.
Eso significa que ejecutar un IEnumerable
objeto obtenido al llamar AsEnumerable
a un IQueryable
objeto ejecutará el subyacente IQueryable
. Una ejecución posterior del IEnumerable
ejecutará nuevamente el IQueryable
. Lo cual puede resultar muy caro.
Implementaciones específicas
Hasta ahora, esto se trataba sólo de los métodos de extensión Queryable.AsQueryable
y Enumerable.AsEnumerable
. Pero, por supuesto, cualquiera puede escribir métodos de instancia o métodos de extensión con los mismos nombres (y funciones).
De hecho, un ejemplo común de un AsEnumerable
método de extensión específico es DataTableExtensions.AsEnumerable
. DataTable
no implementa IQueryable
or IEnumerable
, por lo que los métodos de extensión habituales no se aplican.
Listar()
- Ejecutar la consulta inmediatamente.
ComoEnumerable()
- perezoso (ejecuta la consulta más tarde)
- Parámetro:
Func<TSource, bool>
- Cargue CADA registro en la memoria de la aplicación y luego maneje/filtrelos. (por ejemplo, Dónde/Tomar/Omitir, seleccionará * de la tabla1, en la memoria, luego seleccionará los primeros X elementos) (En este caso, lo que hizo: Linq-to-SQL + Linq-to-Object)
Como consultable()
- perezoso (ejecuta la consulta más tarde)
- Parámetro:
Expression<Func<TSource, bool>>
- Convierta Expression en T-SQL (con el proveedor específico), realice consultas de forma remota y cargue el resultado en la memoria de su aplicación.
- Es por eso que DbSet (en Entity Framework) también hereda IQueryable para obtener la consulta eficiente.
- No cargue todos los registros, por ejemplo, si Take(5), generará una selección de los 5 mejores * SQL en segundo plano. Esto significa que este tipo es más amigable con SQL Database y es por eso que este tipo generalmente tiene un mayor rendimiento y se recomienda cuando se trata de una base de datos.
- Por lo
AsQueryable()
general, funciona mucho más rápido queAsEnumerable()
cuando genera T-SQL al principio, lo que incluye todas las condiciones donde en su Linq.