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

C# Stringi – Interpolacja i Formatowanie w Logach i Komunikatach Błędów

Stringi – Interpolacja, string.Format i Formatowanie Tekstu

Kąt widzenia: backend/operacyjny – logi aplikacyjne, error messages, odpowiedzi API
Przykład przewodni: system przetwarzania zamówień w ASP.NET Core


Dlaczego stringi w backendzie to osobny temat?

Kiedy uczysz się interpolacji stringów, przykłady zazwyczaj wyglądają tak:

Console.WriteLine($"Cześć, {name}!");

W produkcyjnym backendzie stringi mają zupełnie inne zastosowanie. Zamiast wyświetlać tekst użytkownikowi, budujesz:

  • logi aplikacyjne – kto, co, kiedy zrobił i z jakim wynikiem,
  • komunikaty błędów – co poszło nie tak i dlaczego,
  • odpowiedzi API – ustrukturyzowane wiadomości dla klienta,
  • wyjątki z kontekstem – żeby InnerException miało sens o 3 w nocy.

Każda z tych kategorii ma inne wymagania. Błędne podejście potrafi zrujnować debugowanie w produkcji.


Scenariusz: system przetwarzania zamówień

Pracujemy nad OrderProcessingService w ASP.NET Core. Serwis przyjmuje zamówienie, waliduje je, rezerwuje stock i emituje zdarzenie do kolejki.

public class Order
{
    public Guid Id { get; init; }
    public string CustomerId { get; init; } = string.Empty;
    public List<OrderItem> Items { get; init; } = [];
    public decimal TotalAmount { get; init; }
    public DateTimeOffset CreatedAt { get; init; }
}

public record OrderItem(string ProductId, int Quantity, decimal UnitPrice);

Na tym przykładzie przeprowadzimy wszystkie tematy.


Pułapka #1: interpolacja w ILogger

To jeden z najczęstszych błędów, jakie trafiają na code review.

Błędny kod

_logger.LogInformation($"Przetwarzam zamówienie {order.Id} dla klienta {order.CustomerId}");

Kod działa – ale jest zły z kilku powodów:

  1. String jest budowany zawsze, nawet gdy poziom logowania Information jest wyłączony w produkcji.
  2. Tracisz structured logging – zamiast pól do przeszukiwania, masz jeden blok tekstu.
  3. Jeśli używasz Seq, Datadog lub Elastic, tracisz możliwość filtrowania po order.Id.

Poprawny kod

_logger.LogInformation(
    "Przetwarzam zamówienie {OrderId} dla klienta {CustomerId}",
    order.Id,
    order.CustomerId);

ILogger używa message templates, nie interpolacji. Nazwy w nawiasach klamrowych to nazwy pól w structured log – nie zmienne C#. To fundamentalna różnica.

W Seq lub Grafanie możesz teraz zapytać: OrderId = "3fa85f64-..." i dostać wszystkie logi dla konkretnego zamówienia.

Kiedy interpolacja w loggerze jest dopuszczalna?

Nigdy w LogInformation, LogWarning, LogError. Jedyny wyjątek to sytuacja, gdy budujesz wiadomość wyjątku (nie log) – ale o tym za chwilę.


Formatowanie w message templates

W message templates możesz używać tych samych specyfikatorów co w interpolacji:

_logger.LogInformation(
    "Zamówienie {OrderId} | kwota {TotalAmount:C2} | czas {ProcessingTime:F3}ms",
    order.Id,
    order.TotalAmount,
    stopwatch.Elapsed.TotalMilliseconds);

Wynik w logu:

Zamówienie 3fa85f64-... | kwota 1 249,99 zł | czas 47,832ms

Specyfikatory działają identycznie jak w string.Format. Więcej o tym niżej.


Budowanie komunikatów wyjątków

Tutaj interpolacja jest właściwym narzędziem – Exception.Message to zwykły string.

Podstawowy przypadek

if (order.Items.Count == 0)
{
    throw new ArgumentException(
        $"Zamówienie {order.Id} nie zawiera żadnych pozycji.");
}

Wyjątek z kontekstem operacyjnym

Typowy błąd: wyjątek bez kontekstu, który nic nie mówi o tym, gdzie i przy jakich danych wystąpił.

// Złe — co to za zamówienie? Dla kogo? Co się nie udało?
throw new InvalidOperationException("Niewystarczający stock.");

// Dobre — masz kontekst
throw new InvalidOperationException(
    $"Niewystarczający stock dla produktu '{item.ProductId}' " +
    $"(żądano: {item.Quantity}, dostępne: {availableStock}) " +
    $"w zamówieniu {order.Id}.");

Kiedy to trafi do logu o 3:00, wiesz dokładnie co sprawdzić.


string.Format — kiedy ma sens w backendzie

string.Format pojawia się w backendzie w sytuacjach, gdy szablon pochodzi z zewnątrz – bazy danych, pliku konfiguracyjnego, systemu tłumaczeń.

// Szablon przychodzi z bazy — np. customowe komunikaty dla klientów
public string FormatOrderStatusMessage(string template, Order order)
{
    return string.Format(
        template,                          // np. "Zamówienie {0} zostało {1}."
        order.Id.ToString("D"),
        order.Status.ToLocalizedString());
}

Interpolacja działa w czasie kompilacji – nie możesz wstawić szablonu z bazy do $"". string.Format działa w runtime – właśnie do tego służy.

Uwaga na pułapkę przy string.Format z wieloma parametrami:

// Ten kod się skompiluje, ale dane będą w złej kolejności
string.Format("{0} kupił {1} za {2}", totalAmount, customerId, productName);
//                                     ^^^^^^^^^^   ^^^^^^^^^^   ^^^^^^^^^^^
//                             Pomieszane — totalAmount idzie jako {0} (customerId!)

// Bezpieczniejszy zapis — nazwij zmienne przed wywołaniem
var message = string.Format(
    "{0} kupił {1} za {2}",
    customerId,
    productName,
    totalAmount);

Alignment i padding – tabele w logach i raportach CLI

Czasem w logach chcesz wyrównać kolumny – np. podsumowanie przetwarzania batch.

var orders = GetProcessedOrders();

var header = $"{"Zamówienie",-36} {"Klient",-20} {"Kwota",10}";
var separator = new string('-', 68);

var sb = new StringBuilder();
sb.AppendLine(header);
sb.AppendLine(separator);

foreach (var o in orders)
{
    sb.AppendLine($"{o.Id,-36} {o.CustomerId,-20} {o.TotalAmount,10:F2}");
}

_logger.LogInformation("Raport batch:\n{Report}", sb.ToString());

Wynik:

Zamówienie                           Klient               Kwota
--------------------------------------------------------------------
3fa85f64-5717-4562-b3fc-2c963f66afa6 CUST-001             1 249,99
7c9e6679-7425-40de-944b-e07fc1f90ae7 CUST-002               89,00

Składnia {wartość,-36} oznacza: wyrównaj do lewej w polu o szerokości 36 znaków.
{wartość,10} – wyrównaj do prawej w polu o szerokości 10.


nameof() w komunikatach – refactor-safe

Częsty błąd: twarde stringi z nazwami właściwości lub argumentów, które rozjeżdżają się przy rename.

// Złe — po rename "CustomerId" na "ClientId" string zostaje stary
if (string.IsNullOrWhiteSpace(order.CustomerId))
    throw new ArgumentException("CustomerId nie może być pusty.", "CustomerId");

// Dobre — refactoring działa poprawnie
if (string.IsNullOrWhiteSpace(order.CustomerId))
    throw new ArgumentException(
        $"{nameof(order.CustomerId)} nie może być pusty.",
        nameof(order.CustomerId));

nameof() jest obliczany w czasie kompilacji i zwraca nazwę elementu jako string. Nie ma narzutu w runtime.


Escape characters – gdzie realnie trafiasz na nie w backendzie

W backendzie escape characters pojawiają się głównie w trzech miejscach:

JSON w logach (ręcznie, bez serializera)

// Źle — cudzysłowy nie są escaped
var json = $"{"orderId": "{order.Id}"}";

// Dobrze
var json = $"{{\"orderId\": \"{order.Id}\"}}";

// Najlepiej — użyj System.Text.Json
var json = JsonSerializer.Serialize(new { orderId = order.Id });

Wniosek: w backendzie nie buduj JSON ręcznie – zawsze serializator.

Ścieżki w konfiguracji (pobrane z appsettings)

// appsettings.json ma już poprawne wartości — nie escapujesz w JSON
// "ArchivePath": "C:\\Orders\\Archive"
// lub
// "ArchivePath": "C:/Orders/Archive"

var archivePath = _config["ArchivePath"]; // string już gotowy
var filePath = Path.Combine(archivePath, $"{order.Id:D}.json");

Używaj Path.Combine zamiast sklejać ścieżki stringami.

Wyrażenia regularne

// Verbatim string — czytelne regexy bez podwójnych backslashy
private static readonly Regex OrderIdPattern =
    new(@"^ORD-\d{4}-\d{6}$", RegexOptions.Compiled);

if (!OrderIdPattern.IsMatch(externalOrderId))
{
    throw new FormatException(
        $"Niepoprawny format ID zamówienia: '{externalOrderId}'. " +
        @"Oczekiwany format: ORD-YYYY-NNNNNN");
}

Odpowiedź błędu w API – spójny format

W ASP.NET Core odpowiedzi błędów powinny mieć spójny format. Przykład z ProblemDetails:

[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
    try
    {
        var order = await _orderService.ProcessAsync(request);

        _logger.LogInformation(
            "Zamówienie {OrderId} przetworzone pomyślnie w {ElapsedMs}ms",
            order.Id,
            stopwatch.ElapsedMilliseconds);

        return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
    }
    catch (InsufficientStockException ex)
    {
        _logger.LogWarning(
            "Brak stock dla {ProductId} w zamówieniu {OrderId}",
            ex.ProductId,
            request.CorrelationId);

        return Problem(
            title: "Niewystarczający stan magazynowy",
            detail: $"Produkt '{ex.ProductId}' jest niedostępny w żądanej ilości ({ex.RequestedQuantity} szt.).",
            statusCode: StatusCodes.Status409Conflict,
            instance: $"/orders/{request.CorrelationId}");
    }
}

Zwróć uwagę na rozdzielenie: log używa message template, odpowiedź API używa interpolacji – bo tu budujemy string dla klienta, nie structured log.


Checklist – stringi w backendzie

MiejsceNarzędzieDlaczego
ILogger.*message templates {Name}structured logging, lazy evaluation
Exception.Messageinterpolacja $""czytelny kontekst błędu
string.Formatszablony z zewnątrz (baza, config)runtime template
Ścieżki plikówPath.Combine + verbatim @""cross-platform, czytelność
JSONSystem.Text.Jsonnie buduj ręcznie
Nazwy argumentównameof()refactor-safe
Wyrównanie kolumn{value,-N} / {value,N}tabele w logach/CLI
Regex patternverbatim @""czytelność bez double-backslash

Najczęstsze błędy w backendzie

Interpolacja w loggerze

// Nie rób tego
_logger.LogError($"Błąd przy przetwarzaniu {order.Id}: {ex.Message}");

// Rób tak
_logger.LogError(ex, "Błąd przy przetwarzaniu {OrderId}", order.Id);

Drugi argument ex przekazany do LogError zostanie automatycznie zserializowany przez większość log sinkó – nie musisz go wklejać do stringa.

Konkatenacja w pętli

// Złe — O(n²) alokacje
string report = "";
foreach (var item in order.Items)
    report += $"{item.ProductId}: {item.Quantity}\n";

// Dobre — jeden StringBuilder
var sb = new StringBuilder(order.Items.Count * 40); // pre-allocate
foreach (var item in order.Items)
    sb.AppendLine($"{item.ProductId}: {item.Quantity}");
string report = sb.ToString();

Brak kontekstu w wyjątkach

// Złe — bez kontekstu
catch (DbUpdateException)
{
    throw new InvalidOperationException("Błąd zapisu do bazy.");
}

// Dobre — z kontekstem + inner exception
catch (DbUpdateException ex)
{
    throw new OrderPersistenceException(
        $"Nie udało się zapisać zamówienia {order.Id} (klient: {order.CustomerId}).",
        ex); // inner exception zachowany
}

Podsumowanie

Stringi w backendzie to nie dekoracja – to krwiobieg debugowania i monitoringu. Trzy zasady, które zapamiętasz:

  1. ILogger + message templates – nigdy interpolacja w loggerze.
  2. Wyjątki z kontekstem$"...{orderId}...{productId}..." zamiast "Coś poszło nie tak.".
  3. nameof() + Path.Combine – refactor-safe, cross-platform.

Reszta (alignment, verbatim strings, string.Format dla zewnętrznych szablonów) to narzędzia do konkretnych sytuacji – teraz wiesz kiedy sięgać po które.


Następny post: Stringi w C# — Generowanie Dokumentów i Raportów dla Użytkownika — ten sam temat, zupełnie inna perspektywa: faktury, raporty, maile i formatowanie uwzględniające kulturę użytkownika.


🔗 Zobacz też


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

Jeśli materiał był pomocny:

👉 zostaw komentarz,

👉 sprawdź moje kursy .NET i C#.

👉 udostępnij artykuł komuś, kto zaczyna z programowaniem

👉 subskrybuj nasz kanał na YT!


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ę →