Zapisywanie danych do pliku w C# wygląda prosto tylko na pierwszy rzut oka. W praktyce szybko pojawiają się pytania o to, czy nadpisać istniejący plik, dopisywać kolejne linie, użyć zapisu synchronicznego czy asynchronicznego oraz jak nie zepsuć ścieżki albo kodowania. Poniżej pokazuję metody, które faktycznie sprawdzają się w codziennej pracy, wraz z przykładami, różnicami i pułapkami, których najczęściej nie widać na początku.
Najprostszy zapis pliku w C# zależy od tego, czy chcesz nadpisywać, dopisywać czy serializować dane
- Do krótkiego tekstu najczęściej wystarczy
File.WriteAllText. - Jeśli dokładasz kolejne wpisy, lepsze będą
File.AppendAllTextlubStreamWriter. - Przy większych plikach i pracy w tle warto przejść na wersje asynchroniczne.
- Obiekty biznesowe zwykle zapisuje się jako JSON, a nie ręcznie sklejany tekst.
- Najwięcej problemów robią: błędna ścieżka, brak katalogu docelowego i złe uprawnienia.
Najprostszy zapis tekstu do pliku
Jeśli chcesz po prostu zapisać kilka linii tekstu, najkrótsza droga prowadzi przez klasę File. Dokumentacja Microsoft Learn zwraca uwagę, że te metody tworzą plik, zapisują zawartość i zamykają go od razu po operacji. To dobry wybór przy notatkach, prostych eksportach, konfiguracji albo wynikach testów.
using System.IO;
var path = Path.Combine("dane", "notatka.txt");
File.WriteAllText(path, "Pierwsza linia tekstu");W tym wariancie istniejący plik zostanie nadpisany. To ważne, bo początkujący często zakładają, że zapis tylko dopisze treść na końcu. Jeśli chcesz zapisać kilka wierszy naraz, użyj File.WriteAllLines:
var lines = new[]
{
"Linia 1",
"Linia 2",
"Linia 3"
};
File.WriteAllLines(path, lines);Domyślnie zapis odbywa się w UTF-8 bez BOM, więc w większości nowych projektów to bezpieczny i przewidywalny wybór. Jeśli integrujesz się z narzędziem, które wymaga innego kodowania, skorzystaj z przeciążenia z Encoding. Gdy jednak dane mają trafiać do już istniejącego pliku bez kasowania wcześniejszej zawartości, przechodzę do innej techniki.
Dopisywanie danych bez kasowania poprzedniej zawartości
W logach, dziennikach zdarzeń i prostych plikach historii najczęściej nie chcesz nadpisywać starego wpisu, tylko dodać nowy. Do tego służą metody dopisujące. Najszybsza opcja to File.AppendAllText, a gdy pracujesz z wieloma liniami albo iterujesz po danych, sensowniej użyć StreamWriter w trybie dopisywania.
using System.IO;
var path = Path.Combine("dane", "log.txt");
File.AppendAllText(path, "Nowy wpis logu" + Environment.NewLine);Jeśli zapisujesz wiele rekordów w pętli, otwieranie i zamykanie pliku przy każdej linijce kosztuje więcej niż trzeba. Wtedy lepiej otworzyć strumień raz i pisać wielokrotnie:
using System.IO;
var path = Path.Combine("dane", "log.txt");
using var writer = new StreamWriter(path, append: true);
writer.WriteLine("Pierwszy wpis");
writer.WriteLine("Drugi wpis");
writer.WriteLine("Trzeci wpis");Tu właśnie widać praktyczną różnicę: AppendAllText jest wygodne i krótkie, ale StreamWriter lepiej znosi większą liczbę zapisów. Jeśli aplikacja zapisuje dane sporadycznie, prostota wygrywa. Jeśli zapisów jest dużo, liczy się mniej otwarć pliku i lepsza kontrola nad strumieniem. I właśnie dlatego warto porównać te metody bardziej świadomie.
Kiedy File.WriteAllText wystarczy, a kiedy lepszy jest StreamWriter
Ja zwykle rozbijam wybór na trzy pytania: czy zapisuję mało danych, czy dopisuję kolejne fragmenty, oraz czy plik może urosnąć na tyle, że chcę pisać etapami. Poniższa tabela porządkuje najważniejsze różnice bez teoretyzowania.
| Metoda | Kiedy używać | Plusy | Minusy |
|---|---|---|---|
File.WriteAllText |
Krótkie teksty, konfiguracja, prosty eksport | Najmniej kodu, szybkie do wdrożenia | Nadpisuje zawartość, nie nadaje się do wielu małych zapisów w pętli |
File.AppendAllText |
Dopisywanie wpisów, logi, historia zmian | Bardzo wygodne, nie kasuje poprzednich danych | Przy wielu operacjach może być mniej wydajne niż jeden otwarty strumień |
StreamWriter |
Więcej linii, większy plik, zapis w pętli | Lepsza kontrola, jedno otwarcie pliku, obsługa async | Trochę więcej kodu i odpowiedzialności |
FileStream |
Pełna kontrola nad bajtami i formatem | Najbardziej elastyczny, dobry do danych binarnych | Najmniej wygodny przy zwykłym tekście |
W praktyce File.WriteAllText wygrywa tam, gdzie liczy się szybkość implementacji, a StreamWriter tam, gdzie zapis ma się powtarzać. FileStream zostawiam na sytuacje, w których tekst już nie wystarcza albo chcę pisać dokładnie bajt po bajcie. To prowadzi prosto do kolejnego kroku: zapisu obiektów i danych binarnych.
Zapis obiektów, JSON i dane binarne
Jeśli pracujesz z modelami danych, ręczne składanie stringów zwykle kończy się problemami z formatowaniem i utrzymaniem. W nowoczesnym C# najczęściej zapisuję obiekt jako JSON. To czytelny format, wygodny do debugowania i naturalny dla aplikacji webowych, API oraz prostych wymian danych między usługami.
using System.IO;
using System.Text.Json;
public record Uzytkownik(string Imie, int Wiek);
var uzytkownik = new Uzytkownik("Ala", 29);
var json = JsonSerializer.Serialize(uzytkownik, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync("dane/uzytkownik.json", json);To rozwiązanie ma jedną ważną zaletę: dane zachowują strukturę, a plik jest nadal zwykłym tekstem. Jeśli później trzeba go odczytać, od razu widać, co się w nim znajduje. Gdy zapisuję większe obiekty, często od razu przechodzę na wersję asynchroniczną, żeby nie blokować wątku UI albo żądania HTTP.
Inaczej wygląda zapis danych binarnych, takich jak obraz, archiwum, plik PDF czy dowolny własny format. Tutaj tekstowe klasy nie mają sensu. Lepiej użyć File.WriteAllBytes albo FileStream, bo pracujesz bezpośrednio na bajtach:
byte[] dane = { 0x01, 0x02, 0x03, 0x04 };
File.WriteAllBytes("dane/binarny.dat", dane);To ważne rozróżnienie: nie zapisuj plików binarnych przez StreamWriter. Taka pomyłka szybko psuje format, a potem plik nie otwiera się w docelowym programie. Zanim jednak zapis w ogóle zadziała poprawnie, trzeba dobrze przygotować ścieżkę i obsłużyć typowe błędy.
Ścieżki, katalogi i błędy, które psują zapis
W projektach produkcyjnych problemem rzadko bywa sama metoda zapisu. Częściej coś psuje ścieżka, brak katalogu albo uprawnienia do folderu. Ja w takich sytuacjach zawsze zaczynam od zbudowania ścieżki przez Path.Combine, bo ręczne doklejanie separatorów prędzej czy później tworzy błąd trudny do wychwycenia.
using System.IO;
var folder = Path.Combine(AppContext.BaseDirectory, "export");
Directory.CreateDirectory(folder);
var path = Path.Combine(folder, "raport.txt");
File.WriteAllText(path, "Treść raportu");Directory.CreateDirectory jest przydatne, bo utworzy folder, jeśli nie istnieje, a jeśli istnieje, niczego nie zepsuje. To małe zabezpieczenie oszczędza sporo czasu przy pierwszym uruchomieniu aplikacji albo po wdrożeniu na nowy serwer. Warto też pamiętać, że jeśli któryś fragment ścieżki jest absolutny, Path.Combine może zignorować wcześniejsze części, więc dane wejściowe trzeba traktować ostrożnie.
W aplikacjach desktopowych wybór pliku często robi użytkownik przez SaveFileDialog, ale sama operacja zapisu nadal odbywa się przez te same klasy z System.IO. To dobry wzorzec: UI odpowiada za wybór miejsca, a warstwa logiki za bezpieczny zapis. Przy pracy z plikami często obsługuję też wyjątki takie jak IOException, UnauthorizedAccessException i DirectoryNotFoundException, bo to one najczęściej pokazują realny problem środowiskowy. Gdy zapis ma działać płynnie przy większym obciążeniu, wchodzi jeszcze jeden element: asynchroniczność.
Asynchroniczny zapis bez blokowania aplikacji
Jeśli aplikacja zapisuje dane w tle, obsługuje wielu użytkowników albo ma interfejs, który nie może „zamarzać”, wybieram wersje asynchroniczne. W C# to zwykle File.WriteAllTextAsync, StreamWriter.WriteAsync albo File.WriteAllBytesAsync. Różnica nie polega na magicznym przyspieszeniu samego dysku, tylko na tym, że wątek nie czeka bezczynnie na zakończenie operacji.
using System.IO;
using System.Text.Json;
var raport = new
{
Tytul = "Stan zamówienia",
Data = DateTime.UtcNow,
Pozycje = 12
};
var json = JsonSerializer.Serialize(raport, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync("dane/raport.json", json);W ASP.NET i aplikacjach desktopowych to często najlepszy kompromis: kod pozostaje prosty, a użytkownik nie widzi przycięć podczas zapisu. Nie przesadzam jednak z async wszędzie. Dla jednego małego pliku konfiguracyjnego różnica będzie minimalna, a prostszy kod synchroniczny bywa po prostu czytelniejszy. Asynchroniczność ma sens tam, gdzie zapis trwa dłużej, powtarza się często albo działa na zasobach sieciowych.
Najkrótsza mapa wyboru metody w codziennych scenariuszach
Gdybym miał sprowadzić cały temat do kilku praktycznych reguł, wyglądałoby to tak:
-
Krótkie dane tekstowe zapisuję przez
File.WriteAllText. -
Kolejne wpisy dopisuję przez
File.AppendAllTextalboStreamWriter. - Obiekty i struktury serializuję do JSON, a potem zapisuję jako tekst.
-
Bajty i pliki binarne zapisuję przez
File.WriteAllByteslubFileStream. - Większe operacje i UI przenoszę na wersje asynchroniczne.
-
Ścieżki docelowe buduję przez
Path.Combinei wcześniej tworzę katalog.
Ja w projektach webowych najczęściej zaczynam od najprostszej metody, a dopiero potem przechodzę do strumienia albo async, jeśli naprawdę widać potrzebę. To oszczędza czas i zmniejsza ryzyko wprowadzenia błędu tam, gdzie wystarczałby prosty zapis. Jeśli trzymasz się tej logiki, zapis pliku w C# przestaje być problemem technicznym, a staje się zwykłym, przewidywalnym elementem aplikacji.