Entity Framework Queryable async

97

Estoy trabajando en algunas cosas de la API web usando Entity Framework 6 y uno de mis métodos de controlador es un "Obtener todo" que espera recibir el contenido de una tabla de mi base de datos como IQueryable<Entity>. En mi repositorio, me pregunto si hay alguna razón ventajosa para hacer esto de forma asincrónica, ya que soy nuevo en el uso de EF con async.

Básicamente se reduce a

 public async Task<IQueryable<URL>> GetAllUrlsAsync()
 {
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
 }

vs

 public IQueryable<URL> GetAllUrls()
 {
    return context.Urls.AsQueryable();
 }

¿La versión asíncrona realmente producirá beneficios de rendimiento aquí o estoy incurriendo en gastos generales innecesarios al proyectar primero a una Lista (usando asíncrono, claro) y LUEGO a IQueryable?

Jesse Carter
fuente
1
context.Urls es de tipo DbSet <URL> que implementa IQueryable <URL> por lo que .AsQueryable () es redundante. msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx Suponiendo que ha seguido los patrones que proporciona EF, o ha utilizado las herramientas que crean el contexto para usted.
Sean B

Respuestas:

223

El problema parece ser que no ha entendido cómo funciona async / await con Entity Framework.

Acerca de Entity Framework

Entonces, veamos este código:

public IQueryable<URL> GetAllUrls()
{
    return context.Urls.AsQueryable();
}

y ejemplo de su uso:

repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()

¿Qué pasa ahí?

  1. Obtenemos el IQueryableobjeto (aún no accedemos a la base de datos) usandorepo.GetAllUrls()
  2. Creamos un nuevo IQueryableobjeto con una condición especificada usando.Where(u => <condition>
  3. Creamos un nuevo IQueryableobjeto con límite de paginación especificado usando.Take(10)
  4. Recuperamos resultados de la base de datos usando .ToList(). Nuestro IQueryableobjeto se compila en sql (como select top 10 * from Urls where <condition>). Y la base de datos puede usar índices, el servidor SQL le envía solo 10 objetos de su base de datos (no todos los mil millones de URL almacenados en la base de datos)

Bien, veamos el primer código:

public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
}

Con el mismo ejemplo de uso obtuvimos:

  1. Estamos cargando en la memoria todos los mil millones de direcciones URL almacenadas en su base de datos utilizando await context.Urls.ToListAsync();.
  2. Tenemos memoria desbordada. Manera correcta de matar su servidor

Acerca de async / await

¿Por qué se prefiere usar async / await? Veamos este código:

var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));

¿Qué pasa aquí?

  1. Comenzando en la línea 1 var stuff1 = ...
  2. Enviamos una solicitud al servidor SQL para el que queremos obtener algunas cosas1 userId
  3. Esperamos (el hilo actual está bloqueado)
  4. Esperamos (el hilo actual está bloqueado)
  5. .....
  6. El servidor SQL nos envía una respuesta
  7. Pasamos a la línea 2 var stuff2 = ...
  8. Enviamos una solicitud al servidor SQL para el que queremos obtener algunas cosas2. userId
  9. Esperamos (el hilo actual está bloqueado)
  10. Y otra vez
  11. .....
  12. El servidor SQL nos envía una respuesta
  13. Hacemos vista

Así que veamos una versión asincrónica:

var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));

¿Qué pasa aquí?

  1. Enviamos una solicitud al servidor sql para obtener stuff1 (línea 1)
  2. Enviamos una solicitud al servidor sql para obtener stuff2 (línea 2)
  3. Esperamos las respuestas del servidor SQL, pero el hilo actual no está bloqueado, puede manejar consultas de otros usuarios
  4. Hacemos vista

Manera correcta de hacerlo

Tan buen código aquí:

using System.Data.Entity;

public IQueryable<URL> GetAllUrls()
{
   return context.Urls.AsQueryable();
}

public async Task<List<URL>> GetAllUrlsByUser(int userId) {
   return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}

Tenga en cuenta que debe agregar using System.Data.Entitypara usar el método ToListAsync()para IQueryable.

Tenga en cuenta que si no necesita filtrado, paginación y demás, no necesita trabajar con IQueryable. Puede usar await context.Urls.ToListAsync()y trabajar con materialized List<Url>.

Viktor Lova
fuente
3
@Korijn mirando la imagen i2.iis.net/media/7188126/… de Introducción a la arquitectura IIS Puedo decir que todas las solicitudes en IIS se procesan de manera asincrónica
Viktor Lova
7
Dado que no está actuando sobre el conjunto de resultados en el GetAllUrlsByUsermétodo, no es necesario que sea asíncrono. Simplemente devuelva la tarea y evite que el compilador genere una máquina de estado innecesaria.
Johnathon Sullinger
1
@JohnathonSullinger Aunque eso funcionaría en un flujo feliz, ¿no tiene eso el efecto secundario de que cualquier excepción no emergerá aquí y se propagará al primer lugar que tiene una espera? (No es que eso sea necesariamente malo, pero ¿es un cambio de comportamiento?)
Henry Been
9
Es interesante que nadie se dé cuenta de que el segundo ejemplo de código en "Acerca de async / await" no tiene ningún sentido, porque arrojaría una excepción ya que ni EF ni EF Core son seguros para subprocesos, por lo que intentar ejecutar en paralelo solo arrojará una excepción
Tseng
1
Aunque esta respuesta es correcta, recomendaría evitar usar asyncy awaitsi NO está haciendo nada con la lista. Deje que la persona que llama lo haga await. Cuando espera la llamada en esta etapa return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();, está creando un contenedor asíncrono adicional cuando descompila el ensamblado y mira el IL.
Ali Khakpouri
10

Hay una gran diferencia en el ejemplo que ha publicado, la primera versión:

var urls = await context.Urls.ToListAsync();

Esto es malo , básicamente lo hace select * from table, devuelve todos los resultados a la memoria y luego aplica los resultados whereen la colección de memoria en lugar de hacerlo select * from table where...en la base de datos.

El segundo método no llegará a la base de datos hasta que se aplique una consulta a IQueryable(probablemente a través de una operación de .Where().Select()estilo linq que solo devolverá los valores de la base de datos que coincidan con la consulta.

Si sus ejemplos fueran comparables, la asyncversión generalmente será un poco más lenta por solicitud, ya que hay más sobrecarga en la máquina de estado que genera el compilador para permitir la asyncfuncionalidad.

Sin embargo, la principal diferencia (y beneficio) es que la asyncversión permite más solicitudes simultáneas, ya que no bloquea el hilo de procesamiento mientras espera que IO se complete (consulta de base de datos, acceso a archivos, solicitud web, etc.).

Trevor Pilley
fuente
7
hasta que se aplica una consulta a IQueryable .... ni IQueryable.Where ni IQueryable.Select fuerzan la ejecución de la consulta. El prior aplica un predicado y el segundo aplica una proyección. No se ejecuta hasta que se utiliza un operador de materialización, como ToList, ToArray, Single o First.
JJS
0

En pocas palabras,
IQueryableestá diseñado para posponer el proceso RUN y, en primer lugar, construir la expresión junto con otras IQueryableexpresiones, y luego interpretar y ejecutar la expresión como un todo.
Pero el ToList()método (o algunos tipos de métodos como ese), está indicado para ejecutar la expresión instantáneamente "tal cual".
Su primer método ( GetAllUrlsAsync), se ejecutará inmediatamente, porque es IQueryableseguido por ToListAsync()método. por lo tanto, se ejecuta instantáneamente (asincrónico) y devuelve un montón de IEnumerables.
Mientras tanto, su segundo método ( GetAllUrls), no se ejecutará. En cambio, devuelve una expresión y CALLER de este método es responsable de ejecutar la expresión.

Rzassar
fuente