SqlException do Entity Framework - Nova transação não é permitida porque há outros threads em execução na sessão

600

No momento, estou recebendo este erro:

System.Data.SqlClient.SqlException: nova transação não é permitida porque há outros threads em execução na sessão.

ao executar este código:

public class ProductManager : IProductManager
{
    #region Declare Models
    private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
    private RivWorks.Model.NegotiationAutos.RivFeedsEntities _dbFeed = RivWorks.Model.Stores.FeedEntities(AppSettings.FeedAutosEntities_connString);
    #endregion

    public IProduct GetProductById(Guid productId)
    {
        // Do a quick sync of the feeds...
        SyncFeeds();
        ...
        // get a product...
        ...
        return product;
    }

    private void SyncFeeds()
    {
        bool found = false;
        string feedSource = "AUTO";
        switch (feedSource) // companyFeedDetail.FeedSourceTable.ToUpper())
        {
            case "AUTO":
                var clientList = from a in _dbFeed.Client.Include("Auto") select a;
                foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
                {
                    var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
                    foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                    {
                        if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                        {
                            var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                            foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                            {
                                foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                                {
                                    if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                    {
                                        found = true;
                                        break;
                                    }
                                }
                                if (!found)
                                {
                                    var newProduct = new RivWorks.Model.Negotiation.Product();
                                    newProduct.alternateProductID = sourceProduct.AutoID;
                                    newProduct.isFromFeed = true;
                                    newProduct.isDeleted = false;
                                    newProduct.SKU = sourceProduct.StockNumber;
                                    company.Product.Add(newProduct);
                                }
                            }
                            _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                        }
                    }
                }
                break;
        }
    }
}

Modelo 1 - Este modelo fica em um banco de dados em nosso servidor de desenvolvimento. Modelo # 1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

Modelo 2 - Este modelo fica em um banco de dados em nosso Prod Server e é atualizado diariamente por feeds automáticos. texto alternativo http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

Nota - Os itens circulados em vermelho no Modelo 1 são os campos que eu uso para "mapear" para o Modelo 2. Por favor, ignore os círculos vermelhos no Modelo 2: isso é de outra pergunta que eu tive e que agora está respondida.

Nota: Ainda preciso fazer uma verificação isDeleted para que eu possa excluí-la do DB1 se ela saiu do inventário do cliente.

Tudo o que eu quero fazer, com esse código específico, é conectar uma empresa no DB1 a um cliente no DB2, obter sua lista de produtos no DB2 e INSERIR no DB1, se ainda não estiver lá. Na primeira vez, deve haver uma atração completa do inventário. Cada vez que é executado lá, nada deve acontecer, a menos que um novo inventário chegue ao feed durante a noite.

Então a grande questão - como resolver o erro de transação que estou recebendo? Preciso soltar e recriar meu contexto toda vez através dos loops (não faz sentido para mim)?

Keith Barrows
fonte
6
Esta é a pergunta mais detalhada que eu já vi.
9
Alguém já perdeu os procedimentos armazenados?
David

Respostas:

690

Depois de muito arrancar os cabelos, descobri que os foreachlaços eram os culpados. O que precisa acontecer é chamar EF, mas devolvê-lo para um IList<T>desse tipo de destino e depois fazer um loop no IList<T>.

Exemplo:

IList<Client> clientList = from a in _dbFeed.Client.Include("Auto") select a;
foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
{
   var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
    // ...
}
Keith Barrows
fonte
14
Sim, isso também me causou dor de cabeça. Eu quase caí da cadeira quando encontrei o problema! Entendo as razões técnicas por trás do problema, mas isso não é intuitivo e não está ajudando o desenvolvedor a cair no "poço do sucesso" blogs.msdn.com/brada/archive/2003/10/02/50420. aspx
Doctor Jones
9
Isso não é ruim para o desempenho de grandes conjuntos de dados? Se você possui milhões de registros na tabela. ToList () sugará todos eles para a memória. Estou enfrentando esse mesmo problema e queria saber se o seguinte seria viável a) Desanexe a entidade b) Crie um novo ObjectContext e anexe a entidade desanexada a ele. c) chamada SaveChanges () sobre o novo ObjectContext d) Separar a entidade a partir do novo ObjectContext e) Anexar-lo de volta para o velho ObjectContext
Abhijeet Patel
150
O problema é que você não pode ligar SaveChangesenquanto ainda está obtendo resultados do banco de dados. Portanto, outra solução é apenas salvar as alterações assim que o loop for concluído.
Drew Noakes
4
Tendo sido mordido também, adicionei isso ao Microsoft Connect: connect.microsoft.com/VisualStudio/feedback/details/612369/… Fique à vontade para votar.
Ian Mercer
36
Nossos desenvolvedores tendem a anexar .ToList () a qualquer consulta LINQ sem pensar nas consequências. Esta deve ser a primeira vez que anexar .ToList () é realmente útil!
Marc
267

Como você já identificou, não é possível salvar de dentro de um foreachque ainda esteja sendo extraído do banco de dados por meio de um leitor ativo.

Ligar ToList()ou não ToArray()é bom para pequenos conjuntos de dados, mas quando você tem milhares de linhas, estará consumindo uma grande quantidade de memória.

É melhor carregar as linhas em pedaços.

public static class EntityFrameworkUtil
{
    public static IEnumerable<T> QueryInChunksOf<T>(this IQueryable<T> queryable, int chunkSize)
    {
        return queryable.QueryChunksOfSize(chunkSize).SelectMany(chunk => chunk);
    }

    public static IEnumerable<T[]> QueryChunksOfSize<T>(this IQueryable<T> queryable, int chunkSize)
    {
        int chunkNumber = 0;
        while (true)
        {
            var query = (chunkNumber == 0)
                ? queryable 
                : queryable.Skip(chunkNumber * chunkSize);
            var chunk = query.Take(chunkSize).ToArray();
            if (chunk.Length == 0)
                yield break;
            yield return chunk;
            chunkNumber++;
        }
    }
}

Dado os métodos de extensão acima, você pode escrever sua consulta assim:

foreach (var client in clientList.OrderBy(c => c.Id).QueryInChunksOf(100))
{
    // do stuff
    context.SaveChanges();
}

O objeto consultável no qual você chama esse método deve ser solicitado. Isso ocorre porque o Entity Framework suporta apenas IQueryable<T>.Skip(int)consultas ordenadas, o que faz sentido quando você considera que várias consultas para diferentes intervalos exigem que a ordem seja estável. Se a ordem não for importante para você, basta solicitar pela chave primária, pois é provável que tenha um índice em cluster.

Esta versão consultará o banco de dados em lotes de 100. Observe que SaveChanges()é chamado para cada entidade.

Se você deseja melhorar drasticamente sua taxa de transferência, ligue com SaveChanges()menos frequência. Use um código como este:

foreach (var chunk in clientList.OrderBy(c => c.Id).QueryChunksOfSize(100))
{
    foreach (var client in chunk)
    {
        // do stuff
    }
    context.SaveChanges();
}

Isso resulta em 100 vezes menos chamadas de atualização do banco de dados. É claro que cada uma dessas ligações leva mais tempo para ser concluída, mas você ainda sai à frente no final. Sua milhagem pode variar, mas isso foi um mundo mais rápido para mim.

E contorna a exceção que você estava vendo.

EDIT Eu revisitei essa pergunta depois de executar o SQL Profiler e atualizei algumas coisas para melhorar o desempenho. Para quem estiver interessado, aqui está um exemplo de SQL que mostra o que é criado pelo banco de dados.

O primeiro loop não precisa pular nada, por isso é mais simples.

SELECT TOP (100)                     -- the chunk size 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM [dbo].[Clients] AS [Extent1]
ORDER BY [Extent1].[Id] ASC

As chamadas subseqüentes precisam ignorar trechos anteriores de resultados, para introduzir o uso de row_number:

SELECT TOP (100)                     -- the chunk size
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM (
    SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], row_number()
    OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
    FROM [dbo].[Clients] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 100   -- the number of rows to skip
ORDER BY [Extent1].[Id] ASC
Drew Noakes
fonte
17
Obrigado. Sua explicação foi muito mais útil do que a marcada como "Respondida".
Wagner da Silva
1
Isso é ótimo. apenas uma coisa: se você estiver consultando uma coluna e atualizando o valor dessa coluna, precisará conhecer o chunkNumber ++; . Digamos que você tenha uma coluna "ModifiedDate" e esteja consultando .Where (x => x.ModifiedDate! = Null) e, no final do foreach, você define um valor para ModifiedDate. Dessa forma, você não está repetindo metade dos registros, porque metade deles está sendo ignorada.
Arvand #
Infelizmente, em grandes conjuntos de dados, você obtém a explicação OutofMemoryException - veja no conjunto de dados grande da estrutura do Entity, exceção de falta de memória . Eu descrevi como renovar o seu contexto cada lote em SqlException de Entity Framework - Nova transação não é permitido porque existem outros threads em execução na sessão
Michael Freidgeim
Eu acho que isso deve funcionar. var pular = 0; const int take = 100; Listar <Empregado> emps; while ((emps = db.Employees.Skip (skip) .Take (take) .ToList ()). Count> 0) {skip + = take; foreach (var emp in emps) {// Faça coisas aqui}} Eu formularia essa resposta, mas ela seria enterrada abaixo das pilhas de respostas abaixo e está relacionada a esta pergunta.
Jwize 26/03
123

Publicamos agora uma resposta oficial ao bug aberto no Connect . As soluções alternativas que recomendamos são as seguintes:

Este erro ocorre devido ao Entity Framework criar uma transação implícita durante a chamada SaveChanges (). A melhor maneira de solucionar o erro é usar um padrão diferente (ou seja, não salvar enquanto estiver no meio da leitura) ou declarar explicitamente uma transação. Aqui estão três soluções possíveis:

// 1: Save after iteration (recommended approach in most cases)
using (var context = new MyContext())
{
    foreach (var person in context.People)
    {
        // Change to person
    }
    context.SaveChanges();
}

// 2: Declare an explicit transaction
using (var transaction = new TransactionScope())
{
    using (var context = new MyContext())
    {
        foreach (var person in context.People)
        {
            // Change to person
            context.SaveChanges();
        }
    }
    transaction.Complete();
}

// 3: Read rows ahead (Dangerous!)
using (var context = new MyContext())
{
    var people = context.People.ToList(); // Note that this forces the database
                                          // to evaluate the query immediately
                                          // and could be very bad for large tables.

    foreach (var person in people)
    {
        // Change to person
        context.SaveChanges();
    }
} 
Mark Stafford - MSFT
fonte
6
Se você seguir a rota Transaction, apenas a introdução de um TransactionScope poderá não corrigi-lo - não se esqueça de estender o Timeout se o que você estiver fazendo puder demorar muito tempo - por exemplo, se você estiver depurando interativamente o código criando o Chamada de banco de dados. Código de aqui estender o tempo limite da transação a uma hora: using (var transação = new TransactionScope (TransactionScopeOption.Required, novo TimeSpan (1, 0, 0)))
Chris Moschini
Eu deparei com esse erro desde a primeira vez que me desviei do "caminho do tutorial" para um exemplo real sozinho! Para mim, no entanto, a solução mais simples, SALVAR APÓS A ITERAÇÃO, melhor! (Acho que 99% das vezes este é o caso, e apenas 1% realmente deve executar uma base de dados save dentro do loop)
spiderman
Bruto. Acabei de topar com este erro. Muito nojento. A segunda sugestão funcionou como um encanto para mim, além de mover minhas SaveChanges para o loop. Eu pensei que salvar alterações fora do loop era melhor para alterações em lote. Mas tudo bem. Eu acho que não?! :(
Sr. Young
Não funcionou para mim .NET 4.5. Quando usado o TransactionScope, recebi o seguinte erro "O provedor subjacente falhou no EnlistTransaction. {" O gerenciador de transações parceiras desativou seu suporte para transações remotas / de rede. (Exceção de HRESULT: 0x8004D025) "}". Acabo fazendo o trabalho fora da iteração.
Diganta Kumar
O uso do TransactionScope é perigoso, porque a tabela está bloqueada durante todo o tempo da transação.
Michael Freidgeim
19

Na verdade, você não pode salvar as alterações dentro de um foreachloop em C # usando o Entity Framework.

context.SaveChanges() O método atua como uma confirmação em um sistema de banco de dados regular (RDMS).

Apenas faça todas as alterações (que o Entity Framework armazenará em cache) e salve todas elas de uma vez chamando SaveChanges()após o loop (fora dele), como um comando de confirmação do banco de dados.

Isso funciona se você puder salvar todas as alterações de uma só vez.

Edgardo Pichardo C.
fonte
2
Eu pensei que era interessante ver "sistema de banco de dados regular (RDMS)" aqui
Dinerdo
1
Isso parece errado, já que chamar repetidamente SaveChanges é bom em 90% dos contextos no EF.
Pxtl
Parece que chamar repetidamente SaveChanges é bom, a menos que o loop foreach esteja iterando sobre uma Entidade db.
Kerbasaurus 19/04/19
1
Aha! Trazer o contexto para cada loop! (pffft ... o que eu estava pensando? ..) Obrigado!
Adam Cox
18

Basta colocar context.SaveChanges()após o final do seu foreach(loop).

Majid
fonte
Esta é a opção melhor eu descobri no meu caso, devido à economia de foreach dentro
Almeida
2
Isso nem sempre é uma opção.
Pxtl
9

Sempre use sua seleção como lista

Por exemplo:

var tempGroupOfFiles = Entities.Submited_Files.Where(r => r.FileStatusID == 10 && r.EventID == EventId).ToList();

Em seguida, percorra a coleção enquanto salva as alterações

 foreach (var item in tempGroupOfFiles)
             {
                 var itemToUpdate = item;
                 if (itemToUpdate != null)
                 {
                     itemToUpdate.FileStatusID = 8;
                     itemToUpdate.LastModifiedDate = DateTime.Now;
                 }
                 Entities.SaveChanges();

             }
mzonerz
fonte
1
Isso não é uma boa prática. Você não deve executar SaveChanges que, muitas vezes, se você não precisa, e você definitivamente não deve "Sempre usar sua seleção como List"
Dinerdo
@ Dinerdo isso realmente depende do cenário. No meu caso, eu tenho 2 loops foreach. O externo tinha a consulta db como a lista. Por exemplo, esse foreach percorre dispositivos de hardware. O foreach interno recupera vários dados de cada dispositivo. Conforme o requisito, preciso salvar no banco de dados os dados após serem recuperados de cada dispositivo, um por um. Não é uma opção para salvar todos os dados no final do processo. Encontrei o mesmo erro, mas a solução do mzonerz funcionou.
Jstuardo 24/05/19
@jstuardo Mesmo com lotes?
Dinerdo 24/05/19
@Dinerdo Concordo que não é uma boa prática no nível filosófico. No entanto, existem várias situações em que, dentro do loop for, o código chama outro método (digamos, um método AddToLog ()) que inclui uma chamada para db.SaveChanges () localmente. Nessa situação, você realmente não pode controlar a chamada para db.Save Changes. Nesse caso, usar um ToList () ou uma estrutura semelhante funcionará como sugerido por mzonerz. Obrigado!
A. Varma
Na prática, isso vai machucá-lo mais do que ajudará. Eu mantenho o que disse - ToList () definitivamente não deve ser usado o tempo todo, e salvar as alterações após cada item é algo a ser evitado sempre que possível em um aplicativo de alto desempenho. Isso seria uma correção temporária do IMO. Qualquer que seja o método de registro que você tenha, também idealmente aproveite o buffer.
Dinerdo 9/03
8

FYI: de um livro e algumas linhas ajustadas porque ainda é válida:

A chamada do método SaveChanges () inicia uma transação que reverte automaticamente todas as alterações persistidas no banco de dados se ocorrer uma exceção antes da conclusão da iteração; caso contrário, a transação será confirmada. Você pode ser tentado a aplicar o método após cada atualização ou exclusão de entidade, e não após a conclusão da iteração, especialmente quando você estiver atualizando ou excluindo um grande número de entidades.

Se você tentar chamar SaveChanges () antes de todos os dados terem sido processados, ocorrerá uma exceção "Nova transação não é permitida porque existem outros threads em execução na sessão". A exceção ocorre porque o SQL Server não permite iniciar uma nova transação em uma conexão que tenha um SqlDataReader aberto, mesmo com o MARS (Multiple Active Record Sets) ativado pela string de conexão (a string de conexão padrão do EF habilita MARS)

Às vezes é melhor entender por que as coisas estão acontecendo ;-)

Herman Van Der Blom
fonte
1
Uma boa maneira de evitar isso é quando você tem um leitor aberto para abrir um segundo e colocar essas operações no segundo leitor. Isso é algo que você pode precisar quando estiver atualizando o mestre / detalhes na estrutura da entidade. Você abre a primeira conexão para o registro mestre e a segunda para os registros detalhados. se você está apenas lendo, não deve haver problemas. os problemas ocorrem durante a atualização.
Herman Van Der Blom
Explicação útil. você está certo, é bom entender por que as coisas estão acontecendo.
Dov Miller
8

Tornar suas listas consultáveis ​​em .ToList () e deve funcionar bem.

Wojciech Seweryn
fonte
1
Forneça um exemplo em vez de apenas postar uma solução.
Ronnie Oosting 30/08/19
5

Eu estava recebendo esse mesmo problema, mas em uma situação diferente. Eu tinha uma lista de itens em uma caixa de listagem. O usuário pode clicar em um item e selecionar excluir, mas estou usando um processo armazenado para excluir o item, porque há muita lógica envolvida na exclusão do item. Quando eu chamo o processo armazenado, a exclusão funciona bem, mas qualquer chamada futura para SaveChanges causará o erro. Minha solução foi chamar o proc armazenado fora da EF e isso funcionou bem. Por alguma razão, quando eu chamo o proc armazenado usando a maneira EF de fazer as coisas, deixa algo em aberto.

MikeKulls
fonte
3
Teve um problema semelhante recentemente: o motivo no meu caso foi uma SELECTdeclaração no procedimento armazenado que produziu um conjunto de resultados vazio e, se esse conjunto de resultados não foi lido, SaveChangeslançou essa exceção.
n0rd
A mesma coisa com resultado não lida de SP, muito obrigado pela dica)
Pavel K
4

Aqui estão outras 2 opções que permitem chamar SaveChanges () em um para cada loop.

A primeira opção é usar um DBContext para gerar os objetos da lista para iterar e criar um segundo DBContext para chamar SaveChanges (). Aqui está um exemplo:

//Get your IQueryable list of objects from your main DBContext(db)    
IQueryable<Object> objects = db.Object.Where(whatever where clause you desire);

//Create a new DBContext outside of the foreach loop    
using (DBContext dbMod = new DBContext())
{   
    //Loop through the IQueryable       
    foreach (Object object in objects)
    {
        //Get the same object you are operating on in the foreach loop from the new DBContext(dbMod) using the objects id           
        Object objectMod = dbMod.Object.Find(object.id);

        //Make whatever changes you need on objectMod
        objectMod.RightNow = DateTime.Now;

        //Invoke SaveChanges() on the dbMod context         
        dbMod.SaveChanges()
    }
}

A segunda opção é obter uma lista de objetos de banco de dados do DBContext, mas selecionar apenas os IDs. E, em seguida, percorra a lista de IDs (presumivelmente um int) e obtenha o objeto correspondente a cada int e invoque SaveChanges () dessa maneira. A idéia por trás desse método é pegar uma grande lista de números inteiros, é muito mais eficiente do que obter uma grande lista de objetos db e chamar .ToList () em todo o objeto. Aqui está um exemplo deste método:

//Get the list of objects you want from your DBContext, and select just the Id's and create a list
List<int> Ids = db.Object.Where(enter where clause here)Select(m => m.Id).ToList();

var objects = Ids.Select(id => db.Objects.Find(id));

foreach (var object in objects)
{
    object.RightNow = DateTime.Now;
    db.SaveChanges()
}
jjspierx
fonte
Essa é uma ótima alternativa que eu pensei e fiz, mas isso precisa ser votado. Nota: i) você pode iterar como enumerável, o que é bom para conjuntos muito grandes; ii) Você pode usar o comando NoTracking para evitar problemas ao carregar tantos registros (se esse for o seu cenário); iii) Gosto muito da opção apenas de chave primária - isso é muito inteligente, porque você está carregando muito menos dados na memória, mas não está lidando com o Take / Skip em um conjunto de dados subjacente potencialmente dinâmico.
Todd
4

Se você receber esse erro devido ao foreach e realmente precisar salvar uma entidade primeiro no loop interno e usar a identidade gerada ainda mais no loop, como foi no meu caso, a solução mais fácil é usar outro DBContext para inserir a entidade que retornará o ID e usará esse ID no contexto externo

Por exemplo

    using (var context = new DatabaseContext())
    {
        ...
        using (var context1 = new DatabaseContext())
        {
            ...
               context1.SaveChanges();
        }                         
        //get id of inserted object from context1 and use is.   
      context.SaveChanges();
   }
Hemant Sakta
fonte
2

Assim, no projeto eram Eu tive esse mesmo problema exato o problema não estava no foreachou o .toList()que era realmente na configuração Autofac usamos. Isso criou algumas situações estranhas, onde o erro acima foi lançado, mas também vários outros erros equivalentes.

Esta foi a nossa correção: Alterado isso:

container.RegisterType<DataContext>().As<DbContext>().InstancePerLifetimeScope();
container.RegisterType<DbFactory>().As<IDbFactory>().SingleInstance();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();

Para:

container.RegisterType<DataContext>().As<DbContext>().As<DbContext>();
container.RegisterType<DbFactory>().As<IDbFactory>().As<IDbFactory>().InstancePerLifetimeScope();
container.RegisterType<UnitOfWork>().As<IUnitOfWork>().As<IUnitOfWork>();//.InstancePerRequest();
VeldMuijz
fonte
Você poderia elaborar o que acha que era o problema? você resolveu isso criando um novo Dbcontext a cada vez?
eran otzap
2

Eu sei que é uma pergunta antiga, mas enfrentei esse erro hoje.

e descobri que, esse erro pode ser lançado quando um gatilho da tabela do banco de dados obtém um erro.

Para sua informação, você também pode verificar os gatilhos de suas tabelas quando receber esse erro.

nadir
fonte
2

Eu precisava ler um ResultSet enorme e atualizar alguns registros na tabela. Eu tentei usar pedaços como sugerido na Noakes tirei 's resposta .

Infelizmente, após 50000 registros, obtive OutofMemoryException. A resposta Entidade estrutura grande conjunto de dados, exceção de falta de memória , explica que

O EF cria uma segunda cópia dos dados que são utilizados para a detecção de alterações (para que possa persistir alterações no banco de dados). A EF mantém esse segundo conjunto para a vida útil do contexto e é esse conjunto que está deixando você sem memória.

A recomendação é recriar seu contexto para cada lote.

Então, eu recuperei os valores Mínimo e Máximo da chave primária - as tabelas têm chaves primárias como números inteiros incrementais automáticos. Após o processamento, o contexto do bloco fecha e libera a memória. Ele garante que o uso da memória não esteja aumentando.

Abaixo está um trecho do meu código:

  public void ProcessContextByChunks ()
  {
        var tableName = "MyTable";
         var startTime = DateTime.Now;
        int i = 0;
         var minMaxIds = GetMinMaxIds();
        for (int fromKeyID= minMaxIds.From; fromKeyID <= minMaxIds.To; fromKeyID = fromKeyID+_chunkSize)
        {
            try
            {
                using (var context = InitContext())
                {   
                    var chunk = GetMyTableQuery(context).Where(r => (r.KeyID >= fromKeyID) && (r.KeyID < fromKeyID+ _chunkSize));
                    try
                    {
                        foreach (var row in chunk)
                        {
                            foundCount = UpdateRowIfNeeded(++i, row);
                        }
                        context.SaveChanges();
                    }
                    catch (Exception exc)
                    {
                        LogChunkException(i, exc);
                    }
                }
            }
            catch (Exception exc)
            {
                LogChunkException(i, exc);
            }
        }
        LogSummaryLine(tableName, i, foundCount, startTime);
    }

    private FromToRange<int> GetminMaxIds()
    {
        var minMaxIds = new FromToRange<int>();
        using (var context = InitContext())
        {
            var allRows = GetMyTableQuery(context);
            minMaxIds.From = allRows.Min(n => (int?)n.KeyID ?? 0);  
            minMaxIds.To = allRows.Max(n => (int?)n.KeyID ?? 0);
        }
        return minMaxIds;
    }

    private IQueryable<MyTable> GetMyTableQuery(MyEFContext context)
    {
        return context.MyTable;
    }

    private  MyEFContext InitContext()
    {
        var context = new MyEFContext();
        context.Database.Connection.ConnectionString = _connectionString;
        //context.Database.Log = SqlLog;
        return context;
    }

FromToRange é uma estrutura simples com propriedades De e Para.

Michael Freidgeim
fonte
Não consegui ver como você estava "renovando" seu contexto. Parece que você está simplesmente criando um novo contexto para cada parte.
Suncat2000
@ Suncat2000, você está certo, o contexto deve ser um objeto de vida curta stackoverflow.com/questions/43474112/…
Michael Freidgeim
2

Começamos a ver esse erro "Nova transação não é permitida porque há outros threads em execução na sessão" após a migração do EF5 para o EF6.

O Google nos trouxe aqui, mas não estamos ligando para SaveChanges()dentro do loop. Os erros foram gerados ao executar um procedimento armazenado usando o ObjectContext.ExecuteFunction dentro de uma leitura de loop foreach do banco de dados.

Qualquer chamada para ObjectContext.ExecuteFunction agrupa a função em uma transação. Iniciar uma transação enquanto já existe um leitor aberto causa o erro.

É possível desativar a quebra do SP em uma transação, configurando a opção a seguir.

_context.Configuration.EnsureTransactionsForFunctionsAndCommands = false;

A EnsureTransactionsForFunctionsAndCommandsopção permite que o SP seja executado sem criar sua própria transação e o erro não é mais gerado.

Propriedade DbContextConfiguration.EnsureTransactionsForFunctionsAndCommands

JamPickle
fonte
1

Eu também estava enfrentando o mesmo problema.

Aqui está a causa e a solução.

http://blogs.msdn.com/b/cbiyikoglu/archive/2006/11/21/mars-transactions-and-sql-error-3997-3988-or-3983.aspx

Antes de disparar comandos de manipulação de dados, como inserções, atualizações, você fechou todos os leitores SQL ativos anteriores.

O erro mais comum são funções que lêem dados do banco de dados e retornam valores. Por exemplo, funções como isRecordExist.

Nesse caso, retornamos imediatamente da função se encontramos o registro e esquecemos de fechar o leitor.

Vinod T. Patil
fonte
7
O que significa "fechar um leitor" no Entity Framework? Não há leitor visível em uma consulta como var result = from customer em myDb.Customers em que customer.Id == customerId seleciona customer; retornar result.FirstOrDefault ();
Anthony
@ Anthony Como outras respostas dizem, se você usar o EF para enumerar em uma consulta LINQ (IQueryable), o DataReader subjacente permanecerá aberto até a última linha ser repetida. Porém, embora o MARS seja um recurso importante a ser ativado em uma cadeia de conexão, o problema no OP ainda não foi resolvido apenas com o MARS. O problema está tentando SaveChanges enquanto um DataReader subjacente ainda está aberto.
Todd
1

O código abaixo funciona para mim:

private pricecheckEntities _context = new pricecheckEntities();

...

private void resetpcheckedtoFalse()
{
    try
    {
        foreach (var product in _context.products)
        {
            product.pchecked = false;
            _context.products.Attach(product);
            _context.Entry(product).State = EntityState.Modified;
        }
        _context.SaveChanges();
    }
    catch (Exception extofException)
    {
        MessageBox.Show(extofException.ToString());

    }
    productsDataGrid.Items.Refresh();
}
user2918896
fonte
2
Bem-vindo ao SO! Considere adicionar uma explicação e / ou links descrevendo por que isso funciona para você. As respostas somente de código geralmente são consideradas de baixa qualidade para SO.
codeMagic
1

No meu caso, o problema apareceu quando chamei Procedimento armazenado via EF e, em seguida, SaveChanges lança essa exceção. O problema estava em chamar o procedimento, o enumerador não foi descartado. Corrigi o código da seguinte maneira:

public bool IsUserInRole(string username, string roleName, DataContext context)
{          
   var result = context.aspnet_UsersInRoles_IsUserInRoleEF("/", username, roleName);

   //using here solved the issue
   using (var en = result.GetEnumerator()) 
   {
     if (!en.MoveNext())
       throw new Exception("emty result of aspnet_UsersInRoles_IsUserInRoleEF");
     int? resultData = en.Current;

     return resultData == 1;//1 = success, see T-SQL for return codes
   }
}
Tomas Kubes
fonte
0

Estou muito atrasado para a festa, mas hoje enfrentei o mesmo erro e como resolvi era simples. Meu cenário era semelhante a esse código que eu estava fazendo transações de banco de dados dentro de loops aninhados para cada.

O problema é que uma transação de banco de dados único leva um pouco mais de tempo para cada loop, portanto, uma vez que a transação anterior não é concluída, a nova tração gera uma exceção. Portanto, a solução é criar um novo objeto no loop de cada onde você está fazendo uma transação de banco de dados.

Para os cenários mencionados acima, a solução será assim:

foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                {
private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
                    if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                    {
                        var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                        foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                        {
                            foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                            {
                                if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                {
                                    found = true;
                                    break;
                                }
                            }
                            if (!found)
                            {
                                var newProduct = new RivWorks.Model.Negotiation.Product();
                                newProduct.alternateProductID = sourceProduct.AutoID;
                                newProduct.isFromFeed = true;
                                newProduct.isDeleted = false;
                                newProduct.SKU = sourceProduct.StockNumber;
                                company.Product.Add(newProduct);
                            }
                        }
                        _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                    }
                }
Usman
fonte
0

Estou um pouco atrasado, mas também tive esse erro. Resolvi o problema verificando quais eram os valores que estavam atualizando.

Descobri que minha consulta estava errada e que havia mais de 250 edições pendentes. Então, corrigi minha consulta e agora ela está correta.

Então, na minha situação: Verifique se há erros na consulta, depurando o resultado que a consulta retorna. Depois disso, corrija a consulta.

Espero que isso ajude a resolver problemas futuros.

Máx.
fonte