Sdílet prostřednictvím


Zachycovací systémy

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.

Průsečíky se liší od protokolování a diagnostiky v tom, že umožňují úpravy nebo potlačení zachycené operace. Jednoduché protokolování nebo Microsoft.Extensions.Logging jsou lepší volbou pro protokolování.

Zachytávače jsou registrovány pro každou instanci DbContext při konfiguraci kontextu. Pomocí diagnostického posluchače získejte stejné informace, ale pro všechny instance DbContext v procesu.

Registrace průsečíků

Zachytávače se registrují pomocí AddInterceptors při konfiguraci instance DbContext. To se obvykle provádí v přepsání DbContext.OnConfiguring. Například:

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

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

Návod

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

Zachytávače jsou často bezstavové, což znamená, že jednu instanci zachytávače lze použít pro všechny instance DbContext. Napří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í.

Odposlech databáze na nízké úrovni je rozdělen do tří rozhraní uvedených v následující tabulce.

Stíhačka Zachycené databázové operace
IDbCommandInterceptor Vytváření příkazů; Provádění příkazů; Selhání příkazů; Uvolnění DbDataReader příkazu
IDbConnectionInterceptor Otevírání a zavírání připojení
Selhání připojení
IDbTransactionInterceptor Vytváření transakcí
Použití existujících transakcí
Potvrzování transakcí
Vracení zpět transakcí
Vytváření a používání bodů obnovení
Chyby transakcí

Základní třídy DbCommandInterceptor, DbConnectionInterceptora DbTransactionInterceptor obsahují no-op implementace pro každou metodu v odpovídajícím rozhraní. Použijte základní třídy, abyste se vyhnuli potřebě implementovat metody zachycení, které nejsou používány.

Metody u každého typu zachytávače přicházejí ve dvojicích, přičemž první je volána 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

Návod

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

SQL lze upravit pomocí IDbCommandInterceptor před jeho odesláním do databáze. Tento příklad ukazuje, jak upravit SQL tak, aby obsahoval nápovědu k dotazu.

Nejzáludnější část odposlechu je určení toho, 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. Například:

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

Tuto značku pak lze detekovat v zachytávači, 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)";
        }
    }
}

Oznámení:

  • Zachycovač dědí z DbCommandInterceptor, aby nemusel implementovat každou metodu 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

Návod

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). Napří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;
    }
}

Návod

Microsoft.Data.SqlClient teď podporuje ověřování AAD prostřednictvím připojovacího řetězce. Další informace naleznete na SqlAuthenticationMethod.

Výstraha

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

Výstraha

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

Návod

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

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

  • Řekněte EF Core, aby potlačilo provádění zachytávané operace.
  • Změňte výsledek operace, který je odeslán zpět do EF Core.

Tento příklad ukazuje interceptor, který tyto funkce používá k chování jako primitivní cache druhé úrovně. V mezipaměti uložené výsledky dotazu jsou vráceny pro určitý dotaz, čímž se zabrání nutnosti opětovného dotazování na databázi.

Výstraha

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 rovněž demonstruje koncepty interceptoru; není určen 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čku místo toho používá EF Core pro zpracování výsledků 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 SQL nemusí být platný, protože dotaz nyní nebude spuštěn.

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 provedení se pak vrátí k metodě interceptoru Executed. V tomto okamžiku, pokud výsledek ještě není čtenářem mezipaměti, pak se nové ID zprávy a řetězec extrahují ze skutečného čtenáře a ukládají do mezipaměti 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 cachovacího interceptoru obsahuje jednoduchou konzolovou aplikaci, která vyhledává denní zprávy pro testování 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;

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í uložených změn

Návod

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 . Pokud jde o ostatní zachycovače, je základní třída SaveChangesInterceptor s metodami no-op poskytována pro usnadnění.

Návod

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, které jsou k dispozici v DbContextu.

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

SaveChanges lze zachytit a vytvořit nezávislý auditní záznam 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í vlastností zachytávání.

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 registrována nová instance interceptoru. Důvodem je to, že auditovací interceptor 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í se zachycovačem 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ě, oz zpráva auditu se aktualizuje, aby označovala ú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í synchronních i asynchronních metod zajišťuje, že auditování proběhne bez ohledu na to, jestli jsou volány SaveChanges nebo SaveChangesAsync. Všimněte si také, že asynchronní přetížení samo o sobě dokáže provádět neblokující asynchronní I/O operace na auditní databázi. 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 volala SaveChangesAsync a nikdy SaveChanges.

Zpráva auditu

Každá zachytávací metoda má parametr eventData, který poskytuje 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.

Návod

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. Například volání 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. K dosažení úspěchu se vyžaduje ISaveChangesInterceptor.SavedChanges nebo ISaveChangesInterceptor.SavedChangesAsync.

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á podobně jako úspěch, ale v metodě ISaveChangesInterceptor.SaveChangesFailed nebo ISaveChangesInterceptor.SaveChangesFailedAsync. Data události obsahují výjimku, která byla vyvolána.

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 auditu obsahuje jednoduchou konzolovou aplikaci, která provede změny v databázi blogu a pak zobrazí provedený audit.

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

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