Bagikan melalui


Interseptor

Pencegat Entity Framework Core (EF Core) memungkinkan intersepsi, modifikasi, dan/atau penindasan operasi EF Core. Ia termasuk operasi database tingkat rendah seperti menjalankan perintah, serta operasi tingkat yang lebih tinggi seperti panggilan ke SaveChanges.

Interseptor berbeda dari pengelogan dan diagnostik, karena memungkinkan modifikasi atau pencegatan penekanan operasi. Pengelogan sederhana atau Microsoft.Extensions.Logging merupakan pilihan yang lebih baik untuk pengelogan.

Interseptor didaftarkan per instans DbContext saat konteks dikonfigurasi. Menggunakan pendengar diagnostik untuk mendapatkan informasi yang sama, namun untuk seluruh instans DbContext dalam prosesnya.

Mendaftarkan pencegat

Pencegat didaftarkan menggunakan AddInterceptors saat mengonfigurasi instans DbContext. Ini umumnya dilakukan dalam penimpaan DbContext.OnConfiguring. Contohnya:

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

Secara bergantian, AddInterceptors dapat dipanggil sebagai bagian AddDbContext dari atau saat membuat DbContextOptions instans untuk diteruskan ke konstruktor DbContext.

Tip

OnConfiguring masih dipanggil ketika AddDbContext digunakan atau instans DbContextOptions diteruskan ke konstruktor DbContext. Ini menjadikannya tempat yang ideal untuk menerapkan konfigurasi konteks terlepas dari bagaimana DbContext dibangun.

Pencegat sering tanpa status, yang berarti bahwa satu instans pencegat dapat digunakan untuk semua instans DbContext. Contohnya:

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

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

Setiap instans pencegat harus menerapkan satu atau beberapa antarmuka yang berasal dari IInterceptor. Setiap instans hanya boleh didaftarkan sekali bahkan jika mengimplementasikan beberapa antarmuka intersepsi; EF Core akan merutekan peristiwa untuk setiap antarmuka yang sesuai.

Penyadapan database

Catatan

Intersepsi database hanya tersedia untuk penyedia database relasional.

Intersepsi database tingkat rendah dibagi menjadi tiga antarmuka yang diperlihatkan dalam tabel berikut.

Interceptor Operasi database disadap
IDbCommandInterceptor Membuat perintah
Menjalankan perintah
Perintah gagal
Membuang DbDataReader perintah
IDbConnectionInterceptor Membuka dan menutup koneksi
Koneksi ion gagal
IDbTransactionInterceptor Membuat transaksi
Menggunakan transaksi
yang ada Melakukan transaksi
Rolling back transaksi
Membuat dan menggunakan titik penyimpanan
Kegagalan transaksi

Kelas dasar DbCommandInterceptor, DbConnectionInterceptor, dan DbTransactionInterceptor berisi implementasi tanpa operasi untuk setiap metode di antarmuka yang sesuai. Gunakan kelas dasar untuk menghindari kebutuhan untuk menerapkan metode intersepsi yang tidak digunakan.

Metode pada setiap jenis pencegat berpasangan, dengan yang pertama dipanggil sebelum operasi database dimulai, dan yang kedua setelah operasi selesai. Misalnya, DbCommandInterceptor.ReaderExecuting dipanggil sebelum kueri dijalankan, dan DbCommandInterceptor.ReaderExecuted dipanggil setelah kueri dikirim ke database.

Setiap pasangan metode memiliki variasi sinkronisasi dan asinkron. Ini memungkinkan I/O asinkron, seperti meminta token akses, terjadi sebagai bagian dari mencegat operasi database asinkron.

Contoh: Intersepsi perintah untuk menambahkan petunjuk kueri

Tip

Anda dapat mengunduh sampel pencegat perintah dari GitHub.

Dapat IDbCommandInterceptor digunakan untuk memodifikasi SQL sebelum dikirim ke database. Contoh ini memperlihatkan cara mengubah SQL untuk menyertakan petunjuk kueri.

Seringkali, bagian tersulit dari intersepsi adalah menentukan kapan perintah sesuai dengan kueri yang perlu dimodifikasi. Mengurai SQL adalah salah satu opsi, tetapi cenderung rapuh. Opsi lain adalah menggunakan tag kueri EF Core untuk menandai setiap kueri yang harus dimodifikasi. Contohnya:

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

Tag ini kemudian dapat dideteksi dalam pencegat karena akan selalu disertakan sebagai komentar di baris pertama teks perintah. Saat mendeteksi tag, kueri SQL dimodifikasi untuk menambahkan petunjuk yang sesuai:

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

Pemberitahuan:

  • Pencegat mewarisi dari DbCommandInterceptor untuk menghindari harus menerapkan setiap metode di antarmuka pencegat.
  • Pencegat mengimplementasikan metode sinkronisasi dan asinkron. Ini memastikan bahwa petunjuk kueri yang sama diterapkan untuk menyinkronkan dan kueri asinkron.
  • Pencegat mengimplementasikan Executing metode yang dipanggil oleh EF Core dengan SQL yang dihasilkan sebelum dikirim ke database. Kontras ini dengan Executed metode, yang dipanggil setelah panggilan database kembali.

Menjalankan kode dalam contoh ini menghasilkan hal berikut saat kueri ditandai:

-- Use hint: robust plan

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

Di sisi lain, ketika kueri tidak ditandai, maka kueri dikirim ke database yang tidak dimodifikasi:

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

Contoh: penyadapan Koneksi untuk autentikasi SQL Azure menggunakan AAD

Tip

Anda dapat mengunduh sampel pencegat koneksi dari GitHub.

Dapat IDbConnectionInterceptor digunakan untuk memanipulasi DbConnection sebelum digunakan untuk menyambungkan ke database. Ini dapat digunakan untuk mendapatkan token akses Azure Active Directory (AAD). Contohnya:

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 sekarang mendukung autentikasi AAD melalui string koneksi. Lihat SqlAuthenticationMethod untuk informasi lebih lanjut.

Peringatan

Perhatikan bahwa pencegat melempar jika panggilan sinkronisasi dilakukan untuk membuka koneksi. Ini karena tidak ada metode non-asinkron untuk mendapatkan token akses dan tidak ada cara universal dan sederhana untuk memanggil metode asinkron dari konteks non-asinkron tanpa memperkirakan kebuntuan.

Peringatan

dalam beberapa situasi token akses mungkin tidak di-cache secara otomatis penyedia Token Azure. Tergantung pada jenis token yang diminta, Anda mungkin perlu menerapkan penembolokan Anda sendiri di sini.

Contoh: Intersepsi perintah tingkat lanjut untuk penembolokan

Tip

Anda dapat mengunduh sampel pencegat perintah tingkat lanjut dari GitHub.

Pencegat Inti EF dapat:

  • Beri tahu EF Core untuk menekan eksekusi operasi yang dicegat
  • Mengubah hasil operasi yang dilaporkan kembali ke EF Core

Contoh ini menunjukkan pencegat yang menggunakan fitur-fitur ini untuk berulah seperti cache tingkat kedua primitif. Hasil kueri yang di-cache dikembalikan untuk kueri tertentu, menghindari roundtrip database.

Peringatan

Berhati-hatilah saat mengubah perilaku default EF Core dengan cara ini. EF Core mungkin berperilaku dengan cara yang tidak terduga jika mendapatkan hasil abnormal yang tidak dapat diproses dengan benar. Selain itu, contoh ini menunjukkan konsep pencegat; ini tidak dimaksudkan sebagai templat untuk implementasi cache tingkat kedua yang kuat.

Dalam contoh ini, aplikasi sering menjalankan kueri untuk mendapatkan "pesan harian" terbaru:

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

Kueri ini ditandai sehingga dapat dengan mudah terdeteksi di pencegat. Idenya adalah hanya mengkueri database untuk pesan baru sekali setiap hari. Di lain waktu aplikasi akan menggunakan hasil yang di-cache. (Sampel menggunakan penundaan 10 detik dalam sampel untuk mensimulasikan hari baru.)

Status pencegat

Pencegat ini stateful: ia menyimpan ID dan teks pesan dari pesan harian terbaru yang dikueri, ditambah waktu ketika kueri tersebut dijalankan. Karena status ini, kita juga memerlukan kunci karena penembolokan mengharuskan pencegat yang sama harus digunakan oleh beberapa instans konteks.

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

Sebelum eksekusi

Executing Dalam metode (yaitu sebelum melakukan panggilan database), pencegat mendeteksi kueri yang ditandai lalu memeriksa apakah ada hasil yang di-cache. Jika hasil seperti itu ditemukan, maka kueri ditekan dan hasil cache digunakan sebagai gantinya.

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

Perhatikan bagaimana kode memanggil InterceptionResult<TResult>.SuppressWithResult dan meneruskan penggantian DbDataReader yang berisi data cache. InterceptionResult ini kemudian dikembalikan, menyebabkan penindasan eksekusi kueri. Pembaca pengganti digunakan oleh EF Core sebagai hasil kueri.

Pencegat ini juga memanipulasi teks perintah. Manipulasi ini tidak diperlukan, tetapi meningkatkan kejelasan dalam pesan log. Teks perintah tidak perlu SQL yang valid karena kueri sekarang tidak akan dijalankan.

Setelah eksekusi

Jika tidak ada pesan cache yang tersedia, atau jika telah kedaluwarsa, maka kode di atas tidak menekan hasilnya. Oleh karena itu, EF Core akan menjalankan kueri seperti biasa. Kemudian akan kembali ke metode pencegat Executed setelah eksekusi. Pada titik ini jika hasilnya belum menjadi pembaca yang di-cache, maka ID pesan dan string baru diekstrak dari pembaca nyata dan di-cache siap untuk penggunaan kueri ini berikutnya.

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

Demonstrasi

Sampel pencegat penembolokan berisi aplikasi konsol sederhana yang meminta pesan harian untuk menguji penembolokan:

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

Perintah ini menghasilkan output berikut:

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

Perhatikan dari output log bahwa aplikasi terus menggunakan pesan cache sampai batas waktu habis, di mana database dikueri lagi untuk pesan baru apa pun.

Penyadapan SaveChanges

Tip

Anda dapat mengunduh sampel pencegat SaveChanges dari GitHub.

SaveChanges dan SaveChangesAsync titik intersepsi didefinisikan oleh ISaveChangesInterceptor antarmuka. Sedangkan untuk pencegat lainnya, SaveChangesInterceptor kelas dasar dengan metode no-op disediakan sebagai kenyamanan.

Tip

Pencegat itu kuat. Namun, dalam banyak kasus mungkin lebih mudah untuk mengambil alih metode SaveChanges atau menggunakan peristiwa .NET untuk SaveChanges yang diekspos di DbContext.

Contoh: Penyadapan SaveChanges untuk audit

SaveChanges dapat dicegat untuk membuat catatan audit independen dari perubahan yang dibuat.

Catatan

Ini tidak dimaksudkan untuk menjadi solusi audit yang kuat. Sebaliknya, ini adalah contoh sederhana yang digunakan untuk menunjukkan fitur intersepsi.

Konteks aplikasi

Sampel untuk audit menggunakan DbContext sederhana dengan blog dan posting.

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

Perhatikan bahwa instans baru pencegat terdaftar untuk setiap instans DbContext. Ini karena pencegat audit berisi status yang ditautkan ke instans konteks saat ini.

Konteks audit

Sampel juga berisi DbContext dan model kedua yang digunakan untuk database audit.

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

Pencegat

Ide umum untuk mengaudit dengan pencegat adalah:

  • Pesan audit dibuat di awal SaveChanges dan ditulis ke database audit
  • SaveChanges diizinkan untuk melanjutkan
  • Jika SaveChanges berhasil, maka pesan audit diperbarui untuk menunjukkan keberhasilan
  • Jika SaveChanges gagal, maka pesan audit diperbarui untuk menunjukkan kegagalan

Tahap pertama ditangani sebelum perubahan apa pun dikirim ke database menggunakan penimpaan ISaveChangesInterceptor.SavingChanges dan 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;
}

Mengesampingkan metode sinkronisasi dan asinkron memastikan bahwa audit akan terjadi terlepas dari apakah SaveChanges atau SaveChangesAsync dipanggil. Perhatikan juga bahwa kelebihan asinkron itu sendiri dapat melakukan I/O asinkron non-pemblokiran ke database audit. Anda mungkin ingin melempar dari metode sinkronisasi SavingChanges untuk memastikan bahwa semua I/O database adalah asinkron. Ini kemudian mengharuskan aplikasi selalu memanggil SaveChangesAsync dan tidak pernah SaveChanges.

Pesan audit

Setiap metode pencegat memiliki parameter yang eventData memberikan informasi kontekstual tentang peristiwa yang dicegat. Dalam hal ini aplikasi saat ini DbContext disertakan dalam data peristiwa, yang kemudian digunakan untuk membuat pesan audit.

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

Hasilnya adalah SaveChangesAudit entitas dengan kumpulan EntityAudit entitas, satu untuk setiap sisipan, pembaruan, atau penghapusan. Pencegat kemudian menyisipkan entitas ini ke dalam database audit.

Tip

ToString ditimpa di setiap kelas data peristiwa EF Core untuk menghasilkan pesan log yang setara untuk peristiwa tersebut. Misalnya, panggilan ContextInitializedEventData.ToString menghasilkan "Entity Framework Core 5.0.0 inisialisasi 'BlogsContext' menggunakan penyedia 'Microsoft.EntityFrameworkCore.Sqlite' dengan opsi: None".

Mendeteksi keberhasilan

Entitas audit disimpan pada pencegat sehingga dapat diakses lagi setelah SaveChanges berhasil atau gagal. Untuk keberhasilan, ISaveChangesInterceptor.SavedChanges atau ISaveChangesInterceptor.SavedChangesAsync dipanggil.

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

Entitas audit dilampirkan ke konteks audit, karena sudah ada dalam database dan perlu diperbarui. Kami kemudian mengatur Succeeded dan EndTime, yang menandai properti ini sebagai dimodifikasi sehingga SaveChanges akan mengirim pembaruan ke database audit.

Mendeteksi kegagalan

Kegagalan ditangani dengan cara yang sama seperti keberhasilan, tetapi dalam ISaveChangesInterceptor.SaveChangesFailed metode atau ISaveChangesInterceptor.SaveChangesFailedAsync . Data peristiwa berisi pengecualian yang dilemparkan.

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

Demonstrasi

Sampel audit berisi aplikasi konsol sederhana yang membuat perubahan pada database blogging lalu menunjukkan audit yang dibuat.

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

Hasilnya memperlihatkan konten database audit:

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