Zachycovače

Průsečíky Entity Framework Core (EF Core) umožňují zachycení, úpravy a potlačení operací EF Core. To zahrnuje databázové operace nízké úrovně, jako je spuštění příkazu, a také operace vyšší úrovně, jako jsou volání SaveChanges.

Zachytávače se liší od protokolování a diagnostiky v tom, že umožňují úpravu nebo potlačení zachycované operace. K protokolování je vhodnější použít jednoduché protokolování nebo Microsoft.Extensions.Logging.

Zachytávače se zaregistrují pro jednotlivé instance DbContextu při konfiguraci kontextu. Pomocí diagnostického naslouchacího procesu můžete získat stejné informace, ale pro všechny instance DbContextu v rámci tohoto procesu.

Registrace průsečíků

Průsečíky se registrují při AddInterceptorskonfiguraci instance DbContext. To se obvykle provádí v přepsání .DbContext.OnConfiguring Příklad:

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

Alternativně AddInterceptors lze volat jako součást AddDbContext nebo při vytváření DbContextOptions instance předat DbContext konstruktoru.

Tip

OnConfiguring je stále volána, když AddDbContext je použit nebo DbContextOptions instance je předán DbContext konstruktoru. Díky tomu je ideálním místem pro použití konfigurace kontextu bez ohledu na to, jak je dbContext vytvořen.

Průsečíky jsou často bezstavové, což znamená, že jednu instanci zachytávání lze použít pro všechny instance DbContext. Příklad:

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

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

Každá instance průsečíku musí implementovat jedno nebo více rozhraní odvozených z IInterceptor. Každá instance by měla být registrována pouze jednou, i když implementuje více rozhraní průsečíku; EF Core bude podle potřeby směrovat události pro každé rozhraní.

Zachycení databáze

Poznámka

Zachycení databáze je k dispozici pouze pro zprostředkovatele relačních databází.

Průsečík databáze nízké úrovně je rozdělený do tří rozhraní uvedených v následující tabulce.

Interceptor Zachycené databázové operace
IDbCommandInterceptor Vytváření příkazů, které spouští příkazy


, selhání příkazu Disposing the command's DbDataReader
IDbConnectionInterceptor Otevírání a zavírání připojení
Připojení selhání
IDbTransactionInterceptor Vytváření transakcí
pomocí existujících transakcí
Potvrzující transakce vrácení zpět transakce

Vytváření a používání savepoints Transaction failures

Základní třídy DbCommandInterceptor, DbConnectionInterceptora DbTransactionInterceptor obsahují no-op implementace pro každou metodu v odpovídajícím rozhraní. Základní třídy použijte, abyste se vyhnuli nutnosti implementovat nepoužívané metody zachycení.

Metody jednotlivých typů zachytávání přicházejí ve dvojicích, přičemž první volané před spuštěním databázové operace a druhá po dokončení operace. Volá se například DbCommandInterceptor.ReaderExecuting před spuštěním dotazu a DbCommandInterceptor.ReaderExecuted volá se po odeslání dotazu do databáze.

Každá dvojice metod má synchronizované i asynchronní varianty. To umožňuje asynchronní vstupně-výstupní operace, jako je například vyžádání přístupového tokenu, jako součást zachycení asynchronní databázové operace.

Příklad: Zachytávání příkazů pro přidání tipů pro dotazy

Tip

Ukázku zachytávání příkazů si můžete stáhnout z GitHubu.

Před IDbCommandInterceptor odesláním do databáze lze použít k úpravě SQL. Tento příklad ukazuje, jak upravit SQL tak, aby obsahoval nápovědu k dotazu.

Nejsná část průsečíku často určuje, kdy příkaz odpovídá dotazu, který je potřeba upravit. Analýza SQL je jednou z možností, ale obvykle je křehká. Další možností je použít značky dotazů EF Core k označení každého dotazu, který by se měl upravit. Příklad:

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

Tuto značku pak lze zjistit v průsečíku, protože bude vždy zahrnuta jako komentář v prvním řádku textu příkazu. Při zjišťování značky se dotaz SQL upraví tak, aby přidal odpovídající nápovědu:

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

Poznámka:

  • Zachycovač dědí z toho, aby DbCommandInterceptor se zabránilo implementaci každé metody v rozhraní zachytávání.
  • Průsečík implementuje synchronizační i asynchronní metody. Tím se zajistí, že se pro synchronizaci a asynchronní dotazy použije stejný tip dotazu.
  • Zachycovač implementuje Executing metody, které ef Core volá s vygenerovaným SQL před odesláním do databáze. Porovnejte to s metodami Executed , které se volají po vrácení volání databáze.

Spuštění kódu v tomto příkladu vygeneruje následující kód při označení dotazu:

-- Use hint: robust plan

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

Pokud ale dotaz není označený, odešle se do databáze beze změny:

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

Příklad: zachycení Připojení pro ověřování SQL Azure pomocí AAD

Tip

Ukázku zachytávání připojení si můžete stáhnout z GitHubu.

Lze IDbConnectionInterceptor použít k manipulaci s DbConnection před tím, než se použije pro připojení k databázi. Dá se použít k získání přístupového tokenu Azure Active Directory (AAD). Příklad:

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

Tip

Microsoft.Data.SqlClient teď podporuje ověřování AAD prostřednictvím připojovací řetězec. Další informace naleznete v tématu SqlAuthenticationMethod.

Upozorňující

Všimněte si, že průsečík vyvolá, pokud se provede synchronizační hovor pro otevření připojení. Důvodem je to, že neexistuje žádná nesynchronní metoda pro získání přístupového tokenu a neexistuje žádný univerzální a jednoduchý způsob, jak volat asynchronní metodu z nesynchronních kontextů bez rizika zablokování.

Upozorňující

v některých situacích nemusí být přístupový token automaticky uložen do mezipaměti zprostředkovatele tokenů Azure. V závislosti na typu požadovaného tokenu možná budete muset implementovat vlastní ukládání do mezipaměti.

Příklad: Pokročilé zachycení příkazů pro ukládání do mezipaměti

Průsečíky EF Core můžou:

  • Řekněte EF Core, aby potlačit provádění zachytávané operace.
  • Změna výsledku operace hlášené zpět na EF Core

Tento příklad ukazuje průsečík, který tyto funkce používá k chování jako primitivní mezipaměť druhé úrovně. Výsledky dotazu v mezipaměti se vrátí pro konkrétní dotaz, aby nedocházelo k zaokrouhlování databáze.

Upozorňující

Při změně výchozího chování EF Core tímto způsobem dbejte na to. EF Core se může chovat neočekávaně, pokud získá neobvyklý výsledek, který nemůže správně zpracovat. Tento příklad také ukazuje koncepty průsečíku; není určena jako šablona pro robustní implementaci mezipaměti druhé úrovně.

V tomto příkladu aplikace často spouští dotaz, který získá nejnovější "denní zprávu":

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

Tento dotaz je označen tak, aby ho bylo možné snadno rozpoznat v průsečíku. Cílem je dotazovat se na databázi pouze jednou denně na novou zprávu. Jindy aplikace použije výsledek uložený v mezipaměti. (Ukázka používá zpoždění 10 sekund ve vzorku k simulaci nového dne.)

Stav průsečíku

Tento průsečík je stavový: ukládá ID a text zprávy posledního denního dotazu a čas provedení dotazu. Kvůli tomuto stavu potřebujeme také zámek , protože ukládání do mezipaměti vyžaduje, aby stejný průsečík musel používat více kontextových instancí.

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

Před spuštěním

Executing V metodě (tj. před voláním databáze) průsečík rozpozná označený dotaz a pak zkontroluje, jestli existuje výsledek uložený v mezipaměti. Pokud se takový výsledek najde, potlačí se dotaz a místo toho se použijí výsledky uložené v mezipaměti.

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

Všimněte si, jak kód volá InterceptionResult<TResult>.SuppressWithResult a předává náhradu DbDataReader obsahující data uložená v mezipaměti. Tato funkce InterceptionResult se pak vrátí, což způsobí potlačení provádění dotazu. Náhradní čtečka se místo toho používá ef Core jako výsledky dotazu.

Tento průsečík také manipuluje s textem příkazu. Tato manipulace není nutná, ale zlepšuje přehlednost zpráv protokolu. Text příkazu nemusí být platný, protože dotaz se teď nespustí.

Po spuštění

Pokud není k dispozici žádná zpráva uložená v mezipaměti nebo pokud vypršela jeho platnost, výše uvedený kód nepotlačí výsledek. EF Core proto provede dotaz jako obvykle. Po spuštění se pak vrátí k metodě průsečíku Executed . V tomto okamžiku, pokud výsledek ještě není čtenářem v mezipaměti, pak se nové ID zprávy a řetězec extrahuje ze skutečného čtenáře a ukládá se do mezipaměti připravené pro další použití tohoto dotazu.

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

Ukázka

Ukázka průsečíku ukládání do mezipaměti obsahuje jednoduchou konzolovou aplikaci, která se dotazuje na denní zprávy pro testování ukládání do mezipaměti:

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

Výsledkem je následující výstup:

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

Všimněte si z výstupu protokolu, že aplikace bude dál používat zprávu uloženou v mezipaměti, dokud nevyprší časový limit, kdy se databáze znovu dotazuje na jakoukoli novou zprávu.

Zachytávání SaveChanges

Tip

Ukázku průsečíku SaveChanges si můžete stáhnout z GitHubu.

SaveChanges a SaveChangesAsync průsečíkové body jsou definovány rozhraním ISaveChangesInterceptor . Stejně jako u jiných průsečíků SaveChangesInterceptor je základní třída s metodami no-op poskytována jako pohodlí.

Tip

Průsečíky jsou výkonné. V mnoha případech ale může být jednodušší přepsat metodu SaveChanges nebo použít události .NET pro SaveChanges vystavené v DbContext.

Příklad: Zachycení SaveChanges pro auditování

Funkce SaveChanges je možné zachytit a vytvořit nezávislý záznam auditu provedených změn.

Poznámka

Nejedná se o robustní řešení auditování. Spíše se jedná o zjednodušený příklad, který slouží k předvedení funkcí průsečíku.

Kontext aplikace

Ukázka pro auditování používá jednoduchý DbContext s blogy a příspěvky.

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

Všimněte si, že pro každou instanci DbContext je zaregistrovaná nová instance zachytávání. Důvodem je to, že zachytávání auditování obsahuje stav propojený s aktuální instancí kontextu.

Kontext auditu

Ukázka obsahuje také druhý DbContext a model používaný pro databázi auditování.

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

Průsečík

Obecnou myšlenkou auditování s průsečíkem je:

  • Na začátku aplikace SaveChanges se vytvoří zpráva auditu a zapíše se do databáze auditování.
  • SaveChanges může pokračovat
  • Pokud saveChanges proběhne úspěšně, zpráva o auditu se aktualizuje, aby značil úspěch.
  • Pokud funkce SaveChanges selže, zpráva o auditu se aktualizuje, aby značí selhání.

První fáze je zpracována před odesláním jakýchkoli změn do databáze pomocí přepsání ISaveChangesInterceptor.SavingChanges a 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;
}

Přepsání synchronizačních i asynchronních metod zajišťuje, že auditování proběhne bez ohledu na to, jestli SaveChanges se volají nebo SaveChangesAsync jestli se volají. Všimněte si také, že asynchronní přetížení je samo o sobě schopné provádět neblokující asynchronní vstupně-výstupní operace do databáze auditování. Můžete chtít vyvolat metodu synchronizace SavingChanges , abyste měli jistotu, že všechny vstupně-výstupní operace databáze jsou asynchronní. To pak vyžaduje, aby aplikace vždy volá SaveChangesAsync a nikdy SaveChanges.

Zpráva auditu

Každá metoda průsečíku eventData má parametr poskytující kontextové informace o zachycené události. V tomto případě je aktuální aplikace DbContext zahrnuta do dat události, která se pak používá k vytvoření zprávy auditu.

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

Výsledkem je entita SaveChangesAudit s kolekcí EntityAudit entit, jednou pro každou vložení, aktualizaci nebo odstranění. Zachycovač pak tyto entity vloží do databáze auditu.

Tip

ToString je přepsán v každé datové třídě událostí EF Core, aby se vygenerovala ekvivalentní zpráva protokolu pro událost. Volání například ContextInitializedEventData.ToString vygeneruje "Entity Framework Core 5.0.0 inicializováno 'BlogsContext' pomocí zprostředkovatele Microsoft.EntityFrameworkCore.Sqlite s možnostmi: None".

Zjištění úspěchu

Entita auditu je uložena v průsečíku, aby k ní bylo možné znovu přistupovat, jakmile operace SaveChanges proběhne úspěšně nebo selže. Pro úspěch ISaveChangesInterceptor.SavedChanges nebo ISaveChangesInterceptor.SavedChangesAsync je volána.

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

Entita auditu je připojená k kontextu auditu, protože už v databázi existuje a je potřeba ji aktualizovat. Potom nastavíme Succeeded a EndTime, což označí tyto vlastnosti jako změněné, takže SaveChanges odešle aktualizaci do databáze auditu.

Zjišťování selhání

Selhání se zpracovává stejným způsobem jako úspěch, ale v ISaveChangesInterceptor.SaveChangesFailed metodě.ISaveChangesInterceptor.SaveChangesFailedAsync Data události obsahují výjimku, která byla vyvolán.

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

Ukázka

Ukázka auditování obsahuje jednoduchou konzolovou aplikaci, která provede změny v databázi blogování a pak zobrazí vytvořené auditování.

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

Výsledek ukazuje obsah databáze auditování:

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