Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Le transazioni consentono l'elaborazione di diverse operazioni di database in modo atomico. Se viene eseguito il commit della transazione, tutte le operazioni vengono applicate correttamente al database. Se viene eseguito il rollback della transazione, nessuna delle operazioni viene applicata al database.
Comportamento predefinito delle transazioni
Per impostazione predefinita, se il provider di database supporta le transazioni, tutte le modifiche apportate a una singola chiamata a SaveChanges vengono applicate in una transazione. Se una delle modifiche ha esito negativo, viene eseguito il rollback della transazione e nessuna delle modifiche viene applicata al database. Ciò significa che SaveChanges è garantito avere successo completo oppure lasciare il database invariato se si verifica un errore.
Per la maggior parte delle applicazioni, questo comportamento predefinito è sufficiente. È consigliabile controllare manualmente le transazioni solo se i requisiti dell'applicazione lo ritengono necessario.
Controllo delle transazioni
È possibile usare l'API DbContext.Database per avviare, eseguire il commit e il rollback delle transazioni. L'esempio seguente illustra due operazioni SaveChanges e una query LINQ eseguita in una singola transazione:
using var context = new BloggingContext();
await using var transaction = await context.Database.BeginTransactionAsync();
try
{
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context.SaveChangesAsync();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
await context.SaveChangesAsync();
var blogs = await context.Blogs
.OrderBy(b => b.Url)
.ToListAsync();
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
await transaction.CommitAsync();
}
catch (Exception)
{
// TODO: Handle failure
}
Mentre tutti i provider di database relazionali supportano le transazioni, altri tipi di provider possono generare un'eccezione o un errore 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 ulteriori informazioni, vedere Resilienza delle connessioni.
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.
Avvertimento
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();
await using var transaction = await context.Database.BeginTransactionAsync();
try
{
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
await context.SaveChangesAsync();
await transaction.CreateSavepointAsync("BeforeMoreBlogs");
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (Exception)
{
// If a failure occurred, we rollback to the savepoint and can continue the transaction
await transaction.RollbackToSavepointAsync("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 relazionali perché richiede l'uso di DbTransaction e DbConnection, specifici per i database relazionali.
Per condividere una transazione, i contesti devono condividere sia un DbConnection che un DbTransaction.
Consenti alla connessione di essere fornita esternamente
La condivisione di un DbConnection richiede la possibilità di passare una connessione in un contesto durante la costruzione.
Il modo più semplice per consentire DbConnection di essere fornito esternamente consiste nell'interrompere l'uso del metodo DbContext.OnConfiguring per configurare il contesto e creare esternamente DbContextOptions e passarli al costruttore di contesto.
Suggerimento
DbContextOptionsBuilder è l'API usata in DbContext.OnConfiguring per configurare il contesto, che verrà ora usata esternamente per creare DbContextOptions.
public class BloggingContext : DbContext
{
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{
}
public DbSet<Blog> Blogs { get; set; }
}
Un'alternativa consiste nel continuare a usare DbContext.OnConfiguring, ma accettare un DbConnection salvato e quindi usato 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 la connessione e la transazione
È ora possibile creare più istanze di contesto che condividono la stessa connessione. Usare quindi l'API DbContext.Database.UseTransaction(DbTransaction) per integrare 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);
await using var transaction = await context1.Database.BeginTransactionAsync();
try
{
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context1.SaveChangesAsync();
using (var context2 = new BloggingContext(options))
{
await context2.Database.UseTransactionAsync(transaction.GetDbTransaction());
var blogs = await context2.Blogs
.OrderBy(b => b.Url)
.ToListAsync();
context2.Blogs.Add(new Blog { Url = "http://dot.net" });
await context2.SaveChangesAsync();
}
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
await transaction.CommitAsync();
}
catch (Exception)
{
// TODO: Handle failure
}
Uso di DbTransactions esterno (solo database relazionali)
Se si usano più tecnologie di accesso ai dati per accedere a un database relazionale, è possibile condividere una transazione tra le operazioni eseguite da queste diverse tecnologie.
Nell'esempio seguente viene illustrato come eseguire un'operazione ADO.NET SqlClient e un'operazione Entity Framework Core nella stessa transazione.
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
await using var transaction = (SqlTransaction)await connection.BeginTransactionAsync();
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))
{
await context.Database.UseTransactionAsync(transaction);
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context.SaveChangesAsync();
}
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
await transaction.CommitAsync();
}
catch (Exception)
{
// TODO: Handle failure
}
Utilizzo di System.Transactions
È possibile usare le transazioni di ambiente se è necessario coordinare un ambito più ampio.
using (var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
try
{
// Run raw ADO.NET command in the transaction
var command = connection.CreateCommand();
command.CommandText = "DELETE FROM dbo.Blogs";
await command.ExecuteNonQueryAsync();
// 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" });
await context.SaveChangesAsync();
}
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
scope.Complete();
}
catch (Exception)
{
// TODO: Handle failure
}
}
È anche possibile iscriversi a 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))
{
await context.Database.OpenConnectionAsync();
context.Database.EnlistTransaction(transaction);
// Run raw ADO.NET command in the transaction
var command = connection.CreateCommand();
command.CommandText = "DELETE FROM dbo.Blogs";
await command.ExecuteNonQueryAsync();
// Run an EF Core command in the transaction
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context.SaveChangesAsync();
await context.Database.CloseConnectionAsync();
}
// 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, è importante specificare TransactionScopeAsyncFlowOption.Enabled nel costruttore TransactionScope per garantire che la transazione ambientale possa fluire tra le chiamate asincrone.
Per ulteriori informazioni su TransactionScope e sulle transazioni ambientali, vedere questa documentazione.
Limitazioni di System.Transactions
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 completamente ignorate. SqlClient lo supporta.
Importante
È consigliabile verificare che l'API si comporti correttamente con il provider prima di basarsi su di essa per la gestione delle transazioni. In caso contrario, è consigliabile contattare il gestore del provider di database.
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.
TransactionScope non supporta il commit/rollback asincrono; ciò significa che eliminandolo in modo sincrono blocca il thread in esecuzione fino al completamento dell'operazione.