Verwenden von Diagnoselistenern in EF Core

Tipp

Sie können das Beispiel in diesem Artikel von GitHub herunterladen.

Diagnoselistener ermöglichen das Lauschen auf EF Core-Ereignisse, die im aktuellen .NET-Prozess auftreten. Die DiagnosticListener-Klasse ist Teil eines gemeinsamen Mechanismus in .NET zum Abrufen von Diagnoseinformationen aus laufenden Anwendungen.

Diagnoselistener sind nicht geeignet, um Ereignisse aus einer einzelnen DbContext-Instanz abzurufen. EF Core-Abfangfunktionen ermöglichen Zugriff auf dieselben Ereignisse mit Pro-Kontext-Registrierung.

Diagnoselistener sind nicht für Protokollierung konzipiert. Erwägen Sie die Verwendung einfacher Protokollierung oder Microsoft.Extensions.Logging für die Protokollierung.

Beispiel: Beobachten von Diagnoseereignissen

Das Auflösen von EF Core-Ereignissen ist ein zweistufiger Prozess. Zuerst muss ein Beobachter für DiagnosticListener selber geschaffen werden:

public class DiagnosticObserver : IObserver<DiagnosticListener>
{
    public void OnCompleted()
        => throw new NotImplementedException();

    public void OnError(Exception error)
        => throw new NotImplementedException();

    public void OnNext(DiagnosticListener value)
    {
        if (value.Name == DbLoggerCategory.Name) // "Microsoft.EntityFrameworkCore"
        {
            value.Subscribe(new KeyValueObserver());
        }
    }
}

Die OnNext-Methode sucht nach dem DiagnosticListener, der aus EF Core stammt. Dieser Listener hat den Namen „Microsoft.EntityFrameworkCore“, der wie gezeigt aus der DbLoggerCategory-Klasse abgerufen werden kann.

Dieser Beobachter muss dann global registriert werden, z. B. in der Main-Methode der Anwendung:

DiagnosticListener.AllListeners.Subscribe(new DiagnosticObserver());

Zweitens wird nach dem Auffinden des EF Core-DiagnosticListener ein neuer Schlüsselwertbeobachter erstellt, um die tatsächlichen EF Core-Ereignisse zu abonnieren. Beispiel:

public class KeyValueObserver : IObserver<KeyValuePair<string, object>>
{
    public void OnCompleted()
        => throw new NotImplementedException();

    public void OnError(Exception error)
        => throw new NotImplementedException();

    public void OnNext(KeyValuePair<string, object> value)
    {
        if (value.Key == CoreEventId.ContextInitialized.Name)
        {
            var payload = (ContextInitializedEventData)value.Value;
            Console.WriteLine($"EF is initializing {payload.Context.GetType().Name} ");
        }

        if (value.Key == RelationalEventId.ConnectionOpening.Name)
        {
            var payload = (ConnectionEventData)value.Value;
            Console.WriteLine($"EF is opening a connection to {payload.Connection.ConnectionString} ");
        }
    }
}

Die OnNext-Methode wird dieses Mal mit einem Schlüssel/Wert-Paar für jedes EF Core-Ereignis aufgerufen. Der Schlüssel ist der Name des Ereignisses, das aus einer der folgenden Aktionen abgerufen werden kann:

  • CoreEventId für Ereignisse, die allen EF Core-Datenbankanbietern gemeinsam sind
  • RelationalEventId für Ereignisse, die allen relationalen Datenbankanbietern gemeinsam sind
  • Eine ähnliche Klasse für Ereignisse, die für den aktuellen Datenbankanbieter spezifisch sind. Beispiel: SqlServerEventId für den SQL Server-Anbieter.

Der Wert des Schlüssel/Wert-Paares ist ein Nutzdatentyp, der für das Ereignis spezifisch ist. Der zu erwartende Nutzdatentyp wird für jedes in diesen Ereignisklassen definierte Ereignis dokumentiert.

Der obige Code behandelt beispielsweise die ContextInitialized- und die ConnectionOpening-Ereignisse. Für das erste dieser Ereignisse sind die Nutzdaten ContextInitializedEventData. Für das zweite sind sie ConnectionEventData.

Tipp

ToString wird in jeder EF Core-Ereignisdatenklasse überschrieben, um die entsprechende Protokollmeldung für das Ereignis zu generieren. Beispielsweise generiert der Aufruf von ContextInitializedEventData.ToString den Eintrag „Entity Framework Core 5.0.0 initialisierte „BlogsContext“ mithilfe des Anbieters „Microsoft.EntityFrameworkCore.Sqlite“ mit den Optionen: Keine“.

Das Beispiel enthält eine einfache Konsolenanwendung, die Änderungen an der Blogdatenbank vorgibt und die aufgetretenen Diagnoseereignisse ausgibt.

public static void Main()
{
    DiagnosticListener.AllListeners.Subscribe(new DiagnosticObserver());

    using (var context = new BlogsContext())
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        context.Add(
            new Blog { Name = "EF Blog", Posts = { new Post { Title = "EF Core 3.1!" }, new Post { Title = "EF Core 5.0!" } } });

        context.SaveChanges();
    }

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

Die Ausgabe dieses Codes zeigt die erkannten Ereignisse:

EF is initializing BlogsContext
EF is opening a connection to Data Source=blogs.db;Mode=ReadOnly
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to Data Source=blogs.db;Mode=ReadOnly
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to DataSource=blogs.db
EF is initializing BlogsContext
EF is opening a connection to DataSource=blogs.db
EF is opening a connection to DataSource=blogs.db