Comment écrire une requête Asynchronous LINQ?

Après avoir lu un tas de trucs liés à LINQ, je me suis soudain rendu compte qu’aucun article ne présentait comment écrire une requête LINQ asynchrone.

Supposons que nous utilisions LINQ to SQL, la déclaration ci-dessous est claire. Toutefois, si la firebase database SQL répond lentement, le thread qui utilise ce bloc de code sera bloqué.

var result = from item in Products where item.Price > 3 select item.Name; foreach (var name in result) { Console.WriteLine(name); } 

Il semblerait que la spécification de requête LINQ actuelle ne prenne pas en charge cette fonctionnalité.

Y at-il un moyen de faire la programmation asynchrone LINQ? Cela fonctionne comme s’il y avait une notification de rappel lorsque les résultats sont prêts à être utilisés sans délai de blocage sur les E / S.

Bien que LINQ ne dispose pas vraiment de cela, le framework lui-même le fait … Vous pouvez facilement lancer votre propre exécuteur de requêtes asynchrone en 30 lignes environ … En fait, je viens de le rassembler 🙂

EDIT: En écrivant ceci, j’ai découvert pourquoi ils ne l’ont pas implémenté. Il ne peut pas gérer les types anonymes car ils sont de scope locale. Ainsi, vous n’avez aucun moyen de définir votre fonction de rappel. Ceci est une chose très importante car beaucoup de choses de linq à sql les créent dans la clause select. Toutes les suggestions ci-dessous subissent le même sort, alors je pense toujours que celle-ci est la plus facile à utiliser!

EDIT: La seule solution est de ne pas utiliser de types anonymes. Vous pouvez déclarer le rappel comme prenant simplement IEnumerable (pas d’arguments de type) et utiliser la reflection pour accéder aux champs (ICK !!). Une autre façon serait de déclarer le rappel comme “dynamic” … oh … attendez … Ce n’est pas encore sorti. 🙂 Ceci est un autre bon exemple de la façon dont la dynamic pourrait être utilisée. Certains peuvent l’appeler abus.

Jetez ceci dans votre bibliothèque d’utilitaires:

 public static class AsynchronousQueryExecutor { public static void Call(IEnumerable query, Action> callback, Action errorCallback) { Func, IEnumerable> func = new Func, IEnumerable>(InnerEnumerate); IEnumerable result = null; IAsyncResult ar = func.BeginInvoke( query, new AsyncCallback(delegate(IAsyncResult arr) { try { result = ((Func, IEnumerable>)((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 InnerEnumerate(IEnumerable query) { foreach (var item in query) //the method hangs here while the query executes { yield return item; } } } 

Et vous pourriez l’utiliser comme ceci:

 class Program { public static void Main(ssortingng[] 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 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 TestSlowLoadingEnumerable() { Thread.Sleep(5000); foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 }) { yield return i; } } } 

Aller à mettre cela sur mon blog maintenant, très pratique.

Les solutions de TheSoftwareJedi et ulrikb (alias user316318) conviennent à tout type LINQ, mais (comme le souligne Chris Moschini ), ne pas déléguer aux appels asynchrones sous-jacents qui exploitent les ports d’achèvement des E / S Windows.

Le post Asynchronous DataContext de Wesley Bakker (déclenché par un article de blog de Scott Hanselman ) décrit la classe pour LINQ to SQL qui utilise sqlCommand.BeginExecuteReader / sqlCommand.EndExecuteReader, qui exploite les ports d’achèvement d’E / S Windows.

Les ports d’achèvement d’E / S fournissent un modèle de threading efficace pour traiter plusieurs demandes d’E / S asynchrones sur un système multiprocesseur.

Sur la base de la réponse de Michael Freidgeim et de l’ article de Scott Hansellman mentionné et du fait que vous pouvez utiliser async / await , vous pouvez implémenter une méthode réutilisable ExecuteAsync(...) , qui exécute SqlCommand sous-jacente de manière asynchrone:

 protected static async Task> ExecuteAsync(IQueryable 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(reader); } 

Et puis vous pouvez (re) l’utiliser comme ceci:

 public async Task WriteNamesToConsoleAsync(ssortingng connectionSsortingng, CancellationToken token = default(CancellationToken)) { using (var ctx = new DataContext(connectionSsortingng)) { 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); } } } 

J’ai lancé un simple projet github nommé Asynq pour exécuter une requête LINQ-to-SQL asynchrone. L’idée est assez simple, bien que “fragile” à ce stade (au 16/08/2011):

  1. Laissez LINQ-to-SQL faire le gros travail de traduction de votre IQueryable en DbCommand via le DataContext.GetCommand() .
  2. Pour SQL 200 [058], DbCommand instance abstraite DbCommand obtenue à partir de GetCommand() pour obtenir un SqlCommand . Si vous utilisez SQL CE, vous n’avez pas de chance car SqlCeCommand n’expose pas le modèle asynchrone pour BeginExecuteReader et EndExecuteReader .
  3. Utilisez BeginExecuteReader et EndExecuteReader de SqlCommand à l’aide du modèle d’E / S asynchrone du framework .NET standard pour obtenir un DbDataReader dans le délégué de rappel d’achèvement que vous transmettez à la méthode BeginExecuteReader .
  4. Maintenant, nous avons un DbDataReader dont nous n’avons aucune idée des colonnes qu’il contient, ni comment mapper ces valeurs sur le ElementType IQueryable (probablement un type anonyme dans le cas des jointures). Bien sûr, à ce stade, vous pouvez écrire à la main votre propre outil de mappage de colonne qui matérialise ses résultats dans votre type anonyme ou autre. Vous devrez en écrire un nouveau pour chaque type de résultat de requête, en fonction de la manière dont LINQ-to-SQL traite votre IQueryable et du code SQL qu’il génère. C’est une option assez méchante et je ne le recommande pas car ce n’est pas maintenable et ce ne serait pas toujours correct. LINQ-to-SQL peut modifier votre formulaire de requête en fonction des valeurs de paramètre que vous transmettez, par exemple query.Take(10).Skip(0) produit un query.Take(10).Skip(10) SQL différent de query.Take(10).Skip(10) , et peut-être un schéma de jeu de résultats différent. Votre meilleur pari est de gérer ce problème de matérialisation par programmation:
  5. DbDataReader ” un matérialiseur d’object d’exécution simpliste qui extrait les colonnes du DbDataReader dans un ordre défini en fonction des atsortingbuts de mappage LINQ-SQL du type ElementType pour le IQueryable . Mettre en œuvre correctement est probablement la partie la plus difficile de cette solution.

Comme d’autres l’ont découvert, la méthode DataContext.Translate() ne gère pas les types anonymes et ne peut mapper directement un DbDataReader vers un object proxy LINQ-to-SQL correctement atsortingbué. Comme la plupart des requêtes à écrire dans LINQ impliquent des jointures complexes qui finissent inévitablement par nécessiter des types anonymes pour la clause select finale, il est inutile d’utiliser cette méthode DataContext.Translate() .

Cette solution présente quelques inconvénients mineurs lors de l’utilisation du fournisseur IQueryable LINQ-to-SQL existant:

  1. Vous ne pouvez pas mapper une seule instance d’object à plusieurs propriétés de type anonyme dans la clause select finale de votre IQueryable , par exemple à from x in db.Table1 select new { a = x, b = x } . LINQ-to-SQL conserve en interne le suivi des ordonnées de colonne correspondant aux propriétés; il n’expose pas cette information à l’utilisateur final, vous n’avez donc aucune idée des colonnes qui sont réutilisées dans DbDataReader et qui sont “distinctes”.
  2. Vous ne pouvez pas inclure de valeurs constantes dans votre clause select finale – celles-ci ne sont pas traduites en SQL et seront absentes de DbDataReader . Vous devrez donc construire une logique personnalisée pour extraire ces valeurs constantes de l’arborescence Expression IQueryable . être assez compliqué et n’est tout simplement pas justifiable.

Je suis sûr qu’il existe d’autres modèles de requête susceptibles de se casser, mais ce sont les deux plus importants à mon avis qui pourraient poser problème dans une couche d’access aux données LINQ-to-SQL existante.

Ces problèmes sont faciles à résoudre – ne les faites tout simplement pas dans vos requêtes, car aucun des deux modèles n’apporte de bénéfice au résultat final de la requête. Espérons que ce conseil s’applique à tous les modèles de requête susceptibles de causer des problèmes de matérialisation d’object :-P. C’est un problème difficile à résoudre si vous n’avez pas access aux informations de mappage de colonnes LINQ-to-SQL.

Une approche plus «complète» pour résoudre le problème consisterait à ré-implémenter efficacement la quasi-totalité de LINQ-to-SQL, ce qui prend un peu plus de temps :-P. À partir d’une qualité, l’implémentation d’un fournisseur LINQ-to-SQL open-source serait un bon moyen d’y arriver. La raison pour laquelle vous devez le réimplémenter est que vous ayez access à toutes les informations de mappage de colonne utilisées pour matérialiser les résultats de DbDataReader sur une instance d’object sans aucune perte d’informations.