Intercettori

Gli intercettori Entity Framework Core (EF Core) abilitano l'intercettazione, la modifica e/o l'eliminazione delle operazioni di EF Core. Sono incluse le operazioni di database di basso livello, ad esempio l'esecuzione di un comando, nonché operazioni di livello superiore, ad esempio le chiamate a SaveChanges.

Gli intercettori sono diversi dalla registrazione e dalla diagnostica per il fatto che consentono la modifica o l'eliminazione dell'operazione intercettata. L'uso della registrazione semplice o di Microsoft.Extensions.Logging è una scelta migliore per la registrazione.

Gli intercettori vengono registrati per ogni istanza di DbContext quando il contesto è configurato. Usare un listener di diagnostica per ottenere le stesse informazioni, ma per tutte le istanze di DbContext nel processo.

Registrazione degli intercettori

Gli intercettori vengono registrati usando AddInterceptors durante la configurazione di un'istanza DbContext. Questa operazione viene in genere eseguita in un override di DbContext.OnConfiguring. Ad esempio:

public class ExampleContext : BlogsContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.AddInterceptors(new TaggedQueryCommandInterceptor());
}

In alternativa, AddInterceptors è possibile chiamare come parte di AddDbContext o quando si crea un'istanza DbContextOptions da passare al costruttore DbContext.

Suggerimento

OnConfiguring viene comunque chiamato quando si usa AddDbContext o un'istanza DbContextOptions viene passata al costruttore DbContext. Ciò rende la posizione ideale per applicare la configurazione del contesto indipendentemente dalla modalità di costruzione di DbContext.

Gli intercettori sono spesso senza stato, il che significa che una singola istanza dell'intercettore può essere usata per tutte le istanze di DbContext. Ad esempio:

public class TaggedQueryCommandInterceptorContext : BlogsContext
{
    private static readonly TaggedQueryCommandInterceptor _interceptor
        = new TaggedQueryCommandInterceptor();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.AddInterceptors(_interceptor);
}

Ogni istanza dell'intercettore deve implementare una o più interfacce derivate da IInterceptor. Ogni istanza deve essere registrata una sola volta anche se implementa più interfacce di intercettazione; EF Core instrada gli eventi per ogni interfaccia in base alle esigenze.

Intercettazione del database

Nota

L'intercettazione del database è disponibile solo per i provider di database relazionali.

L'intercettazione di database di basso livello è suddivisa in tre interfacce illustrate nella tabella seguente.

Intercettore Operazioni di database intercettate
IDbCommandInterceptor Creazione di
comandi Esecuzione dei
comandi Errori
di eliminazione del DbDataReader del comando
IDbConnectionInterceptor Errori di apertura e chiusura delle connessioni
Connessione
IDbTransactionInterceptor Creazione di transazioni utilizzando transazioni
esistenti che esemettono il commit di transazioni eseguendo il rollback delle transazioni

Creazione e utilizzo di punti di
salvataggio

Le classi DbCommandInterceptordi base , DbConnectionInterceptore DbTransactionInterceptor contengono implementazioni no-op per ogni metodo nell'interfaccia corrispondente. Usare le classi di base per evitare la necessità di implementare metodi di intercettazione inutilizzati.

I metodi per ogni tipo di intercettore sono in coppia, con il primo chiamato prima dell'avvio dell'operazione di database e il secondo dopo il completamento dell'operazione. Ad esempio, DbCommandInterceptor.ReaderExecuting viene chiamato prima dell'esecuzione di una query e DbCommandInterceptor.ReaderExecuted viene chiamato dopo l'invio della query al database.

Ogni coppia di metodi ha varianti di sincronizzazione e asincrone. Ciò consente di eseguire operazioni di I/O asincrone, ad esempio la richiesta di un token di accesso, come parte dell'intercettazione di un'operazione asincrona del database.

Esempio: Intercettazione dei comandi per aggiungere hint per la query

Suggerimento

È possibile scaricare l'esempio di intercettore di comandi da GitHub.

Un IDbCommandInterceptor oggetto può essere usato per modificare SQL prima che venga inviato al database. In questo esempio viene illustrato come modificare SQL per includere un hint per la query.

Spesso, la parte più complessa dell'intercettazione è determinare quando il comando corrisponde alla query che deve essere modificata. L'analisi di SQL è un'opzione, ma tende a essere fragile. Un'altra opzione consiste nell'usare i tag di query di EF Core per contrassegnare ogni query che deve essere modificata. Ad esempio:

var blogs1 = context.Blogs.TagWith("Use hint: robust plan").ToList();

Questo tag può quindi essere rilevato nell'intercettore perché verrà sempre incluso come commento nella prima riga del testo del comando. Nel rilevamento del tag, la query SQL viene modificata per aggiungere l'hint appropriato:

public class TaggedQueryCommandInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result)
    {
        ManipulateCommand(command);

        return result;
    }

    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        ManipulateCommand(command);

        return new ValueTask<InterceptionResult<DbDataReader>>(result);
    }

    private static void ManipulateCommand(DbCommand command)
    {
        if (command.CommandText.StartsWith("-- Use hint: robust plan", StringComparison.Ordinal))
        {
            command.CommandText += " OPTION (ROBUST PLAN)";
        }
    }
}

Avviso:

  • L'intercettore eredita da DbCommandInterceptor per evitare di dover implementare ogni metodo nell'interfaccia dell'intercettore.
  • L'intercettore implementa sia metodi di sincronizzazione che asincroni. In questo modo si garantisce che lo stesso hint di query venga applicato alle query sincrone e asincrone.
  • L'intercettore implementa i Executing metodi chiamati da EF Core con sql generato prima dell'invio al database. A differenza dei Executed metodi, chiamati dopo la restituzione della chiamata al database.

L'esecuzione del codice in questo esempio genera quanto segue quando viene contrassegnata una query:

-- Use hint: robust plan

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b] OPTION (ROBUST PLAN)

D'altra parte, quando una query non è contrassegnata, viene inviata al database senza modifiche:

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]

Esempio: intercettazione Connessione per l'autenticazione di SQL Azure tramite AAD

Suggerimento

È possibile scaricare l'esempio di intercettore di connessione da GitHub.

Un IDbConnectionInterceptor oggetto può essere utilizzato per modificare l'oggetto DbConnection prima che venga utilizzato per connettersi al database. Può essere usato per ottenere un token di accesso di Azure Active Directory (AAD). Ad esempio:

public class AadAuthenticationInterceptor : DbConnectionInterceptor
{
    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
        => throw new InvalidOperationException("Open connections asynchronously when using AAD authentication.");

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
    {
        var sqlConnection = (SqlConnection)connection;

        var provider = new AzureServiceTokenProvider();
        // Note: in some situations the access token may not be cached automatically the Azure Token Provider.
        // Depending on the kind of token requested, you may need to implement your own caching here.
        sqlConnection.AccessToken = await provider.GetAccessTokenAsync("https://database.windows.net/", null, cancellationToken);

        return result;
    }
}

Suggerimento

Microsoft.Data.SqlClient supporta ora l'autenticazione AAD tramite stringa di connessione. Per altre informazioni, vedere SqlAuthenticationMethod.

Avviso

Si noti che l'intercettore genera un'eccezione se viene effettuata una chiamata di sincronizzazione per aprire la connessione. Questo perché non esiste alcun metodo non asincrono per ottenere il token di accesso e non esiste un modo universale e semplice per chiamare un metodo asincrono dal contesto non asincrono senza rischiare il deadlock.

Avviso

in alcune situazioni, il token di accesso potrebbe non essere memorizzato automaticamente nella cache del provider di token di Azure. A seconda del tipo di token richiesto, potrebbe essere necessario implementare la propria memorizzazione nella cache qui.

Esempio: Intercettazione avanzata dei comandi per la memorizzazione nella cache

Suggerimento

È possibile scaricare l'esempio avanzato di intercettore di comandi da GitHub.

Gli intercettori EF Core possono:

  • Indicare a EF Core di eliminare l'esecuzione dell'operazione intercettata
  • Modificare il risultato dell'operazione restituita a EF Core

Questo esempio mostra un intercettore che usa queste funzionalità per comportarsi come una cache di secondo livello primitiva. I risultati delle query memorizzati nella cache vengono restituiti per una query specifica, evitando un round trip del database.

Avviso

Prestare attenzione quando si modifica il comportamento predefinito di EF Core in questo modo. EF Core può comportarsi in modi imprevisti se ottiene un risultato anomalo che non è in grado di elaborare correttamente. In questo esempio vengono inoltre illustrati i concetti dell'intercettore; non è concepito come modello per un'implementazione affidabile della cache di secondo livello.

In questo esempio l'applicazione esegue spesso una query per ottenere il "messaggio giornaliero" più recente:

async Task<string> GetDailyMessage(DailyMessageContext context)
    => (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;

Questa query viene contrassegnata in modo che possa essere facilmente rilevata nell'intercettore. L'idea è eseguire una query solo sul database per un nuovo messaggio una volta al giorno. In altri casi, l'applicazione userà un risultato memorizzato nella cache. L'esempio usa un ritardo di 10 secondi nell'esempio per simulare un nuovo giorno.

Stato dell'intercettore

Questo intercettore è con stato: archivia l'ID e il testo del messaggio del messaggio giornaliero più recente, più l'ora in cui è stata eseguita la query. A causa di questo stato è necessario anche un blocco perché la memorizzazione nella cache richiede che lo stesso intercettatore debba essere usato da più istanze di contesto.

private readonly object _lock = new object();
private int _id;
private string _message;
private DateTime _queriedAt;

Prima dell'esecuzione

Executing Nel metodo (ad esempio, prima di effettuare una chiamata al database), l'intercettore rileva la query con tag e quindi controlla se è presente un risultato memorizzato nella cache. Se viene trovato un risultato di questo tipo, la query viene eliminata e i risultati memorizzati nella cache vengono invece usati.

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)
{
    if (command.CommandText.StartsWith("-- Get_Daily_Message", StringComparison.Ordinal))
    {
        lock (_lock)
        {
            if (_message != null
                && DateTime.UtcNow < _queriedAt + new TimeSpan(0, 0, 10))
            {
                command.CommandText = "-- Get_Daily_Message: Skipping DB call; using cache.";
                result = InterceptionResult<DbDataReader>.SuppressWithResult(new CachedDailyMessageDataReader(_id, _message));
            }
        }
    }

    return new ValueTask<InterceptionResult<DbDataReader>>(result);
}

Si noti che il codice chiama InterceptionResult<TResult>.SuppressWithResult e passa una sostituzione DbDataReader contenente i dati memorizzati nella cache. L'oggetto InterceptionResult viene quindi restituito, causando l'eliminazione dell'esecuzione della query. Il lettore sostitutivo viene invece usato da EF Core come risultati della query.

Questo intercettore modifica anche il testo del comando. Questa manipolazione non è necessaria, ma migliora la chiarezza nei messaggi di log. Il testo del comando non deve essere SQL valido perché la query non verrà ora eseguita.

Dopo l'esecuzione

Se non è disponibile alcun messaggio memorizzato nella cache o se è scaduto, il codice precedente non elimina il risultato. EF Core eseguirà quindi la query come di consueto. Verrà quindi restituito al metodo dell'intercettore Executed dopo l'esecuzione. A questo punto, se il risultato non è già un lettore memorizzato nella cache, il nuovo ID messaggio e la nuova stringa vengono estratti dal lettore reale e memorizzati nella cache pronti per l'uso successivo di questa query.

public override async ValueTask<DbDataReader> ReaderExecutedAsync(
    DbCommand command,
    CommandExecutedEventData eventData,
    DbDataReader result,
    CancellationToken cancellationToken = default)
{
    if (command.CommandText.StartsWith("-- Get_Daily_Message", StringComparison.Ordinal)
        && !(result is CachedDailyMessageDataReader))
    {
        try
        {
            await result.ReadAsync(cancellationToken);

            lock (_lock)
            {
                _id = result.GetInt32(0);
                _message = result.GetString(1);
                _queriedAt = DateTime.UtcNow;
                return new CachedDailyMessageDataReader(_id, _message);
            }
        }
        finally
        {
            await result.DisposeAsync();
        }
    }

    return result;
}

Dimostrazione

L'esempio di intercettore di memorizzazione nella cache contiene una semplice applicazione console che esegue query per i messaggi giornalieri per testare la memorizzazione nella cache:

// 1. Initialize the database with some daily messages.
using (var context = new DailyMessageContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.AddRange(
        new DailyMessage { Message = "Remember: All builds are GA; no builds are RTM." },
        new DailyMessage { Message = "Keep calm and drink tea" });

    await context.SaveChangesAsync();
}

// 2. Query for the most recent daily message. It will be cached for 10 seconds.
using (var context = new DailyMessageContext())
{
    Console.WriteLine(await GetDailyMessage(context));
}

// 3. Insert a new daily message.
using (var context = new DailyMessageContext())
{
    context.Add(new DailyMessage { Message = "Free beer for unicorns" });

    await context.SaveChangesAsync();
}

// 4. Cached message is used until cache expires.
using (var context = new DailyMessageContext())
{
    Console.WriteLine(await GetDailyMessage(context));
}

// 5. Pretend it's the next day.
Thread.Sleep(10000);

// 6. Cache is expired, so the last message will not be queried again.
using (var context = new DailyMessageContext())
{
    Console.WriteLine(await GetDailyMessage(context));
}

async Task<string> GetDailyMessage(DailyMessageContext context)
    => (await context.DailyMessages.TagWith("Get_Daily_Message").OrderBy(e => e.Id).LastAsync()).Message;

Si ottiene l'output seguente:

info: 10/15/2020 12:32:11.801 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      -- Get_Daily_Message

      SELECT "d"."Id", "d"."Message"
      FROM "DailyMessages" AS "d"
      ORDER BY "d"."Id" DESC
      LIMIT 1

Keep calm and drink tea

info: 10/15/2020 12:32:11.821 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Free beer for unicorns' (Size = 22)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "DailyMessages" ("Message")
      VALUES (@p0);
      SELECT "Id"
      FROM "DailyMessages"
      WHERE changes() = 1 AND "rowid" = last_insert_rowid();

info: 10/15/2020 12:32:11.826 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      -- Get_Daily_Message: Skipping DB call; using cache.

Keep calm and drink tea

info: 10/15/2020 12:32:21.833 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      -- Get_Daily_Message

      SELECT "d"."Id", "d"."Message"
      FROM "DailyMessages" AS "d"
      ORDER BY "d"."Id" DESC
      LIMIT 1

Free beer for unicorns

Si noti che l'output del log indica che l'applicazione continua a usare il messaggio memorizzato nella cache fino alla scadenza del timeout, a questo punto viene nuovamente eseguita una query sul database per qualsiasi nuovo messaggio.

Intercettazione SaveChanges

Suggerimento

È possibile scaricare l'esempio di intercettore SaveChanges da GitHub.

SaveChanges e SaveChangesAsync i punti di intercettazione sono definiti dall'interfaccia ISaveChangesInterceptor . Per quanto riguarda altri intercettori, la SaveChangesInterceptor classe base con metodi no-op viene fornita per praticità.

Suggerimento

Gli intercettori sono potenti. In molti casi, tuttavia, potrebbe essere più semplice eseguire l'override del metodo SaveChanges o usare gli eventi .NET per SaveChanges esposti in DbContext.

Esempio: Intercettazione SaveChanges per il controllo

È possibile intercettare SaveChanges per creare un record di controllo indipendente delle modifiche apportate.

Nota

Non si tratta di una soluzione di controllo affidabile. Piuttosto è un esempio semplicistico usato per dimostrare le caratteristiche dell'intercettazione.

Contesto dell'applicazione

L'esempio per il controllo usa un oggetto DbContext semplice con blog e post.

public class BlogsContext : DbContext
{
    private readonly AuditingInterceptor _auditingInterceptor = new AuditingInterceptor("DataSource=audit.db");

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .AddInterceptors(_auditingInterceptor)
            .UseSqlite("DataSource=blogs.db");

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

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }

    public Blog Blog { get; set; }
}

Si noti che una nuova istanza dell'intercettore viene registrata per ogni istanza di DbContext. Ciò è dovuto al fatto che l'intercettore di controllo contiene lo stato collegato all'istanza del contesto corrente.

Contesto di controllo

L'esempio contiene anche un secondo dbContext e un modello usati per il database di controllo.

public class AuditContext : DbContext
{
    private readonly string _connectionString;

    public AuditContext(string connectionString)
    {
        _connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlite(_connectionString);

    public DbSet<SaveChangesAudit> SaveChangesAudits { get; set; }
}

public class SaveChangesAudit
{
    public int Id { get; set; }
    public Guid AuditId { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
    public bool Succeeded { get; set; }
    public string ErrorMessage { get; set; }

    public ICollection<EntityAudit> Entities { get; } = new List<EntityAudit>();
}

public class EntityAudit
{
    public int Id { get; set; }
    public EntityState State { get; set; }
    public string AuditMessage { get; set; }

    public SaveChangesAudit SaveChangesAudit { get; set; }
}

Intercettore

L'idea generale per il controllo con l'intercettore è:

  • Un messaggio di controllo viene creato all'inizio di SaveChanges e viene scritto nel database di controllo
  • SaveChanges può continuare
  • Se SaveChanges ha esito positivo, il messaggio di controllo viene aggiornato per indicare l'esito positivo
  • Se SaveChanges ha esito negativo, il messaggio di controllo viene aggiornato per indicare l'errore

La prima fase viene gestita prima che tutte le modifiche vengano inviate al database usando override di ISaveChangesInterceptor.SavingChanges e ISaveChangesInterceptor.SavingChangesAsync.

public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
    DbContextEventData eventData,
    InterceptionResult<int> result,
    CancellationToken cancellationToken = default)
{
    _audit = CreateAudit(eventData.Context);

    using var auditContext = new AuditContext(_connectionString);

    auditContext.Add(_audit);
    await auditContext.SaveChangesAsync();

    return result;
}

public InterceptionResult<int> SavingChanges(
    DbContextEventData eventData,
    InterceptionResult<int> result)
{
    _audit = CreateAudit(eventData.Context);

    using var auditContext = new AuditContext(_connectionString);
    auditContext.Add(_audit);
    auditContext.SaveChanges();

    return result;
}

L'override dei metodi di sincronizzazione e asincroni garantisce che il controllo venga eseguito indipendentemente dal fatto SaveChanges che venga chiamato o SaveChangesAsync meno. Si noti anche che l'overload asincrono è in grado di eseguire operazioni di I/O asincrone non bloccanti nel database di controllo. È possibile generare un'eccezione dal metodo di sincronizzazione SavingChanges per assicurarsi che tutte le operazioni di I/O del database siano asincrone. A questo punto è necessario che l'applicazione chiami SaveChangesAsync sempre e mai SaveChanges.

Messaggio di controllo

Ogni metodo intercettore ha un eventData parametro che fornisce informazioni contestuali sull'evento intercettato. In questo caso l'applicazione corrente DbContext è inclusa nei dati dell'evento, che viene quindi usata per creare un messaggio di controllo.

private static SaveChangesAudit CreateAudit(DbContext context)
{
    context.ChangeTracker.DetectChanges();

    var audit = new SaveChangesAudit { AuditId = Guid.NewGuid(), StartTime = DateTime.UtcNow };

    foreach (var entry in context.ChangeTracker.Entries())
    {
        var auditMessage = entry.State switch
        {
            EntityState.Deleted => CreateDeletedMessage(entry),
            EntityState.Modified => CreateModifiedMessage(entry),
            EntityState.Added => CreateAddedMessage(entry),
            _ => null
        };

        if (auditMessage != null)
        {
            audit.Entities.Add(new EntityAudit { State = entry.State, AuditMessage = auditMessage });
        }
    }

    return audit;

    string CreateAddedMessage(EntityEntry entry)
        => entry.Properties.Aggregate(
            $"Inserting {entry.Metadata.DisplayName()} with ",
            (auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");

    string CreateModifiedMessage(EntityEntry entry)
        => entry.Properties.Where(property => property.IsModified || property.Metadata.IsPrimaryKey()).Aggregate(
            $"Updating {entry.Metadata.DisplayName()} with ",
            (auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");

    string CreateDeletedMessage(EntityEntry entry)
        => entry.Properties.Where(property => property.Metadata.IsPrimaryKey()).Aggregate(
            $"Deleting {entry.Metadata.DisplayName()} with ",
            (auditString, property) => auditString + $"{property.Metadata.Name}: '{property.CurrentValue}' ");
}

Il risultato è un'entità SaveChangesAudit con una raccolta di EntityAudit entità, una per ogni inserimento, aggiornamento o eliminazione. L'intercettore inserisce quindi queste entità nel database di controllo.

Suggerimento

ToString viene sottoposto a override in ogni classe di dati di evento di EF Core per generare il messaggio di log equivalente per l'evento. Ad esempio, la chiamata ContextInitializedEventData.ToString genera "Entity Framework Core 5.0.0 inizializzato "BlogsContext" usando il provider 'Microsoft.EntityFrameworkCore.Sqlite' con opzioni: Nessuno".

Rilevamento dell'esito positivo

L'entità di controllo viene archiviata nell'intercettore in modo che sia possibile accedervi di nuovo una volta che SaveChanges ha esito positivo o negativo. Per l'esito positivo o ISaveChangesInterceptor.SavedChangesISaveChangesInterceptor.SavedChangesAsync viene chiamato .

public int SavedChanges(SaveChangesCompletedEventData eventData, int result)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = true;
    _audit.EndTime = DateTime.UtcNow;

    auditContext.SaveChanges();

    return result;
}

public async ValueTask<int> SavedChangesAsync(
    SaveChangesCompletedEventData eventData,
    int result,
    CancellationToken cancellationToken = default)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = true;
    _audit.EndTime = DateTime.UtcNow;

    await auditContext.SaveChangesAsync(cancellationToken);

    return result;
}

L'entità di controllo è collegata al contesto di controllo, poiché esiste già nel database e deve essere aggiornata. Si impostano Succeeded quindi e EndTime, che contrassegna queste proprietà come modificate in modo che SaveChanges invierà un aggiornamento al database di controllo.

Rilevamento di un errore

L'errore viene gestito in modo analogo all'esito ISaveChangesInterceptor.SaveChangesFailed positivo, ma nel metodo o ISaveChangesInterceptor.SaveChangesFailedAsync . I dati dell'evento contengono l'eccezione generata.

public void SaveChangesFailed(DbContextErrorEventData eventData)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = false;
    _audit.EndTime = DateTime.UtcNow;
    _audit.ErrorMessage = eventData.Exception.Message;

    auditContext.SaveChanges();
}

public async Task SaveChangesFailedAsync(
    DbContextErrorEventData eventData,
    CancellationToken cancellationToken = default)
{
    using var auditContext = new AuditContext(_connectionString);

    auditContext.Attach(_audit);
    _audit.Succeeded = false;
    _audit.EndTime = DateTime.UtcNow;
    _audit.ErrorMessage = eventData.Exception.InnerException?.Message;

    await auditContext.SaveChangesAsync(cancellationToken);
}

Dimostrazione

L'esempio di controllo contiene una semplice applicazione console che apporta modifiche al database di blogging e quindi mostra il controllo creato.

// Insert, update, and delete some entities

using (var context = new BlogsContext())
{
    context.Add(
        new Blog { Name = "EF Blog", Posts = { new Post { Title = "EF Core 3.1!" }, new Post { Title = "EF Core 5.0!" } } });

    await context.SaveChangesAsync();
}

using (var context = new BlogsContext())
{
    var blog = context.Blogs.Include(e => e.Posts).Single();

    blog.Name = "EF Core Blog";
    context.Remove(blog.Posts.First());
    blog.Posts.Add(new Post { Title = "EF Core 6.0!" });

    context.SaveChanges();
}

// Do an insert that will fail

using (var context = new BlogsContext())
{
    try
    {
        context.Add(new Post { Id = 3, Title = "EF Core 3.1!" });

        await context.SaveChangesAsync();
    }
    catch (DbUpdateException)
    {
    }
}

// Look at the audit trail

using (var context = new AuditContext("DataSource=audit.db"))
{
    foreach (var audit in context.SaveChangesAudits.Include(e => e.Entities).ToList())
    {
        Console.WriteLine(
            $"Audit {audit.AuditId} from {audit.StartTime} to {audit.EndTime} was{(audit.Succeeded ? "" : " not")} successful.");

        foreach (var entity in audit.Entities)
        {
            Console.WriteLine($"  {entity.AuditMessage}");
        }

        if (!audit.Succeeded)
        {
            Console.WriteLine($"  Error: {audit.ErrorMessage}");
        }
    }
}

Il risultato mostra il contenuto del database di controllo:

Audit 52e94327-1767-4046-a3ca-4c6b1eecbca6 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was successful.
  Inserting Blog with Id: '-2147482647' Name: 'EF Blog'
  Inserting Post with Id: '-2147482647' BlogId: '-2147482647' Title: 'EF Core 3.1!'
  Inserting Post with Id: '-2147482646' BlogId: '-2147482647' Title: 'EF Core 5.0!'
Audit 8450f57a-5030-4211-a534-eb66b8da7040 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was successful.
  Inserting Post with Id: '-2147482645' BlogId: '1' Title: 'EF Core 6.0!'
  Updating Blog with Id: '1' Name: 'EF Core Blog'
  Deleting Post with Id: '1'
Audit 201fef4d-66a7-43ad-b9b6-b57e9d3f37b3 from 10/14/2020 9:10:17 PM to 10/14/2020 9:10:17 PM was not successful.
  Inserting Post with Id: '3' BlogId: '' Title: 'EF Core 3.1!'
  Error: SQLite Error 19: 'UNIQUE constraint failed: Post.Id'.