Bagikan melalui


Pencegat

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

Pencegat berbeda dari pencatatan dan diagnostik karena pencegat memungkinkan modifikasi atau penghentian operasi yang dicegat. Pengelogan sederhana atau Microsoft.Extensions.Logging adalah pilihan yang lebih baik untuk pengelogan.

Pencegat didaftarkan per instans DbContext saat konteks dikonfigurasi. Gunakan pendengar diagnostik untuk mendapatkan informasi yang sama tetapi untuk semua instance DbContext pada proses.

Mendaftarkan interseptor

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

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

Alternatifnya, AddInterceptors dapat dipanggil sebagian dari AddDbContext atau saat membuat instans DbContextOptions untuk diteruskan ke konstruktor DbContext.

Petunjuk / Saran

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

Nota

Penyadapan database hanya tersedia untuk penyedia database relasional.

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

Interseptor Operasi database disadap
IDbCommandInterceptor Membuat perintah
Menjalankan perintah
Perintah gagal
Membuang DbDataReader perintah
IDbConnectionInterceptor Membuka dan menutup koneksi
Kegagalan koneksi
IDbTransactionInterceptor Membuat transaksi
Menggunakan transaksi yang ada
Melakukan komit transaksi
Mengembalikan transaksi
Membuat dan menggunakan savepoints
Kegagalan transaksi

Kelas DbCommandInterceptordasar , DbConnectionInterceptor, dan DbTransactionInterceptor berisi implementasi no-op 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

Petunjuk / Saran

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 = await context.Blogs.TagWith("Use hint: robust plan").ToListAsync();

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:

  • Interseptor mewarisi dari DbCommandInterceptor untuk menghindari penerapan setiap metode di antarmuka interseptor.
  • Pencegat mengimplementasikan metode sinkron dan asinkron. Ini memastikan bahwa petunjuk kueri yang sama diterapkan untuk kueri sinkron dan asinkron.
  • Pencegat mengimplementasikan Executing metode yang dipanggil oleh EF Core dengan SQL yang dihasilkan sebelum dikirim ke database. Bandingkan ini dengan metode Executed, yang dipanggil setelah panggilan database telah 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: Intersepsi koneksi untuk autentikasi SQL Azure menggunakan AAD

Petunjuk / Saran

Anda dapat mengunduh sampel pencegat koneksi dari GitHub.

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

Petunjuk / Saran

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 risiko 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 mekanisme caching Anda sendiri di sini.

Contoh: Intersepsi perintah tingkat lanjut untuk penyimpanan sementara

Petunjuk / Saran

Anda dapat mengunduh sampel pencegat perintah tingkat lanjut dari GitHub.

Interseptor EF Core dapat:

  • Instruksikan EF Core untuk mencegah eksekusi operasi yang diintersepsi
  • Mengubah hasil dari operasi yang dilaporkan kembali kepada EF Core

Contoh ini menunjukkan pencegat yang menggunakan fitur-fitur ini untuk berperilaku seperti cache tingkat kedua yang bersifat primitif. Hasil kueri yang di-cache dikembalikan untuk kueri tertentu, menghindari perjalanan bolak-balik ke 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 pemblokir

Pencegat ini bersifat berkondisi tetap (stateful): ia menyimpan ID dan teks pesan dari yang dikuerikan dari pesan harian terbaru, ditambah waktu ketika kueri tersebut dijalankan. Karena status ini, kita juga memerlukan kunci karena caching mengharuskan interceptor yang sama digunakan oleh beberapa instance konteks.

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

Sebelum eksekusi

Di dalam metode Executing (yaitu sebelum melakukan panggilan ke database), interceptor mendeteksi kueri yang ditandai lalu memeriksa apakah ada hasil yang telah 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 penghentian eksekusi kueri. Pembaca pengganti digunakan oleh EF Core sebagai hasil dari 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 Executed dari pencegat tersebut setelah eksekusi. Pada titik ini, jika hasilnya belum berupa pembaca yang di-cache, maka ID pesan dan string baru diekstrak dari pembaca yang sebenarnya dan di-cache agar siap digunakan pada kueri 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

Petunjuk / Saran

Anda dapat mengunduh sampel pencegat SaveChanges dari GitHub.

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

Petunjuk / Saran

Pencegat itu perkasa. 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.

Nota

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

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 pengintersepsi audit memiliki status yang ditautkan ke instans konteks saat ini.

Konteks audit

Contoh tersebut juga mencakup 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-entitas ini ke dalam database audit.

Petunjuk / Saran

ToString dioverride di setiap kelas data peristiwa EF Core untuk menghasilkan pesan log yang sesuai 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 interceptor sehingga dapat diakses kembali baik ketika SaveChanges berhasil maupun 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 metode ISaveChangesInterceptor.SaveChangesFailed 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 = 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}");
        }
    }
}

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