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

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
InnerExceptionmiał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:
- String jest budowany zawsze, nawet gdy poziom logowania
Informationjest wyłączony w produkcji. - Tracisz structured logging – zamiast pól do przeszukiwania, masz jeden blok tekstu.
- 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,832msSpecyfikatory 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,00Skł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
| Miejsce | Narzędzie | Dlaczego |
|---|---|---|
ILogger.* | message templates {Name} | structured logging, lazy evaluation |
Exception.Message | interpolacja $"" | czytelny kontekst błędu |
string.Format | szablony z zewnątrz (baza, config) | runtime template |
| Ścieżki plików | Path.Combine + verbatim @"" | cross-platform, czytelność |
| JSON | System.Text.Json | nie buduj ręcznie |
| Nazwy argumentów | nameof() | refactor-safe |
| Wyrównanie kolumn | {value,-N} / {value,N} | tabele w logach/CLI |
| Regex pattern | verbatim @"" | 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:
ILogger+ message templates – nigdy interpolacja w loggerze.- Wyjątki z kontekstem —
$"...{orderId}...{productId}..."zamiast"Coś poszło nie tak.". 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ż
- C# Podstawy Programowania: Twój Pierwszy Krok w Świat Kodowania
- AI w .NET: Zostań Architektem Inteligentnych Aplikacji!
- C# Clean Architecture w Praktyce
- C# – Zbuduj Własnego Tetrisa! Kompletny Przewodnik
- 7 Dniowe Wyzwanie C# Tic Tac Toe
- C# Zbuduj Profesjonalny Portal Randkowy od Podstaw!
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!
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ę →
1 comment