Engelleyiciler

Entity Framework Core (EF Core) önleyicileri, EF Core işlemlerinin engellenmesini, değiştirilmesini ve/veya bastırılmasını sağlar. Bu, komut yürütme gibi alt düzey veritabanı işlemlerinin yanı sıra SaveChanges çağrıları gibi daha üst düzey işlemleri içerir.

Yakalama araçları, durdurulan işlemi değiştirme veya engelleme izni verdiği için günlüğe kaydetme ve tanılama işlemlerinden farklıdır. Basit günlük kaydı veya Microsoft.Extensions.Logging , günlüğe kaydetme için daha iyi seçeneklerdir.

Bağlam yapılandırıldığında ara kesiciler her DbContext örneğine göre kaydedilir. Aynı bilgileri almak için ancak işlemdeki tüm DbContext örnekleri için bir tanılama dinleyicisi kullanın.

Kullanılabilir önleyiciler

Aşağıdaki tabloda kullanılabilir kesme noktası arabirimleri gösterilmektedir:

Önleyici Engellenen işlemler Singleton
IDbCommandInterceptor Komutları oluşturma
Komutları yürütme
Komut hataları
Komutun DbDataReader'ını yok etme
Hayı
IDbConnectionInterceptor Bağlantıların açılması ve kapatılması
Bağlantı oluşturma
Bağlantı hataları
Hayı
IDbTransactionInterceptor İşlem oluşturma
Mevcut işlemleri kullanma
İşlemleri tamamlama
İşlemleri geri alma
Savepoints oluşturma ve kullanma
İşlem hataları
Hayı
ISaveChangesInterceptor SaveChanges/SavedChanges
SaveChangesFailed
İyimser eşzamanlılık işleme
Hayı
IMaterializationInterceptor Sorgu sonuçlarından varlık örnekleri oluşturma, başlatma ve sonlandırma Evet
IQueryExpressionInterceptor Sorgu derilmeden önce LINQ ifade ağacını değiştirme Evet
IIdentityResolutionInterceptor Varlıkları izleme sırasında kimlik çakışmalarını çözme Evet

Kesme noktası oluşturucuları kaydetme

DbContext örneği yapılandırılırken kesiciler AddInterceptors kullanılarak kaydedilir. Bu genellikle DbContext.OnConfiguring içinde bir geçersiz kılma işlemi olarak yapılır. Örneğin:

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

Alternatif olarak, AddInterceptors bir AddDbContext örneği oluşturulurken veya DbContextOptions parçası olarak DbContext yapıcısına geçmek üzere çağrılabilir.

Tavsiye

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 bilgisine sahip değildir, bu da tek bir kesici örneğinin tüm DbContext örnekleri için kullanılabilmesi anlamına gelir. Örneğin:

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

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

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

Tekli kesişimciler

Bazı kesiciler ISingletonInterceptor'ü uygulayanlardır (yukarıdaki tabloya bakın); bu kesiciler EF Core'un iç hizmet sağlayıcısında singleton hizmetler olarak kaydedilir, yani aynı hizmet sağlayıcısını kullanan tüm DbContext örneklerinde tek bir örnek paylaşılır.

Tekil kesiciler, EF Core'un iç hizmet yapılandırmasının bir parçası haline geldiğinden dolayı, her ayrı kesici örneği yeni bir iç hizmet sağlayıcısının oluşturulmasına yol açar. Her yapılandırıldığında singleton kesici örneğinin yeni bir örneğini (örneğin, içinde DbContext) geçirmek sonunda bir AddDbContext tetikler ve performansı düşürür.

Uyarı

Her zaman tüm DbContext örnekler için aynı tekli kesme noktası örneğini yeniden kullanabilirsiniz. Bağlam her yapılandırıldığında yeni bir örnek oluşturmayın.

Örneğin, her bağlam yapılandırması için yeni bir kesme noktası örneği oluşturulduğundan aşağıdakiler yanlıştır :

// Don't do this! A new instance each time causes a new internal service provider to be built.
services.AddDbContext<CustomerContext>(
    b => b.UseSqlServer(connectionString)
          .AddInterceptors(new MyMaterializationInterceptor()));

Bunun yerine aynı örneği yeniden kullanın:

// Correct: reuse a single interceptor instance
var interceptor = new MyMaterializationInterceptor();
services.AddDbContext<CustomerContext>(
    b => b.UseSqlServer(connectionString)
          .AddInterceptors(interceptor));

Veya statik bir alan kullanın:

public class CustomerContext : DbContext
{
    private static readonly MyMaterializationInterceptor _interceptor = new();

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

Bu kesiciler tekil olduğundan, iş parçacığı açısından güvenli olmalıdırlar. Bunlar genellikle değiştirilebilir durumda olmamalıdır. Her interceptör yöntemine aktarılan olay verilerinde belirtilen geçerli DbContext gibi belirli kapsamdaki hizmetlere erişmeniz gerekiyorsa, Context veya benzer özellikleri kullanın.

Veritabanı önleme

Uyarı

Veritabanı durdurma 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.

Önleyici Veritabanı işlemleri durduruldu
IDbCommandInterceptor Komutları oluşturma
Komutları yürütme
Komut hataları
Komutun DbDataReader'ını yok etme
IDbConnectionInterceptor Bağlantıların açılması ve kapatılması
Bağlantı oluşturma
Bağlantı hataları
IDbTransactionInterceptor İşlem oluşturma
Mevcut işlemleri kullanma
İşlemleri tamamlama
İşlemleri geri alma
Savepoints oluşturma ve kullanma
İşlem hataları

DbCommandInterceptor, DbConnectionInterceptor ve DbTransactionInterceptor temel sınıfları, ilgili arabirimdeki her yöntem için no-op uygulamaları içerir. Temel sınıfları kullanarak, kullanılmayan kesme yöntemlerini uygulama gereksinimini önleyin.

Her interceptör türündeki yöntemler, veritabanı işlemi başlatılmadan önce ilki ve işlem tamamlandıktan sonra ikincisi çağrılmak üzere çiftler halinde 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 bir yöntem çiftinin hem senkron hem de asenkron varyasyonları vardır. Bu, erişim belirteci isteme gibi asenkron G/Ç'nin, asenkron veritabanı işleminin engellenmesinin bir parçası olarak gerçekleşmesini sağlar.

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

, 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, önleme işleminin en karmaşık kısmı, komutun değiştirilmesi gereken sorguyla 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. Örneğin:

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

Daha sonra bu etiket, komut metninin ilk satırına her zaman açıklama olarak ekleneceğinden, engelleyicide 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)";
        }
    }
}

Dikkat

  • Yakalıyıcı, yakalıyıcı arabirimindeki her yöntemi uygulamak zorunda kalmamak için DbCommandInterceptor'dan devralır.
  • Yakayıcı hem senkron hem de asenkron yöntemleri uygular. Bu, senkron ve asenkron sorgulara aynı sorgu ipucunun uygulanmasını sağlar.
  • Yakalama sistemi, oluşturulan SQL Executing ile EF Core tarafından çağrılan yöntemleri, veritabanına gönderilmeden önce uygular. Bunu, veritabanı çağrısı döndükten sonra çağrılan Executed yöntemlerle karşılaştırın.

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ğlantı kesme

Tavsiye

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

Veritabanına bağlanmak için kullanılmadan önce bir IDbConnectionInterceptor, DbConnection üzerinde işlem yapmak için kullanılabilir. Bu, Azure Active Directory (AAD) erişim belirtecini almak için kullanılabilir. Örneğin:

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

Tavsiye

Microsoft.Data.SqlClient artık bağlantı dizesi aracılığıyla AAD kimlik doğrulamayı destekliyor. Daha fazla bilgi 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ı

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

Örnek: Bağlantı dizesinin yavaş başlatılması

Bağlantı dizeleri genellikle yapılandırma dosyasından okunan statik varlıklardır. Bunlar, bir UseSqlServer yapılandırırken DbContext veya benzeri bir şekilde kolayca geçirilebilir. Ancak, bazen bağlantı dizesi her bağlam örneği için değişebilir. Örneğin, çok kiracılı bir sistemdeki her kiracının farklı bir bağlantı dizesi olabilir.

, IDbConnectionInterceptor dinamik bağlantıları ve bağlantı dizelerini işlemek için kullanılabilir. Bu, herhangi bir bağlantı dizesi olmadan DbContext öğesini yapılandırma yeteneğiyle başlar. Örneğin:

services.AddDbContext<CustomerContext>(
    b => b.UseSqlServer());

Bu yöntemlerden IDbConnectionInterceptor biri, kullanılmadan önce bağlantıyı yapılandırmak için uygulanabilir. Zamansız bir işlem gerçekleştirebildiğinden, ConnectionOpeningAsync, bağlantı dizesini almak, erişim jetonunu bulmak vb. için iyi bir seçimdir. Örneğin, geçerli istek kapsamında olan ve geçerli kiracıyı anlayan bir hizmet düşünün.

services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();

Uyarı

Bir bağlantı dizesi, erişim belirteci veya benzer bir zaman uyumsuz aramanın her gerektiğinde gerçekleştirilmesi çok yavaş olabilir. Bu öğeleri önbelleğe almayı ve yalnızca önbelleğe alınan dizeyi veya belirteci düzenli aralıklarla yenilemeyi göz önünde bulundurun. Örneğin, erişim belirteçleri genellikle yenilenmesi gerekmeden önce önemli bir süre kullanılabilir.

Bu, oluşturucu eklemesi kullanılarak her DbContext örneğe eklenebilir:

public class CustomerContext : DbContext
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

    public CustomerContext(
        DbContextOptions<CustomerContext> options,
        ITenantConnectionStringFactory connectionStringFactory)
        : base(options)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    // ...
}

Bu hizmet daha sonra bağlam için önleyici uygulaması oluşturulurken kullanılır.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(
        new ConnectionStringInitializationInterceptor(_connectionStringFactory));

Son olarak, engelleyici, bağlantı dizesini zaman uyumsuz olarak almak ve bağlantının ilk kez kullanımında ayarlamak için bu servisi kullanır.

public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

    public ConnectionStringInitializationInterceptor(ITenantConnectionStringFactory connectionStringFactory)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
        => throw new NotSupportedException("Synchronous connections not supported.");

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
        CancellationToken cancellationToken = new())
    {
        if (string.IsNullOrEmpty(connection.ConnectionString))
        {
            connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
        }

        return result;
    }
}

Uyarı

bağlantı dizesi yalnızca ilk kez bağlantı kullanıldığında elde edilir. Bundan sonra, üzerinde DbConnection depolanan bağlantı dizesi yeni bir bağlantı dizesi aramadan kullanılır.

Tavsiye

Bu interceptor, bağlantı dizesini almak için hizmetin asenkron bir kod yolundan çağrılması gerektiğinden, ConnectionOpening yöntemini geçersiz kılar ve bir istisna fırlatır.

Örnek: Önbelleğe alma için gelişmiş komut yakalama

Tavsiye

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

EF Core müdahalecileri şunları yapabilir:

  • EF Core'a önleme uygulanan işlemin yürütülmesini engellemesini söyleyin
  • İşlemin sonucunu değiştirip EF Core'a geri bildir

Bu örnek, bu özellikleri kullanarak ilkel ikinci düzey önbellek gibi davranan bir önleyici göstermektedir. Ö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 yakalayıcı durum tutan bir yapıya sahiptir: Sorgulanan en son günlük iletinin kimliği ve ileti metninin yanı sıra, bu sorgunun gerçekleştirildiği zamanı da 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 InterceptionResult<TResult>.SuppressWithResult çağırıp DbDataReader geçirdiğine dikkat edin. Bu InterceptionResult döndürülür ve sorgu yürütmenin engellenmesine neden olur. Yedek okuyucu, sorgu sonuçları olarak EF Core tarafından kullanılır.

Bu önleyici, komut metnini de değiştirir. Bu işlem zorunlu değildir, ancak günlük iletilerinde 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. Yürütüldükten sonra kesicinin Executed yöntemine geri 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 çıktıyı üretir.

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

Uygulamanın, zaman aşımı süresi dolana kadar önbelleğe alınmış mesajı kullanmaya devam ettiğini ve bu sürenin sonunda yeni bir mesaj olup olmadığını kontrol etmek için veritabanının tekrar sorgulandığını günlük çıktısından fark edin.

Örnek: SQL Server sorgu istatistiklerini kaydetme

Bu örnekte, SQL Server sorgu istatistiklerini uygulama günlüğüne göndermek için birlikte çalışan iki önleyici gösterilmektedir. İstatistikleri oluşturmak için iki şey yapmamız gerekir IDbCommandInterceptor .

İlk olarak, önleyici komutları SET STATISTICS IO ON'yi ön ekleyecek ve SQL Server'a, bir sonuç kümesi tüketildikten sonra, istemciye istatistik göndermesini söyler.

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)
{
    command.CommandText = "SET STATISTICS IO ON;" + Environment.NewLine + command.CommandText;

    return new(result);
}

İkinci olarak, tutumlayıcı, DataReaderClosingAsync yöntemini sonuçların tüketilmesi tamamlandıktan sonra, ancak DbDataReader kapatılmadan önce çağıracaktır. SQL Server istatistik gönderdiğinde, bunları veri okuyucusunda ikinci bir sonuca yerleştirir. Bu yüzden bu noktada ara kesici, bağlantıya istatistikleri aktaran NextResultAsync çağrısını yaparak sonuçları okur.

public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
    DbCommand command,
    DataReaderClosingEventData eventData,
    InterceptionResult result)
{
    await eventData.DataReader.NextResultAsync();

    return result;
}

İkinci kesme noktası, bağlantıdan istatistikleri almak ve bunları uygulamanın günlükçüsine yazmak için gereklidir. Bunun için bir IDbConnectionInterceptor kullanacağız ve ConnectionCreated yöntemini uygulayacağız. ConnectionCreated , EF Core bir bağlantı oluşturduktan hemen sonra çağrılır ve bu nedenle bu bağlantının ek yapılandırmasını gerçekleştirmek için kullanılabilir. Bu durumda, yakalayıcı bir ILogger alır ve ardından SqlConnection.InfoMessage olayına takılarak iletileri günlüğe kaydeder.

public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection result)
{
    var logger = eventData.Context!.GetService<ILoggerFactory>().CreateLogger("InfoMessageLogger");
    ((SqlConnection)eventData.Connection).InfoMessage += (_, args) =>
    {
        logger.LogInformation(1, args.Message);
    };
    return result;
}

Önemli

ConnectionCreating ve ConnectionCreated yöntemleri yalnızca EF Core bir DbConnectionoluşturduğunda çağrılır. Uygulama oluşturur DbConnection ve EF Core'a geçirirse çağrılmaz.

Komut kaynağına göre filtreleme

CommandEventData Tanılama kaynaklarına ve kesicilere sağlanan, EF'in hangi bölümünün komutu oluşturmakla sorumlu olduğunu belirten bir CommandSource özelliği içerir. Bu, kesici işlevinde filtre olarak kullanılabilir. Örneğin, yalnızca SaveChanges'dan gelen komutlar için geçerli olacak bir önleyici isteyebiliriz.

public class CommandSourceInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (eventData.CommandSource == CommandSource.SaveChanges)
        {
            Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
            Console.WriteLine();
            Console.WriteLine(command.CommandText);
        }

        return result;
    }
}

SaveChanges müdahale

Tavsiye

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

SaveChanges ve SaveChangesAsync kesme noktalarını ISaveChangesInterceptor arabirimi tanımlar. Diğer kesiştiriciler için, SaveChangesInterceptor sınıfı no-op yöntemleriyle birlikte kullanım kolaylığı sağlamak amacıyla sunulmaktadır.

Tavsiye

Önleyiciler 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 müdahalesi

SaveChanges işlevi, yapılan değişikliklerin bağımsız bir denetim kaydı oluşturulması amacıyla durdurulabilir veya müdahale edilebilir.

Uyarı

Bu, sağlam bir denetim çözümü olarak tasarlanmamıştır. Aslında bu, durdurma ö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 kesicinin geçerli bağlam örneğine bağlı bir 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; }
}

Önleyici

Önleyici ile denetim için genel fikir şöyledir:

  • 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, ISaveChangesInterceptor.SavingChanges ve ISaveChangesInterceptor.SavingChangesAsync geçersiz kılınmaları 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 senkronize hem de zaman uyumsuz yöntemlerin geçersiz kılınması, SaveChanges veya SaveChangesAsync çağrılsın veya çağrılmasın denetimin gerçekleşmesini sağlar. Ayrıca, zaman uyumsuz (asenkron) aşırı yüklemenin, denetim veritabanına engellemesiz zaman uyumsuz G/Ç gerçekleştirebileceğine dikkat edin. Tüm veritabanı G/Ç işlemlerinin zaman uyumsuz olduğundan emin olmak için senkron SavingChanges metodundan fırlatmak isteyebilirsiniz. Daha sonra bu, uygulamanın her zaman SaveChangesAsync öğesini çağırmasını ve hiçbir zaman SaveChanges çağırmamasını 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.

Tavsiye

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, ContextInitializedEventData.ToString çağrıldığında "Entity Framework Core 5.0.0 'BlogsContext'i 'Microsoft.EntityFrameworkCore.Sqlite' sağlayıcısını kullanarak ve seçenekler: Yok ile başlatıldı." ifadesini oluşturur.

Başarıyı algılama

Denetim öğesi, SaveChanges işlemi başarılı veya başarısız olduğunda yeniden erişilebilmesi için kesiciye kaydedilir. 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, bu özellikleri değiştirilmiş olarak işaretleyerek SaveChanges'in denetim veritabanına bir güncelleştirme göndermesini sağlamak için Succeeded ve EndTime değerlerini ayarlarız.

Hatayı algılama

Başarısızlık, başarı gibi işlenir, ancak bu ISaveChangesInterceptor.SaveChangesFailed veya ISaveChangesInterceptor.SaveChangesFailedAsync yöntemlerinde yapılır. Olay verileri, fırlatılan istisnayı 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 = 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}");
        }
    }
}

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

Örnek: İyimser eşzamanlılık yakalama

EF Core, bir güncelleştirmeden veya silmeden gerçekten etkilenen satır sayısının, etkilenmesi beklenen satır sayısıyla aynı olup olmadığını denetleyerek iyimser eşzamanlılık düzenini destekler. Bu genellikle eşzamanlılık anahtarı ile birlikte kullanılır; yani yalnızca beklenen değer okunduktan sonra satır güncelleştirilmediyse beklenen değeriyle eşleşecek bir sütun değeridir.

EF, bir DbUpdateConcurrencyException oluşturarak iyimser eşzamanlılık ihlaline işaret eder. ISaveChangesInterceptor yöntemleri ThrowingConcurrencyException ve ThrowingConcurrencyExceptionAsync, DbUpdateConcurrencyException fırlatılmadan önce çağrılır. Bu kesme noktaları, ihlali çözmek için büyük olasılıkla zaman uyumsuz veritabanı değişiklikleriyle birlikte özel durumun gizlenmesine olanak sağlar.

Örneğin, iki istek aynı varlığı hemen hemen aynı anda silmeyi denerse, veritabanındaki satır artık mevcut olmadığından ikinci silme işlemi başarısız olabilir. İyi olabilir; nihai sonuçta varlık yine de silinmiş durumda. Aşağıdaki önleyici, bunun nasıl yapılacağını gösterir:

public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
    public InterceptionResult ThrowingConcurrencyException(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result)
    {
        if (eventData.Entries.All(e => e.State == EntityState.Deleted))
        {
            Console.WriteLine("Suppressing Concurrency violation for command:");
            Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);

            return InterceptionResult.Suppress();
        }

        return result;
    }

    public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
        => new(ThrowingConcurrencyException(eventData, result));
}

Bu kestirici hakkında dikkate değer birkaç nokta vardır.

  • Hem eşzamanlı hem de eşzamansız kesişim yöntemleri uygulanır. Uygulama SaveChanges veya SaveChangesAsync çağırabilirse bu önemlidir. Ancak, tüm uygulama kodu zaman uyumsuzsa, yalnızca ThrowingConcurrencyExceptionAsync uygulanmalıdır. Benzer şekilde, uygulama hiçbir zaman zaman uyumsuz veritabanı yöntemlerini kullanmıyorsa, yalnızca ThrowingConcurrencyException uygulanması gerekir. Bu, genellikle senkron ve asenkron yöntemlere sahip tüm önleyiciler için geçerlidir.
  • Önleyici, kaydedilen varlıklar için EntityEntry nesnelerine erişebilir. Bu durumda, silme işlemi için eşzamanlılık ihlali olup olmadığını denetlemek için kullanılır.
  • Uygulama ilişkisel veritabanı sağlayıcısı kullanıyorsa, ConcurrencyExceptionEventData nesne bir RelationalConcurrencyExceptionEventData nesneye yayınlanabilir. Bu, gerçekleştirilen veritabanı işlemi hakkında ilişkisel olarak ek bilgiler sağlar. Bu durumda, ilişkisel komut metni konsola yazdırılır.
  • Döndürülen InterceptionResult.Suppress() EF Core'a gerçekleştirmek üzere olduğu eylemi engellemesini bildirir—bu durumda DbUpdateConcurrencyException fırlatma işlemi engellenir. EF Core'un davranışını değiştirmek, sadece EF Core'un ne yaptığını izlemekten ziyade, önleyicilerin en güçlü özelliklerinden biridir.

Malzemeleştirme Müdahalesi

IMaterializationInterceptor , bir varlık örneği oluşturulmadan önce ve sonra ve bu örneğin özellikleri başlatılmadan önce ve sonra kesmeyi destekler. Yakalıyıcı, varlık örneğini değiştirebilir veya yerine yenisini koyabilir. Bu, aşağıdakileri sağlar:

  • Doğrulama, hesaplanan değerler veya bayraklar için gereken eşlenmemiş özellikleri veya çağırma yöntemlerini ayarlama.
  • Örnek oluşturmak için fabrika kullanma.
  • EF'in normalde oluşturacağından farklı bir varlık örneği oluşturmak, örneğin bir önbellekten alınan veya bir proxy türünde olan bir örnek gibi.
  • Bir varlık örneğine hizmet ekleme.

Uyarı

IMaterializationInterceptor bir singleton kesici/tekil müdahalecidir, yani tek bir örnek tüm DbContext örnekleri arasında paylaşılır.

Örnek: Varlık oluşturmayla ilgili basit eylemler

Bir varlığın veritabanından alındığı zamanı takip etmek istediğimizi ve bu sayede verileri düzenleyen bir kullanıcıya görüntülenebileceğini düşünün. Bunu başarmak için önce bir arabirim tanımlayacağız:

public interface IHasRetrieved
{
    DateTime Retrieved { get; set; }
}

Bir arabirim kullanmak, aynı yakalayıcının birçok farklı varlık türüyle çalışmasına izin verdiğinden, yakalayıcılar ile sıkça kullanılır. Örneğin:

public class Customer : IHasRetrieved
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? PhoneNumber { get; set; }

    [NotMapped]
    public DateTime Retrieved { get; set; }
}

özniteliğinin [NotMapped] bu özelliğin yalnızca varlıkla çalışırken kullanıldığını ve veritabanında kalıcı olmaması gerektiğini belirtmek için kullanıldığına dikkat edin.

Interceptor daha sonra uygun yöntemi IMaterializationInterceptor gerçekleştirmeli ve alınan zamanı ayarlamalıdır.

public class SetRetrievedInterceptor : IMaterializationInterceptor
{
    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasRetrieved hasRetrieved)
        {
            hasRetrieved.Retrieved = DateTime.UtcNow;
        }
        
        return instance;
    }
}

Bu durdurucunun bir örneği, DbContext yapılandırılırken kaydedilir.

public class CustomerContext : DbContext
{
    private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();
    
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 
        => optionsBuilder
            .AddInterceptors(_setRetrievedInterceptor)
            .UseSqlite("Data Source = customers.db");
}

Tavsiye

Bu kesici durumsuz olduğundan yaygındır, bu nedenle tek bir örnek oluşturulur ve tüm DbContext örnekler arasında paylaşılır.

Artık veritabanından bir Customer sorgulandığında özelliği Retrieved otomatik olarak ayarlanır. Örneğin:

await using (var context = new CustomerContext())
{
    var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
    Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}

Çıktı oluşturur:

Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'

Örnek: Varlıklara hizmet ekleme

EF Core, bağlam örneklerine bazı özel hizmetler eklemek için zaten yerleşik desteğe sahiptir; örneğin, hizmetini ekleyerek çalışan ILazyLoader.

IMaterializationInterceptor bunu herhangi bir hizmette genelleştirmek için kullanılabilir. Aşağıdaki örnek, varlıkların kendi günlüklerini yapabilmeleri için ILogger'nin nasıl enjekte edileceğini gösterir.

Uyarı

Hizmetlerin varlıklara enjekte edilmesi, bu varlık türlerini eklenen hizmetlere bağımlı hale getirir ki bu da bazı kişilerin bir karşı örüntü olarak kabul ettiği bir durumdur.

Daha önce olduğu gibi, neler yapılabileceğini tanımlamak için bir arabirim kullanılır.

public interface IHasLogger
{
    ILogger? Logger { get; set; }
}

Günlüğe kaydedilecek varlık türlerinin bu arabirimi uygulaması gerekir. Örneğin:

public class Customer : IHasLogger
{
    private string? _phoneNumber;

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public string? PhoneNumber
    {
        get => _phoneNumber;
        set
        {
            Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");

            _phoneNumber = value;
        }
    }

    [NotMapped]
    public ILogger? Logger { get; set; }
}

Bu kez, her varlık örneği oluşturulduktan ve özellik değerleri başlatıldıktan sonra çağrılan IMaterializationInterceptor.InitializedInstance'i uygulaması gereken önleyici olmalıdır. Kesici, bağlamdan bir ILogger alır ve onunla IHasLogger.Logger'i başlatır.

public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
    private ILogger? _logger;

    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasLogger hasLogger)
        {
            _logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
            hasLogger.Logger = _logger;
        }

        return instance;
    }
}

Bu kez, her bir DbContext örneği için yeni bir önleyici örneği kullanılır, çünkü her ILogger örneğinden elde edilen DbContext değişebilir ve ILogger, bu önleyici üzerinde önbellekte saklanır.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());

Şimdi, Customer.PhoneNumber her değiştirildiğinde, bu değişiklik uygulamanın günlüğüne kaydedilir. Örneğin:

info: CustomersLogger[1]
      Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.

Sorgu ifadesi yakalayıcı

IQueryExpressionInterceptor derlenmeden önce sorgu için LINQ ifade ağacının yakalanmasına izin verir. Bu, sorguları uygulama genelinde geçerli olacak şekilde dinamik olarak değiştirmek için kullanılabilir.

Uyarı

IQueryExpressionInterceptor bir singleton kesicidir, yani tek bir örnek genellikle tüm DbContext örnekleri arasında paylaşılır.

Uyarı

Kesiciler güçlü araçlardır, ancak ifade ağaçlarıyla çalışırken yanlışlık yapma olasılığı yüksektir. Sorguyu doğrudan değiştirme gibi istediğiniz işlemi gerçekleştirmenin daha kolay bir yolu olup olmadığını her zaman göz önünde bulundurun.

Örnek: Kararlı sıralama için sorgulara sıralama ekleme

Bir müşteri sayfası döndüren bir yöntem düşünün:

Task<List<Customer>> GetPageOfCustomers(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .Skip(page * 20).Take(20).ToListAsync();
}

Tavsiye

Bu sorgu, EF.Property yöntemini kullanarak sıralama yapılacak özelliği belirtir. Bu, uygulamanın özellik adını dinamik olarak geçirmesini sağlar ve varlık türünün herhangi bir özelliğine göre sıralamaya izin verir. Dizine alınamayan sütunlara göre sıralamanın yavaş olabileceğini unutmayın.

Sıralama için kullanılan özellik her zaman kararlı bir sıralama döndürdüğü sürece bu işlem düzgün çalışır. Ama her zaman böyle olmayabilir. Örneğin, yukarıdaki LINQ sorgusu Customer.City ile sıralandığında, SQLite'te aşağıdakileri oluşturur:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0

aynı Cityolan birden çok müşteri varsa, bu sorgunun sıralaması kararlı değildir. Bu, kullanıcı veriler arasında gezinirken eksik veya yinelenen sonuçlara yol açabilir.

Bu sorunu düzeltmenin yaygın bir yolu, birincil anahtara göre ikincil bir sıralama gerçekleştirmektir. Ancak, bir önleyici bunu her sorguya el ile eklemek yerine ikincil sıralamayı dinamik olarak ekleyebilir. Bunu kolaylaştırmak için, tamsayı birincil anahtarı olan herhangi bir varlık için bir arabirim tanımlarız:

public interface IHasIntKey
{
    int Id { get; }
}

Bu arabirim, ilgilendiğim varlık türleri tarafından uygulanır:

public class Customer : IHasIntKey
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? City { get; set; }
    public string? PhoneNumber { get; set; }
}

Ardından IQueryExpressionInterceptor uygulayan bir önleyiciye ihtiyacımız var.

public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
    public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
        => new KeyOrderingExpressionVisitor().Visit(queryExpression);

    private class KeyOrderingExpressionVisitor : ExpressionVisitor
    {
        private static readonly MethodInfo ThenByMethod
            = typeof(Queryable).GetMethods()
                .Single(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);

        protected override Expression VisitMethodCall(MethodCallExpression? methodCallExpression)
        {
            var methodInfo = methodCallExpression!.Method;
            if (methodInfo.DeclaringType == typeof(Queryable)
                && methodInfo.Name == nameof(Queryable.OrderBy)
                && methodInfo.GetParameters().Length == 2)
            {
                var sourceType = methodCallExpression.Type.GetGenericArguments()[0];
                if (typeof(IHasIntKey).IsAssignableFrom(sourceType))
                {
                    var lambdaExpression = (LambdaExpression)((UnaryExpression)methodCallExpression.Arguments[1]).Operand;
                    var entityParameterExpression = lambdaExpression.Parameters[0];

                    return Expression.Call(
                        ThenByMethod.MakeGenericMethod(
                            sourceType,
                            typeof(int)),
                        methodCallExpression,
                        Expression.Lambda(
                            typeof(Func<,>).MakeGenericType(entityParameterExpression.Type, typeof(int)),
                            Expression.Property(entityParameterExpression, nameof(IHasIntKey.Id)),
                            entityParameterExpression));
                }
            }

            return base.VisitMethodCall(methodCallExpression);
        }
    }
}

Bu muhtemelen oldukça karmaşık görünüyor ve öyle! İfade ağaçlarıyla çalışmak genellikle kolay değildir. Şimdi neler olduğuna bakalım:

  • Temel olarak, yakalayıcı bir ExpressionVisitor kapsüller. Ziyaretçi, sorgu ifade ağacında bir yönteme çağrı olduğunda çağrılan öğesini geçersiz kılar VisitMethodCall.

  • Ziyaretçi, bunun ilgilendiğimiz OrderBy yöntemine yapılan bir çağrı olup olmadığını kontrol eder.

  • Bu durumda, ziyaretçi, yöntem genel çağrısının arabirimimizi IHasIntKey uygulayan bir tür için olup olmadığını daha fazla kontrol eder.

  • Bu noktada yöntem çağrısının biçiminde OrderBy(e => ...)olduğunu biliyoruz. Bu çağrıdan lambda ifadesini ayıklar ve bu ifadede kullanılan parametresini (yani , e) alırız.

  • Şimdi MethodCallExpression'yi Expression.Call oluşturucu yöntemi kullanarak oluşturuyoruz. Bu durumda, çağrılan yöntem şeklindedir ThenBy(e => e.Id). Bunu, yukarıda ayıklanan parametreyi ve Id arabirimindeki IHasIntKey özelliğine erişimi kullanarak oluştururuz.

  • Bu çağrının girişi özgün OrderBy(e => ...) öğesidir ve bu nedenle sonuç, OrderBy(e => ...).ThenBy(e => e.Id) için bir ifadeyle sonuçlanır.

  • Bu değiştirilen ifade ziyaretçiden döndürülür; başka bir deyişle LINQ sorgusu artık bir ThenBy çağrı içerecek şekilde uygun şekilde değiştirilmiştir.

  • EF Core, bu sorgu ifadesini kullanılan veritabanına uygun SQL'e derleyerek sürdürür.

Bu kesici kaydedildiğinde ve GetPageOfCustomers çalıştırıldığında, artık aşağıdaki SQL oluşur:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City", "c"."Id"
LIMIT @__p_1 OFFSET @__p_0

Bu, aynı City olan birden çok müşteri olsa bile artık her zaman kararlı bir sıralama sağlar.

Çoğu durumda, sorgu doğrudan değiştirilerek aynı şey daha basit bir şekilde elde edilebilir. Örneğin:

Task<List<Customer>> GetPageOfCustomers2(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .ThenBy(e => e.Id)
        .Skip(page * 20).Take(20).ToListAsync();
}

Bu durumda, ThenBy basitçe sorguya eklenir. Evet, her sorgu için ayrı ayrı yapılması gerekebilir, ancak basit, anlaşılması kolaydır ve her zaman çalışır.

Kimlik çözümlemesi yakalama

IIdentityResolutionInterceptor, DbContext yeni varlık örneklerini izlemeye başladığında kimlik çözümleme çakışmalarının yakalanmasına izin verir.

Uyarı

Bu kesici, şu anda yalnızca DbContext.Update ve DbContext.Attach gibi yöntemler, aynı anahtarla zaten izlenmekte olan varlıkları takip etmek için kullanıldığında çağrılır. Sorgulardan döndürülen varlıklar için çağrılmaz. Bu, gelecekteki bir sürümde değişebilir; bu soruna bakın.

, DbContext herhangi bir birincil anahtar değerine sahip yalnızca bir varlık örneğini izleyebilir. Bu, aynı anahtar değerine sahip bir varlığın birden çok örneğinin tek bir örneğe çözümlenmesi gerektiği anlamına gelir. Bu tür bir kesme noktası, mevcut izlenen örnek ve yeni örnekle birlikte çağrılır ve yeni örnekteki tüm özellik değerlerini ve ilişki değişikliklerini mevcut örneğe uygulamalıdır. Ardından yeni örnek silinir.

EF Core, UpdatingIdentityResolutionInterceptormevcut izlenen varlığı yeni örnekteki değerlerle güncelleştiren yerleşik bir uygulama sağlar. Bağlam yapılandırıldığında bu kaydedilebilir:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .AddInterceptors(new UpdatingIdentityResolutionInterceptor());

Özel kimlik çözümleme mantığını uygulamak için, IIdentityResolutionInterceptor arayüzünü uygulayan ve UpdateTrackedInstance yöntemini geçersiz kılan bir sınıf oluşturun.

public class CustomIdentityResolutionInterceptor : IIdentityResolutionInterceptor
{
    public void UpdateTrackedInstance(
        IdentityResolutionInterceptionData interceptionData,
        EntityEntry existingEntry,
        object newEntity)
    {
        // Custom logic to merge property values from newEntity into the existing tracked entity
        existingEntry.CurrentValues.SetValues(newEntity);
    }
}