Laden...

Memory Leak beim Indexieren von Dateien - Warum bleibt der DBContext trotz using erhalten?

Erstellt von sandreas vor einem Jahr Letzter Beitrag vor einem Jahr 1.354 Views
S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr
Memory Leak beim Indexieren von Dateien - Warum bleibt der DBContext trotz using erhalten?

Verwendetes Datenbanksystem: Postgres über Docker-Container

Hallo zusammen,

ich entwickle grade an einem kleinen Spaßprojekt von mir (tonehub). Dort sollen die Metadaten meiner Audio-Dateien über einen File-Indexer ausgelesen und in einer Datenbank abgelegt und danach über eine API bereit gestellt werden. Das ganze mache ich mit Entity Framework 6 über einen BackgroundService (IHostedService). Unten verlinke ich noch mal eine Liste mit den meiner Ansicht nach relevanten Code-Stellen...

Jetzt habe ich das Problem, dass mit fortschreitendem Dateidurchlauf das Indexieren immer langsamer wird, also habe ich ein Memory Leak vermutet.

Mir ist bekannt, dass man den DBContext immer nur mit using im Bereich des Unit-Of-Work verwendet und ihn danach disposed, also habe ich folgendes Stück Code geschrieben (gekürzt, Original ist hier auf github):


    private bool UpdateFileTags(IEnumerable<IFileInfo> files, FileSource source, CancellationToken cancellationToken)
    {
        foreach (var file in files)
        {
            // ...

            i++;


            // !!!!!!
            using var db = _dbFactory.CreateDbContext();
            try
            {
                _tagLoader.Initialize(file);
                using var operationHandle = _logger.BeginOperation("- handle file");
                // _debugList.Add($"==> _tagLoader.Initialize: {_stopWatch.Elapsed.TotalMilliseconds}");
                db.FileSources.Attach(source);
                var fileRecord =
                    db.Files.FirstOrDefault(f => f.Location == normalizedLocation && f.Source.Id == source.Id);
                if (fileRecord == null)
                {
                    fileRecord = HandleMissingFile(db, source, file, normalizedLocation);
                }
                else
                {
                    HandleExistingFile(file, normalizedLocation, fileRecord);
                }

                operationHandle.Complete();

                if (fileRecord.IsNew)
                {
                    db.Files.Add(fileRecord);
                }
                else
                {
                    db.Files.Update(fileRecord);
                }

                if (fileRecord.HasChanged)
                {
                    UpdateFileRecordTagsAndJsonValues(db, fileRecord);
                }
                
                db.SaveChanges(); // here is the problem (allocation)

                // db.Dispose();
                operationSave.Complete();
                operationIndexFile.Complete();
            }
            catch (Exception e)
            {

                return false;
            } finally {
                /*
                foreach (var entityEntry in db.ChangeTracker.Entries())
                {
                    entityEntry.State = EntityState.Detached;
                }
                */
                // db.Dispose(); // overriding and calling manually fails
                // GC.Collect();

            }
        }
    }

Jetzt sieht man schon, dass ich im finally mit verschiedenen manuellen Aufräumarbeiten experimentiert habe. Zusätzlich habe ich in meinem AppDbContext mal testweise die Dispose Methode überschrieben:


    public override void Dispose()
    {
        
        var changedEntities = ChangeTracker.Entries().ToList();
        var count = changedEntities.Count();
        /*foreach (var entityEntry in changedEntities)
        {
            entityEntry.State = EntityState.Detached;
        }
        */
        ChangeTracker.Clear();
        base.Dispose();
        GC.Collect();


    }

und beim Debuggen festgestellt, dass die Anzahl der changedEntities immer weiter wächst, obwohl er Dispose aufruft und ich sogar manuell den ChangeTracker.Clear mache. Meine Schlussfolgerung ist nun, dass er aus irgendeinem Grund den Context trotz using und aufruf nicht richtig aufräumt. Wichtig zu wissen wäre noch, dass ich, um den AppDbContext zu laden Dependency Injection und eine IDbContextFactory&lt;AppDbContext&gt; dbFactory nutze:

Kann mir da jemand weiter helfen?Muss ich beim Erstellen des Contexts was beachten, damit er aufgeräumt wird?

Die Relevanten Code-Stellen sind aus meiner Sicht in folgender Reihenfolge:

  • AppDbContext - SaveChanges wurde überschrieben, um CreatedDate und UpdateDate immer automatisch zu setzen
  • Models - alle Model Dateien
  • ScopedFileIndexerService - Verwaltet die Aufrufe des File-Indexers
  • FileIndexerService - Hier liegt das Problem, dass der Context nach getaner Arbeit nicht weggeräumt wird.

Für das Problem relevant ist dabei grob folgende Datenbankstruktur:
Tabelle: Files - speichert die Daten der Dateien* Id

  • GlobalFilterType
  • MimeMediaType
  • MimeSubType
  • Hash
  • Location
  • Size
  • ModifiedDate
  • LastCheckDate
  • SourceId
  • CreatedDate
  • UpdatedDate
  • Disabled

Tabelle: FileTags - Speichert für jede Datei eine Zuordnung eines Werts und dessen Typ (Beispiel: Type=TagType.Author, TagId=1[Peter Müller], FileId=1[MeinHörbuch.m4b])* Id

  • Namespace
  • Type
  • TagId
  • FileId
  • CreatedDate
  • UpdatedDate

Tabelle: Tags - Speichert nur Text-Werte für alle möglichen Felder (Autor, Bitrate, Release-Datum, etc.)* Id

  • Value
  • CreatedDate
  • UpdatedDate
T
2.219 Beiträge seit 2008
vor einem Jahr

Warum erstellst du den DbContext in der Schleife jedes mal neu?
Leg dir vor deiner File Schleife einen Context an und arbeite darauf.
Ansonsten wäre auch die Frage ob du deine Klasse nicht ggf. immer mit den gleichen Dateien aufrufst.
In dem Fall wäre auch klar warum immer mehr Dateien geändert werden.

Auch solltest du deinen Code überarbeiten.
Deine Methoden beginnen mit _ oder mal mit kleinen Buchstaben.
Die CompareLists Methode liefert Listen im Tuppel Format zurück.
Beides ist nicht wirklich schön zu lesen, letztes solltest du gerade im Sinne von Unit of Work in einem eigenen Objekt kapseln.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Hey T-Virus,

erstmal danke für deine Mühe, dass du diesen Riesenpost überhaupt gelesen und dann auch noch geantwortet hast 🙂

Warum erstellst du den DbContext in der Schleife jedes mal neu?
Leg dir vor deiner File Schleife einen Context an und arbeite darauf.

Hatte ich vorher. Weil in der Schleife aber ca. 5000 Dateien durchgeackert und für jede Datei alle Tags (>10 Tags, >10 FileTags, >3 JsonTags, 1 File) in die Datenbank geschrieben werden, war das ganze noch wesentlich Speicherlastiger (zumindest, sofern ich das richtig analysiert hatte).

Ansonsten wäre auch die Frage ob du deine Klasse nicht ggf. immer mit den gleichen Dateien aufrufst.
In dem Fall wäre auch klar warum immer mehr Dateien geändert werden.

Das kann ich (denke ich) ausschließen. Ich habe mir die Dateien, die verarbeitet werden, via Serilog in der Kommandozeile ausgeben lassen und die Dateien werden in der richtigen Reihenfolge Seriell (da nicht async) ausgegeben. Die ersten 500 sind flott (<30ms), dann wirds immer langsamer (40ms bei 750 - 350ms bei 2500).

Auch solltest du deinen Code überarbeiten.
Deine Methoden beginnen mit _ oder mal mit kleinen Buchstaben.
Die CompareLists Methode liefert Listen im Tuppel Format zurück.
Beides ist nicht wirklich schön zu lesen, letztes solltest du gerade im Sinne von Unit of Work in einem eigenen Objekt kapseln.

Ok, das notiere ich mir mal... ein refactoring bringt ja oft die Lösung durch Reduzierung von Komplexität. Allerdings denke ich trotzdem, dass ich hier ein Speicherleck habe, weil mir meine IDE (JetBrains Rider) das auch so anzeigt. Der ChangeTracker scheint das Problem zu sein.

Ich kann mir nicht erklären, warum der ChangeTracker TROTZ Dispose und manuellem Clear trotzdem seine Einträge behält... da kann ja irgendwas nicht stimmen. Meines Wissens nach verwende ich die IDbContextFactory aber richtig... und auch das using sollte trotz Schleife ok sein. Sehr komisch.

16.806 Beiträge seit 2008
vor einem Jahr

Man kann von Deinem Code ein Foto machen, aufhängen und drunter schreiben "Genau so, sollte man niemals Datenbank-Code schreiben" 🙂

Hatte ich vorher. Weil in der Schleife aber ca. 5000 Dateien durchgeackert und für jede Datei alle Tags (>10 Tags, >10 FileTags, >3 JsonTags, 1 File) in die Datenbank geschrieben werden, war das ganze noch wesentlich Speicherlastiger (zumindest, sofern ich das richtig analysiert hatte).

Niemals.
Dein aktueller Code, für jede Datei eine Verbindung zu öffnen, ist das aller aller schlimmste, was man mit einer Datenbank-Verbindung jemals tun kann.
Des weiteren hebelst Du damit alle Caching und Low-Allocation Mechanismen aus, die Entity Framework hat - weil alle auf der Instanz einer Verbindung (bzw. des Pools) basieren.

Hinzu kommt, dass im Endeffekt genau so mit dem EF programmierst, wie man es nicht tun soll - zusätzlich zum Fakt, dass Du Logik, Datenbank und IO Code vermischt.
Zum Beispiel kommt Deine Entität FileSource von "Außen", die Du dann attachst und nich irgendwelche Unteroperationen auf der DB ausführst (HandleMissingFile).

Das ist quasi ein Schaubild von Ineffizientem, Allocation-reichen Code, den wir sehen.
Ein Memory Leak würde ich aber aber auf den kurzen Blick verneinen - würde hier der Jetbrains Anzeige nicht vertrauen. Niemals.

Der ChangeTracker scheint das Problem zu sein.

Klar, das ist der teuerste Baustein des EF Frameworks, und gerade mit dem gehst völlig falsch und ineffizient um 🙂

Was ich tun würde:
Quellcode komplett wegwerfen und die Verantwortlichkeiten sauber trennen.
Behandle Entitäten wie Entitäten, nicht wie Modelle. Dein FileSource ist entweder ein Modell oder eine Entität, aber nicht beides.
(Ja, EF suggeriert, dass man das machen kann. Sollte man nicht tun, weil eine Logik in 99,999% der Fälle anders aussieht als die DB Struktur).

Ansonsten einfache EF Basics:

  • Context Sharen
  • Nicht alles tausende Male aus der DB laden
  • Nicht Item für Item Laden, sondern so viele Dinge wie möglich gleichzeitig
  • Materialisierung verstehen und anwenden
  • Verwende asynchrone Programmierung!

So dinge wie


 var fileRecord =   db.Files.FirstOrDefault(f => f.Location == normalizedLocation && f.Source.Id == source.Id);

wird dich 90% der Zeit kosten.
Wird tausende Male aufgerufen, das jedes Mal einen Query zum DB Server auslöst - und selbst bei einer guten DB Verbindung damit jedes Mal ~5ms kostet.
Die einzelnen Fehler addieren und addieren sich...

Meines Wissens nach verwende ich die IDbContextFactory aber richtig

Würde ich auch verneinen, weil die Art und Weise wie hier insgesamt mit dem DB Context umgegangen wird, falsch ist.
IDbContextFactory ist hier ja nur Teil davon, weil die Basis schon nicht stimmt.

PS meine Meinung: gewöhn Dir var ab.
Dein Code ist auf GitHub quasi nicht zu lesen, weil man erraten muss, welcher Datentyp sich hinter var versteckt.
var war mal eine gute Idee; hat schon lange keine Berechtigung mehr in modernem Code Stil.

PPS: wenn Du irgendwann das Gefühl hast, dass Du GC.Collect(); brauchst, dann sollte das für Dich die lauteste Alarmglocke sein, dass grundlegend was an Deinem Code nicht stimmt 🙂

T
2.219 Beiträge seit 2008
vor einem Jahr

Hab noch weitere Punkte gefunden.

  1. Du verwendest .NET 6.0 aber für Json Newtonsoft.Json, warum nicht System.Text.Json?
    Gerade bei .NET 5+ sollte man eher auf System.Text.Json setzen falls es keinen Fall gibt, den System.Text.Json nicht abdeckt.
    Für einfache De-/Serialisierung sollte es aber passen.

  2. In deinen Modeln nutzt du scheinbar JToken für Values.
    Hast du hier kein eindeutiges Json bzw. sind die Objekte variabel?
    Falls nicht leg dafür eigene Klassen an, die dein Vaue abdecken.
    Hier reicht es wenn du das Json aus einer internen Instanz serialisierst.

  3. Im DatabaseSettingsService hälst du eine Instanz des AppDbContext Instanz als Member.
    Die DbContext instanz sollte nur kurzlebig z.B. innerhalb einer Methode genutzt werden.
    Hast du im FileIndexerService besser umgesetzt.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.806 Beiträge seit 2008
vor einem Jahr

Die DbContext instanz sollte nur kurzlebig z.B. innerhalb einer Methode genutzt werden.

Nein. Ein DbContext in einer Methode zu erstellen, hat ganz wenig Anwendungsfälle.
Ein DbContext sollte - ähnlich wie sein Name es sagt - innerhalb eines Context bestehen.

Bei Webanwendungen ist ein Context ein Request, weshalb der DbContext hier pauschal gesagt auch als Scoped registriert werden sollte.
In einer Desktop-Anwendung ist es oft ein ViewModel eines Windows oder eine Instanz der Anwendung.

Je langlebiger ein Context, desto effizienter kann er verwendet werden (weshalb auch Pooling so mächtig ist).

In beiden Fällen ist das suboptimal umgesetzt (weil die gesamte Repository / UoW Schicht fehlt).

Es sollte daher minimal sein:


    private readonly IMyDbContext _db;
    public DatabaseSettingsService(IMyDbContext db)
    {
        _db = db
    }

Ein Service sollte - außer es ist ein bewusster Mechanismus - keine Verantwortung haben, dass der Context erstellt wird.
Ich widersprech daher auch hier und antwote, dass der DatabaseSettingsService in diesem Fall besser programmiert ist als der FileIndexerService (der DatabaseSettingsService hat aber andere EF Probleme).

PS:

Bei Webanwendungen ist ein Context ein Request, weshalb der DbContext hier pauschal gesagt auch als Scoped registriert werden sollte.

Genau so ist auch myCSharp.de umgesetzt. Jeder Request ist genau ein DbContext - niemals mehr.
Der Context wird beim Request-Start erzeugt (über ein Pool) und am Ende wieder beenden (in den Pool zurück gegeben).
Wir haben nicht einen einzigen Fall, dass eine Verbindung mehr als einen Context erstellt und wir haben nicht einen einzigen Fall, dass das Repository oder die Logik den Context manuell erstellen muss.

Einblick:


 // Database
        {
            services.AddDbContextPool<MyCSharpMssqlDbContext>(o =>
            {
                o.UseSqlServer();
                if (databaseLoggingEnabled) { o.UseLoggerFactory(DatabaseDebugLoggerFactory); }
            });
            services.AddScoped<IMyCSharpDbContext>(p => p.GetRequiredService<MyCSharpMssqlDbContext>());

            services.AddScoped<IForumRepository, ForumRepository>();
        }
#
// Repository
public class ForumRepository : BaseRepository, IForumRepository
{
    private readonly IMapper _mapper;

    public ForumRepository(IMyCSharpDbContext dbContext, IMapper mapper) : base(dbContext)
    {
        _mapper = mapper;
    }

public abstract class BaseRepository : IBaseRepository
{
    public IMyCSharpDbContext DbContext { get; }

    protected BaseRepository(IMyCSharpDbContext dbDbContext)
    {
        DbContext = dbDbContext;
    }

T
2.219 Beiträge seit 2008
vor einem Jahr

@Abt
Ah okay, da lag ich dann falsch.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Man kann von Deinem Code ein Foto machen, aufhängen und drunter schreiben "Genau so, sollte man niemals Datenbank-Code schreiben" 🙂

@Abt: Auch danke an deine Mühe, eine so lange Antwort zu verfassen. Du musst entschuldigen, ich werde die Antwort in Etappen verfassen müssen, weil ich im Moment sehr wenig Zeit habe und mir den Post erstmal richtig verinnerlichen muss. Ich versuche mal meine Hauptpunkte kurz zusammen zu fassen.

Dein Post wirkt sehr fundiert, aber er verwundert mich extrem.

Niemals.
Dein aktueller Code, für jede Datei eine Verbindung zu öffnen, ist das aller aller schlimmste, was man mit einer Datenbank-Verbindung jemals tun kann.
Des weiteren hebelst Du damit alle Caching und Low-Allocation Mechanismen aus, die Entity Framework hat - weil alle auf der Instanz einer Verbindung (bzw. des Pools) basieren.

Offensichtlich habe ich ÜBERHAUPT NICHT verstanden, wie das EF einen Unit of Work definiert. Mir war nicht klar, das ein using db = dbFactory.CreateContext() eine neue Datenbankverbindung erstellt. Ich dachte, das sei ein gekapseltes Stück Code für "hier passiert etwas mit Entities, die zusammen gehören - danach kannst du deinen internen Cache wegwerfen".

Hinzu kommt, dass im Endeffekt genau so mit dem EF programmierst, wie man es nicht tun soll - zusätzlich zum Fakt, dass Du Logik, Datenbank und IO Code vermischt.
Zum Beispiel kommt Deine Entität FileSource von "Außen", die Du dann attachst und nich irgendwelche Unteroperationen auf der DB ausführst (HandleMissingFile).

Das ist quasi ein Schaubild von Ineffizientem, Allocation-reichen Code, den wir sehen.
Ein Memory Leak würde ich aber aber auf den kurzen Blick verneinen - würde hier der Jetbrains Anzeige nicht vertrauen. Niemals.

Ok, was mich in dem Fall verwundert ist, dass die Anwendung zunächst "sehr performant" läuft (wobei das ja laut deinen Aussagen wegen des Codes eigentlich nicht performant ist und wesentlich besser sein könnte) und dann mit zunehmendem Verlauf für die selbe Menge Arbeit immer länger braucht.

Klar, das ist der teuerste Baustein des EF Frameworks, und gerade mit dem gehst völlig falsch und ineffizient um 🙂

Was ich tun würde:
Quellcode komplett wegwerfen und die Verantwortlichkeiten sauber trennen.

Geht klar, hatte ich eh vor. Eigentlich wollte ich mich erst um das Frontend kümmern, aber wenn das Backend "hingefummelt" unbenutzbar ist, werde ich wohl nicht drum herum kommen

Behandle Entitäten wie Entitäten, nicht wie Modelle. Dein FileSource ist entweder ein Modell oder eine Entität, aber nicht beides.
(Ja, EF suggeriert, dass man das machen kann. Sollte man nicht tun, weil eine Logik in 99,999% der Fälle anders aussieht als die DB Struktur).

Das muss ich mir erstmal näher Ansehen... ich meine deine Grundaussage verstehe ich, aber ich dachte zum Indexieren von Dateien benötige ich nur die Entitäten (ich habs halt in den Models Namespace gepackt, aber das werde ich mal abändern). Kannst du mir ein Beispiel geben, was wie so ein Model aussehen könnte und warum ich es brauche, damit ich den Unterschied besser verstehe?

Ansonsten einfache EF Basics:

  • Context Sharen
  • Nicht alles tausende Male aus der DB laden
  • Nicht Item für Item Laden, sondern so viele Dinge wie möglich gleichzeitig
  • Materialisierung verstehen und anwenden
  • Verwende asynchrone Programmierung!
    So dinge wie
  
 var fileRecord =   db.Files.FirstOrDefault(f => f.Location == normalizedLocation && f.Source.Id == source.Id);  
  

wird dich 90% der Zeit kosten.
Wird tausende Male aufgerufen, das jedes Mal einen Query zum DB Server auslöst - und selbst bei einer guten DB Verbindung damit jedes Mal ~5ms kostet.
Die einzelnen Fehler addieren und addieren sich...

Hier werde ich noch mal in Ruhe drauf eingehen müssen... aber mal meine ersten Gedanken:

  • - Context Sharen: OK, das muss ich ausprobieren... aber es handelt sich ja hier nicht um einen "Request" sondern um einen BackgroundService der potenziell hunderttausende von Dateien scannen können soll (bei meiner Audio-Bib sinds erstmal ca 5000)
  • **- Nicht alles tausende Male aus der DB laden: **OK, verstehe ich, aber wenn es eine große Menge Daten sind, dann soll ich vermutlich auch nicht alles im Speicher halten oder?
  • - Nicht Item für Item Laden, sondern so viele Dinge wie möglich gleichzeitig: OK, das ist nachvollziehbar, aber wenn ich alle Tags auf einmal erstelle und AddRange gemacht habe, war das wesentlich langsamer. Es muss dann wohl an was anderem gelegen haben. Guck ich mir mal an
  • - Materialisierung verstehen und anwenden: Hier bräuchte ich mehr Details... das meinst du mit Materialisierung denn? *** - Verwende asynchrone Programmierung!: **Geht klar. Das ist kein Problem.

Würde ich auch verneinen, weil die Art und Weise wie hier insgesamt mit dem DB Context umgegangen wird, falsch ist.
IDbContextFactory ist hier ja nur Teil davon, weil die Basis schon nicht stimmt.

Ich dachte, um Dependency Injection richtig verwenden zu können, OHNE den Context zu sharen (wie es ja meine Grundannahme war), share ich die Factory und "produziere" für die Units of Work innerhalb der Klassen dann jeweils einen neuen Kontext. Ich habe diesen Artikel hier wohl völlig missverstanden: Managing DbContext the right way with Entity Framework 6: an in-depth guide

PS meine Meinung: gewöhn Dir var ab.
Dein Code ist auf GitHub quasi nicht zu lesen, weil man erraten muss, welcher Datentyp sich hinter var versteckt.
var war mal eine gute Idee; hat schon lange keine Berechtigung mehr in modernem Code Stil.

Ok, auch das war mir neu im Zusammenhang mit modernem Code-Stil. Da ich das schon mal von jemand anderem gehört habe, werde ich mir das wohl abgewöhnen.

PPS: wenn Du irgendwann das Gefühl hast, dass Du GC.Collect(); brauchst, dann sollte das für Dich die lauteste Alarmglocke sein, dass grundlegend was an Deinem Code nicht stimmt 🙂

Ja das ist logisch... das war ein Experiment aufgrund eines Stack-Overflow Posts (https://stackoverflow.com/a/57652290/3246102)

Vielen Dank nochmal. Ich würd mich freuen, wenn ich noch mal ein paar Fragen stellen dürfte, wenn ich deine Anregungen umgesetzt habe.

16.806 Beiträge seit 2008
vor einem Jahr

Offensichtlich habe ich ÜBERHAUPT NICHT verstanden, wie das EF einen Unit of Work definiert.

Ja, das merkt man am Code 🙂 Der DbContext ist der Unit of Work - man bastelt i.d.R. jedoch was drum herum (Repositories) um weitere Pattern (zB RDY) anwenden zu können.

Ok, was mich in dem Fall verwundert ist, dass die Anwendung zunächst "sehr performant" läuft (wobei das ja laut deinen Aussagen wegen des Codes eigentlich nicht performant ist und wesentlich besser sein könnte) und dann mit zunehmendem Verlauf für die selbe Menge Arbeit immer länger braucht.

Ein relativ typisches Verhalten.
Auch Aufräumen kostet Zeit und Performance - und das macht .NET daher nur, wenn es aufräumen muss.
Hinzu kommt dass Du tausende von Verbindungen hast, die auch den Server belasten. Das heisst auch dieser muss bei steigender Anzahl von Verbindungen mehr arbeiten.
Die ineffiziente Umsetzung schaukelt sich selbst auf.

Die ersten 500 sind flott (<30ms), dann wirds immer langsamer (40ms bei 750 - 350ms bei 2500).

Ein Effekt des Bufferns, weil Dein Code schneller durchläuft als die Queries abgearbeitet werden können.
Der Server wird zugeballert, muss die Verbindungen beantworten und hat weniger Zeit. Hinzu kommen interne Locking Mechanismen, da der SQL Server eben nach ACID arbeitet.
Tausende Schreiboperationen zeitgleich blocken sich also - und je mehr Schreiboperationen auflaufen, desto mehr Buffering gibts. Hinzu kommen Limits Deiner Anwendung (zB durch Windows/Linux), in die Du bei so einem Vorgehen sehr schnell rein läufts (zB maximale Verbindungen).
Im Endeffekt Grundlagen von Datenbanken 🙂

Kannst du mir ein Beispiel geben, was wie so ein Model aussehen könnte und warum ich es brauche, damit ich den Unterschied besser verstehe?

Wenn FileSource Deine Entität ist, dann hab ich das falsch verstanden, da Du Dich nicht an die Namensempfehlungen gehalten hast (aus der Community.
Das EF Marketing ist so, dass Modelle und Entitäten vereinfacht werden können; "der Entwicklung muss sich um Datenbank nicht mehr kümmern".
In einer [Artikel] Drei-Schichten-Architektur sind Modelle und Entitäten jedoch stark getrennt, weil eine effiziente Datenstruktur anders aussieht als ein Modell.
Einige moderne Ansätze (zB Event Sourcing) haben gar keine Modelle mehr, sondern nur noch Events in der Logik und Entitäten und Projektionen in der Datenschicht.
So ist auch das Forum hier umgesetzt: vollständig Event-orientiert, verwendet fürs Lesen ausschließlich Projektionen und nur für sehr wenige Schreiboperationen eben direkt die Entität.
Das ist enorm effizient aus DB sicht.

aber es handelt sich ja hier nicht um einen "Request" sondern um einen BackgroundService der potenziell hunderttausende von Dateien scannen können soll

Völlig egal, was Dein Trigger ist. Bei einer Webanwendnung ist der Trigger eben ein Request, bei Dir vielleicht irgendeine Aktion oder irgendein Zeitpunkt.
Im Endeffekt heisst das Zauberwort hier Scope, was Dependency Injection im Hintergrund automatisch macht.
Ein Request erzeugt einen DI Scope aus, und jeder Scope ist in sich isoliert -> jeder Request hat ergo einen Scope und damit eine DB Verbindung.
Wie Du Deinen Scope auslöst: das ist Deine Aufgabe der Architektur.
Wenn Du einen Trigger hast, der ein mal die Stunde rennt (was wir hier im Forum auch haben für Hintergrundaufgaben, ist also nix anderes), dann erzeugst Du in diesem Trigger einen DI Scope und alles andere passiert automatisch.
Ich sehe hier keinen Anwendungsfall, der das manuelle Erstellen eines DbContext über eine Factory begründen würde.

Im Endeffekt sieht das dann so aus:


  public MyBackgroundTaskService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    // das wird i.d.R. durch den Trigger aufgerufen
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // das hier passiert Dependency Injection i.d.R. automatisch
        //    muss man bei gewissen Szenarien (zB Backgroundservices) wie manuellen Triggern oft selbst machen
        await using AsyncServiceScope scope = _serviceScopeFactory.CreateAsyncScope();
        IMyDbContext myDbContext = scope.GetRequiredService<IMyDbContext>();
      

OK, verstehe ich, aber wenn es eine große Menge Daten sind, dann soll ich vermutlich auch nicht alles im Speicher halten oder?

Ist halt ein Spagat.
Bei einer Anwendung, die 247/ läuft hat man andere Anforderungen als bei etwas, das einmalig in nem gewisse Zeitraum aufgerufen wird.
Wir kennen aber die Anwendung nicht (und ich hab auch kein Zeit nen ganzes GitHub Projekt zu reviewen), aber im Endeffekt brauchst Du dann einen Caching Mechanismus (den EF Teilweise auf Basis einer Kontext Isolierung (durch Find()) eingebaut hat.
Aber wenn man dauernd alles abruft und das auch noch Item für Item: da muss man sich dann nicht wundern, dass es buffert und langsam wird; quasi wie ein physikalisches Gesetz dann: erwartetes Verhalten by Design.

Hier bräuchte ich mehr Details... das meinst du mit Materialisierung denn?

Das ist die Kernfunktion von EF Core - und leider der häufigste Performance-Fehler, weil viele diesen Grundlagenbaustein nicht kennen.
Intermediate materialization (C#)

Ich habe diesen Artikel hier wohl völlig missverstanden: Managing DbContext the right way with Entity Framework 6: an in-depth guide

Der Artikel ist 8 Jahre alt und bezieht sich auf EF 6, also den Vorgänger von EF Core.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Ein relativ typisches Verhalten.
Auch Aufräumen kostet Zeit und Performance - und das macht .NET daher nur, wenn es aufräumen muss.
Hinzu kommt dass Du tausende von Verbindungen hast, die auch den Server belasten. Das heisst auch dieser muss bei steigender Anzahl von Verbindungen mehr arbeiten.
Die ineffiziente Umsetzung schaukelt sich selbst auf. Ein Effekt des Bufferns, weil Dein Code schneller durchläuft als die Queries abgearbeitet werden können.
...
Im Endeffekt Grundlagen von Datenbanken 🙂

Klingt plausibel - wenn ich die interna von EF verstanden hätte (oder verstehen würde), hätte ich das auch nie so gebaut. Vermutlich haben mich falsch gelesene Doku und Code-Beispiele verwirrt und JetBrains Analyse eines Memory-Leaks hat den Rest dazu gegeben.

da Du Dich nicht an die Namensempfehlungen gehalten hast (aus der Community.

Wie wären die denn? (Beispiel wäre super, damit ich das abändern kann)

Völlig egal, was Dein Trigger ist. Bei einer Webanwendnung ist der Trigger eben ein Request, bei Dir vielleicht irgendeine Aktion oder irgendein Zeitpunkt.
Im Endeffekt heisst das Zauberwort hier Scope, was Dependency Injection im Hintergrund automatisch macht.
Ein Request erzeugt einen DI Scope aus, und jeder Scope ist in sich isoliert -> jeder Request hat ergo einen Scope und damit eine DB Verbindung.
Wie Du Deinen Scope auslöst: das ist Deine Aufgabe der Architektur.
Wenn Du einen Trigger hast, der ein mal die Stunde rennt (was wir hier im Forum auch haben für Hintergrundaufgaben, ist also nix anderes), dann erzeugst Du in diesem Trigger einen DI Scope und alles andere passiert automatisch.
Ich sehe hier keinen Anwendungsfall, der das manuelle Erstellen eines DbContext über eine Factory begründen würde.

Im Endeffekt sieht das dann so aus:

  
  public MyBackgroundTaskService(IServiceScopeFactory serviceScopeFactory)  
    {  
        _serviceScopeFactory = serviceScopeFactory;  
    }  
  
    // das wird i.d.R. durch den Trigger aufgerufen  
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        // das hier passiert Dependency Injection i.d.R. automatisch  
        //    muss man bei gewissen Szenarien (zB Backgroundservices) wie manuellen Triggern oft selbst machen  
        await using AsyncServiceScope scope = _serviceScopeFactory.CreateAsyncScope();  
        IMyDbContext myDbContext = scope.GetRequiredService<IMyDbContext>();  
    }  
  

Ist halt ein Spagat.
Bei einer Anwendung, die 247/ läuft hat man andere Anforderungen als bei etwas, das einmalig in nem gewisse Zeitraum aufgerufen wird.
Wir kennen aber die Anwendung nicht (und ich hab auch kein Zeit nen ganzes GitHub Projekt zu reviewen), aber im Endeffekt brauchst Du dann einen Caching Mechanismus (den EF Teilweise auf Basis einer Kontext Isolierung (durch Find()) eingebaut hat.

Dieses Beispiel ist für mich gut nachvollziehbar. Allerdings stellt sich für mich immernoch die Frage, wie ich den DBContext (auch im Scoped Fall) aufräume, wenn ich eine sehr große Anzahl von Dateien indexieren möchte (≥ 100.000) und der Speicher für den DBContext dann bei Nummer 10.000 vollläuft.

Kurzer Überblick über die geplante Funktionsweise des Indexers (die API lasse ich mal außen vor, weil das ne simple JsonApiDotNet Implementierung der Entities ist):* In der Datenbank können beliebig viele FileSource Einträge gemacht werden

  • Jede FileSource steht (zunächst) für ein Verzeichnis, in dem sich Audio-Dateien befinden, die indexiert werden sollen (hier gehe ich erstmal von 1 - 100.000 Dateien pro FileSource aus)
  • Der FileIndexer soll nun für jede FileSource alle Dateien deren Metadaten laden (Album, Artist, Title, TrackNumber, etc.) und in die Datenbank schreiben
  • Zusätzlich erzeuge ich noch einen XXHASH des AudioStreams OHNE Metadaten, damit ich die Datei wiedererkennen kann, auch wenn sich die Metadaten ändern (Beispiel: Verschiebt man eine Datei an einen neuen Ort und ändert die Metadaten, verliere ich normalerweise die Playlists und die Historie, wenn dafür durch den Indexer ein NEUER Eintrag erstellt wird. Erkenne ich aber den XXHash wieder, kann ich die Datei als "verschoben" markieren und behalte die FileID und somit die Einstellungen des Users für Playlists, Anzahl Abspielungen, Bewertungen etc. - außerdem kann ich so einfach eine Suche für Dateiduplikate/Doppelte Tracks anbieten)
  • Statt hier spezifische Tabellen oder Tabellenfelder für die Tags zu verwenden, wird das alles als "string" in der Tabelle "Tags" hinterlegt und über die "FileTags" den "File"-Einträgen zugeordnet

Beispiel:
Tag:Id=1,Value=Back in Black
File:Id=5,Location=music/ACDC/Back_in_black.mp3
FileTag:FileId=5,TagId=1,Type=3(album)

Das ist zwar nicht optimal, aber dahinter steckt eine besondere Idee für eine sehr flexible Suchfunktion, die für die Lösung meines aktuellen Problems aber erstmal nicht relevant ist)

  • Die FileSource-Einträge können zu einem beliebigen Zeitpunkt hinzugefügt, gelöscht oder geändert werden (über die DB oder API)
  • In diesem Fall muss der Indexierungsvorgang eventuell unterbrochen oder angepasst werden, da sich die Quellen verändert haben (Beispiel: Wird eine Quelle hinzugefügt, muss der Indexierungsvorgang um diese Quelle ergänzt, aber nicht unterbrochen werden)

Meine Idee war, dass ich einen BackgroundTask erstelle, der folgendes macht:* Lade alle FileSource-Einträge und führe einen EINMALIGEN Indexierungsvorgang beim Start der Anwendung für jede FileSource durch

  • Erstelle einen FileSystemWatcher für jede FileSource und führe bei Dateiänderungen einen Indexierungsvorgang für NUR die geänderten Dateien durch
  • Überprüfe alle 2 Minuten, ob sich die FileSource-Einträge geändert haben - bei Änderungen führe einen Komplettindexierungsvorgang durch und aktualisiere alle FileSystemWatcher (werfe unnötige weg, füge neue hinzu, etc.)

Die Krux an der Sache ist, dass ich nicht mehr vorhandene Datei-Leichen aus der Datenbank entfernen muss, aber die finde ich NUR bei einem vollständigen Indexierungsvorgang. Folglich muss ich erst alle Dateien auf "potenziell gelöscht" setzen, dann indexieren und für jede gefundene Datei das "potenziell gelöscht" entfernen. Ich löse das aktuell über ein LastCheckDate, das wird vor jedem Indexieren auf im Programm auf "now" gesetzt, dann für jede gefundene Datei in die Datenbank gespeichert und dann werden alle Dateien gelöscht, wo dieses Datum VOR dem aktuellen Zeitpunkt ist (bzw. die nicht gefunden wurden). Dadurch muss ich für jede Datei auch wissen, zu welcher Source sie gehört, um sie indexieren zu können.

Versteht man das?

Und nehmen wir an, ich habe 100.000 Dateien mit jeweils 40 Tags und müsste die alle 10.000 Durchläufe frei geben. Müsste ich sowas machen? Wenn ja, wie?
Beispiel: Muss ich den Scope dann nach z.B. 10.000 indexierten Dateien wegwerfen und neu erzeugen?

16.806 Beiträge seit 2008
vor einem Jahr

Ja, Du musst Dich definitv mit 2 Dingen beschäftigen:

  • Was ist EF und wie funktioniert es
  • Was ist Dependency Injection und wie funktioniert es
    Weil bei beidem, das sieht man am Code und an den Fragen; da haperts. Und ich lehn mich einfach aus dem Fenster und sage, dass das kombinierte Halbwissen aus beidem zu viel falschem Code führt 😉
    Macht aber für mich kein Sinn Dir das 1:1 vorzulesen, was in den Docs steht. Kannst auch einfach in den Docs Dein Wissen sammeln - ja, muss man halt mal Zeit nehmen und alles durchlesen und ausprobieren.
    Das kann man in einem Forenthema nicht abdecken.

Allerdings stellt sich für mich immernoch die Frage, wie ich den DBContext (auch im Scoped Fall) aufräume, wenn ich eine sehr große Anzahl von Dateien indexieren möchte (≥ 100.000) und der Speicher für den DBContext dann bei Nummer 10.000 vollläuft.

Das ist nun so eine Frage, die sich nur stellt, weil Du Dependency Injection noch nicht verstanden hast: man räumt nicht selbst auf 🙂
Das ist ja gerade die Aufgabe des DI Containers aka DI Scopes.


// Du erstellst den Scope (oder je nach Framework wie ASP.NET Core erfolgt das automatisch)
await using AsyncServiceScope scope = _serviceScopeFactory.CreateAsyncScope();

// Du nutzt nur noch Abängigkeiten und lässt alles andere automatisch auflösen
IMyService myService = scope.GetRequiredService<IMyService>();

// und da das AsyncServiceScope ein using hat, wird das Scope automatisch aufgeräumt und damit auch alle Instanzen, die durch das Scope erzeugt wurden

Wie gesagt: bei ASP.NET Core läuft das zB alles automatisch im Rahmen eines Requests.
Wenn Du ein Event manuell feuerst, erzeugst den Scope manuell und am Ende wirds automatisch aufgeräumt.
Kann man hier aber nicht vollständig erklären -> Docs lesen.

Und nehmen wir an, ich habe 100.000 Dateien mit jeweils 40 Tags und müsste die alle 10.000 Durchläufe frei geben. Müsste ich sowas machen? Wenn ja, wie?

Musst Dir selbst überlegen.
Aber: man würde nicht alles auf einmal absenden, wie man auch nicht alle einzeln absendet: man verschickt Chunks. Also Pakete zB immer 1000 Stück.
Wie viel sinn machen (zusammen mit Deiner Logik) musst selbst überlegen. Das Stichwort ist hier Transaction Scopes (Docs).

Beispiel: Muss ich den Scope dann nach z.B. 10.000 indexierten Dateien wegwerfen und neu erzeugen?

Ich weiß nicht welche Art von Scope Du hier meinst; ein Transaction Scope im Sinne der Datenbank ist hat ein "Datenpaket".
Ein Service Scope (Dependency Injection) lebt nur über die Dauer eines "Events" oder "einer Aufgabe".

Wenn Deine Aufgabe ist: Ich will alle 10 Minuten ein Dateisystem überwachen und dann die Änderungen committen, dann ist das Dein Event.
Eine solche Aufgabe haben wir hier im Forum auch (das ist quasi der Baustein des Codes oben), nur dass wir keine Dateien abgleichen, sondern automatisiert Datenbank-Einträge (DSGVO) löschen.
Aber strukturell genau gleiche Aufgabe, sogar technisch, da das bei uns auch auf dem BackgroundService beruht:


public sealed partial class DatabaseCleanupService : BackgroundService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public DatabaseCleanupService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }
    
    // Wird einmalig bei App Start aufgerufen
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // "blockiert" X Stunden -> Code läuft damit alle X stunden
        using PeriodicTimer ticker = new(TimeSpan.FromHours(IntervalInHours));
        // ob der Trigger hier aber eine Zeit ist, oder weil der FileSystem Watcher einen Batch von Changes erkennt: egal.

        while (!stoppingToken.IsCancellationRequested && await ticker.WaitForNextTickAsync(stoppingToken).ConfigureAwaitFalse())
        {
                // Scope wird dann erstellt, wann man ihn braucht und automatisch aufgeräumt
                await using AsyncServiceScope scope = _serviceScopeFactory.CreateAsyncScope();
                IEventDispatcher eventDispatcher = scope.ServiceProvider.GetRequiredService<IEventDispatcher>();

                // Aufgabe ausführen
                await eventDispatcher.Send(new CleanupDatabaseCommand(), stoppingToken).ConfigureAwaitFalse();

                // unser CleanupDatabaseCommand ist damit der Aufräum Event (Event Sourcing)
                // darin werden Service und Datenbank verwendet um verschiedene Tabellen aufzuräumen
                // alles automatisiert durch den Dependency Injection Scope
                // in Deinem Fall wäre eigentlich alles gleich bis auf die Sache, dass Du einen anderen Trigger hast und der Event anders heisst
        }
    }

Wenn also diese Operation 10ms dauert, dann existiert der Service Scope 10ms.
Wenn die Operation 8 Stunden dauert, dann existiert der Service Scope 8 Stunden.

Auf Deine ganze App-Logik geh ich nicht ein; das ist Dein Bier wie das funktionieren soll.
Aber mein Tipp: bevor Du noch paar verschiedene Thesen aufstellst, les Dir doch DI und EF mal paar Stunden durch.
Denke da werden dann viele viele Fragen selbst beantwortet, wenn Du die Konzepte verstanden hast.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Macht aber für mich kein Sinn Dir das 1:1 vorzulesen, was in den Docs steht. Kannst auch einfach in den Docs Dein Wissen sammeln - ja, muss man halt mal Zeit nehmen und alles durchlesen und ausprobieren.
Das kann man in einem Forenthema nicht abdecken.

Absolut. Ich würde sogar noch weiter gehen und sagen, dass es bei dem Wissen über Tasks, async/await und CancellationTokens auch noch Nachholbedarf gibt. Leider habe ich aktuell sehr wenig Zeit am Stück (hab immer nur so 30 Minuten Happen), was ich zum durchackern von Doku bräuchte, aber ohne Lesen und verstehen gehts wohl einfach nicht mehr. Mal sehn was ich machen kann.

Ich komm mit der Microsoft-Dokumentation extrem schlecht zurecht (warum hab ich noch nicht rausgefunden), aber ich werds mal versuchen. Falls du Links zu Artikeln in der Doku zu den genannten Themen hast, die man in jedem Fall gelesen haben sollte, würde ich mich sehr darüber freuen, je mehr desto besser. Hab wenn ich selbst suche immer das Gefühl, dass ich die falschen Sachen lese :p

Jedenfalls vielen Dank für die ausführlichen Posts und die Beispiele aus dem Forum, ich glaube das bringt mich schon mal ein gutes Stück weiter.

EDIT: Ich habe ein paar meiner Ansicht nach ganz gute Videos gefunden, mit denen ich die Materie vielleicht besser verstehe, wenn ich mit der Doku nicht weiter komme, ich schreib die mal hier rein, damit ich die nicht vergesse 🙂 (Auch wieder altes wissen? 🙂

16.806 Beiträge seit 2008
vor einem Jahr

Die Microsoft Dokumentation ist halt eine Dokumentation und hat nur wenige Tutorial-Komponenten (was ich sehr gut finde, weil es den Missbrauch minimiert).
Das war früher mal so und hat damit geendet, das Leute 1:1 Zeugs kopiert hatten und sich dann beschwert haben, dass es nicht in deren Umgebung funktioniert.
Man hat lange und zusammen mit der Community nun eine Dokumentation geschaffen, die aus Bausteinen besteht: es wird kurz und fokussiert eine Sache erklärt.
Die Aufgabe von Dir sind nun diese Bausteine zusammen zu tragen. Darfst da aber nicht erwarten, dass dort ein Beispiel steht, wie 1:1 Deine Anwendung umzusetzen ist (was Du vielleicht nun nicht hast, aber viele andere). So funktioniert nirgends eine Doc.

Die Microsoft Doc ist aber mit die mächtigste am gesamten Software Markt.
Allein die Dependency Injection Doc (Dependency injection - .NET) umfasst ~20 Din A4 Seiten - und trotzdem hört man immer wieder (auch von meinen Kunden) XYZ würde da nicht stehen.
Steht da alles: man muss es nur lesen 🙂

Übrigens steht auch dort, wie ich gerade sehe, 1:1 das Service Scope Beispiel, das wir auch verwenden.
Die Info war also bereits in den Docs enthalten - und arg viel andere Dinge als dort, halt komprimierter, hab ich Dir nun erzählt.

Scope scenarios
The IServiceScopeFactory is always registered as a singleton, but the IServiceProvider can vary based on the lifetime of the containing class. For example, if you resolve services from a scope, and any of those services take an IServiceProvider, it'll be a scoped instance.

To achieve scoping services within implementations of IHostedService, such as the BackgroundService, do not inject the service dependencies via constructor injection. Instead, inject IServiceScopeFactory, create a scope, then resolve dependencies from the scope to use the appropriate service lifetime.

  
namespace WorkerScope.Example;  
  
public class Worker : BackgroundService  
{  
    private readonly ILogger<Worker> _logger;  
    private readonly IServiceScopeFactory _serviceScopeFactory;  
  
    public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory) =>  
        (_logger, _serviceScopeFactory) = (logger, serviceScopeFactory);  
  
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        while (!stoppingToken.IsCancellationRequested)  
        {  
            using (IServiceScope scope = _serviceScopeFactory.CreateScope())  
            {  
                try  
                {  
                    _logger.LogInformation(  
                        "Starting scoped work, provider hash: {hash}.",  
                        scope.ServiceProvider.GetHashCode());  
  
                    var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();  
                    var next = await store.GetNextAsync();  
                    _logger.LogInformation("{next}", next);  
  
                    var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();  
                    await processor.ProcessAsync(next);  
                    _logger.LogInformation("Processing {name}.", next.Name);  
  
                    var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();  
                    await relay.RelayAsync(next);  
                    _logger.LogInformation("Processed results have been relayed.");  
  
                    var marked = await store.MarkAsync(next);  
                    _logger.LogInformation("Marked as processed: {next}", marked);  
                }  
                finally  
                {  
                    _logger.LogInformation(  
                        "Finished scoped work, provider hash: {hash}.{nl}",  
                        scope.ServiceProvider.GetHashCode(), Environment.NewLine);  
                }  
            }  
        }  
    }  
}  
  
S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

@Abt

Sorry das ich noch mal drauf zurück komme, aber es war noch eine Frage offen:

Wenn FileSource Deine Entität ist, dann hab ich das falsch verstanden, da Du Dich nicht an die Namensempfehlungen gehalten hast (aus der Community.

Wie sind die Namensempfehlungen für Entitäten denn?

Übrigens bin ich nach viel Doku lesen trotz deiner Beteuerungen nicht ganz sicher, dass ich mit meiner ursprünglichen Vermutung so falsch lag, dass bei Bulk-Inserts in BackgroundServices der DbContext NICHT erhalten bleiben sollte. Mit dem häufigen "SaveChanges" und "FirstOrDefault" hattest du Recht, keine Frage... aber ich verstehe die Doku so, dass der DbContext nach einigen Aufrufen aufgeräumt werden sollte. Siehe auch hier (Änderungsnachverfolgung – EF Core):

DbContext ist dazu konzipiert, eine kurzlebige Arbeitseinheit darzustellen, wie in DbContext-Lebensdauer, -Konfiguration und -Initialisierung beschrieben. Dies bedeutet, dass das Verwerfen von DbContext die normale Art und Weise ist, die Nachverfolgung von Entitäten zu beenden. Anders ausgedrückt, die Lebensdauer einer DbContext-Instanz sollte so aussehen:

Erstellen der DbContext-Instanz  
Nachverfolgen **einiger** Entitäten  
Vornehmen von Änderungen an den Entitäten  
Aufrufen von SaveChanges zum Aktualisieren der Datenbank  
Verwerfen der DbContext-Instanz  

[...]

Es ist nicht erforderlich, die Änderungsnachverfolgung zu löschen oder Entitätsinstanzen explizit zu trennen, wenn Sie derart vorgehen. Wenn Sie jedoch Entitäten trennen müssen, ist das Aufrufen von ChangeTracker.Clear effizienter, als Entitäten einzeln zu trennen.

Selbiges entnehme ich auch aus einem Stackoverflow Thread und meinem Monitoring der Postgres-Datenbank - die scheint das locker zu schaffen, was ich da auf sie abfeuere und es sieht nicht aus, als wäre die das Bottleneck... Aber ich werde trotzdem deinen Weg mal ausprobieren und schauen, wie weit ich damit komme und ob das mein Problem löst. Vermutlich werde ich das ganze über ein Producer/Consumer Pattern neu implementieren, da das Iterieren des Verzeichnisses deutlich schneller sein sollte, als das Auslesen der Tags und danach das Importieren in die Datenbank.

Zusätzlich werde ich mal ein bisschen mit den BulkExtensions ausprobieren, wobei ich da beim ersten Versuch gleich nen Bug gefunden habe und den Lösungsvorschlag des Entwicklers gar nicht so versiert fand.

Viele Grüße
sandreas

16.806 Beiträge seit 2008
vor einem Jahr

Wie sind die Namensempfehlungen für Entitäten denn?

"Entity" als Suffix, hier also FileSourceEntity. Projektionen dann eben FileSourceProjection. View dann eben FileSourceView.
Wobei Projektionen und Views wiederum i.d.R. spezifische Namen haben.

Übrigens bin ich nach viel Doku lesen trotz deiner Beteuerungen nicht ganz sicher, dass ich mit meiner ursprünglichen Vermutung so falsch lag, dass bei Bulk-Inserts in BackgroundServices der DbContext NICHT erhalten bleiben sollte.

Niemand hat gesagt, dass Du den ganzen Lebenszyklus einen Kontext erhalten sollst.
Einen DbContext erstellt man dann, wenn man ihn braucht und man behält ihn auch so lange wie man ihn braucht: eben das Unit of Work.

Das Feedback war, weil Du eben einen Kontext innerhalb jeder kleinsten Aktion erzeugt hast.
Das ist halt quatsch und steht auch nicht in der Doku oder irgendo in den SO Links.

Daher schrieb ich auch:

Nein. Ein DbContext in einer Methode zu erstellen, hat ganz wenig Anwendungsfälle.
Ein DbContext sollte - ähnlich wie sein Name es sagt - innerhalb eines Context bestehen.

Dein Kontext ist also ein Event. Es ist aber nicht die Idee, dass Du in jeder Methode einen eigenen DbContext hast.
Trotzdem: die Regel, dass ein Kontext nur sehr kurz leben soll, hat Ausnahmen.
Es gibt berechtigte Fälle einen Kontext auch ein paar Minuten / über mehrere Aktionen offen zu halten.

Bei der Migration dieses Forums damals, von MySQL auf den SqlServer, bestand der Kontext über 30 Minuten - so lange hat die Migration gebraucht.
Und genau für solche langen Bulk Operationen ist das eine legitime Sache. Die Doku äußert hier immer noch eine Empfehlung, kein Grundgesetz.
Eine Empfehlung für eine WPF-Anwendung mit einem Einzelaufruf ist halt nicht so wirklich immer anwendbar auf Bulk Operationen.
Da muss man das Evaluieren und Anwenden von Wissen walten lassen.
Kannst EF Core in Blazor auch nicht so umsetzen wie in nem Windows Service, auf ner Handy App... alles verschiedene Rahmenbedingungen.

--> Es kommt einfach drauf an, was der Kontext ist.

Wie in Deinem Fall der Kontext eine legitime, passende Lebenszeit hat, hab ich Dir oben erklärt im Zusammenhang mit dem Backgroundservice.
Und da steht mit keinem Wort, dass der Kontext dauerhaft offen ist.

Dein Link und Dokumentation bestätigt meine Aussage, denn

Create the DbContext instance <<<< Das passiert beim Event Eintritt
Track some entities <<<< Das ist Deine Logik
Make some changes to the entities <<<< Das ist Deine Logik
Call SaveChanges to update the database <<<< Das ist Deine Logik
Dispose the DbContext instance <<<< Event Ende, passiert automatisch wenn man DI ServiceScopes für das Erzeugen verwendet

Die Kunst der Software Entwicklung ist ganz oft zu verstehen, was eine Dokumentation inhaltlich aussagt und dies auf den eigenen Fall anzuwenden.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

"Entity" als Suffix, hier also FileSourceEntity. Projektionen dann eben FileSourceProjection. View dann eben FileSourceView.
Wobei Projektionen und Views wiederum i.d.R. spezifische Namen haben.

Super, danke. Das werde ich dann bei Zeiten entsprechend anpassen. Das mit dem var wohl auch.

Niemand hat gesagt, dass Du den ganzen Lebenszyklus einen Kontext erhalten sollst.
Einen DbContext erstellt man dann, wenn man ihn braucht und man behält ihn auch so lange wie man ihn braucht: eben das Unit of Work.

Das Feedback war, weil Du eben einen Kontext innerhalb jeder kleinsten Aktion erzeugt hast.
Das ist halt quatsch und steht auch nicht in der Doku oder irgendo in den SO Links.

Ok.

Bei der Migration dieses Forums damals, von MySQL auf den SqlServer, bestand der Kontext über 30 Minuten - so lange hat die Migration gebraucht.
Und genau für solche langen Bulk Operationen ist das eine legitime Sache. Die Doku äußert hier immer noch eine Empfehlung, kein Grundgesetz.
Eine Empfehlung für eine WPF-Anwendung mit einem Einzelaufruf ist halt nicht so wirklich immer anwendbar auf Bulk Operationen.
Da muss man das Evaluieren und Anwenden von Wissen walten lassen.
Kannst EF Core in Blazor auch nicht so umsetzen wie in nem Windows Service, auf ner Handy App... alles verschiedene Rahmenbedingungen.

Das es keine Silver-Bullet gibt, ist mir klar.

Für meine ursprüngliches Problem habe ich auch in der Doku dennoch immer noch keine Lösung gefunden, nämlich warum der DbContext TROTZ using UND ChangeTracker.Clear() den ChangeTracker NICHT aufräumt sondern die darin enthaltenen Entitäten beibehält und somit auch den Speicher nicht frei gibt.

Mein reduziertes Beispiel mit "shared" context und Async API:


private async Task<bool> UpdateFileTags(IEnumerable<IFileInfo> files, CancellationToken cancellationToken)
    // _db enthält den über den Konstruktor injezierten shared context, der komplett vom DI container verwaltet wird.
    foreach (var file in files) {
        var fileRecord = ComposeFileRecord(file); // hier wird NUR noch ein FileModel anhand der gelesenen Tags erstellt, sonst nichts mit der db
        _db.Files.Add(fileRecord);
    }
    await db.SaveChangesAsync();
    return true;
}

Das Verarbeiten mehrerer FileSource-Elemente mit diesem Code-Abschnitt (bewusst ohne Attach der FileSource und Löschen entfernter Einträge) produziert hier nämlich trotzdem noch einen persistenten ChangeTracker, der die Änderungen auch NACH dem Kontextwechsel UND einem Aufruf Dispose beibehält. Auch wenn ich im Dispose meines AppDbContext explizit sage: ChangeTracker.Clear(), so wie es in der Doku vermerkt ist. Und DAS verstehe ich absolut nicht. Wenn der Kontext Disposed wird, muss der ChangeTracker laut Doku eigentlich schon zurück gesetzt werden - aber mit einem manuellen ChangeTracker.Clear() in jedem Fall, trotzdem wird er das nicht.


public class AppDbContext : DbContext
{
    // ...
    public override void Dispose()
    {
        var changedEntities = ChangeTracker.Entries();
        var count = changedEntities.Count(); // count erhöht sich bei jedem Aufruf von dispose und wird nie zurück gesetzt oder wenigstens kleiner
        ChangeTracker.Clear(); // bewirkt offenbar nichts
        base.Dispose();
    }
}

Das gilt übrigens auch, wenn ich den Context über die Factory jedes mal neu erstelle:



private async Task<bool> UpdateFileTags(IEnumerable<IFileInfo> files, CancellationToken cancellationToken)
    using var db = _dbFactory.CreateDbContext();
    foreach (var file in files) {
        var fileRecord = ComposeFileRecord(file); // hier wird NUR noch ein FileModel erstellt
        db.Files.Add(fileRecord);
    }
    await db.SaveChangesAsync();
    return true;
}

16.806 Beiträge seit 2008
vor einem Jahr

Auch wenn ich im Dispose meines AppDbContext explizit sage: ChangeTracker.Clear(),

Trotz weit über 10 Jahren Erfahrung mit EF (und dann noch EF Core), weiß ich nicht mal, was Du da vor hast bzw. wozu das gut sein soll.
So ein Konstrukt, wie Du es hast, hab ich selbst bei sehr komplexen EF Dingen noch nie benötigt oder gesehen (vielleicht hab ich doch noch nie so komplexe Dinge gemacht?).

Ich kann mich nur noch an die EF (nicht Core) Devise erinnern: niemals Dispose manuell aufzurufen oder zu überschreiben.
Bin mir nicht sicher, aber ich glaube, dass das auch für EF Core (bzw. jetzt ja wieder Naming ohne Core) gilt (Kurzer Blick in Google sagt: nein, man soll das immer noch nicht tun).

Wenn man bedenkt, dass der Dispose Pattern besagt, dass Dispose immer idempotent implementiert sein soll, kann ich mir nicht vorstellen das das, was Du da tust, richtig ist - weil das eben nicht (wirklich) idempotent ist.

Gehe jedoch weiterhin davon aus und bin der Meinung, dass Du ein Folgefehler hast, weil Du einfach mit EF, dem Kontext und dem ChangeTracker falsch umgehst.
Nen kurzer Blick in den Code zeigt auch, dass der ChangeTracker aus einer Factory kommt und nicht direkt am DbContext hängt.
Weiterhin sehe ich nicht, dass das Dispose eine Auswirkung auf den Changetracker hat, weshalb hier Dein Verhalten auch so ist, wie es ist. Es wird nur die Referenz auf den ChangeTracker entfernt, aber nicht dessen Instanz beeinflusst.
Es ist also wohl so, was man im Code sehen kann, dass mehrere DB Context den ChangeTracker teilen - erklärt hier also alles.

In meinen Augen bleibts aber dabei: Folgefehler durch völlig falschen Code-Aufbau und Verwendung von EF.

Wenn Du meinst, dass Du alles richtig gemacht hast (ich sage nein), dann musst nen Issue auf GitHub aufmachen.
Die Wahrscheinlichkeit, dass das ein Bug ist, ist extremst gering - das wäre schon lang aufgefallen. Ergo -> Codefehler.

2.078 Beiträge seit 2012
vor einem Jahr

Du zeigst nur die Methode, aber nicht, woher der DbContext kommt?

Der DbContext wird üblicherweise als Scoped registriert.
Wenn Du nun einen BackgroundService hast, der einen DbContext im Konstruktor erhält, dann hast Du da eine DbContext-Instanz, die die ganze Zeit mit dem BackgroundService (also Singleton) lebt - ist vielleicht das dein Fehler?
Deshalb verwendet Abt in seinem Code auch die IServiceScopeFactory, denn dann hast Du deinen Scope, der den DbContext managed - z.B. ein Scope nach Zeit X öffnen, arbeiten, schließen

Also zeig nicht nur die Methode, sondern auch woher der DbContext kommt und wie DbContext und BackgroundService registriert werden.

S
sandreas Themenstarter:in
29 Beiträge seit 2022
vor einem Jahr

Trotz weit über 10 Jahren Erfahrung mit EF (und dann noch EF Core), weiß ich nicht mal, was Du da vor hast bzw. wozu das gut sein soll.
So ein Konstrukt, wie Du es hast, hab ich selbst bei sehr komplexen EF Dingen noch nie benötigt oder gesehen (vielleicht hab ich doch noch nie so komplexe Dinge gemacht?).

Das war nicht Bestandteil von dem, was ich vorhabe, sondern ein Verhalten, das ich nicht verstanden habe.

Ich kann mich nur noch an die EF (nicht Core) Devise erinnern: niemals Dispose manuell aufzurufen oder zu überschreiben.
Bin mir nicht sicher, aber ich glaube, dass das auch für EF Core (bzw. jetzt ja wieder Naming ohne Core) gilt (Kurzer Blick in Google sagt: nein, man soll das immer noch nicht tun).

[...]
Gehe jedoch weiterhin davon aus und bin der Meinung, dass Du ein Folgefehler hast, weil Du einfach mit EF, dem Kontext und dem ChangeTracker falsch umgehst.
Nen kurzer Blick in den Code zeigt auch, dass der ChangeTracker aus
>
und nicht direkt am DbContext hängt.
Weiterhin sehe ich nicht, dass das Dispose eine Auswirkung auf den Changetracker hat, weshalb hier Dein Verhalten auch so ist, wie es ist. Es wird nur die Referenz auf den ChangeTracker entfernt, aber nicht dessen Instanz beeinflusst.
Es ist also wohl so, was man im Code sehen kann, dass mehrere DB Context den ChangeTracker teilen - erklärt hier also alles.

Das ist ein entscheidener Hinweis, vielen Dank für die Recherche. Ich hab das nicht gefunden. Und ich WILL gar nicht Dispose aufrufen oder nachimplementieren. Ich hab das gemacht, um meinem Problem auf die Schliche zu kommen. Hat nicht geholfen, offenbar der falsche Weg, aber das hattest du ja auch schon gesagt. Ich danke dir für deine Mühe, mir das irgendwie beizubringen 🙂

In meinen Augen bleibts aber dabei: Folgefehler durch völlig falschen Code-Aufbau und Verwendung von EF.

Wenn Du meinst, dass Du alles richtig gemacht hast (ich sage nein), dann musst nen Issue auf GitHub aufmachen.
Die Wahrscheinlichkeit, dass das ein Bug ist, ist extremst gering - das wäre schon lang aufgefallen. Ergo -> Codefehler.

Nee, du hast recht und das habe ich auch nie bezweifelt. Ich tendiere dazu, wenn ich in ein Problem laufe, nicht einfach "hinzunehmen", dass man es "halt anders macht" sondern zu verstehen, warum. Und in diesem Fall habe ich versucht, die interna von EFCore besser zu verstehen und zu kapieren, warum der ChangeTracker nicht gecleart werden kann. So einigermaßen habe ich das jetzt wohl.

Es nun so umzusetzen, wie von dir empfohlen, wird ein größeres Problem. Auch, wenn es wirklich gute Beispiele sind, werde ich wohl noch in das ein oder andere Messer reinlaufen müssen - der FileIndexer den ich vorhabe ist kein ganz so einfaches Stück code, wenn man noch nicht Jahrelang mit C# arbeitet 🙂

Du zeigst nur die Methode, aber nicht, woher der DbContext kommt?

Wenn Du nun einen BackgroundService hast, der einen DbContext im Konstruktor erhält, dann hast Du da eine DbContext-Instanz, die die ganze Zeit mit dem BackgroundService (also Singleton) lebt - ist vielleicht das dein Fehler?
Deshalb verwendet Abt in seinem Code auch die IServiceScopeFactory, denn dann hast Du deinen Scope, der den DbContext managed - z.B. ein Scope nach Zeit X öffnen, arbeiten, schließen

Danke. Du hast das Problem auf Anhieb verstanden 🙂 Manchmal drücke ich mich wohl etwas unklar aus. Exakt das werde ich im neuen Branch so umsetzen. Dort werde ich auch den FileIndexer-Code komplett überarbeiten, damit der DbContext nicht voll läuft. Aktuell experimentiere ich nur rum, um einen für mich nachvollziehbaren Weg zu finden.

Problem ist, dass der FileIndexer, der DbSaver und der FileSystemWatcher entkoppelt werden müssen. Denn der FileSystemWatcher muss ja offen bleiben, um Änderungen zu überwachen, ohne das der DbContext offen bleiben muss... Ich probiere mich mal aus und melde mich wieder, wenn ich ne Lösung habe. Dann dürft ihr sie mir gerne wieder in der Luft zerreißen, äh ich meine konstruktive Kritik üben 🙂

2.078 Beiträge seit 2012
vor einem Jahr

Denn der FileSystemWatcher muss ja offen bleiben, um Änderungen zu überwachen

Dann mach ihn zum Singleton und der DbContext bleibt Scoped.
Und nutze es im BackgroundService, wie Abt zeigt, mit einem ServiceScope

16.806 Beiträge seit 2008
vor einem Jahr

Problem ist, dass der FileIndexer, der DbSaver und der FileSystemWatcher entkoppelt werden müssen. Denn der FileSystemWatcher muss ja offen bleiben, um Änderungen zu überwachen, ohne das der DbContext offen bleiben muss

Ja, das is die Basis, dass das je funktionieren kann.

Der FileSystemWatcher ist derjenige, der den Event auslöst.
Und erst im Event findet dann das Öffnen des DbContext statt.