Usar transacciones

Las transacciones permiten procesar varias operaciones de base de datos de manera atómica. Si se confirma la transacción, todas las operaciones se aplicaron correctamente a la base de datos. Si se revierte la transacción, ninguna de las operaciones se aplicó a la base de datos.

Sugerencia

Puede ver un ejemplo de este artículo en GitHub.

Comportamiento predeterminado de las transacciones

De manera predeterminada, si el proveedor de base de datos admite las transacciones, todos los cambios de una llamada sencilla a SaveChanges se aplican a una transacción. Si cualquiera de los cambios presenta un error, la transacción se revertirá y no se aplicará ninguno de los cambios a la base de datos. Esto significa que se garantiza que SaveChanges se complete correctamente o deje sin modificaciones la base de datos si se produce un error.

Este comportamiento predeterminado es suficiente para la mayoría de las aplicaciones. Solo debe controlar manualmente las transacciones si los requisitos de la aplicación lo consideran necesario.

Control de las transacciones

Puede usar la API DbContext.Database para iniciar, confirmar y revertir las transacciones. En el ejemplo siguiente se muestran dos operaciones SaveChanges y una consulta LINQ que se ejecuta en una sola transacción:

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
}

Si bien todos los proveedores de bases de datos relacionales admiten transacciones, otros tipos de proveedores pueden generar errores o no operar cuando se llama a las API de transacciones.

Nota:

Controlar manualmente las transacciones de esta manera no es compatible con las estrategias de ejecución de reintento invocadas implícitamente. Consulte Resistencia de conexión para más información.

Puntos de retorno

Cuando se invoca a SaveChanges y ya hay una transacción en curso en el contexto, EF crea automáticamente un punto de retorno antes de guardar los datos. Los puntos de retorno son puntos dentro de una transacción de base de datos a los que se puede revertir más tarde en caso de que ocurra un error o por cualquier otro motivo. Si SaveChanges encuentra algún error, revierte automáticamente la transacción al punto de retorno, y la transacción se mantiene en el mismo estado que si nunca se hubiera iniciado. Esto le permite posiblemente corregir problemas y volver a intentar guardar, en particular cuando ocurren problemas de simultaneidad optimista.

Advertencia

Los puntos de guardado no son compatibles con los conjuntos de resultados activos múltiples (MARS) de SQL Server. EF no creará puntos de guardado cuando MARS esté habilitado en la conexión, incluso si MARS no está en uso activamente. Si se produce un error durante SaveChanges, la transacción puede dejarse en un estado desconocido.

También es posible administrar los puntos de retorno de forma manual, del mismo modo que con las transacciones. En el ejemplo siguiente se crea un punto de retorno dentro de una transacción y se revierte cuando se produce un error:

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
}

Transacción entre contextos

También puede compartir una transacción en varias instancias de contexto. Esta funcionalidad solo está disponible cuando se usa un proveedor de base de datos relacionales porque requiere el uso de DbTransaction y DbConnection, específicos para las bases de datos relacionales.

Para compartir una transacción, los contextos deben compartir tanto DbConnection como DbTransaction.

Permitir conexiones proporcionadas externamente

Compartir una DbConnection requiere la capacidad de pasar una conexión a un contexto cuando se construya.

La manera más sencilla de permitir que DbConnection se proporcione de manera externa es dejar de usar el método DbContext.OnConfiguring para configurar el contexto y crear externamente DbContextOptions y pasarlas al constructor del contexto.

Sugerencia

DbContextOptionsBuilder es la API que usó en DbContext.OnConfiguring para configurar el contexto y ahora va a usarla para crear externamente DbContextOptions.

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

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

Una alternativa es seguir usando DbContext.OnConfiguring, pero aceptar una DbConnection que se guarda y luego se usa en 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);
    }
}

Compartir conexión y transacción

Ahora puede crear varias instancias de contexto que comparten la misma conexión. Luego use la API DbContext.Database.UseTransaction(DbTransaction) para inscribir ambos contextos en la misma transacción.

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
}

Uso de DbTransactions externas (solo bases de datos relacionales)

Si usa varias tecnologías de acceso a datos para acceder a una base de datos relacional, es posible que quiera compartir una transacción entre las operaciones que estas distintas tecnologías realizan.

En el ejemplo siguiente se muestra cómo realizar una operación SqlClient de ADO.NET y una operación de Entity Framework Core en la misma transacción.

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
}

Utilizar System.Transactions

Es posible usar transacciones de ambiente si necesita coordinar en un ámbito mayor.

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
    }
}

También es posible inscribir en una transacción 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
    }
}

Nota:

Si usa API asincrónicas, asegúrese de especificar TransactionScopeAsyncFlowOption.Enabled en el constructor TransactionScope para asegurarse de que la transacción ambiente fluye a través de llamadas asincrónicas.

Para obtener más información sobre TransactionScope y transacciones ambientales, consulte esta documentación.

Limitaciones de System.Transactions

  1. EF Core se basa en los proveedores de base de datos para implementar la compatibilidad con System.Transactions. Si un proveedor no implementa la compatibilidad con System.Transactions, es posible que las llamadas a estas API se omitan completamente. SqlClient lo admite.

    Importante

    Se recomienda probar que la API se comporte correctamente con el proveedor antes de usarla para administrar las transacciones. Si no es así, recomendamos que se ponga en contacto con el mantenedor del proveedor de base de datos.

  2. La compatibilidad con transacciones distribuidas en System.Transactions se agregó a .NET 7.0 solo para Windows. Se producirá un error en cualquier intento de usar transacciones distribuidas en versiones anteriores de .NET o en plataformas que no son de Windows.

  3. TransactionScope no admite la confirmación o reversión asincrónicas; esto significa que eliminarlo de forma sincrónica bloquea el subproceso en ejecución hasta que se complete la operación.