🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Stringi w C# – Generowanie Dokumentów i Raportów dla Użytkownika

Stringi w C#

Kąt widzenia: user-facing/biznesowy — faktury, raporty, maile, eksporty
Przykład przewodni: system fakturowania z obsługą wielu kultur


Czym różni się string dla użytkownika od stringa w backendzie?

Gdy budujesz loga lub wyjątek, odbiorcą jest programista — liczy się precyzja i kontekst techniczny. Gdy budujesz fakturę, raport lub maila, odbiorcą jest użytkownik lub klient biznesowy — i tu reguły są zupełnie inne:

  • Liczba 1249.99 powinna wyglądać jako 1 249,99 zł (polski klient) albo $1,249.99 (klient z USA).
  • Data 2026-06-15 powinna wyglądać jako 15.06.2026 albo June 15, 2026 zależnie od kultury.
  • Kolumny w raporcie muszą być wyrównane, żeby były czytelne.
  • Długi dokument generujesz raz i zapisujesz — nie chcesz tworzyć tysięcy pośrednich stringów.

Ten post pokrywa dokładnie te scenariusze.


Scenariusz: system fakturowania

Budujemy InvoiceGenerator – serwis generujący faktury VAT w formacie tekstowym. Faktura trafia:

  • do pliku .txt (archiwum),
  • jako treść maila (plain text),
  • ewentualnie do PDF przez zewnętrzną bibliotekę (ale my generujemy tekst źródłowy).
public class Invoice
{
    public string Number { get; init; } = string.Empty;
    public DateOnly IssueDate { get; init; }
    public DateOnly DueDate { get; init; }
    public Company Seller { get; init; } = default!;
    public Company Buyer { get; init; } = default!;
    public List<InvoiceItem> Items { get; init; } = [];
    public CultureInfo Culture { get; init; } = CultureInfo.GetCultureInfo("pl-PL");
}

public record Company(string Name, string TaxId, string Address);
public record InvoiceItem(string Description, int Quantity, decimal UnitNetPrice, decimal VatRate);

CultureInfo – formatowanie liczb i dat dla konkretnego użytkownika

To najważniejszy temat w tej kategorii, którego nie znajdziesz w tutorialach o “podstawach interpolacji”.

Problem z domyślnym formatowaniem

decimal amount = 1249.99m;
DateTime date = DateTime.Now;

// To zależy od ustawień systemu operacyjnego serwera — nie klienta!
Console.WriteLine($"{amount:F2}"); // "1249,99" albo "1249.99" — nie wiesz które
Console.WriteLine($"{date:d}");    // "15.06.2026" albo "6/15/2026" — nie wiesz które

Jeśli Twój serwer jest skonfigurowany jako en-US, polski użytkownik dostanie fakturę z kropką dziesiętną. Albo gorzej — kwoty wyglądają jak 1.249,99 bo serwer jest inaczej skonfigurowany niż środowisko deweloperskie.

Rozwiązanie: jawne podawanie kultury

public class InvoiceGenerator
{
    public string Generate(Invoice invoice)
    {
        var culture = invoice.Culture; // pl-PL, en-US, de-DE, etc.

        // Formatowanie z jawną kulturą — zawsze przewidywalny wynik
        string formattedAmount = invoice.TotalGross.ToString("N2", culture);
        string formattedDate = invoice.IssueDate.ToString("d", culture);
    }
}

Kultury w praktyce

var amount = 1249.99m;

Console.WriteLine(amount.ToString("C2", CultureInfo.GetCultureInfo("pl-PL")));
// 1 249,99 zł

Console.WriteLine(amount.ToString("C2", CultureInfo.GetCultureInfo("en-US")));
// $1,249.99

Console.WriteLine(amount.ToString("C2", CultureInfo.GetCultureInfo("de-DE")));
// 1.249,99 €

Console.WriteLine(amount.ToString("N2", CultureInfo.GetCultureInfo("pl-PL")));
// 1 249,99  (bez symbolu waluty — do obliczeń w dokumencie)

C2 — format walutowy z symbolem i dwoma miejscami po przecinku.
N2 — format liczbowy bez symbolu waluty.
d — krótka data w formacie kultury.


StringBuilder – wydajne budowanie długich dokumentów

Faktura może mieć dziesiątki pozycji. Każde + tworzy nowy obiekt string w pamięci – przy 50 pozycjach to 50+ niepotrzebnych alokacji.

Dlaczego StringBuilder tu ma sens (a w backendzie też)

// Złe dla dużych dokumentów — O(n²) alokacje
string invoice = "";
invoice += "FAKTURA VAT\n";
invoice += $"Numer: {invoiceNumber}\n";
foreach (var item in items)
    invoice += $"{item.Description}: {item.Total:N2}\n";

// Dobre — jeden bufor, jeden string na końcu
var sb = new StringBuilder(capacity: 4096); // pre-allocate dla faktury
sb.AppendLine("FAKTURA VAT");
sb.AppendLine($"Numer: {invoiceNumber}");
foreach (var item in items)
    sb.AppendLine($"{item.Description}: {item.Total:N2}");
string invoice = sb.ToString(); // jeden string na wyjściu

capacity: 4096 – hint dla alokacji. Jeśli wiesz z grubsza ile znaków wygenerujesz, pomaga uniknąć realokacji bufora.


PadLeft / PadRight – wyrównanie kolumn w dokumencie tekstowym

Faktury i raporty tekstowe wymagają wyrównania. PadLeft/PadRight jest czytelniejsze i bardziej elastyczne niż specyfikatory alignment w interpolacji gdy masz zmienne szerokości kolumn.

private string FormatItemRow(InvoiceItem item, CultureInfo culture)
{
    string description = item.Description.PadRight(40);
    string qty         = item.Quantity.ToString().PadLeft(5);
    string unitPrice   = item.UnitNetPrice.ToString("N2", culture).PadLeft(12);
    string total       = (item.Quantity * item.UnitNetPrice).ToString("N2", culture).PadLeft(12);

    return $"{description}{qty}{unitPrice}{total}";
}

Wynik:

Usługa projektowa — etap 1              2       800,00    1 600,00
Licencja oprogramowania roczna          1     2 400,00    2 400,00
Szkolenie online (5h)                   3       350,00    1 050,00

Alternatywnie, specyfikatory alignment w interpolacji:

return $"{item.Description,-40}{item.Quantity,5}{item.UnitNetPrice,12:N2}{total,12:N2}";

Oba podejścia dają ten sam efekt. PadLeft/PadRight jest bardziej czytelne gdy wyrównanie zależy od obliczonej wartości, specyfikatory ,-N są zwięźlejsze dla stałych szerokości.


Raw string literals (C# 11) – wieloliniowe szablony bez chaosu

Jeśli generujesz szablony maili lub dokumenty HTML w kodzie, raw string literals eliminują problem cudzysłowów i escape characters.

// Szablon maila jako raw string literal
string emailTemplate = """
    Szanowni Państwo,

    W załączniku przesyłamy fakturę VAT nr {0} z dnia {1}.

    Kwota do zapłaty: {2}
    Termin płatności: {3}

    Prosimy o dokonanie przelewu na numer konta:
    PL 61 1090 1014 0000 0712 1981 2874

    W razie pytań prosimy o kontakt: faktury@firma.pl

    Z poważaniem,
    Dział Finansowy
    """;

string email = string.Format(emailTemplate,
    invoice.Number,
    invoice.IssueDate.ToString("d", invoice.Culture),
    invoice.TotalGross.ToString("C2", invoice.Culture),
    invoice.DueDate.ToString("d", invoice.Culture));

Raw string literal zaczyna się i kończy """. Możesz wewnątrz używać cudzysłowów, backslashy, nawiasów klamrowych – wszystko jest traktowane dosłownie. Wcięcie pierwszego """ zamykającego określa, ile spacji jest usuwane z każdej linii (dedentacja).

Interpolowany raw string literal

Jeśli chcesz łączyć raw strings z interpolacją:

string subject = $$"""
    Faktura VAT nr {{invoice.Number}} — kwota {{invoice.TotalGross:C2}}
    """;

$$ oznacza: użyj {{ i }} jako separatorów interpolacji. Pozwala na wstawianie { i } w treści bez escapowania.


string.Join – listy i wyliczenia w dokumentach

Częsty case: lista pozycji jako jeden string, np. w treści maila lub raportu CSV.

// Lista opisów pozycji faktury
var descriptions = invoice.Items.Select(i => i.Description);
string itemList = string.Join(", ", descriptions);
// "Usługa projektowa, Licencja oprogramowania, Szkolenie online"

// CSV export pozycji
var csvLines = invoice.Items.Select(i =>
    $"{i.Description};{i.Quantity};{i.UnitNetPrice.ToString("F2", culture)}");
string csv = string.Join(Environment.NewLine, csvLines);

string.Join jest wydajniejszy niż ręczna pętla z += — używa StringBuilder wewnętrznie.


Verbatim strings – ścieżki do plików archiwum

public class InvoiceArchiveService
{
    private readonly string _archiveRoot;

    public InvoiceArchiveService(IConfiguration config)
    {
        // Ścieżka z konfiguracji — już jako poprawny string
        _archiveRoot = config["Invoice:ArchivePath"]
            ?? @"C:\Faktury\Archiwum"; // fallback jako verbatim string
    }

    public string GetInvoicePath(Invoice invoice)
    {
        // Rok/miesiąc jako podfoldery
        string yearMonth = invoice.IssueDate.ToString("yyyy-MM");

        // Path.Combine — nie sklejaj ścieżek ręcznie!
        return Path.Combine(
            _archiveRoot,
            yearMonth,
            $"{invoice.Number.Replace("/", "-")}.txt");
    }
}

@"C:\Faktury\Archiwum" – verbatim string w wartości domyślnej: brak podwójnych backslashy, czytelna ścieżka Windows. W produkcji jednak wartość powinna zawsze pochodzić z konfiguracji, nie z hardcoded stringa.


Kompletny generator faktury

Teraz łączymy wszystko razem:

public class InvoiceGenerator
{
    public string Generate(Invoice invoice)
    {
        var culture = invoice.Culture;
        var sb = new StringBuilder(capacity: 4096);

        // --- Nagłówek ---
        string separator = new string('=', 70);
        sb.AppendLine(separator);
        sb.AppendLine("FAKTURA VAT".PadLeft(45));
        sb.AppendLine(separator);

        // --- Dane dokumentu ---
        sb.AppendLine($"Numer:             {invoice.Number}");
        sb.AppendLine($"Data wystawienia:  {invoice.IssueDate.ToString("d", culture)}");
        sb.AppendLine($"Termin płatności:  {invoice.DueDate.ToString("d", culture)}");
        sb.AppendLine();

        // --- Sprzedawca / Nabywca ---
        string sellerBlock = FormatCompanyBlock("SPRZEDAWCA", invoice.Seller);
        string buyerBlock  = FormatCompanyBlock("NABYWCA",   invoice.Buyer);

        // Kolumnowy układ: sprzedawca po lewej, nabywca po prawej
        var sellerLines = sellerBlock.Split('\n');
        var buyerLines  = buyerBlock.Split('\n');
        int rowCount    = Math.Max(sellerLines.Length, buyerLines.Length);

        for (int i = 0; i < rowCount; i++)
        {
            string left  = i < sellerLines.Length ? sellerLines[i] : "";
            string right = i < buyerLines.Length  ? buyerLines[i]  : "";
            sb.AppendLine($"{left.PadRight(35)}{right}");
        }

        sb.AppendLine();

        // --- Tabela pozycji ---
        string itemHeader = $"{"Opis",-40}{"Ilość",5}{"Cena netto",12}{"VAT%",6}{"Wartość brutto",15}";
        sb.AppendLine(new string('-', 70));
        sb.AppendLine(itemHeader);
        sb.AppendLine(new string('-', 70));

        decimal totalNet   = 0;
        decimal totalGross = 0;

        foreach (var item in invoice.Items)
        {
            decimal netTotal   = item.Quantity * item.UnitNetPrice;
            decimal vatAmount  = netTotal * item.VatRate;
            decimal grossTotal = netTotal + vatAmount;

            totalNet   += netTotal;
            totalGross += grossTotal;

            sb.AppendLine(
                $"{item.Description,-40}" +
                $"{item.Quantity,5}" +
                $"{item.UnitNetPrice.ToString("N2", culture),12}" +
                $"{item.VatRate * 100,5:F0}%" +
                $"{grossTotal.ToString("N2", culture),15}");
        }

        sb.AppendLine(new string('-', 70));

        // --- Podsumowanie ---
        sb.AppendLine($"{"RAZEM NETTO:",-55}{totalNet.ToString("N2", culture),15}");
        sb.AppendLine($"{"RAZEM BRUTTO:",-55}{totalGross.ToString("N2", culture),15}");
        sb.AppendLine($"{"DO ZAPŁATY:",-55}{totalGross.ToString("C2", culture),15}");
        sb.AppendLine(separator);

        return sb.ToString();
    }

    private static string FormatCompanyBlock(string label, Company company)
    {
        return $"{label}\n{company.Name}\nNIP: {company.TaxId}\n{company.Address}";
    }
}

Przykładowy wynik dla kultury pl-PL:

======================================================================
                                    FAKTURA VAT
======================================================================
Numer:             FV/2026/06/0047
Data wystawienia:  15.06.2026
Termin płatności:  29.06.2026

SPRZEDAWCA                         NABYWCA
Acme Software sp. z o.o.           XYZ Corp Sp. k.
NIP: 123-456-78-90                 NIP: 987-654-32-10
ul. Programistów 1, 00-001 Warszawa ul. Kliencka 5, 30-001 Kraków

----------------------------------------------------------------------
Opis                                    Ilość  Cena netto  VAT%  Wartość brutto
----------------------------------------------------------------------
Usługa projektowa — etap 1                  2      800,00   23%       1 968,00
Licencja oprogramowania roczna              1    2 400,00   23%       2 952,00
Szkolenie online (5h)                       3      350,00   23%       1 291,50
----------------------------------------------------------------------
RAZEM NETTO:                                                       3 550,00
RAZEM BRUTTO:                                                      6 211,50
DO ZAPŁATY:                                                    6 211,50 zł
======================================================================

Zmiana kultury – ta sama faktura po angielsku

var invoice = new Invoice
{
    // ... te same dane ...
    Culture = CultureInfo.GetCultureInfo("en-US")
};

string invoiceEn = generator.Generate(invoice);

Wynik — bez żadnych zmian w logice generatora:

DO ZAPŁATY:                                                       $6,211.50
Data wystawienia:  6/15/2026

Cała logika formatowania działa przez kulturę — nie ma if (language == "pl") w kodzie.


Edge case: string.IsNullOrWhiteSpace w danych wejściowych

Zanim wygenerujesz dokument, waliduj dane. Puste pole Company.Name w nagłówku faktury to błąd nie do zaakceptowania.

public string Generate(Invoice invoice)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(invoice.Number, nameof(invoice.Number));

    if (invoice.Items.Count == 0)
        throw new InvalidOperationException(
            $"Faktura {invoice.Number} nie zawiera żadnych pozycji.");

    if (invoice.DueDate < invoice.IssueDate)
        throw new ArgumentException(
            $"Termin płatności ({invoice.DueDate:d}) nie może być wcześniejszy " +
            $"niż data wystawienia ({invoice.IssueDate:d}).",
            nameof(invoice.DueDate));

    // ... generowanie
}

ArgumentException.ThrowIfNullOrWhiteSpace — dostępny od .NET 7, zastępuje ręczne sprawdzenie + throw.


Checklist — stringi w dokumentach dla użytkownika

  • Zawsze podawaj CultureInfo – nigdy nie polegaj na kulturze systemowej serwera.
  • Używaj C2 dla walut – z kulturą; N2 gdy chcesz liczbę bez symbolu waluty.
  • StringBuilder dla dokumentów – jeśli generujesz więcej niż kilka linii w pętli.
  • string.Join zamiast ręcznej pętli z += dla list.
  • Raw string literals (""") dla wieloliniowych szablonów maili, HTML, SQL.
  • PadLeft/PadRight lub specyfikatory alignment dla wyrównanych kolumn.
  • Path.Combine zamiast ręcznego sklejania ścieżek – zawsze.
  • Waliduj dane wejściowe przed generowaniem – pusty NIP na fakturze to problem.

Podsumowanie

Generowanie dokumentów dla użytkownika wymaga precyzji innego rodzaju niż backend – nie chodzi o structured logging ani refactor-safe nazwy, ale o to, żeby liczba wyglądała poprawnie niezależnie od tego, gdzie stoi serwer. CultureInfo to punkt wyjścia. StringBuilder to wydajność. Raw string literals to czytelność szablonów. Reszta to wyrównanie i detal, który decyduje czy wydrukowana faktura wygląda profesjonalnie.


Poprzedni post: C# Stringi – Interpolacja i Formatowanie w Logach i Komunikatach Błędów — ten sam temat, zupełnie inna perspektywa: ILogger, message templates, wyjątki z kontekstem i odpowiedzi API.


Zobacz także — powiązane artykuły

👉 MCP w .NET (C#) – jak zbudować serwer AI krok po kroku

👉 Tworzenie klas i obiektów w C# — kompletny przewodnik

👉 Pattern Matching w C# – switch expressions i type patterns


CTA

📌 Jakiego podejścia do stringów używasz najczęściej?
Zostaw komentarz pod artykułem.

📌 Chcesz więcej praktycznego C# i .NET?

👉subskrybuj nasz kanał na YT!

👉 sprawdź moje kursy .NET i C#.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak COIG, Kamsoft czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

czytanie to początek

Zamień wiedzę w umiejętności

Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.

Pobieram roadmapę →