Daten-Persistenz: JSON & CSV in Dateien
Globals sind perfekt für einzelne Werte: ein Zähler, der letzte Gewinner, ein Flag. Sobald du aber eine wachsende Liste speichern willst, etwa ein Log aller gespielten Matches, oder die Daten ausserhalb von Streamer.bot auswerten möchtest, etwa in Excel oder einem Python-Skript, wird eine Datei zur besseren Wahl. Eine Datei kannst du öffnen, sichern, versionieren und mit anderen Programmen lesen. Dieser Abschnitt zeigt, wie du aus C# heraus mit System.IO Dateien schreibst, eigene Objekte als JSON ablegst und beim Start wieder einliest, plus den einfachen CSV-Weg ganz ohne Library.
Doku:
Wann Datei statt Global
Abschnitt betitelt „Wann Datei statt Global“Beide speichern Zustand über die Action hinaus. Die Frage ist, wie die Daten aussehen und wer sie liest.
| Kriterium | Global | Datei |
|---|---|---|
| Einzelwert (Zähler, Flag) | Ideal | Overkill |
| Wachsende Liste, viele Einträge | Unhandlich | Ideal |
| Lesen ausserhalb von SB (Excel, Editor) | Nein | Ja |
| Strukturierte Objekte | Nur mit JSON-String drumherum | Nativ als JSON-Datei |
| Geschwindigkeit | Schneller (RAM) | Langsamer (Disk) |
| Backup, Versionierung | Nur über Export | Datei einfach kopieren |
Faustregel: Einzelwerte bleiben im Global. Listen, Tabellen und alles was ein Mensch oder ein anderes Programm lesen soll, kommt in eine Datei. Das No-Code-Pendant dazu ist das File I/O Pattern.
System.IO: lesen, schreiben, anhängen
Abschnitt betitelt „System.IO: lesen, schreiben, anhängen“Die File-Klasse aus System.IO deckt fast alles ab, was du brauchst. Drei Methoden reichen für den Anfang:
using System.IO;
public class CPHInline { public bool Execute() { string path = @"D:\streamerbot-data\notiz.txt";
// komplett überschreiben (legt die Datei an falls nicht vorhanden) File.WriteAllText(path, "Hallo Welt");
// eine Zeile anhängen (legt die Datei ebenfalls an falls nötig) File.AppendAllText(path, "\nNoch eine Zeile");
// den ganzen Inhalt als String einlesen string inhalt = File.ReadAllText(path); CPH.LogInfo($"Datei-Inhalt: {inhalt}");
return true; }}File.WriteAllTextschreibt einen String und überschreibt eine bestehende Datei komplett.File.AppendAllTexthängt an, ohne den alten Inhalt zu löschen. Genau richtig für Logs.File.ReadAllTextliest den gesamten Inhalt als einen String.
Das using System.IO; gehört nach oben in deinen Code. DLL-Referenzen und Using-Direktiven verwaltest du sonst im References-Tab der C#-Action, beschrieben in der offiziellen Doku zu Custom-DLLs.
Pfade richtig schreiben
Abschnitt betitelt „Pfade richtig schreiben“In C#-Strings ist der Backslash ein Escape-Zeichen. Ein Windows-Pfad mit einfachen Backslashes wird deshalb falsch interpretiert. Zwei saubere Wege:
// 1) Backslashes verdoppelnstring a = "D:\\streamerbot-data\\matches.json";
// 2) verbatim-String mit @ davor, dann zählt der Backslash normalstring b = @"D:\streamerbot-data\matches.json";Der @-Weg ist lesbarer und der empfohlene Standard. Nutze immer einen absoluten Pfad. Ein relativer Pfad wie "matches.json" landet sonst irgendwo im Streamer.bot-Programmverzeichnis, was du selten willst und schwer wiederfindest.
Eigene Klasse als JSON-Liste speichern
Abschnitt betitelt „Eigene Klasse als JSON-Liste speichern“Der eigentliche Mehrwert: du speicherst nicht nur Text, sondern echte Objekte. Definiere eine kleine Klasse, sammle sie in einer List<T> und serialisiere die ganze Liste mit Newtonsoft in einen JSON-String. Newtonsoft.Json ist in C#-Actions standardmässig referenziert, die Methoden findest du in der C#-Methoden-Referenz.
using System;using System.IO;using System.Collections.Generic;using Newtonsoft.Json;
public class MatchResult { public string Map { get; set; } public bool Won { get; set; } public int Kills { get; set; } public long Timestamp { get; set; }}
public class CPHInline { private const string Path = @"D:\streamerbot-data\matches.json";
public bool Execute() { // 1) bestehende Liste laden (oder leere Liste beim ersten Lauf) List<MatchResult> matches; if (File.Exists(Path)) { string json = File.ReadAllText(Path); matches = JsonConvert.DeserializeObject<List<MatchResult>>(json) ?? new List<MatchResult>(); } else { matches = new List<MatchResult>(); }
// 2) neuen Eintrag aus Argumenten bauen (TryGetArg statt args[...]) CPH.TryGetArg("map", out string map); long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; matches.Add(new MatchResult { Map = map ?? "unknown", Won = true, Kills = 17, Timestamp = now });
// 3) komplette Liste als JSON zurückschreiben (formatiert = lesbar) string output = JsonConvert.SerializeObject(matches, Formatting.Indented); File.WriteAllText(Path, output);
CPH.SendMessage($"📊 Match gespeichert, insgesamt {matches.Count} Matches im Log."); return true; }}Das Muster ist immer dasselbe: laden, ändern, ganz zurückschreiben. Beim ersten Lauf existiert die Datei nicht, File.Exists ist false und du startest mit einer leeren Liste. JsonConvert.DeserializeObject<List<MatchResult>> macht aus dem JSON-Text wieder echte C#-Objekte, JsonConvert.SerializeObject mit Formatting.Indented schreibt sie menschenlesbar zurück. Den ?? new List<MatchResult>()-Teil brauchst du, weil DeserializeObject bei leerem oder kaputtem Inhalt null liefern kann. Den map-Wert holst du mit CPH.TryGetArg, weil ein direkter Zugriff über args["map"] bei fehlendem Key eine Exception wirft und die Instanz blockieren kann.
CSV simpel bauen und parsen
Abschnitt betitelt „CSV simpel bauen und parsen“Wenn die Daten flach sind und in Excel landen sollen, ist CSV oft praktischer als JSON. Für einfache Fälle reichen string.Join zum Bauen und Split zum Lesen, ganz ohne Library.
using System;using System.IO;using System.Collections.Generic;
public class CPHInline { private const string Path = @"D:\streamerbot-data\matches.csv";
public bool Execute() { // Bauen: eine Zeile zusammensetzen und anhängen CPH.TryGetArg("map", out string map); CPH.TryGetArg("kills", out string kills); string zeile = string.Join(";", map ?? "", kills ?? "0", "true"); File.AppendAllText(Path, zeile + "\n");
// Parsen: alle Zeilen lesen und in Felder zerlegen if (File.Exists(Path)) { foreach (string line in File.ReadAllLines(Path)) { if (string.IsNullOrWhiteSpace(line)) continue; string[] felder = line.Split(';'); string m = felder[0]; string k = felder[1]; CPH.LogInfo($"Map: {m}, Kills: {k}"); } }
return true; }}Im deutschsprachigen Excel ist das Semikolon ; als Trenner oft praktischer als das Komma, weil Excel-DE Komma als Dezimaltrennzeichen liest. File.ReadAllLines gibt dir ein string[] mit allen Zeilen, Split(';') zerlegt jede Zeile in ihre Felder.
Datei-Zugriffe mit try-catch absichern
Abschnitt betitelt „Datei-Zugriffe mit try-catch absichern“Datei-Zugriffe können fehlschlagen, weil die Datei fehlt, gesperrt ist oder der Pfad nicht stimmt. Ohne Absicherung bricht die ganze Action mit einer Exception ab. Pack den Zugriff deshalb in einen try-catch und logge den Fehler, statt die Action stillschweigend sterben zu lassen.
using System.IO;
public class CPHInline { public bool Execute() { string path = @"D:\streamerbot-data\matches.json"; try { string json = File.ReadAllText(path); CPH.LogInfo($"Geladen, {json.Length} Zeichen."); return true; } catch (FileNotFoundException) { CPH.LogWarn("Datei noch nicht vorhanden, das ist beim ersten Lauf normal."); return false; } catch (IOException ex) { // z.B. Datei von einem anderen Programm gesperrt CPH.LogError($"Datei-Fehler: {ex.Message}"); return false; } }}So weisst du im Log genau, was schiefging, und der Rest deiner Automation läuft kontrolliert weiter, statt mit einer rohen Exception abzubrechen.
Häufige Fallen
Abschnitt betitelt „Häufige Fallen“- Datei existiert beim ersten Lauf nicht: Jeder Lese-Zugriff muss den allerersten Start aushalten, an dem noch nichts geschrieben wurde. Immer mit
File.Exists(path)prüfen oder dentry-catchaufFileNotFoundExceptionnutzen und mit einer leeren Liste starten. - Relativer Pfad zeigt ins SB-Verzeichnis: Schreibst du nur
"matches.json"statt eines absoluten Pfads, landet die Datei im aktuellen Arbeitsverzeichnis von Streamer.bot, nicht dort wo du sie vermutest. Immer absolut, etwa@"D:\streamerbot-data\matches.json". - Backslash nicht escaped:
"D:\daten\x.json"wird in C# falsch interpretiert, weil\dund\xEscape-Sequenzen sind. Entweder@"D:\daten\x.json"mit@oder"D:\\daten\\x.json"mit doppelten Backslashes. - Ordner fehlt:
File.WriteAllTextlegt die Datei an, aber nicht den Ordner. Verzeichnis vorher anlegen oderDirectory.CreateDirectory(...)aufrufen, sonstDirectoryNotFoundException. - Argumente direkt indizieren:
args["map"]wirft bei fehlendem Key eineKeyNotFoundExceptionund kann die Instanz blockieren. ImmerCPH.TryGetArg("map", out string map)nutzen und einen Default setzen. - Gleichzeitiger Zugriff: Wenn zwei Actions oder ein externes Programm dieselbe Datei zur selben Zeit anfassen, kann eine
IOExceptionfliegen oder die Datei kurz leer wirken. Bei häufigen Schreibvorgängentry-catchsetzen und im Zweifel über die Action-Queue serialisieren, statt parallel zu schreiben. - DeserializeObject liefert null: Bei leerem oder beschädigtem JSON gibt
JsonConvert.DeserializeObject<List<T>>einnullzurück. Immer mit?? new List<T>()absichern, sonst kommt beim nächsten.AddeineNullReferenceException.