Delen via


Onderscheppers

Entity Framework Core(EF Core)-interceptors maken onderschepping, wijziging en/of onderdrukking van EF Core-bewerkingen mogelijk. Dit omvat databasebewerkingen op laag niveau, zoals het uitvoeren van een opdracht, evenals bewerkingen op een hoger niveau, zoals aanroepen naar SaveChanges.

Interceptors verschillen van loggen en diagnostiek doordat ze het wijzigen of onderdrukken van de bewerking die wordt onderschept mogelijk maken. Eenvoudige logboekregistratie of Microsoft.Extensions.Logging zijn betere opties voor logboekregistratie.

Interceptors worden geregistreerd per DbContext-instantie wanneer de context geconfigureerd wordt. Gebruik een diagnostische listener om dezelfde informatie op te halen, maar voor alle DbContext-exemplaren in het proces.

Het registreren van interceptors

Interceptors worden geregistreerd tijdens AddInterceptorshet configureren van een DbContext-instantie. Dit wordt meestal gedaan in een overschrijving van DbContext.OnConfiguring. Voorbeeld:

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

AddInterceptors kan ook worden aangeroepen als onderdeel van AddDbContext of wanneer een DbContextOptions-exemplaar wordt gemaakt om door te geven aan de DbContext-constructor.

Aanbeveling

OnConfiguring wordt nog steeds aangeroepen wanneer AddDbContext wordt gebruikt of als een DbContextOptions-exemplaar wordt doorgegeven aan de DbContext-constructor. Dit maakt het de ideale plek om contextconfiguratie toe te passen, ongeacht hoe dbContext wordt samengesteld.

Interceptors zijn vaak staatloos, wat betekent dat een enkele interceptorexemplaar kan worden gebruikt voor alle DbContext-instanties. Voorbeeld:

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

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

Elke interceptorexemplaar moet een of meer interfaces implementeren die zijn afgeleid van IInterceptor. Elk exemplaar mag slechts eenmaal worden geregistreerd, zelfs als er meerdere snijpuntinterfaces worden geïmplementeerd; EF Core routeeert gebeurtenissen voor elke interface, indien van toepassing.

Database onderschepping

Opmerking

Databaseonderschepping is alleen beschikbaar voor relationele databaseproviders.

Databaseonderschepping op laag niveau wordt gesplitst in de drie interfaces die in de volgende tabel worden weergegeven.

Onderschepper Databasebewerkingen onderschept
IDbCommandInterceptor
Opdrachten maken
Opdrachten uitvoeren
Opdrachtfouten
Het verwijderen van de DbDataReader van de opdracht
IDbConnectionInterceptor Het openen en sluiten van verbindingen
Verbindingsfouten
IDbTransactionInterceptor
Transacties creëren
Bestaande transacties gebruiken
Transacties committen
Transacties terugdraaien (rollbacken)
Savepoints creëren en gebruikenTransactiefouten

De basisklassen DbCommandInterceptoren DbConnectionInterceptorDbTransactionInterceptor bevatten no-op implementaties voor elke methode in de bijbehorende interface. Gebruik de basisklassen om te voorkomen dat ongebruikte onderscheppingsmethoden moeten worden geïmplementeerd.

De methoden voor elk snijpunttype komen in paren, waarbij de eerste wordt aangeroepen voordat de databasebewerking wordt gestart en de tweede nadat de bewerking is voltooid. Wordt bijvoorbeeld DbCommandInterceptor.ReaderExecuting aangeroepen voordat een query wordt uitgevoerd en DbCommandInterceptor.ReaderExecuted wordt aangeroepen nadat de query naar de database is verzonden.

Elk paar methoden hebben zowel synchronisatie- als asynchrone variaties. Hierdoor kan asynchrone I/O, zoals het aanvragen van een toegangstoken, plaatsvinden als onderdeel van het onderscheppen van een asynchrone databasebewerking.

Voorbeeld: Opdrachtonderschepping om queryhints toe te voegen

Aanbeveling

U kunt het command-interceptor voorbeeld downloaden van GitHub.

Een IDbCommandInterceptor kan worden gebruikt om SQL te wijzigen voordat deze naar de database wordt verzonden. In dit voorbeeld ziet u hoe u de SQL kunt wijzigen om een queryhint op te nemen.

Vaak bepaalt het lastigste deel van de interceptie wanneer de opdracht overeenkomt met de query die moet worden gewijzigd. Het parseren van de SQL is één optie, maar is meestal kwetsbaar. Een andere optie is om EF Core-querytags te gebruiken om elke query te taggen die moet worden gewijzigd. Voorbeeld:

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

Deze tag kan vervolgens worden gedetecteerd in de interceptor, omdat deze altijd wordt opgenomen als opmerking op de eerste regel van de opdrachttekst. Bij het detecteren van de tag wordt de query SQL gewijzigd om de juiste hint toe te voegen:

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)";
        }
    }
}

Kennisgeving:

  • De interceptor neemt over van DbCommandInterceptor om te voorkomen dat elke methode in de interceptor-interface moet worden geïmplementeerd.
  • De interceptor implementeert zowel synchronisatie- als asynchrone methoden. Dit zorgt ervoor dat dezelfde queryhint wordt toegepast op synchronisatie- en asynchrone query's.
  • De interceptor implementeert de Executing methoden die worden aangeroepen door EF Core met de gegenereerde SQL voordat deze naar de database wordt verzonden. Vergelijk dit met de Executed methoden die worden aangeroepen nadat de databaseaanroep is geretourneerd.

Als u de code in dit voorbeeld uitvoert, wordt het volgende gegenereerd wanneer een query wordt getagd:

-- Use hint: robust plan

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

Aan de andere kant, wanneer een query niet wordt getagd, wordt deze verzonden naar de database niet gewijzigd:

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

Voorbeeld: Interceptie van verbindingen voor SQL Azure-verificatie met behulp van AAD

Een IDbConnectionInterceptor kan worden gebruikt om de DbConnection aan te passen voordat deze wordt gebruikt om verbinding te maken met de database. Dit kan worden gebruikt om een AAD-toegangstoken (Azure Active Directory) te verkrijgen. Voorbeeld:

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

Aanbeveling

Microsoft.Data.SqlClient ondersteunt nu AAD-verificatie via verbindingsreeks. Zie SqlAuthenticationMethod voor meer informatie.

Waarschuwing

U ziet dat de interceptor wordt gegooid als er een synchronisatieaanroep wordt uitgevoerd om de verbinding te openen. Dit komt doordat er geen niet-asynchrone methode is om het toegangstoken te verkrijgen en er geen universele en eenvoudige manier is om een asynchrone methode aan te roepen vanuit een niet-asynchrone context zonder dat er een impasse ontstaat.

Waarschuwing

in sommige situaties wordt het toegangstoken mogelijk niet automatisch in de cache opgeslagen bij de Azure-tokenprovider. Afhankelijk van het type token dat u hebt aangevraagd, moet u mogelijk hier uw eigen caching implementeren.

Voorbeeld: Geavanceerde onderschepping van opdrachten voor caching

EF Core-interceptors kunnen:

  • Geef EF Core de aanwijzing om de uitvoering van de onderschepte bewerking te onderdrukken.
  • Wijzig het resultaat van de bewerking dat wordt gerapporteerd aan EF Core

In dit voorbeeld ziet u een interceptor die gebruikmaakt van deze functies om zich te gedragen als een primitieve cache op het tweede niveau. Queryresultaten in de cache worden geretourneerd voor een specifieke query, waardoor een database-ronde wordt vermeden.

Waarschuwing

Wees voorzichtig bij het wijzigen van het standaardgedrag van EF Core op deze manier. EF Core kan zich op onverwachte manieren gedragen als het een abnormaal resultaat krijgt dat het niet correct kan worden verwerkt. In dit voorbeeld worden ook onderscheppingsconcepten gedemonstreert; het is niet bedoeld als sjabloon voor een robuuste implementatie van cache op het tweede niveau.

In dit voorbeeld voert de toepassing vaak een query uit om het meest recente 'dagelijkse bericht' te verkrijgen:

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

Deze query wordt getagd , zodat deze eenvoudig kan worden gedetecteerd in de interceptor. Het idee is om slechts één keer per dag een query uit te voeren op de database voor een nieuw bericht. Op andere momenten gebruikt de toepassing een resultaat in de cache. (In het voorbeeld wordt een vertraging van 10 seconden gebruikt om een nieuwe dag te simuleren.)

Onderscheppingsstatus

Deze interceptor is statusgevoelig: de ID en berichttekst van de meest recent opgevraagde dagelijkse boodschap worden opgeslagen, plus het tijdstip waarop die query is uitgevoerd. Vanwege deze status hebben we ook een vergrendeling nodig omdat voor de caching vereist is dat dezelfde interceptor door meerdere contextexemplaren moet worden gebruikt.

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

Vóór uitvoering

In de Executing methode (bijvoorbeeld voordat u een databaseoproep maakt), detecteert de interceptor de getagde query en controleert vervolgens of er een resultaat in de cache is. Als een dergelijk resultaat wordt gevonden, wordt de query onderdrukt en worden de resultaten in de cache gebruikt.

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

Merk op hoe de code InterceptionResult<TResult>.SuppressWithResult aanroept en een vervanging DbDataReader doorgeeft die de gegevens in de cache bevat. Dit InterceptionResult wordt vervolgens geretourneerd, waardoor de uitvoering van queries wordt onderdrukt. De vervangende lezer wordt in plaats daarvan door EF Core gebruikt als de resultaten van de query.

Deze interceptor bewerkt ook de opdrachttekst. Deze manipulatie is niet vereist, maar verbetert de duidelijkheid in logboekberichten. De opdrachttekst hoeft geen geldige SQL te zijn omdat de query nu niet wordt uitgevoerd.

Na uitvoering

Als er geen bericht in de cache beschikbaar is of als het is verlopen, onderdrukt de bovenstaande code het resultaat niet. EF Core voert daarom de query als normaal uit. Vervolgens keert deze na de uitvoering terug naar de methode van de interceptor Executed. Op dit moment, als het resultaat nog geen reader in de cache is, wordt de nieuwe bericht-ID en tekenreeks uit het echte reader object geëxtraheerd en in de cache opgeslagen voor volgend gebruik van deze 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;
}

Demonstratie

Het voorbeeld van een caching-interceptor bevat een eenvoudige consoletoepassing die query's uitvoert op dagelijkse berichten om de caching te testen:

// 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;

Dit resulteert in de volgende uitvoer:

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

U ziet in de logboekuitvoer dat de toepassing het bericht in de cache blijft gebruiken totdat de time-out verloopt, waarna de database opnieuw wordt opgevraagd voor een nieuw bericht.

SaveChanges-onderbreking

Aanbeveling

U kunt het SaveChanges-interceptorvoorbeeld downloaden van GitHub.

SaveChanges en SaveChangesAsync interceptiepunten worden gedefinieerd door de ISaveChangesInterceptor interface. Net als bij andere interceptors wordt de SaveChangesInterceptor basisklasse met no-op methoden voor uw gemak geleverd.

Aanbeveling

Interceptors zijn krachtig. In veel gevallen is het echter eenvoudiger om de SaveChanges-methode te overschrijven of de .NET-gebeurtenissen te gebruiken voor SaveChanges die beschikbaar zijn op DbContext.

Voorbeeld: SaveChanges-interceptie voor controle

SaveChanges kan worden onderschept om een onafhankelijke controlerecord te maken van de aangebrachte wijzigingen.

Opmerking

Dit is niet bedoeld als een robuuste controleoplossing. In plaats daarvan is het een simplistisch voorbeeld dat wordt gebruikt om de functies van interceptie te demonstreren.

De toepassingscontext

Het voorbeeld voor controle maakt gebruik van een eenvoudige DbContext met blogs en berichten.

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

U ziet dat een nieuw exemplaar van de interceptor is geregistreerd voor elk DbContext-exemplaar. Dit komt doordat de auditinterceptor status bevat die is gekoppeld aan de huidige contextinstantie.

De auditcontext

Het voorbeeld bevat ook een tweede DbContext en een model dat wordt gebruikt voor de controledatabase.

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

De interceptor

Het algemene idee voor controle met de interceptor is:

  • Aan het begin van SaveChanges wordt een controlebericht gemaakt en naar de controledatabase geschreven
  • SaveChanges mag doorgaan
  • Als SaveChanges slaagt, wordt het controlebericht bijgewerkt om aan te geven dat het is gelukt
  • Als SaveChanges mislukt, wordt het controlebericht bijgewerkt om de fout aan te geven

De eerste fase wordt afgehandeld voordat eventuele wijzigingen naar de database worden verzonden met behulp van overschrijvingen van ISaveChangesInterceptor.SavingChanges en 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;
}

Het overschrijven van zowel synchronisatie- als asynchrone methoden zorgt ervoor dat controle plaatsvindt, ongeacht of SaveChanges of deze SaveChangesAsync worden aangeroepen. Merk ook op dat de asynchrone overload-functie zelf niet-blokkerende asynchrone I/O kan uitvoeren voor de controledatabase. Mogelijk wilt u de synchronisatiemethode SavingChanges weggooien om ervoor te zorgen dat alle database-I/O asynchroon is. Dit vereist vervolgens dat de toepassing altijd aanroept SaveChangesAsync en nooit SaveChanges.

Het controlebericht

Elke interceptormethode heeft een eventData parameter die contextuele informatie biedt over de gebeurtenis die wordt onderschept. In dit geval wordt de huidige toepassing DbContext opgenomen in de gebeurtenisgegevens, die vervolgens wordt gebruikt om een controlebericht te maken.

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}' ");
}

Het resultaat is een SaveChangesAudit entiteit met een verzameling EntityAudit entiteiten, één voor elke invoeg-, update- of verwijderbewerking. De interceptor voegt deze entiteiten vervolgens in de auditdatabase in.

Aanbeveling

ToString wordt overschreven in elke EF Core-gebeurtenisgegevensklasse om het equivalente logboekbericht voor de gebeurtenis te genereren. Als u bijvoorbeeld ContextInitializedEventData.ToString aanroept, wordt 'Entity Framework Core 5.0.0' geïnitialiseerd met 'BlogsContext' en gebruikt daarbij de provider 'Microsoft.EntityFrameworkCore.Sqlite' met opties: Geen.

Succes detecteren

De auditentiteit wordt opgeslagen op de interceptor, zodat deze opnieuw kan worden geopend zodra SaveChanges slaagt of mislukt. Voor een succesvolle procedure wordt ISaveChangesInterceptor.SavedChanges of ISaveChangesInterceptor.SavedChangesAsync aangeroepen.

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

De auditentiteit is gekoppeld aan de auditcontext, omdat deze al bestaat in de database en moet worden bijgewerkt. Vervolgens stellen we Succeeded en EndTime in, waarmee deze eigenschappen als gewijzigd worden gemarkeerd zodat SaveChanges een update naar de auditdatabase verzendt.

Fout detecteren

Mislukking wordt op dezelfde manier afgehandeld als succes, maar in de ISaveChangesInterceptor.SaveChangesFailed of ISaveChangesInterceptor.SaveChangesFailedAsync methode. De gebeurtenisgegevens bevatten de uitzondering die is opgeworpen.

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

Demonstratie

Het controlevoorbeeld bevat een eenvoudige consoletoepassing die wijzigingen aanbrengt in de blogdatabase en vervolgens de controle weergeeft die is gemaakt.

// 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 = await context.Blogs.Include(e => e.Posts).SingleAsync();

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

    await context.SaveChangesAsync();
}

// 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 await context.SaveChangesAudits.Include(e => e.Entities).ToListAsync())
    {
        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}");
        }
    }
}

Het resultaat toont de inhoud van de controledatabase:

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'.