c# - sintaxis - Cómo escribir consultas asincrónicas LINQ?
operadores en linq c# (4)
Basado en la respuesta de Michael Freidgeim y la mencionada publicación de blog de Scott Hansellman y el hecho de que puede usar async
/ await
, puede implementar el ExecuteAsync<T>(...)
reutilizable, que ejecuta SqlCommand
subyacente de forma asincrónica:
protected static async Task<IEnumerable<T>> ExecuteAsync<T>(IQueryable<T> query,
DataContext ctx,
CancellationToken token = default(CancellationToken))
{
var cmd = (SqlCommand)ctx.GetCommand(query);
if (cmd.Connection.State == ConnectionState.Closed)
await cmd.Connection.OpenAsync(token);
var reader = await cmd.ExecuteReaderAsync(token);
return ctx.Translate<T>(reader);
}
Y luego puedes (re) usarlo así:
public async Task WriteNamesToConsoleAsync(string connectionString, CancellationToken token = default(CancellationToken))
{
using (var ctx = new DataContext(connectionString))
{
var query = from item in Products where item.Price > 3 select item.Name;
var result = await ExecuteAsync(query, ctx, token);
foreach (var name in result)
{
Console.WriteLine(name);
}
}
}
Después de leer un montón de cosas relacionadas con LINQ, de repente me di cuenta de que ningún artículo presentaba cómo escribir consultas LINQ asíncronas.
Supongamos que usamos LINQ to SQL, la declaración a continuación es clara. Sin embargo, si la base de datos SQL responde lentamente, el hilo que utiliza este bloque de código se verá obstaculizado.
var result = from item in Products where item.Price > 3 select item.Name;
foreach (var name in result)
{
Console.WriteLine(name);
}
Parece que la especificación actual de consulta LINQ no proporciona soporte para esto.
¿Hay alguna forma de hacer una programación asincrónica LINQ? Funciona como si hubiera una notificación de devolución de llamada cuando los resultados están listos para usar sin ningún retraso de bloqueo en E / S.
Inicié un proyecto simple de github llamado Asynq para realizar una ejecución asíncrona de consultas LINQ-to-SQL. La idea es bastante simple, aunque "frágil" en esta etapa (a partir del 16/08/2011):
- Deje que LINQ-to-SQL haga el trabajo "pesado" de traducir su
IQueryable
en unDbCommand
través deDataContext.GetCommand()
. - Para SQL 200 [058],
DbCommand
instancia deDbCommand
abstracta que obtuviste deGetCommand()
para obtener unSqlCommand
. Si está utilizando SQL CE no tiene suerte, ya queSqlCeCommand
no expone el patrón asíncrono paraBeginExecuteReader
yEndExecuteReader
. - Utilice
BeginExecuteReader
yEndExecuteReader
fuera deSqlCommand
utilizando el patrón de E / S asíncronas de .NET Framework estándar para obtener unDbDataReader
en el delegado de devolución de llamada de finalización que pase al métodoBeginExecuteReader
. - Ahora tenemos un
DbDataReader
que no tenemos idea de qué columnas contiene ni cómo volver a asignar esos valores alIQueryable
deElementType
(lo más probable es que sea un tipo anónimo en el caso de las uniones). Claro, en este punto podría escribir a mano su propio mapeador de columnas que materializa sus resultados en su tipo anónimo o lo que sea. Tendría que escribir uno nuevo para cada tipo de resultado de consulta, dependiendo de cómo trate LINQ-to-SQL su IQueryable y el código SQL que genera. Esta es una opción bastante desagradable y no la recomiendo, ya que no es mantenible ni tampoco será siempre correcta. LINQ-to-SQL puede cambiar su formulario de consulta dependiendo de los valores de parámetros que pase, por ejemploquery.Take(10).Skip(0)
produce SQL diferente dequery.Take(10).Skip(10)
, y quizás un esquema de resultados diferente. Su mejor apuesta es manejar este problema de materialización programáticamente: - "
DbDataReader
" un materializador de objetos en tiempo de ejecución simplificado que saca columnas delDbDataReader
en un orden definido de acuerdo con los atributos de mapeo LINQ-to-SQL deElementType
Type paraIQueryable
. Implementar esto correctamente es probablemente la parte más desafiante de esta solución.
Como otros han descubierto, el método DataContext.Translate()
no maneja tipos anónimos y solo puede asignar un DbDataReader
directamente a un objeto proxy LINQ-to-SQL atribuido correctamente. Dado que la mayoría de las consultas que valen la pena escribir en LINQ implican combinaciones complejas que inevitablemente requieren tipos anónimos para la cláusula de selección final, no tiene sentido utilizar el método diluido proporcionado DataContext.Translate()
.
Esta solución presenta algunos inconvenientes menores cuando aprovecha el proveedor de IQueryable LINQ-SQL maduro existente:
- No puede asignar una única instancia de objeto a múltiples propiedades de tipo anónimo en la cláusula de selección final de su
IQueryable
, por ejemplo,from x in db.Table1 select new { a = x, b = x }
. LINQ-to-SQL realiza un seguimiento interno de los ordinales de columna que se asignan a qué propiedades; no expone esta información al usuario final, por lo que no tiene idea de qué columnas delDbDataReader
se reutilizan y cuáles son "distintas". - No puede incluir valores constantes en la cláusula de selección final; estos no se traducen a SQL y estarán ausentes de
DbDataReader
por lo que tendría que crear una lógica personalizada para extraer estos valores constantes del árbol deExpression
IQueryable, lo que haría ser bastante molesto y simplemente no es justificable.
Estoy seguro de que hay otros patrones de consulta que podrían romperse, pero estos son los dos más grandes que podría pensar que podrían causar problemas en una capa existente de acceso a datos LINQ-a-SQL.
Estos problemas son fáciles de superar, simplemente no los haga en sus consultas ya que ningún patrón proporciona ningún beneficio al resultado final de la consulta. Esperamos que este consejo se aplique a todos los patrones de consulta que podrían causar problemas de materialización del objeto :-P. Es un problema difícil de resolver al no tener acceso a la información de asignación de columnas de LINQ-to-SQL.
Un enfoque más "completo" para resolver el problema sería implementar de nuevo de manera efectiva casi todo LINQ-to-SQL, lo que requiere un poco más de tiempo :-P. A partir de una implementación de proveedores de LINQ-a-SQL de código abierto de calidad sería una buena manera de hacerlo aquí. La razón por la que debe volver a implementarla es para tener acceso a toda la información de asignación de columnas utilizada para materializar los resultados de DbDataReader
en una instancia de objeto sin pérdida de información.
Las soluciones de TheSoftwareJedi y ulrikb (también conocido como user316318) son buenas para cualquier tipo de LINQ, pero (como señala Chris Moschini ) NO delegan en las llamadas asincrónicas subyacentes que aprovechan los puertos de finalización de E / S de Windows.
La publicación Asynchronous DataContext de Wesley Bakker (desencadenada por una publicación de blog de Scott Hanselman ) describe la clase para LINQ to SQL que usa sqlCommand.BeginExecuteReader / sqlCommand.EndExecuteReader, que aprovecha los puertos de finalización de E / S de Windows.
Los puertos de terminación de E / S proporcionan un modelo de subprocesamiento eficiente para procesar múltiples solicitudes de E / S asíncronas en un sistema multiprocesador.
Si bien LINQ no tiene esto en sí mismo, el marco en sí sí lo hace ... Puede ejecutar fácilmente su propio ejecutor de consultas asincrónicas en 30 líneas más o menos ... De hecho, acabo de lanzar esto para usted :)
EDITAR: al escribir esto, descubrí por qué no lo implementaron. No puede manejar tipos anónimos ya que tienen un alcance local. Por lo tanto, no tiene forma de definir su función de devolución de llamada. Esto es algo muy importante ya que muchas cosas de linq a sql las crean en la cláusula de selección. Cualquiera de las siguientes sugerencias sufre el mismo destino, ¡así que todavía creo que esta es la más fácil de usar!
EDITAR: La única solución es no usar tipos anónimos. Puede declarar la devolución de llamada simplemente tomando IEnumerable (sin args de tipo), y use reflection para acceder a los campos (ICK !!). Otra forma sería declarar la devolución de llamada como "dinámica" ... oh ... espera ... Aún no ha salido. :) Este es otro ejemplo decente de cómo se podría utilizar la dinámica. Algunos pueden llamarlo abuso.
Tíralo en tu biblioteca de utilidades:
public static class AsynchronousQueryExecutor
{
public static void Call<T>(IEnumerable<T> query, Action<IEnumerable<T>> callback, Action<Exception> errorCallback)
{
Func<IEnumerable<T>, IEnumerable<T>> func =
new Func<IEnumerable<T>, IEnumerable<T>>(InnerEnumerate<T>);
IEnumerable<T> result = null;
IAsyncResult ar = func.BeginInvoke(
query,
new AsyncCallback(delegate(IAsyncResult arr)
{
try
{
result = ((Func<IEnumerable<T>, IEnumerable<T>>)((AsyncResult)arr).AsyncDelegate).EndInvoke(arr);
}
catch (Exception ex)
{
if (errorCallback != null)
{
errorCallback(ex);
}
return;
}
//errors from inside here are the callbacks problem
//I think it would be confusing to report them
callback(result);
}),
null);
}
private static IEnumerable<T> InnerEnumerate<T>(IEnumerable<T> query)
{
foreach (var item in query) //the method hangs here while the query executes
{
yield return item;
}
}
}
Y podrías usarlo así:
class Program
{
public static void Main(string[] args)
{
//this could be your linq query
var qry = TestSlowLoadingEnumerable();
//We begin the call and give it our callback delegate
//and a delegate to an error handler
AsynchronousQueryExecutor.Call(qry, HandleResults, HandleError);
Console.WriteLine("Call began on seperate thread, execution continued");
Console.ReadLine();
}
public static void HandleResults(IEnumerable<int> results)
{
//the results are available in here
foreach (var item in results)
{
Console.WriteLine(item);
}
}
public static void HandleError(Exception ex)
{
Console.WriteLine("error");
}
//just a sample lazy loading enumerable
public static IEnumerable<int> TestSlowLoadingEnumerable()
{
Thread.Sleep(5000);
foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 })
{
yield return i;
}
}
}
Voy a poner esto en mi blog ahora, bastante útil.