Resilienza della connessione

Connessione resilienza ritenta automaticamente i comandi del database non riusciti. La funzionalità può essere usata con qualsiasi database fornendo una "strategia di esecuzione", che incapsula la logica necessaria per rilevare gli errori e ripetere i comandi. I provider EF Core possono fornire strategie di esecuzione personalizzate in base alle specifiche condizioni di errore del database e ai criteri di ripetizione ottimali dei tentativi.

Ad esempio, il provider SQL Server include una strategia di esecuzione specificamente personalizzata per SQL Server (incluso SQL Azure). È a conoscenza dei tipi di eccezione che possono essere ritentati e ha impostazioni predefinite ragionevoli per i tentativi massimi, ritardo tra tentativi e così via.

Quando si configurano le opzioni per il contesto, viene specificata una strategia di esecuzione. Si tratta in genere del OnConfiguring metodo del contesto derivato:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True;ConnectRetryCount=0",
            options => options.EnableRetryOnFailure());
}

oppure in Startup.cs per un'applicazione ASP.NET Core:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<PicnicContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));
}

Nota

L'abilitazione di nuovi tentativi in caso di errore causa il buffer interno del set di risultati di Entity Framework, che può aumentare significativamente i requisiti di memoria per le query che restituiscono set di risultati di grandi dimensioni. Per altri dettagli, vedere buffering e streaming .

Strategia di esecuzione personalizzata

Esiste un meccanismo per registrare una strategia di esecuzione personalizzata personalizzata se si vuole modificare uno dei valori predefiniti.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseMyProvider(
            "<connection string>",
            options => options.ExecutionStrategy(...));
}

Strategie di esecuzione e transazioni

Una strategia di esecuzione che ritenta automaticamente gli errori deve essere in grado di riprodurre ogni operazione in un blocco di tentativi che ha esito negativo. Quando i tentativi sono abilitati, ogni operazione eseguita tramite EF Core diventa una propria operazione di ripetizione. Ovvero, ogni query e ogni chiamata a SaveChanges() verrà ritentata come unità se si verifica un errore temporaneo.

Tuttavia, se il codice avvia una transazione usando BeginTransaction() si definisce il proprio gruppo di operazioni che devono essere considerate come un'unità e tutto ciò che si trova all'interno della transazione deve essere riprodotto deve verificarsi un errore. Se si tenta di eseguire questa operazione quando si usa una strategia di esecuzione, si riceverà un'eccezione simile alla seguente:

InvalidOperationException: la strategia di esecuzione configurata 'SqlServerRetryingExecutionStrategy' non supporta le transazioni avviate dall'utente. Usare la strategia di esecuzione restituita da 'DbContext.Database.CreateExecutionStrategy()' per eseguire tutte le operazioni nella transazione come un'unità con possibilità di ritentare.

La soluzione consiste nell'richiamare manualmente la strategia di esecuzione con un delegato che rappresenta tutto ciò che deve essere eseguito. Se si verifica un errore temporaneo, la strategia di esecuzione chiamerà nuovamente il delegato.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context = new BloggingContext();
        using var transaction = context.Database.BeginTransaction();

        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();

        transaction.Commit();
    });

Questo approccio può essere usato anche con le transazioni di ambiente.


using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });

var strategy = context1.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

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

        context1.SaveChanges();

        transaction.Complete();
    });

Errore di commit della transazione e problema di idempotenza

In generale, quando si verifica un errore di connessione, viene eseguito il rollback della transazione corrente. Tuttavia, se la connessione viene eliminata mentre viene eseguito il commit della transazione, lo stato risultante della transazione è sconosciuto.

Per impostazione predefinita, la strategia di esecuzione ritenta l'operazione come se fosse stato eseguito il rollback della transazione, ma se non è così, ciò comporterà un'eccezione se il nuovo stato del database è incompatibile o potrebbe causare un danneggiamento dei dati se l'operazione non si basa su uno stato specifico, ad esempio quando si inserisce una nuova riga con valori di chiave generati automaticamente.

Esistono diversi modi per gestire questo problema.

Opzione 1 - Non fare (quasi) nulla

La probabilità di un errore di connessione durante il commit della transazione è bassa, pertanto può essere accettabile che l'applicazione non riesca se questa condizione si verifica effettivamente.

Tuttavia, è necessario evitare di usare chiavi generate dall'archivio per assicurarsi che venga generata un'eccezione anziché aggiungere una riga duplicata. Prendere in considerazione l'uso di un valore GUID generato dal client o di un generatore di valori lato client.

Opzione 2 - Ricompilare lo stato dell'applicazione

  1. Rimuovere l'oggetto corrente DbContext.
  2. Creare un nuovo DbContext e ripristinare lo stato dell'applicazione dal database.
  3. Informare l'utente che l'ultima operazione potrebbe non essere stata completata correttamente.

Opzione 3 - Aggiungere la verifica dello stato

Per la maggior parte delle operazioni che modificano lo stato del database, è possibile aggiungere codice che controlla se ha avuto esito positivo. Ef fornisce un metodo di estensione per semplificare questa operazione: IExecutionStrategy.ExecuteInTransaction.

Questo metodo inizia e esegue il commit di una transazione e accetta anche una funzione nel verifySucceeded parametro richiamato quando si verifica un errore temporaneo durante il commit della transazione.


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
db.Blogs.Add(blogToAdd);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));

db.ChangeTracker.AcceptAllChanges();

Nota

Qui SaveChanges viene richiamato con acceptAllChangesOnSuccess impostato su per false evitare di modificare lo stato dell'entità Blog in Unchanged in caso SaveChanges di esito positivo. In questo modo è possibile ritentare la stessa operazione se il commit ha esito negativo e viene eseguito il rollback della transazione.

Opzione 4 - Tenere traccia manuale della transazione

Se è necessario usare chiavi generate dall'archivio o è necessario un modo generico per gestire gli errori di commit che non dipendono dall'operazione eseguita a ogni transazione potrebbe essere assegnato un ID controllato quando il commit ha esito negativo.

  1. Aggiungere una tabella al database utilizzato per tenere traccia dello stato delle transazioni.
  2. Inserire una riga nella tabella all'inizio di ogni transazione.
  3. Se la connessione non riesce durante il commit, verificare la presenza della riga corrispondente nel database.
  4. Se il commit ha esito positivo, eliminare la riga corrispondente per evitare la crescita della tabella.

using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

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

var transaction = new TransactionRow { Id = Guid.NewGuid() };
db.Transactions.Add(transaction);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
db.SaveChanges();

Nota

Assicurarsi che il contesto usato per la verifica disponga di una strategia di esecuzione definita in quanto è probabile che la connessione abbia esito negativo durante la verifica se non è riuscita durante il commit della transazione.

Risorse aggiuntive