Aracılığıyla paylaş


Durdurucular

Entity Framework Core (EF Core) kesme kesicileri EF Core işlemlerinin kesilmesini, değiştirilmesini ve/veya gizlenmesini sağlar. Bu, komut yürütme gibi düşük düzeyli veritabanı işlemlerinin yanı sıra SaveChanges çağrıları gibi üst düzey işlemleri de içerir.

Durdurucuların günlük ve tanılamalardan farkı, durdurulan işlemin değiştirilmesine veya engellenmesine olanak tanımalarıdır. Basit günlüğe kaydetme veya Microsoft.Extensions.Logging, günlük için daha iyi seçeneklerdir.

Durdurucular, bağlam yapılandırıldığında DbContext örneği başına kaydedilir. İşlemdeki tüm DbContext örneklerine ilişkin olarak aynı bilgileri almak için tanılama dinleyicisi kullanın.

Kesme noktası oluşturucuları kaydetme

Kesiciler, dbContext örneği yapılandırılırken kullanılarak AddInterceptors kaydedilir. Bu genellikle bir geçersiz kılmada DbContext.OnConfiguringyapılır. Örnek:

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

Alternatif olarak, AddInterceptors DbContext oluşturucusunun bir parçası AddDbContext olarak veya bir örnek oluşturulurken DbContextOptions çağrılabilir.

Bahşiş

AddDbContext kullanıldığında veya DbContextOptions örneği DbContext oluşturucusna geçirildiğinde OnConfiguring hala çağrılır. Bu, DbContext'in nasıl yapılandırıldığından bağımsız olarak bağlam yapılandırmasını uygulamak için ideal bir yer olmasını sağlar.

Kesiciler genellikle durum bilgisi yoktur, yani tek bir kesme noktası örneği tüm DbContext örnekleri için kullanılabilir. Örnek:

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

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

Her kesme noktası örneği, 'den IInterceptortüretilen bir veya daha fazla arabirim uygulamalıdır. Her örnek, birden çok kesme arabirimi uygulasa bile yalnızca bir kez kaydedilmelidir; EF Core, her arabirim için olayları uygun şekilde yönlendirir.

Veritabanı kesme noktası

Dekont

Veritabanı kesme yalnızca ilişkisel veritabanı sağlayıcıları için kullanılabilir.

Alt düzey veritabanı kesme işlemi aşağıdaki tabloda gösterilen üç arabirime ayrılmıştır.

Interceptor Veritabanı işlemleri durduruldu
IDbCommandInterceptor Komut
oluşturma Komutları
yürütme Komut hataları
Komutun DbDataReader'ını yok etme
IDbConnectionInterceptor Bağlantıları
açma ve kapatma Bağlan ion hataları
IDbTransactionInterceptor İşlem oluşturma
Mevcut işlemleri kullanma İşlemleri
işleme İşlemleri

geri alınıyor Savepoints
oluşturma ve kullanma İşlem hataları

, ve DbTransactionInterceptor temel sınıflarıDbConnectionInterceptorDbCommandInterceptor, karşılık gelen arabirimdeki her yöntem için op olmayan uygulamalar içerir. Kullanılmayan kesme yöntemleri uygulama gereksinimini önlemek için temel sınıfları kullanın.

Her kesme noktası türündeki yöntemler, ilki veritabanı işlemi başlatılmadan önce çağrılır ve ikincisi işlem tamamlandıktan sonra çift olarak gelir. Örneğin, DbCommandInterceptor.ReaderExecuting sorgu yürütülmeden önce çağrılır ve DbCommandInterceptor.ReaderExecuted sorgu veritabanına gönderildikten sonra çağrılır.

Her yöntem çiftinin hem eşitleme hem de zaman uyumsuz varyasyonları vardır. Bu, erişim belirteci isteme gibi zaman uyumsuz G/Ç'nin zaman uyumsuz veritabanı işleminin kesilmesinin bir parçası olarak gerçekleşmesini sağlar.

Örnek: Sorgu ipuçları eklemek için komut kesme noktası

Bahşiş

GitHub'dan komut kesme noktası örneğini indirebilirsiniz.

, IDbCommandInterceptor veritabanına gönderilmeden önce SQL'i değiştirmek için kullanılabilir. Bu örnekte SQL'in sorgu ipucu içerecek şekilde nasıl değiştirileceği gösterilmektedir.

Genellikle, kesme noktasının en karmaşık kısmı, komutun değiştirilmesi gereken sorguya ne zaman karşılık geldiğini belirlemektir. SQL'i ayrıştırmak bir seçenektir, ancak kırılgan olma eğilimindedir. Başka bir seçenek, değiştirilmesi gereken her sorguyu etiketlemek için EF Core sorgu etiketlerini kullanmaktır. Örnek:

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

Daha sonra bu etiket, komut metninin ilk satırına her zaman açıklama olarak eklendiğinden, kesme makinesinde algılanabilir. Etiketi algılarken sorgu SQL'i uygun ipucunu eklemek için değiştirilir:

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

Not:

  • Kesme noktası, kesme noktası arabirimindeki her yöntemi uygulamak zorunda kalmamak için öğesini devralır DbCommandInterceptor .
  • Kesme noktası hem eşitleme hem de zaman uyumsuz yöntemler uygular. Bu, eşitleme ve zaman uyumsuz sorgulara aynı sorgu ipucunun uygulanmasını sağlar.
  • Kesme noktası, veritabanına gönderilmeden önce oluşturulan SQL ile EF Core tarafından çağrılan yöntemleri uygularExecuting. Bunu, veritabanı çağrısı döndürüldükten sonra çağrılan yöntemlerle Executed karşıtlık edin.

Bu örnekte kodu çalıştırmak, bir sorgu etiketlendiğinde aşağıdakini oluşturur:

-- Use hint: robust plan

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

Öte yandan, bir sorgu etiketlenmediğinde veritabanına değiştirilmeden gönderilir:

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

Örnek: AAD kullanarak SQL Azure kimlik doğrulaması için Bağlan kesme

Bahşiş

Bağlantı kesme noktası örneğini GitHub'dan indirebilirsiniz.

, IDbConnectionInterceptor veritabanına bağlanmak için kullanılmadan önce öğesini işlemek DbConnection için kullanılabilir. Bu, Azure Active Directory (AAD) erişim belirtecini almak için kullanılabilir. Örnek:

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

Bahşiş

Microsoft.Data.SqlClient artık bağlantı dizesi aracılığıyla AAD kimlik doğrulamayı destekliyor. Daha fazla bilgi edinmek için bkz. SqlAuthenticationMethod.

Uyarı

Bağlantıyı açmak için bir eşitleme çağrısı yapıldığında kesme noktasının attığına dikkat edin. Bunun nedeni, erişim belirtecini almak için zaman uyumsuz bir yöntem olmaması ve kilitlenme riski olmadan zaman uyumsuz bağlamdan zaman uyumsuz bir yöntemi çağırmanın evrensel ve basit bir yolu olmamasıdır.

Uyarı

bazı durumlarda erişim belirteci Azure Belirteç Sağlayıcısı otomatik olarak önbelleğe alınamayabilir. İstenen belirteç türüne bağlı olarak, burada kendi önbelleğe alma işleminizi uygulamanız gerekebilir.

Örnek: Önbelleğe alma için gelişmiş komut kesme noktası

Bahşiş

Gelişmiş komut kesme noktası örneğini GitHub'dan indirebilirsiniz.

EF Core kesiciler:

  • EF Core'a kesilen işlemi yürütmeyi engellemesini söyleyin
  • Bildirilen işlemin sonucunu EF Core olarak değiştirme

Bu örnekte, bu özellikleri ilkel ikinci düzey önbellek gibi davranan bir kesme noktası gösterilmektedir. Önbelleğe alınan sorgu sonuçları, veritabanı gidiş dönüşlerinden kaçınarak belirli bir sorgu için döndürülür.

Uyarı

EF Core varsayılan davranışını bu şekilde değiştirirken dikkatli olun. EF Core, doğru işleyemediği anormal bir sonuç alırsa beklenmedik şekilde davranabilir. Ayrıca bu örnek, kesme noktası kavramlarını gösterir; sağlam bir ikinci düzey önbellek uygulaması için şablon olarak tasarlanmamıştır.

Bu örnekte, uygulama sık sık en son "günlük ileti" almak için bir sorgu yürütür:

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

Bu sorgu, kesicide kolayca algılanması için etiketlendi . Fikir, veritabanını her gün yalnızca bir kez yeni bir ileti için sorgulamaktır. Diğer durumlarda uygulama önbelleğe alınmış bir sonuç kullanır. (Örnek, yeni bir günün benzetimini yapmak için örnekteki 10 saniyelik gecikmeyi kullanır.)

Kesme noktası durumu

Bu kesme noktası durum bilgisi var: Sorgulanan en son günlük iletinin kimlik ve ileti metninin yanı sıra sorgunun yürütülme zamanını depolar. Bu durumdan dolayı, önbelleğe alma işlemi birden çok bağlam örneği tarafından aynı kesme noktasının kullanılmasını gerektirdiğinden de bir kilitlenmeye ihtiyacımız vardır.

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

Yürütmeden önce

yönteminde Executing (bir veritabanı çağrısı yapmadan önce), kesici etiketli sorguyu algılar ve önbelleğe alınmış bir sonuç olup olmadığını denetler. Böyle bir sonuç bulunursa sorgu gösterilmez ve bunun yerine önbelleğe alınan sonuçlar kullanılır.

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

Kodun önbelleğe alınan verileri içeren bir değişikliği DbDataReader çağırıp InterceptionResult<TResult>.SuppressWithResult geçirdiğine dikkat edin. Bu InterceptionResult daha sonra döndürülür ve sorgu yürütmenin gizlenmesine neden olur. Bunun yerine, değiştirme okuyucusu sorgunun sonuçları olarak EF Core tarafından kullanılır.

Bu kesme noktası, komut metnini de işler. Bu işleme gerekli değildir, ancak günlük iletilerindeki netliği artırır. Sorgu yürütülmeyecek olduğundan komut metninin geçerli SQL olması gerekmez.

Yürütmeden sonra

Önbelleğe alınmış ileti yoksa veya süresi dolduysa yukarıdaki kod sonucu gizlemez. Bu nedenle EF Core sorguyu normal şekilde yürütür. Ardından yürütmeden sonra kesicinin Executed yöntemine döner. Bu noktada, sonuç zaten önbelleğe alınmış bir okuyucu değilse, yeni ileti kimliği ve dizesi gerçek okuyucudan ayıklanır ve bu sorgunun bir sonraki kullanımı için önbelleğe alınır.

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

Tanıtım

Önbelleğe alma kesme noktası örneği, önbelleğe almayı test etmek için günlük iletileri sorgulayan basit bir konsol uygulaması içerir:

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

Bu, aşağıdaki çıkışı döndürür:

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

Günlük çıkışında, uygulamanın zaman aşımı süresi dolana kadar önbelleğe alınmış iletiyi kullanmaya devam ettiği ve bu noktada veritabanının yeni ileti için yeniden sorgulandığına dikkat edin.

SaveChanges kesme noktası

Bahşiş

SaveChanges kesme noktası örneğini GitHub'dan indirebilirsiniz.

SaveChanges ve SaveChangesAsync kesme noktaları arabirim tarafından ISaveChangesInterceptor tanımlanır. Diğer kesme makineleri için olduğu gibi, SaveChangesInterceptor çalışma dışı yöntemlere sahip temel sınıf kolaylık olarak sağlanır.

Bahşiş

Kesiciler güçlü. Ancak çoğu durumda SaveChanges yöntemini geçersiz kılmak veya DbContext'te kullanıma sunulan SaveChanges için .NET olaylarını kullanmak daha kolay olabilir.

Örnek: Denetim için SaveChanges kesme noktası

Yapılan değişikliklerin bağımsız bir denetim kaydı oluşturmak için SaveChanges kesilebilir.

Dekont

Bu, sağlam bir denetim çözümü olarak tasarlanmamıştır. Bunun yerine, kesme özelliğini göstermek için kullanılan basit bir örnektir.

Uygulama bağlamı

Denetim örneği, bloglar ve gönderiler içeren basit bir DbContext kullanır.

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

Her DbContext örneği için kesme noktasının yeni bir örneğinin kayıtlı olduğuna dikkat edin. Bunun nedeni, denetim kesme noktasının geçerli bağlam örneğine bağlı durumu içermesidir.

Denetim bağlamı

Örnek ayrıca denetim veritabanı için kullanılan ikinci bir DbContext ve model içerir.

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

Kesme noktası

Kesme noktası ile denetim için genel fikir şudur:

  • SaveChanges'in başında bir denetim iletisi oluşturulur ve denetim veritabanına yazılır
  • SaveChanges'in devam etmesi için izin verilir
  • SaveChanges başarılı olursa, denetim iletisi başarılı olduğunu gösterecek şekilde güncelleştirilir
  • SaveChanges başarısız olursa, denetim iletisi hatayı gösterecek şekilde güncelleştirilir

İlk aşama, ve ISaveChangesInterceptor.SavingChangesAsyncgeçersiz kılmaları ISaveChangesInterceptor.SavingChanges kullanılarak veritabanına herhangi bir değişiklik gönderilmeden önce işlenir.

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

Hem eşitleme hem de zaman uyumsuz yöntemlerin geçersiz kılınması, denetimin çağrılıp çağrılmadığına SaveChangesSaveChangesAsync bakılmaksızın gerçekleşmesini sağlar. Ayrıca, zaman uyumsuz aşırı yüklemenin denetim veritabanına engelleyici olmayan zaman uyumsuz G/Ç gerçekleştirebileceğine de dikkat edin. Tüm veritabanı G/Ç'sinin zaman uyumsuz olduğundan emin olmak için eşitleme SavingChanges yönteminden oluşturmak isteyebilirsiniz. Daha sonra bu, uygulamanın her zaman öğesini çağırmasını ve hiçbir zaman SaveChangesçağırmasını SaveChangesAsync gerektirir.

Denetim iletisi

Her kesme noktası yöntemi, kesilen olay hakkında bağlamsal bilgiler sağlayan bir eventData parametreye sahiptir. Bu durumda geçerli uygulama DbContext, olay verilerine eklenir ve ardından denetim iletisi oluşturmak için kullanılır.

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

Sonuç, SaveChangesAudit her ekleme, güncelleştirme veya silme işlemi için bir varlık koleksiyonuna EntityAudit sahip bir varlıktır. Kesme noktası daha sonra bu varlıkları denetim veritabanına ekler.

Bahşiş

ToString, her EF Core olay veri sınıfında geçersiz kılınarak olay için eşdeğer günlük iletisi oluşturulur. Örneğin, çağrısı ContextInitializedEventData.ToString "Microsoft.EntityFrameworkCore.Sqlite' sağlayıcısını kullanarak "Entity Framework Core 5.0.0 tarafından başlatılan 'BlogsContext' oluşturur ve seçenekler: Yok".

Başarıyı algılama

Denetim varlığı, SaveChanges başarılı veya başarısız olduğunda yeniden erişilebilmesi için kesme aracında depolanır. Başarı için ISaveChangesInterceptor.SavedChanges veya ISaveChangesInterceptor.SavedChangesAsync çağrılır.

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

Denetim varlığı, veritabanında zaten var olduğundan ve güncelleştirilmesi gerektiğinden denetim bağlamlarına eklenir. Ardından saveChanges'in denetim veritabanına bir güncelleştirme göndermesi için bu özellikleri değiştirilmiş olarak işaretleyen ve EndTimedeğerlerini ayarlarızSucceeded.

Hatayı algılama

Başarısızlık, başarı ile çok aynı şekilde, ancak veya ISaveChangesInterceptor.SaveChangesFailedAsync yönteminde ISaveChangesInterceptor.SaveChangesFailed işlenir. Olay verileri, oluşan özel durumu içerir.

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

Tanıtım

Denetim örneği , blog veritabanında değişiklik yapan ve ardından oluşturulan denetimi gösteren basit bir konsol uygulaması içerir.

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

Sonuç, denetim veritabanının içeriğini gösterir:

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