Utilizzo di transazioni

Le transazioni consentono di elaborare varie operazioni di database in modo atomico. Se viene eseguito il commit della transazione, tutte le operazioni vengono applicate correttamente nel database. Se viene eseguito il rollback della transazione, nessuna delle operazioni viene applicata nel database.

Suggerimento

È possibile visualizzare l'esempio di questo articolo in GitHub.

Comportamento delle transazioni predefinito

Per impostazione predefinita, se il provider di database supporta le transazioni, tutte le modifiche in una singola chiamata a SaveChanges vengono applicate in una transazione. Se le modifiche hanno esito negativo, viene eseguito il rollback della transazione e nessuna delle modifiche viene applicata al database. Ciò significa che è garantito che l'operazione SaveChanges venga completata correttamente oppure che lasci il database non modificato se si verifica un errore.

Per la maggior parte delle applicazioni, questo comportamento predefinito è sufficiente. È consigliabile controllare le transazioni manualmente solo se i requisiti dell'applicazione lo rendono necessario.

Controllo delle transazioni

È possibile usare l'API DbContext.Database per avviare le transazioni, eseguirne il commit ed eseguirne il rollback. L'esempio seguente illustra due SaveChanges operazioni e una query LINQ eseguita in una singola transazione:

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
}

Anche se tutti i provider di database relazionali supportano le transazioni, altri tipi di provider possono generare o no-op quando vengono chiamate le API delle transazioni.

Nota

Il controllo manuale delle transazioni in questo modo non è compatibile con le strategie di esecuzione richiamate in modo implicito. Per altre informazioni, vedere Connessione resilienza.

Punti di salvataggio

Quando SaveChanges viene richiamato e una transazione è già in corso nel contesto, EF crea automaticamente un punto di salvataggio prima di salvare i dati. I punti di salvataggio sono punti all'interno di una transazione di database a cui può essere eseguito il rollback in un secondo momento, se si verifica un errore o per qualsiasi altro motivo. Se SaveChanges si verifica un errore, esegue automaticamente il rollback della transazione al punto di salvataggio, lasciando la transazione nello stesso stato di se non fosse mai stata avviata. In questo modo è possibile correggere i problemi e riprovare a salvare, in particolare quando si verificano problemi di concorrenza ottimistica.

Avviso

I punti di salvataggio non sono compatibili con mars (Multiple Active Result Sets) di SQL Server. I punti di salvataggio non verranno creati da EF quando MARS è abilitato nella connessione, anche se MARS non è attivamente in uso. Se si verifica un errore durante SaveChanges, la transazione potrebbe essere lasciata in uno stato sconosciuto.

È anche possibile gestire manualmente i punti di salvataggio, proprio come con le transazioni. Nell'esempio seguente viene creato un punto di salvataggio all'interno di una transazione e ne viene eseguito il rollback in caso di errore:

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
}

Transazione tra contesti

È anche possibile condividere una transazione tra più istanze di contesto. Questa funzionalità è disponibile solo quando si usa un provider di database relazionale, perché richiede l'uso di DbTransaction e DbConnection, specifici per i database relazionali.

Per condividere una transazione, è necessario che i contesti condividano sia DbConnection che DbTransaction.

Consentire connessioni dall'esterno

La condivisione di DbConnection richiede la possibilità di passare una connessione in un contesto durante la costruzione.

Il modo più semplice per consentire DbConnection dall'esterno consiste nell'interrompere l'uso del metodo DbContext.OnConfiguring per configurare il contesto e nel creare esternamente DbContextOptions e passare tale opzioni al costruttore del contesto.

Suggerimento

DbContextOptionsBuilder è l'API usata in DbContext.OnConfiguring per configurare il contesto. In questo caso viene usata esternamente per creare DbContextOptions.

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

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

In alternativa è possibile continuare a usare DbContext.OnConfiguring, accettando però una DbConnection che viene salvata e quindi usata in 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);
    }
}

Condividere connessione e transazione

È ora possibile creare più istanze di contesto che condividono la stessa connessione. Usare quindi l'API DbContext.Database.UseTransaction(DbTransaction) per includere entrambi i contesti nella stessa transazione.

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 di DbTransaction esterne (solo database relazionali)

Se si usano più tecnologie di accesso ai dati per accedere a un database relazionale, può essere utile condividere una transazione tra le operazioni eseguite da queste diverse tecnologie.

L'esempio seguente mostra come eseguire un'operazione ADO.NET SqlClient e un'operazione di Entity Framework Core nella stessa transazione.

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
}

Utilizzo di System.Transactions

È possibile usare le transazioni di ambiente, se è necessario coordinarle in un ambito più ampio.

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

È anche supportato l'inserimento in una transazione esplicita.

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

Se si usano API asincrone, assicurarsi di specificare TransactionScopeAsyncFlowOption.Enabled nel TransactionScope costruttore per assicurarsi che la transazione di ambiente scorre tra chiamate asincrone.

Per altre informazioni sulle TransactionScope transazioni di ambiente e , vedere questa documentazione.

Limitazioni di System.Transactions

  1. EF Core si basa sui provider di database per implementare il supporto per System.Transactions. Se un provider non implementa il supporto per System.Transactions, è possibile che le chiamate a queste API vengano ignorate completamente. SqlClient lo supporta.

    Importante

    È consigliabile verificare che il comportamento dell'API con il provider sia corretto prima di basarsi su di essa per la gestione delle transazioni. In caso contrario, è consigliabile contattare il gestore del provider del database.

  2. Il supporto delle transazioni distribuite in System.Transactions è stato aggiunto solo a .NET 7.0 per Windows. Qualsiasi tentativo di usare transazioni distribuite in versioni precedenti di .NET o su piattaforme non Windows avrà esito negativo.

  3. TransactionScope non supporta il commit/rollback asincrono; ciò significa che eliminandolo in modo sincrono blocca il thread in esecuzione fino al completamento dell'operazione.