Entity Framework: il existe déjà un DataReader ouvert associé à cette commande

285

J'utilise Entity Framework et parfois j'obtiendrai cette erreur.

EntityCommandExecutionException
{"There is already an open DataReader associated with this Command which must be closed first."}
   at System.Data.EntityClient.EntityCommandDefinition.ExecuteStoreCommands...

Même si je ne fais aucune gestion de connexion manuelle.

cette erreur se produit par intermittence.

code qui déclenche l'erreur (raccourci pour faciliter la lecture):

        if (critera.FromDate > x) {
            t= _tEntitites.T.Where(predicate).ToList();
        }
        else {
            t= new List<T>(_tEntitites.TA.Where(historicPredicate).ToList());
        }

en utilisant Dispose pattern afin d'ouvrir une nouvelle connexion à chaque fois.

using (_tEntitites = new TEntities(GetEntityConnection())) {

    if (critera.FromDate > x) {
        t= _tEntitites.T.Where(predicate).ToList();
    }
    else {
        t= new List<T>(_tEntitites.TA.Where(historicPredicate).ToList());
    }

}

toujours problématique

pourquoi EF ne réutiliserait-il pas une connexion si elle était déjà ouverte.

Sonic Soul
la source
1
Je me rends compte que cette question est ancienne, mais je serais intéressé de savoir de quel type sont vos variables predicateet historicPredicate. J'ai découvert que si vous passez Func<T, bool>à Where()cela, il se compilera et fonctionnera parfois (car il fait le "où" en mémoire). Ce que vous devriez être en train de faire est passer Expression<Func<T, bool>>à Where().
James

Réponses:

351

Il ne s'agit pas de fermer la connexion. EF gère correctement la connexion. Ma compréhension de ce problème est qu'il existe plusieurs commandes de récupération de données exécutées sur une seule connexion (ou une seule commande avec plusieurs sélections) tandis que le DataReader suivant est exécuté avant que le premier ait terminé la lecture. La seule façon d'éviter l'exception est d'autoriser plusieurs DataReaders imbriqués = activer MultipleActiveResultSets. Un autre scénario lorsque cela se produit toujours est lorsque vous parcourez le résultat de la requête (IQueryable) et que vous déclencherez un chargement paresseux pour l'entité chargée à l'intérieur de l'itération.

Ladislav Mrnka
la source
2
cela aurait du sens. mais il n'y a qu'une seule sélection dans chaque méthode.
Sonic Soul
1
@Sonic: Telle est la question. Il y a peut-être plus d'une commande exécutée mais vous ne la voyez pas. Je ne sais pas si cela peut être retracé dans Profiler (une exception peut être levée avant l'exécution du deuxième lecteur). Vous pouvez également essayer de caster la requête en ObjectQuery et appeler ToTraceString pour voir la commande SQL. C'est difficile à suivre. J'allume toujours MARS.
Ladislav Mrnka
2
@Sonic: Non, mon intention était de vérifier les commandes SQL exécutées et terminées.
Ladislav Mrnka
11
génial, mon problème était le deuxième scénario: «lorsque vous parcourez le résultat de la requête (IQueryable) et que vous déclenchez un chargement paresseux pour l'entité chargée à l'intérieur de l'itération».
Amr Elgarhy
6
L'activation de MARS peut apparemment avoir de mauvais effets secondaires: designlimbo.com/?p=235
Søren Boisen
126

Alternativement à l'utilisation de MARS (MultipleActiveResultSets), vous pouvez écrire votre code afin de ne pas ouvrir plusieurs jeux de résultats.

Ce que vous pouvez faire est de récupérer les données en mémoire, de cette façon, vous n'aurez pas le lecteur ouvert. Elle est souvent causée par l'itération d'un ensemble de résultats tout en essayant d'ouvrir un autre ensemble de résultats.

Exemple de code:

public class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

public class Blog
{
    public int BlogID { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int PostID { get; set; }
    public virtual Blog Blog { get; set; }
    public string Text { get; set; }
}

Disons que vous effectuez une recherche dans votre base de données contenant ces éléments:

var context = new MyContext();

//here we have one resultset
var largeBlogs = context.Blogs.Where(b => b.Posts.Count > 5); 

foreach (var blog in largeBlogs) //we use the result set here
{
     //here we try to get another result set while we are still reading the above set.
    var postsWithImportantText = blog.Posts.Where(p=>p.Text.Contains("Important Text"));
}

Nous pouvons faire une solution simple à cela en ajoutant .ToList () comme ceci:

var largeBlogs = context.Blogs.Where(b => b.Posts.Count > 5).ToList();

Cela oblige entityframework à charger la liste en mémoire, donc lorsque nous l'itérons dans la boucle foreach, il n'utilise plus le lecteur de données pour ouvrir la liste, il est plutôt en mémoire.

Je me rends compte que cela pourrait ne pas être souhaité si vous voulez par exemple charger des propriétés par lazyload. Il s'agit principalement d'un exemple qui, nous l'espérons, explique comment / pourquoi vous pourriez avoir ce problème, afin que vous puissiez prendre des décisions en conséquence

Jim Wolff
la source
7
Cette solution a fonctionné pour moi. Ajoutez .ToList () juste après la requête et avant de faire quoi que ce soit d'autre avec le résultat.
TJKjaer
9
Soyez prudent avec cela et faites preuve de bon sens. Si vous ToListingérez mille objets, cela augmentera la mémoire d'une tonne. Dans cet exemple spécifique, vous feriez mieux de combiner la requête interne avec la première afin qu'une seule requête soit générée plutôt que deux.
kamranicus
4
@subkamran Mon point était exactement cela, penser à quelque chose et choisir ce qui est bon pour la situation, pas seulement faire. L'exemple est juste quelque chose de aléatoire que j'ai pensé à expliquer :)
Jim Wolff
3
Certainement, je voulais juste le signaler explicitement pour les gens heureux de copier / coller :)
kamranicus
Ne me tirez pas dessus, mais ce n'est en aucun cas une solution à la question. Depuis quand "extraire des données en mémoire" est-il une solution à un problème lié à SQL? J'aime être bavard avec la base de données, donc je ne préférerais en aucun cas tirer quelque chose en mémoire "car sinon une exception SQL est levée". Néanmoins, dans votre code fourni, il n'y a aucune raison de contacter la base de données deux fois. Facile à faire en un seul appel. Soyez prudent avec des messages comme celui-ci. ToList, First, Single, ... Ne doit être utilisé que lorsque les données sont nécessaires en mémoire (donc uniquement les données que vous souhaitez), pas lorsqu'une exception SQL se produit autrement.
Frederik Prijck
70

Il existe une autre façon de surmonter ce problème. Que ce soit une meilleure façon dépend de votre situation.

Le problème résulte du chargement paresseux, donc une façon de l'éviter est de ne pas avoir de chargement paresseux, en utilisant Include:

var results = myContext.Customers
    .Include(x => x.Orders)
    .Include(x => x.Addresses)
    .Include(x => x.PaymentMethods);

Si vous utilisez les Includes appropriés , vous pouvez éviter d'activer MARS. Mais si vous en manquez, vous obtiendrez l'erreur, donc l'activation de MARS est probablement le moyen le plus simple de le corriger.

Ryan Lundy
la source
1
A fonctionné comme un charme. .Includeest une bien meilleure solution que l'activation de MARS, et beaucoup plus facile que d'écrire votre propre code de requête SQL.
Nolonar
15
Si quelqu'un rencontre le problème que vous ne pouvez écrire que le .Include ("chaîne") et non un lambda, vous devez ajouter "using System.Data.Entity" car la méthode d'extension se trouve là.
Jim Wolff
46

Vous obtenez cette erreur, lorsque la collection que vous essayez d'itérer est une sorte de chargement paresseux (IQueriable).

foreach (var user in _dbContext.Users)
{    
}

La conversion de la collection IQueriable en une autre collection énumérable résoudra ce problème. exemple

_dbContext.Users.ToList()

Remarque: .ToList () crée à chaque fois un nouvel ensemble et cela peut entraîner des problèmes de performances si vous traitez des données volumineuses.

Nalan Madheswaran
la source
1
La solution la plus simple possible! Big UP;)
Jacob Sobus
1
La récupération de listes illimitées peut entraîner de graves problèmes de performances! Comment peut-on voter contre cela?
SandRock
1
@SandRock pas pour quelqu'un qui travaille pour une petite entreprise - SELECT COUNT(*) FROM Users= 5
Simon_Weaver
5
Réfléchissez-y à deux fois. Un jeune développeur lisant ce Q / R peut penser que c'est une solution de tous les temps alors que ce n'est absolument pas le cas. Je vous suggère de modifier votre réponse pour avertir les lecteurs du danger de récupérer des listes illimitées à partir de db.
SandRock
1
@SandRock Je pense que ce serait un bon endroit pour lier une réponse ou un article décrivant les meilleures pratiques.
Sinjai
13

J'ai résolu le problème facilement (pragmatique) en ajoutant l'option au constructeur. Ainsi, je l'utilise uniquement en cas de besoin.

public class Something : DbContext
{
    public Something(bool MultipleActiveResultSets = false)
    {
        this.Database
            .Connection
            .ConnectionString = Shared.ConnectionString /* your connection string */
                              + (MultipleActiveResultSets ? ";MultipleActiveResultSets=true;" : "");
    }
...
Harvey Triana
la source
2
Je vous remercie. Ça marche. Je viens d'ajouter le MultipleActiveResultSets = true dans la chaîne de connexion directement dans web.config
Mosharaf Hossain
11

Essayez de définir votre chaîne de connexion MultipleActiveResultSets=true. Cela permet le multitâche sur la base de données.

Server=yourserver ;AttachDbFilename=database;User Id=sa;Password=blah ;MultipleActiveResultSets=true;App=EntityFramework

Cela fonctionne pour moi ... que votre connexion dans app.config ou que vous la définissiez par programme ... j'espère que cela vous sera utile

Mohamed Hocine
la source
MultipleActiveResultSets = true ajouté à votre chaîne de connexion résoudra probablement le problème. Cela n'aurait pas dû être rejeté.
Aaron Hudon
oui, bien sûr, j'ai montré comment ajouter à votre chaîne de connexion
Mohamed Hocine
4

J'avais initialement décidé d'utiliser un champ statique dans ma classe API pour référencer une instance de l'objet MyDataContext (où MyDataContext est un objet de contexte EF5), mais c'est ce qui semblait créer le problème. J'ai ajouté du code semblable à ce qui suit à chacune de mes méthodes API et cela a résolu le problème.

using(MyDBContext db = new MyDBContext())
{
    //Do some linq queries
}

Comme d'autres personnes l'ont déclaré, les objets EF Data Context ne sont PAS thread-safe. Donc, les placer dans l'objet statique finira par provoquer l'erreur "lecteur de données" dans les bonnes conditions.

Mon hypothèse initiale était que la création d'une seule instance de l'objet serait plus efficace et permettrait une meilleure gestion de la mémoire. D'après ce que j'ai rassemblé en recherchant cette question, ce n'est pas le cas. En fait, il semble plus efficace de traiter chaque appel à votre API comme un événement isolé et sûr pour les threads. Veiller à ce que toutes les ressources soient correctement libérées, car l'objet est hors de portée.

Cela a du sens, surtout si vous amenez votre API à la prochaine progression naturelle qui serait de l'exposer en tant que WebService ou API REST.

Divulgation

  • Système d'exploitation: Windows Server 2012
  • .NET: installé 4.5, projet utilisant 4.0
  • Source de données: MySQL
  • Cadre d'application: MVC3
  • Authentification: formulaires
Jeffrey A. Gochin
la source
3

J'ai remarqué que cette erreur se produit lorsque j'envoie un IQueriable à la vue et que je l'utilise dans une double foreach, où la foreach intérieure doit également utiliser la connexion. Exemple simple (ViewBag.parents peut être IQueriable ou DbSet):

foreach (var parent in ViewBag.parents)
{
    foreach (var child in parent.childs)
    {

    }
}

La solution simple est d'utiliser .ToList()sur la collection avant de l'utiliser. Notez également que MARS ne fonctionne pas avec MySQL.

cen
la source
MERCI! Tout ici disait "les boucles imbriquées sont le problème" mais personne n'a dit comment y remédier. J'ai mis un ToList()sur mon premier appel pour obtenir une collection de la base de données. Ensuite, j'ai fait un foreachsur cette liste et les appels suivants ont parfaitement fonctionné au lieu de donner l'erreur.
AlbatrossCafe
@AlbatrossCafe ... mais personne ne mentionne que dans ce cas, vos données seront chargées en mémoire et la requête sera exécutée en mémoire, au lieu de DB
Lightning3
3

J'ai constaté que j'avais la même erreur, et cela s'est produit lorsque j'utilisais un Func<TEntity, bool>au lieu d'un Expression<Func<TEntity, bool>>pour votre predicate.

Une fois que je l' ai changé toutes Func'sà Expression'sl'exception cessé d' être jeté.

Je crois que cela EntityFramworkfait des choses intelligentes avec Expression'slesquelles il ne fait tout simplement pasFunc's

sQuir3l
la source
Cela nécessite plus de votes positifs. J'essayais de créer une méthode dans ma classe DataContext en prenant un (MyTParent model, Func<MyTChildren, bool> func)pour que mes ViewModels puissent spécifier une certaine whereclause à la méthode Generic DataContext. Rien ne fonctionnait jusqu'à ce que je fasse cela.
Justin
3

2 solutions pour atténuer ce problème:

  1. Forcer la mise en cache de la mémoire en gardant le chargement paresseux avec .ToList()après votre requête, afin que vous puissiez ensuite itérer à travers elle en ouvrant un nouveau DataReader.
  2. .Include(/ entités supplémentaires que vous souhaitez charger dans la requête /) cela s'appelle un chargement enthousiaste, qui vous permet (en effet) d'inclure des objets (entités) associés lors de l'exécution d'une requête avec le DataReader.
Stefano Beltrame
la source
2

Un bon compromis entre l'activation de MARS et la récupération de l'ensemble des résultats en mémoire consiste à récupérer uniquement les ID dans une requête initiale, puis à parcourir les ID matérialisant chaque entité au fur et à mesure.

Par exemple (en utilisant les exemples d'entités "Blog et publications" comme dans cette réponse ):

using (var context = new BlogContext())
{
    // Get the IDs of all the items to loop through. This is
    // materialized so that the data reader is closed by the
    // time we're looping through the list.
    var blogIds = context.Blogs.Select(blog => blog.Id).ToList();

    // This query represents all our items in their full glory,
    // but, items are only materialized one at a time as we
    // loop through them.
    var blogs =
        blogIds.Select(id => context.Blogs.First(blog => blog.Id == id));

    foreach (var blog in blogs)
    {
        this.DoSomethingWith(blog.Posts);

        context.SaveChanges();
    }
}

Cela signifie que vous ne tirez que quelques milliers d'entiers en mémoire, par opposition à des milliers de graphiques d'objets entiers, ce qui devrait minimiser l'utilisation de la mémoire tout en vous permettant de travailler élément par élément sans activer MARS.

Un autre avantage intéressant de cela, comme le montre l'exemple, est que vous pouvez enregistrer les modifications en parcourant chaque élément, au lieu d'avoir à attendre la fin de la boucle (ou une autre solution de rechange), comme cela serait nécessaire même avec MARS activé (voir ici et ici ).

Paul
la source
context.SaveChanges();à l'intérieur de la boucle :(. Ce n'est pas bon. il doit être à l'extérieur de la boucle.
Jawand Singh
1

Dans mon cas, j'ai constaté qu'il manquait des instructions "en attente" avant les appels à myContext.SaveChangesAsync (). L'ajout de l'attente avant ces appels asynchrones a résolu les problèmes de lecteur de données pour moi.

Elijah Lofgren
la source
0

Si nous essayons de regrouper une partie de nos conditions dans une méthode d'extension Func <>, nous obtiendrons cette erreur, supposons que nous ayons un code comme celui-ci:

public static Func<PriceList, bool> IsCurrent()
{
  return p => (p.ValidFrom == null || p.ValidFrom <= DateTime.Now) &&
              (p.ValidTo == null || p.ValidTo >= DateTime.Now);
}

Or

public static IEnumerable<PriceList> IsCurrent(this IEnumerable<PriceList> prices) { .... }

Cela lèvera l'exception si nous essayons de l'utiliser dans un Where (), ce que nous devrions faire à la place est de construire un prédicat comme ceci:

public static Expression<Func<PriceList, bool>> IsCurrent()
{
    return p => (p.ValidFrom == null || p.ValidFrom <= DateTime.Now) &&
                (p.ValidTo == null || p.ValidTo >= DateTime.Now);
}

Plus d'informations peuvent être lues sur: http://www.albahari.com/nutshell/predicatebuilder.aspx

Arvand
la source
0

Ce problème peut être résolu simplement en convertissant les données en liste

 var details = _webcontext.products.ToList();


            if (details != null)
            {
                Parallel.ForEach(details, x =>
                {
                    Products obj = new Products();
                    obj.slno = x.slno;
                    obj.ProductName = x.ProductName;
                    obj.Price = Convert.ToInt32(x.Price);
                    li.Add(obj);

                });
                return li;
            }
Debendra Dash
la source
ToList () effectue l'appel mais le code ci-dessus ne dispose toujours pas de la connexion. donc votre _webcontext risque toujours d'être fermé au moment de la ligne 1
Sonic Soul
0

Dans ma situation, le problème s'est produit en raison d'un enregistrement d'injection de dépendance. J'injectais un service d'étendue par demande qui utilisait un dbcontext dans un service enregistré singleton. Par conséquent, le dbcontext a été utilisé dans plusieurs requêtes et donc l'erreur.

E. Staal
la source
0

Dans mon cas, le problème n'avait rien à voir avec la chaîne de connexion MARS mais avec la sérialisation json. Après avoir mis à niveau mon projet de NetCore2 vers 3, j'ai eu cette erreur.

Plus d'informations peuvent être trouvées ici

Ou sinon
la source
-6

J'ai résolu ce problème en utilisant la section de code suivante avant la deuxième requête:

 ...first query
 while (_dbContext.Connection.State != System.Data.ConnectionState.Closed)
 {
     System.Threading.Thread.Sleep(500);
 }
 ...second query

vous pouvez changer l'heure du sommeil en millisecondes

PD Utile lors de l'utilisation de threads

i31nGo
la source
13
Ajouter arbitrairement Thread.Sleep dans n'importe quelle solution est une mauvaise pratique - et est particulièrement mauvais lorsqu'il est utilisé pour contourner un problème différent où l'état d'une certaine valeur n'est pas entièrement compris. J'aurais pensé que "Utiliser des threads" comme indiqué au bas de la réponse signifierait avoir au moins une compréhension de base du threading - mais cette réponse ne prend aucun contexte en compte, en particulier les circonstances où c'est une très mauvaise idée pour utiliser Thread.Sleep - comme sur un thread d'interface utilisateur.
Mike Tours