Usando transações

As transações permitem que várias operações de banco de dados sejam processadas de forma atômica. Se a transação for confirmada, todas as operações serão aplicadas com êxito ao banco de dados. Se a transação for revertida, nenhuma operação será aplicadas ao banco de dados.

Dica

Veja o exemplo deste artigo no GitHub.

Comportamento de transação padrão

Por padrão, se o provedor de banco de dados oferecer suporte a transações, todas as alterações em uma única chamada para SaveChanges serão aplicadas em uma transação. Se qualquer uma das alterações falhar, a transação é revertida e nenhuma das alterações será aplicada ao banco de dados. Isso significa que é garantido que o SaveChanges terá êxito ou sairá do banco de dados sem modificação caso ocorra algum erro.

Para a maioria dos aplicativos, esse comportamento padrão é suficiente. Você só deve controlar as transações manualmente se os requisitos do aplicativo considerarem necessário.

Como controlar transações

Você pode usar a API do DbContext.Database para iniciar, confirmar e reverter transações. O exemplo a seguir mostra duas operações SaveChanges e uma consulta LINQ sendo executada em uma única transação:

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context.SaveChanges();

    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
    context.SaveChanges();

    var blogs = context.Blogs
        .OrderBy(b => b.Url)
        .ToList();

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

Embora todos os provedores de bancos de dados relacionais ofereçam suporte a transações, outros tipos de provedores podem gerar um no-op quando as APIs de transação são chamadas.

Observação

O controle manual de transações dessa forma é incompatível com estratégias de execução de repetição invocadas implicitamente. Consulte Resiliência de Conexão para obter mais informações.

Pontos de salvamento

Quando SaveChanges é invocado e uma transação já está em andamento no contexto, o EF cria automaticamente um ponto de salvamento antes de salvar os dados. Os pontos de salvamento são pontos dentro de uma transação de banco de dados para os quais é possível reverter posteriormente, se ocorrer um erro ou por qualquer outro motivo. Se SaveChanges encontrar algum erro, ele reverterá automaticamente a transação para o ponto de salvamento, deixando a transação no mesmo estado como se nunca tivesse sido iniciada. Isso permite que você corrija problemas e tente salvar novamente, especialmente quando ocorrerem problemas de simultaneidade otimista.

Aviso

Os pontos de salvamento são incompatíveis com MARS (Conjunto de resultados ativos múltiplos) do SQL Server. Os pontos de salvamento não serão criados pelo EF quando o MARS estiver habilitado na conexão, mesmo que o MARS não esteja ativamente em uso. Se ocorrer um erro durante SaveChanges, a transação poderá ser deixada em um estado desconhecido.

Também é possível gerenciar manualmente os pontos de salvamento, assim como é com transações. O exemplo a seguir cria um ponto de salvamento em uma transação e reverte para ele em caso de falha:

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
    context.SaveChanges();

    transaction.CreateSavepoint("BeforeMoreBlogs");

    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
    context.SaveChanges();

    transaction.Commit();
}
catch (Exception)
{
    // If a failure occurred, we rollback to the savepoint and can continue the transaction
    transaction.RollbackToSavepoint("BeforeMoreBlogs");

    // TODO: Handle failure, possibly retry inserting blogs
}

Transação entre contextos

Você também pode compartilhar uma transação em várias instâncias de contexto. Esta funcionalidade só está disponível ao usar um provedor de banco de dados relacional, porque ele requer o uso do DbTransaction e DbConnection, que são específicos para bancos de dados relacionais.

Para compartilhar uma transação, os contextos devem compartilhar um DbConnection e um DbTransaction.

Permitir que a conexão seja fornecido externamente

Compartilhar um DbConnection requer a capacidade de aprovar uma conexão em um contexto ao construí-la.

A maneira mais fácil de permitir que o DbConnection seja fornecido externamente é parar de usar o método DbContext.OnConfiguring para configurar o contexto e criar externamente DbContextOptions e aprová-los para o construtor de contexto.

Dica

DbContextOptionsBuilder é a API usada no DbContext.OnConfiguring para configurar o contexto e agora você irá usá-la externamente para criar DbContextOptions.

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

Uma alternativa é continuar usando o DbContext.OnConfiguring, mas aceitar um DbConnection que seja salvo e, depois, usado no DbContext.OnConfiguring.

public class BloggingContext : DbContext
{
    private DbConnection _connection;

    public BloggingContext(DbConnection connection)
    {
      _connection = connection;
    }

    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connection);
    }
}

Compartilhar uma conexão e transação

Agora você pode criar várias instâncias de contexto que compartilham a mesma conexão. Em seguida, use a API do DbContext.Database.UseTransaction(DbTransaction) para inscrever os dois contextos na mesma transação.

using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
    .UseSqlServer(connection)
    .Options;

using var context1 = new BloggingContext(options);
using var transaction = context1.Database.BeginTransaction();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context1.SaveChanges();

    using (var context2 = new BloggingContext(options))
    {
        context2.Database.UseTransaction(transaction.GetDbTransaction());

        var blogs = context2.Blogs
            .OrderBy(b => b.Url)
            .ToList();
            
        context2.Blogs.Add(new Blog { Url = "http://dot.net" });
        context2.SaveChanges();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

Usando DbTransactions externos (somente bancos de dados relacionais)

Se você estiver usando várias tecnologias de acesso a dados para acessar um banco de dados relacional, é possível compartilhar uma transação entre operações executadas por essas diferentes tecnologias.

O exemplo a seguir mostra como executar uma operação do SqlClient do ADO.NET e uma operação do Entity Framework Core na mesma transação.

using var connection = new SqlConnection(connectionString);
connection.Open();

using var transaction = connection.BeginTransaction();
try
{
    // Run raw ADO.NET command in the transaction
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "DELETE FROM dbo.Blogs";
    command.ExecuteNonQuery();

    // Run an EF Core command in the transaction
    var options = new DbContextOptionsBuilder<BloggingContext>()
        .UseSqlServer(connection)
        .Options;

    using (var context = new BloggingContext(options))
    {
        context.Database.UseTransaction(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

Usando System.Transactions

É possível usar transações ambientes se você precisar coordenar um escopo mais amplo.

using (var scope = new TransactionScope(
           TransactionScopeOption.Required,
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        command.CommandText = "DELETE FROM dbo.Blogs";
        command.ExecuteNonQuery();

        // Run an EF Core command in the transaction
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        scope.Complete();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

Também é possível se inscrever em uma transação explícita.

using (var transaction = new CommittableTransaction(
           new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    var connection = new SqlConnection(connectionString);

    try
    {
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Database.OpenConnection();
            context.Database.EnlistTransaction(transaction);

            // Run raw ADO.NET command in the transaction
            var command = connection.CreateCommand();
            command.CommandText = "DELETE FROM dbo.Blogs";
            command.ExecuteNonQuery();

            // Run an EF Core command in the transaction
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
            context.Database.CloseConnection();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        transaction.Commit();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

Observação

Se você estiver usando APIs assíncronas, especifique TransactionScopeAsyncFlowOption.Enabled no construtor TransactionScope para garantir que a transação ambiente flua entre chamadas assíncronas.

Para obter mais informações sobre TransactionScope e transações de ambiente, consulte esta documentação.

Limitações do System.Transactions

  1. O EF Core depende dos provedores de banco de dados para implementar o suporte para System.Transactions. Se um provedor não implementar o suporte para System.Transactions, é possível que as chamadas para essas APIs sejam ignoradas completamente. O SqlClient dá suporte a ele.

    Importante

    Recomendamos testar se a API funciona corretamente com seu provedor antes de depender dela para o gerenciamento de transações. Caso ela não funcione, fale com o mantenedor do provedor do banco de dados.

  2. O suporte a transações distribuídas no System.Transactions foi adicionado apenas ao .NET 7.0 para Windows. Qualquer tentativa de usar transações distribuídas em versões mais antigas do .NET ou em plataformas não Windows falhará.

  3. O TransactionScope não oferece suporte a confirmação/reversão assíncrona, o que significa que o descarte bloqueia de forma síncrona o thread de execução até que a operação seja concluída.