Laden...

Forenbeiträge von sandreas Ingesamt 29 Beiträge

07.09.2022 - 09:53 Uhr

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 🙂

06.09.2022 - 18:36 Uhr

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

05.09.2022 - 21:37 Uhr

@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

04.09.2022 - 17:38 Uhr

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? 🙂

04.09.2022 - 15:48 Uhr

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?

04.09.2022 - 08:31 Uhr

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.

03.09.2022 - 18:56 Uhr

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.

03.09.2022 - 17:54 Uhr

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
28.08.2022 - 03:31 Uhr

Inzwischen wurde die 0.0.9 released, falls noch mal jemand reingucken will. Hat sich schon einiges getan.

23.08.2022 - 09:16 Uhr

Firma dankt. Thema ist beantwortet.

22.08.2022 - 07:27 Uhr

So, erstmal vielen Dank für die Antwort, die hat sehr geholfen.

Die anderen Themen, die Du hast, sind Task Best Practises (zB keine Kaskadierung von Parallelität etc..).

Hast du da eine gute Kurz-Referenz oder anderen Lesestoff für mich (also für "Task Best Practises")?

PS: bitte keine Full Quotes.

Geht klar... das Forum macht es einem nicht unbedingt leicht damit, aber ich achte in Zukunft drauf.

20.08.2022 - 16:33 Uhr

Vielen Dank für die hilfreiche Antwort. Ja, das mit dem Select steht schon recht weit oben auf meiner Liste 🙂 Der Code ist echt Mist so. Ich hatte mal geplant, den Grad Parallelisierung einstellbar zu machen (z.B. mit --jobs=4 oder sowas) um Remote-Dateisysteme schneller zu scannen, aber ich sollte wohl eher YAGNI verfolgen und es sequenziell machen.

Aber deine Bemerkung

Dann lässt sich der Cancellation Token auch viel einfacher anwenden.

hab ich noch nicht richtig verstanden... ich denke, das klügste wäre für den Anfang, statt eines Select mit Tasks einfach ein foreach zu machen, also:


    public override async Task<int> ExecuteAsync(CommandContext context, TagCommandSettings settings,
        CancellationToken cancellation)
    {
        var showDryRunMessage = false;
        foreach (var p in packages)
        {
            if (await ProcessAudioBookPackage(p, settings, cancellation))
            {
                showDryRunMessage = true;
            }
        }
    }
    
    private async Task<bool> ProcessAudioBookPackage(AudioBookPackage p, TagCommandSettings settings,
        CancellationToken cancellation)
    {
        var showDryRunMessage = false;
        foreach (var file in p.Files)
        {
            if (cancellation.IsCancellationRequested)
            {
                break;
            }

            var track = new MetadataTrack(file)
            {
                BasePath = p.BaseDirectory?.FullName
            };
            var status = await _tagger.UpdateAsync(track);

            if (!status)
            {
                _console.Error.WriteLine($"Could not update tags for file {file}: {status.Error}");
                continue;
            }

            var currentMetadata = new MetadataTrack(file);
            var diffListing = track.Diff(currentMetadata);
            if (diffListing.Count == 0)
            {
                if (settings.DryRun || !settings.Force)
                {
                    _console.Write(new Rule($"[green]unchanged: {Markup.Escape(track.Path ?? "")}[/]")
                        .LeftAligned());
                    continue;
                }

                var path = Markup.Escape(track.Path ?? "");
                var message = !track.Save()
                    ? $"[red]Force update failed: {path}[/]"
                    : $"[green]Forced update: {path}[/]";
                _console.Write(new Rule(message)
                    .LeftAligned());
            }
            else
            {
                showDryRunMessage = settings.DryRun;
                var diffTable = new Table().Expand();
                diffTable.Title = new TableTitle($"[blue]DIFF: {Markup.Escape(track.Path ?? "")}[/]");
                diffTable.AddColumn("property")
                    .AddColumn("current")
                    .AddColumn("new");
                foreach (var (property, currentValue, newValue) in diffListing)
                {
                    diffTable.AddRow(
                        property.ToString(),
                        Markup.Escape(newValue?.ToString() ?? "<null>"),
                        Markup.Escape(currentValue?.ToString() ?? "<null>")
                    );
                }

                if (settings.DryRun)
                {
                    _console.Write(diffTable);
                    continue;
                }

                var path = Markup.Escape(track.Path ?? "");
                var message = !track.Save()
                    ? $"[red]Update failed: {path}[/]"
                    : $"[green]Updated: {path}[/]";
                diffTable.Caption = new TableTitle(message);
                _console.Write(diffTable);
            }
        }

        return showDryRunMessage;
    }


oder sehe ich das falsch?

20.08.2022 - 14:10 Uhr

Hallo zusammen,

ich habe kürzlich hier meinen Audio-Tagger tone (https://github.com/sandreas/tone) vorgestellt und nun ein (für mich) interessantes Problem. Und zwar hat mir jemand gemeldet, dass das Drücken von STRG+C inmitten der Veränderung der Metadaten dazu führt, dass Dateien "kaputtgeschrieben" werden, weil die Anwendung ohne Vorwarnung terminiert.

Nun habe ich mich etwas eingelesen und einen Vorschlag eines andren Entwicklers für die Kommandozeilen-Bibliothek spectre.console dazu mehr oder minder unverändert übernommen (siehe hier https://github.com/spectreconsole/spectre.console/issues/701#issuecomment-1081834778):


internal class ConsoleAppCancellationTokenSource
{
    private readonly CancellationTokenSource _cts = new();

    public CancellationToken Token => _cts.Token;

    public ConsoleAppCancellationTokenSource()
    {
        Console.CancelKeyPress += OnCancelKeyPress;
        AppDomain.CurrentDomain.ProcessExit += OnProcessExit;

        using var _ = _cts.Token.Register(() =>
        {
            AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
            Console.CancelKeyPress -= OnCancelKeyPress;
        });
    }

    private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
    {
        // NOTE: cancel event, don't terminate the process
        e.Cancel = true;

        _cts.Cancel();
    }

    private void OnProcessExit(object? sender, EventArgs e)
    {
        if (_cts.IsCancellationRequested)
        {
            // NOTE: SIGINT (cancel key was pressed, this shouldn't ever actually hit however, as we remove the event handler upon cancellation of the `cancellationSource`)
            return;
        }

        _cts.Cancel();
    }
}

Dieser sorgt nun dafür, dass ich beim Drücken von STRG+C oder beim einem Prozessignal (z.B. mit kill und Linux) einen CancellationToken abbreche, anstatt die Anwendung hart zu beenden (zumindest, wenn ich es richtig verstanden habe).

Nun muss ich diesen CancellationToken ja noch an die entsprechende Task-Verwaltung-Einheit übergeben. Das tue ich hier:

https://github.com/sandreas/tone/blob/main/tone/Commands/TagCommand.cs

mit


            var tasks = packages.Select(p => Task.Run(async () =>
            {
                foreach (var file in p.Files)
                {
                     // Datei laden und Tags verändern
                     // ...
                     // Änderungen speichern
                     track.Save()
                }
            }, cancellation)).ToArray();

            await Task.Run(() => Task.WaitAll(tasks), cancellation);

Wie zu sehen ist, teile ich die Dateien zunächst in "Datei-Pakete" auf, die ich dann verarbeite. Jedes dieser Pakete wird als async Task verpackt und über Task.Run mit dem CancellationToken wird die Verarbeitung gestartet. Nun möchte ich bei einem Cancel natürlich nicht warten, bis ein solches Paket komplett verarbeitet ist, da es aus vielen Dateien bestehen kann, sondern das Programm möglichst direkt beenden (falls die Bearbeitung des Pakets mit vielen Dateien sehr lange dauert).

Soweit ich verstanden habe, würde das CancelationTokenSource.Cancel() nun dazu führen, dass der angefangene async Task noch zu Ende läuft und das Programm dann terminiert. Demnach müsste ich folgendes tun, um "direkt" zu Beenden (foreach muss vorzeitig beendet werden):


            var tasks = packages.Select(p => Task.Run(async () =>
            {
                foreach (var file in p.Files)
                {

                     // NEUER CODE
                     if(cancellation.IsCancellationRequested) {
                         break;
                     }
                     // NEUER CODE ENDE

                     // Datei laden und Tags verändern
                     // ...
                     // Änderungen speichern
                     track.Save()
                }
            }, cancellation)).ToArray();

            await Task.Run(() => Task.WaitAll(tasks), cancellation);

Wäre das so richtig oder habe ich etwas übersehen / nicht verstanden?

23.07.2022 - 18:47 Uhr

Ich denke du schießt gewaltig über das Ziel mit der Implementierung hinaus.
Das Einzige was du überschreiben musst sind die abstrakten Members von Stream - alle anderen kannst du genau so lassen wie sie sind. Ganz besonders macht es keinen Sinn Member zu überschreiben und dann den Basis-Member wieder eins zu eins aufzurufen...

Cool danke für das Beispiel. Ich werde da jedenfalls noch mal etwas dran verschlanken und alles wegwerfen, was ich nicht brauche. Sehr schön sauberer Code.

23.07.2022 - 08:48 Uhr

... ich finde die StreamLimiter Klasse ist weder Teig noch Mehl. Irgendwie wird der _innerStream beschränkt, dann aber doch nicht so ganz konsequent.
Z.B. Position gibt diese vom _innerStream an.

So einen Typen würde ich eher SubReadStream nennen, der eine "View" über den _innerStream legt, dabei zu Beginn von Außen Offset := 0 und Länge := das Limit ist.
Intern kann das ja auf den _innerStream umgerechnet werden. Z.B. beim Setzen der Position halt den Offset berücksichtigen.

Beim Lesen, egal ob die byte[] od. Span<byte>-Überladung, ist count bzw. der buffer (bei Span-Überladung) entsprechend zu verkleinern, falls über das Limit hinaus gelesen würde.

Für den Verwender sollte dieser Stream jedenfalls nur den nutzbaren Teil (~ "View") widerspiegeln...

Vielen Dank für dein Feedback. Sehe ich tatsächlich ähnlich. Aber interessant, wie man anhand eines Code-Beispiels wieder an den Kern der ursprünglichen Frage zurück kommt.
Natürlich ist das wrappen des Streams hier nicht optimal gelöst, ich denke, ich muss noch ALLE methoden, die den Ursprungs-Stream benutzen und über das festgelegte offset und limit hinausgehen, "clampen", also die gewünschte Position auf den Bereich limitieren.

...sodass ein Wrapper das eigentliche Mittel wäre und keine Vererbung...

Absolut richtig. Leider ist in C# Stream kein Interface, sondern eine Klasse. Die Library verlangt ausdrücklich einen Stream, also kann der Wrapper nur implementiert werden, in dem er auch vom Stream erbt. Schön ist das nicht, aber meines Wissens nach die einzige Möglichkeit, um die Library wie sie ist zu nutzen.

Ich wollte noch kurz Bescheid geben, dass die Klasse bisher tut, was sie soll (ob der Schwächen, die sie hat) und effizient genug ist, um auch große Dateien zu Hashen. Folglich ist mein Problem erstmal gelöst, vielen Dank.

Falls noch jemand einen saubereren Implementierungsvorschlag hat, bin ich gespannt auf die Code-Änderung bzw. die jeweilige Implementierung.

Vielen Dank für eure Hilfe.

19.07.2022 - 19:29 Uhr

So, für alle, die es mal brauchen - hier ist ein netter Beitrag auf StackOverflow: how-to-set-length-in-stream-without-truncated

Ich war so frei und habe selbst eine "verbesserte" Version davon geschrieben - muss diese aber noch auf Herz und Nieren testen. Bisher wirkt es ganz gut und funktioniert, allerdings könnten da durchaus noch off-by-one errors drin sein.


// inspired by https://stackoverflow.com/questions/33354822/how-to-set-length-in-stream-without-truncated
public class StreamLimiter: Stream
{
    private readonly Stream _innerStream;
    private readonly long _limit;
    public StreamLimiter(Stream input, long offset, long length)
    {
        _innerStream = input;
        _innerStream.Position = ClampPosition(_innerStream, offset);
        _limit = ClampPosition(_innerStream, _innerStream.Position + length);
    }

    private static long ClampPosition(Stream input, long offset) => Math.Min(Math.Max(offset, 0), input.Length);

    public override bool CanRead => _innerStream.CanRead;
    public override bool CanSeek => _innerStream.CanSeek;
    public override bool CanWrite => false;
    public override void Flush() => _innerStream.Flush();
    public override long Length => _limit;

    public override long Position
    {
        get => _innerStream.Position;
        set => _innerStream.Position = value; // todo: clamp position to limited length?
    }

    public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, ClampCount(count));

    public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);

    public override void SetLength(long value) => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

    public override bool CanTimeout => _innerStream.CanTimeout;

    public override int ReadTimeout
    {
        get => _innerStream.ReadTimeout;
        set => _innerStream.ReadTimeout = value;
    }

    public override int WriteTimeout
    {
        get => _innerStream.ReadTimeout;
        set => _innerStream.ReadTimeout = value;
    }

    public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => _innerStream.BeginRead(buffer, offset, ClampCount(count), callback, state);

    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw new NotSupportedException();

    // do not close the inner stream
    public override void Close() { }

    public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);

    public override int EndRead(IAsyncResult asyncResult) => _innerStream.EndRead(asyncResult);

    public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.ReadAsync(buffer, offset, ClampCount(count), cancellationToken);

    public override int ReadByte() => ClampCount(1) == 0 ? -1 : _innerStream.ReadByte();

    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException();

    public override void WriteByte(byte value) => throw new NotSupportedException();

    private int ClampCount(int count) => (int)Math.Min(count, ClampPosition(_innerStream, _limit - _innerStream.Position));
}

19.07.2022 - 16:50 Uhr

die Wrapper Klasse wäre vermutlich der einfachste Weg.
Jedoch solltest du nicht SetLength auf dem gewrappten (File)Stream aufrufen. Dadurch verkleinerst deine gewrappte Datei (schneidest Daten am Ende ab), was vermutlich nicht gewollt ist.

Ja, das mit dem "verkleinern" ist mir klar - spielt in meinem Fall aber keine Rolle, da der Stream "readonly" ist und nur innerhalb einer Methode genutzt wird - ist also gekapselt. Zur not könnte ich den StreamLimiter.Dispose überschreiben, dass er die Länge des "innerStream" wieder zurück auf den Originalwert setzt.

ganz verstehe ich das Problem nicht bzw. warum soviel drumherum diskutiert wird.
Schreib einen Stream-Wrapper, dessen Ctor Offset und Länge auch übergeben wird, und das Problem ist gelöst.

Per Definition der Stream.Read-APIs wird maximal count Bytes gelesen, d.h. es kann weniger sein. Falls nichts mehr zu lesen ist, so wird 0 zurückgegeben.
Damit lässt sich das Problem fast trivial lösen.

Ja, ich dachte, das gibt es schon fertig bei nuget (irgendeine StreamUtils lib). Man kann da bestimmt wieder einiges falsch machen, wenn man die interna von Stream nicht genau kennt.

Ich denke, ich habs jetzt gelöst. Ausgiebige Tests stehen noch aus. Vielen Dank für die ganzen Kommentare.

19.07.2022 - 08:34 Uhr

Ich glaube wir haben uns gewaltig missverstanden... die Library, die ich nutzen möchte ist NICHT von mir geschrieben. Daher kann ich an dem Interface der Hash-Funktion auch nichts ändern. Ist also nicht mein Code...

Den Filestream habe ich bereits - durch die Library System.Abstractions.IO, daran kann ich also auch erstmal nix ändern. Natürlich könnte ich die FileSystem-Abstraktionen weglassen und einen eigenen Filestream schreiben, aber ein StreamWrapper schien mir die beste und vor allem am Wenigsten aufwändige Option. Ein Beispiel:


public class StreamLimiter: Stream
{
    private readonly Stream _innerStream;
    private readonly long _offset;
    private readonly long _limit;

    public StreamLimiter(Stream innerStream, long offset, long limit=long.MaxValue)
    {
        _offset = offset;
        _limit = limit;
        innerStream.Position = offset;
        innerStream.SetLength(offset + limit);
        _innerStream = innerStream;
    }

    public override void Flush() => _innerStream.Flush();

    public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);

    public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
    public override void SetLength(long value) => _innerStream.SetLength(Math.Min(value, _limit));

    public override void Write(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }

    public override bool CanRead { get; }
    public override bool CanSeek { get; }
    public override bool CanWrite { get; }
    public override long Length { get; }
    public override long Position { get; set; }
}

Ich versuche es aber mal mit der


stream.SetLength(450)

Methode. Ich hatte nicht gesehen, das es die gibt und gedacht, Length wäre readonly. Wenns damit geht, ist mein Problem gelöst.

18.07.2022 - 23:20 Uhr

Ich habe eine Library mit einer API zum Berechnen eines Hash-Werts, dass einen Stream entgegen nimmt, sonst nix (kein offset, kein limit).


var hash = xxHash64.ComputeHash(s)

Diese nimmt den Stream und berechnet für den gesamten Bytestrom den Hash. Ich habe aber nun einen Stream, wo ich nur für einen Teil des Bytestroms den Hash berechnet haben möchte. Leider kann ich bei der Library nicht sagen: Hör nach Length X auf. Die Position kann ich zwar setzen, aber dann nimmt er ab dieser Position den Rest des Bytestroms und errechnet den Hash, statt nach der Länge X aufzuhören.

Genauer:

  • Ich habe eine Datei mit 500 bytes
  • Ich möchte den Hash von byte 50 bis 450 errechnen, da die ersten 50 bytes und die letzten "veränderliche" daten enthalten, die nicht zum hash gehören sollen.
  • Setze ich beim Filestream die Position auf 50, rechnet er von 50 - 500 den Hash aus (was unerwünscht ist, da die letzten 50 bytes NICHT zum hash gehören sollen)

Leider gibt das Interface der Library das nicht her. Sprich:

  • Variante 1: Entweder kopiere ich den Teil, den ich hashen möchte, in einen neuen Stream, z.B. MemoryStream (Speicherintensiv, da große Dateien möglich sind)
  • Variante 2 (bevorzugt): Oder ich Wrappe den Stream, so das er beim Lesen (Read) nicht erst bei 500 aufhört, sondern schon bei 450 (das wäre meine Bevorzugte variante)

Variante 1:


var partialStream = new MemoryStream();
copyStream(s, partialStream, 50, 450); // eigene Methode, die einen Teil des Streams in einen neuen kopiert

// das hier geht schon, er errechnet den hash nur von 50 - 450, ist aber bei großen Dateien sehr speicherintensiv, da fast die gesamte Datei in den speicher kopiert werden muss
var hash = xxHash64.ComputeHash(partialStream);

Variante 2:


// diese Klasse StreamLimiter gibt es noch nicht, sondern die müsste ich entwickeln (oder eine Library nutzen). StreamLimiter extended dabei Stream, schreibt aber die Position, Length und Read Methodik so um, dass der FileStream immernoch 500 bytes hat, dieser aber nur von 50 - 450 gelesen werden kann. 
// Ich simuliere quasi eine Datei, die keinen Header und Trailer hat OHNE die Datei komplett in den Speicher zu laden.
var wrappedStream = new StreamLimiter(s, 50, 450);
var hash = xxHash64.ComputeHash(wrappedStream);

Das hier macht glaube ich sowas, allerdings nur für MemoryMappedFiles, nicht für Streams oder echte Dateien.
MemoryMappedFile Klasse (System.IO.MemoryMappedFiles)

18.07.2022 - 21:50 Uhr

Wenn du mit Stream arbeitest, kannst du auch die Position setzen.

Die Position ja, die Length leider nicht...

Es wäre auch sinnvoll, wenn du die Daten blockweise liest und dann hasht, dann musst du nicht die ganze Datei lesen.
So macht man es auch mit den .NET Hash Klassen.

Das passiert in der Hash-Funktion selbst schon über einen Buffer, ich gebe nur einen Stream rein, der wird blockweise gelesen und der Hash gebildet. Problem ist aber, dass der Hash über den GANZEN Stream gebildet wird, allerdings soll er nur über einen Teil gebildet werden.
Setze ich die Position auf 343, überspringt er den Header, allerdings stoppt er nicht beim Trailer und nimmt den mit rein...

Ich brauche also wirklich einen Wrapper, um die Library nutzen zu können.

18.07.2022 - 21:36 Uhr

Hallo zusammen,

ich habe folgendes Problem:

Ich möchte via Stream einen xxHash über eine Audio-Datei bilden, allerdings NUR über den Datenstrom der Audiodaten, nicht über die Metadaten...Ich habe zunächst mal einen FileStream von einer mp3-Datei, ganz klassisch von Position=0 bis Position=stream.Length - darin sind aber auch die Metadaten enthalten. Bilde ich nun einen Hash über den gesamten Stream, ändert sich der, sobald ich z.B. das Album ändere. Das möchte ich vermeiden.

Jetzt weiß ich, dass die Audio-Daten z.B. nur von Position=343 bis Position=8838492 (stream.Length - 889) gehen, die Datei also einen Metadaten-Header und Trailer hat, der mich für den Hash nicht interessiert.

Meiner xxHash-Funktion kann ich aber nur einen Stream geben, ohne Einschränkung von wo bis wo er den lesen können soll.


var s = file.OpenRead();
var hashBytes = BitConverter.GetBytes(xxHash64.ComputeHash(s));

Meine erste Idee ist natürlich einen MemoryStream zu nehmen und den relevanten Teil des FileStreams einfach komplett da rein zu kopieren. Allerdings können die Audio-Files seeehr groß werden > 1GB und wesentlich effizienter wäre es, einen StreamWrapper zu schreiben, der das Lesen auf byte 343 bis 8838492 einschränkt, ohne die komplette Datei in den Speicher zu kopieren:


var s = file.OpenRead();
var limited = new StreamLimiter(s, 343, 8838492);
var hashBytes = BitConverter.GetBytes(xxHash64.ComputeHash(limited));

Gibt es da schon was fertiges?
Falls nicht, wie würde ich das am besten machen?

13.07.2022 - 17:26 Uhr

Es mag so klingen... ich verstehe auch deinen Punkt. Aber zunächst mal würde es mir nie einfallen, jemanden unnötig zu kritisieren, der seine Arbeit kostenlos oder mit einer Passion für die Sache macht (Open Source Community). Ich bin selbst Open Source Entwickler und kann daher sehr gut nachvollziehen, wie man sich da fühlen würde.

Ich habe auch nicht gesagt, dass die Open Source Gemeinde Nachholbedarf hat, sondern Microsoft. Wenn die sich darauf verlassen, die Doku kostenlos von Freiwilligen pflegen zu lassen, sehe ich da jedenfalls Nachholbedarf. Das mag man aber anders sehen.

Anders gesagt: Die Doku, die Microsoft zur Verfügung stellt (wer auch immer sie schreibt), holt mich nicht ab / sagt mir nicht zu... Allerdings schweife ich zu weit vom Thema ab: Du hast recht, wenn es nach Meinung der Spezialisten hier (und dieses Attribut schreibe ich euch nach dem Lesen eurer Beiträge hier durchaus zu) die aktuellste / beste Doku ist, die es gibt, dann werde ich mich da durch quälen müssen, auch wenn ich finde, das es viel zu viel Prosa und viel zu wenige Beispiele gibt - ich lese vermutlich einfach lieber sauberen Code 🙂

Nix für ungut, ich bin für jeden Link und für jede Hilfe dankbar... aus meiner Verzweiflung bezüglich der Doku ist dieser Post ja erst entstanden.

13.07.2022 - 15:20 Uhr

Ja, ich sollte mehr auf die Termini achten. Ich bin seit 18 Jahren Entwickler und kenne mich mit Dokumentation normalerweise gut aus. Microsoft schafft es nur irgendwie nicht, die für mich wesentlichen Infos auf einen Nenner zu bringen. Obwohl ich nach langen Artikeln die Materie besser "verstehen" würde, ist es oft der Quickstart, der mich ins Thema reinbringt. Da hat Microsoft auf jeden Fall noch Nachholbedarf wie ich finde.

Aber du hast recht... ich werde mich jetzt noch mal optimistisch an die Sachen dranwagen und einfach mal ein paar Tage nur Lesen / Experimentieren, statt mit einem Projekt vor Augen eine schnelle Lösung finden zu wollen...

Danke für Eure Hilfe. Ich hab jetzt keinen Bedanken Button gefunden... wollte es nur erwähnt haben.

13.07.2022 - 14:21 Uhr

Danke. Das ist genau der Mist, mit dem ich mich aktuell rumschlage... es gibt tonnenweise veraltete Tutorials und dazu noch sehr viele Varianten offiziell von Microsoft... nicht leicht zu durchschauen. Ich hab mit der offiziellen Microsoft-Dokumentation oft noch so meine Schwierigkeiten, das ist irgendwie kein Schreibstil, den ich leicht verinnerliche.

C# bewegt sich echt schnell... Ich werde sicher in den nächten Tagen dazu kommen, mir das mal genauer anzuschauen, dann stelle ich weitere Fragen, falls nicht nicht zurecht komme. Vielen Dank.

13.07.2022 - 06:27 Uhr

Hier solltest du dir mal OAuth2 mit der WebAPI von Microsoft anschauen.

@T-Virus
Cool, schon mal danke, das war lesenswert. Ich hab noch das hier gefunden: https://www.c-sharpcorner.com/UploadFile/ff2f08/token-based-authentication-using-Asp-Net-web-api-owin-and-i/

Das werde ich auch mal ausprobieren. Weitere Resourcen, die lesenswert sind, werde ich hier auch mal posten, damit andere meine Erfahrungen nachlesen können 🙂

11.07.2022 - 08:16 Uhr

Hallo zusammen,

ich würde gerne eine kleine Web-API für meine private Nutzung schreiben. Hier würde ich ungern auf einen Remote-Authentifizierungs-Server oder die Cloud zugreifen, sondern das ganze Anwendungsbezogen speichern, z.B. in einer SQLite Datenbank.

Jetzt möchte ich natürlich auch nicht von anfang an ein riesiges Loch in meine Anwendungs-Sicherheit bohren, daher möchte ich mich vorher mal etwas einlesen.

Ich hätte jetzt erstmal an eine simple Passwort-Authentifizierung mittels Benutzer-Tabelle mit Passwort-Hashes gedacht. Macht man sowas heute noch? Falls ja, hat jemand ein Beispiel oder eine Ressource, wo ich nachlesen kann wie?

Falls nein, wie würde man es machen, ohne gleich die Cloud mit einbeziehen zu müssen?

Vielen Dank und viele Grüße
Andreas

28.06.2022 - 18:40 Uhr

Ja ich mag das Projekt*. Schaut sehr sauber aus, auf GH-ReadMe

Cool... das motiviert. Das mit den Charset-Sachen ist mir aufgefallen, vermutlich ein Copy&Paste fehler. Sollte korrigiert sein 🙂

Bei den Tests noch ein paar mehr Fälle testen (v.a. Randfälle / edge-cases wie keine Tags vorhanden beim Parser, etc.).

Für die Tests kommt ein rewrite, so das ich mindestens 80% abdecke, sobald die "Experimente" mit den Libraries abgeschlossen sind. Aktuell habe ich noch ein paar Probleme mit Jint und CliWrap... aber ich glaube das hab ich bald im Griff.

Die von spectre.console antworten auf meine GH Issues seit Wochen nicht, das ist etwas frustrierend. Daher hab ich das in meine "Known Issues" bei den Release Notes gepackt 😉

28.06.2022 - 13:47 Uhr

Hey gfoidl,

na das nenn ich doch mal rasches und hervorragendes Feedback. Vielen Dank.

auch wenns eine (single file) EXE als Ausgabe gibt und es per-se keine Klassenbibliothek ist, so würde ich interne Typen als internal markieren, da dies Implementierungsdetails sind. Der Vorteil davon ist, dass diese nach belieben geändert werden können ohne das "public api" zu verletzen und entsprechend SemVer eine neue Hauptversionsnummer erfordern würde.

Sind die Implementierungsdetails internal so könnten ein paar Optimierungen bzgl. Effizienz (CPU und Speicher / GC) durchgeführt werden.

Wieder was gelernt. Wusste gar nicht genau, wie sich das Schlüsselwort internal auswirkt. Cool.

Weiters würde ich das Projekt aufteilen, so dass für den Benutzer ...

Das hatte ich tatsächlich sogar schon auf der Liste, allerdings ist gut, nochmal zu hören, dass das kein overengineering wäre (zumindest nicht per se...).

Und allgemein? Magst du das Projekt? Neutral oder eher "so wird das nie was"? 🙂

LG
sandreas

28.06.2022 - 03:47 Uhr

Hallo zusammen,

ich wollte mich schon länger hier registrieren, da ich eifrig mitlese.

Nun nutze ich mal die Gelegenheit, mein Open-Source Projekt hier vorzustellen:

tone

Es ist ein Kommandozeilenprogramm zum Anzeigen und Bearbeiten von Audio-Metadaten und Tags, das unter allen gängigen Betriebssystemen als einzelne Binary laufen müsste - sprich: Runterladen und ausführen, ohne Abhängigkeiten (abgesehen vom M1 Mac, da scheint es noch Probleme zu geben). Ich habe es hauptsächlich geschrieben, um fehlende Features speziell für Hörbücher existierender Programme zu ergänzen, inzwischen hat es sich aber zu einem für mich echt brauchbaren Tool für alles Mögliche an Audio-Files gemausert.

Habe auch schon überlegt, es im Bereich Code-Review zu posten, da es noch ein recht junges Projekt ist. Ich freue mich also über jegliche Art von Feedback (Benutzung, Verbesserungsvorschläge, Code-Kommentare, etc.).

Worauf ich noch hinweisen möchte: Ich lasse das Projekt automatisch auf github über Github-Actions bauen und veröffentlichen. Falls also jemand ein Template für so etwas sucht, darf er sich gerne bedienen.

Features:

  • Unterstützung diverser Datei- und Metadatenformate (id3, ape, mp4, etc.)
  • Sehr viele Meta-Felder inkl. benutzerdefinierter Felder, Kapitel und Cover (Artist, Album, etc.)
  • Taggen über Pfade (Metadaten werden über Templates aus Dateipfad geschrieben, z.B. --path-pattern="audiobooks/%g/%a/%s/%p - %n.m4b")
  • JavaScript-Engine + API zum Erweitern um eigene selbstgeschriebene Features

Langfristig plane ich noch folgende Erweiterungen:

  • Erweiterung von dump um JavaScript-Engine Unterstützung um benutzerdefinierte Ausgabeformate oder Exporte zu erzeugen
  • rename - Audio-Dateien anhand von Tags umbenennen
  • split - um eine Audio-Datei anhand von Kapitel aufzuteilen
  • merge - mehrere Audio-Dateien zu einer zusammenfügen
  • convert - Audio-Dateien konvertieren

Nutzen möchte ich dazu ffmpeg und fdkaac über eine Prozess-Integration (CliWrap).

Im Code genutzt werden:* c# 6

  • spectre.console als Kommandozeilen-Bibliothek
  • Dependency Injection
  • jint als JavaScript interpreter
  • z440.atl.core für das Audio-Tagging

Würde mich sehr über Feedback jeglicher Art freuen (solange es konstruktiv ist 🙂.