Blog Dla Programistów C#/.NET

Jak sprawdzać kod wygenerowany przez AI? 10-punktowa checklista code review w .NET

czwartek, 29 października 2026 Tagi: C#/.NETProgramowanieAI

AI napisze Ci kod w 10 sekund. I w tych samych 10 sekundach potrafi wsadzić do projektu SQL injection, deadlocka i pakiet NuGet, który... nie istnieje. Wiem, bo code review takiego kodu robię praktycznie codziennie - zarówno swojego, jak i kodu mojego zespołu.

Najgorsze jest to, że kod od AI prawie zawsze wygląda dobrze. Ładne nazwy, komentarze, wszystko czyściutko. I właśnie dlatego czytamy go mniej uważnie niż kod od juniora. A powinno być dokładnie odwrotnie.

W tym artykule pokażę Ci 10 rzeczy, które sprawdzam za każdym razem, zanim kod wygenerowany przez Copilota, ChatGPT, Claude czy Cursora trafi do projektu. Każdy punkt z konkretnym przykładem w C#: jak to zwykle generuje AI i jak powinno wyglądać. Na końcu znajdziesz całą checklistę w jednej tabeli - do zapisania albo wydrukowania.

Wersje i dane: przykłady kodu są pisane pod współczesny .NET (8 i nowszy) z ASP.NET Core i EF Core, ale zdecydowana większość zasad dotyczy też starszych projektów, łącznie z .NET Framework. Statystyki pochodzą z raportu Veracode z 2025 roku oraz z badania zaprezentowanego na konferencji USENIX Security 2025.

Jak sprawdzać kod wygenerowany przez AI? 10-punktowa checklista code review w .NET

Dlaczego kod od AI wymaga osobnego code review? Liczba 45%


Zanim przejdziemy do listy, jedna liczba. Firma Veracode przebadała ponad 100 modeli AI na 80 zadaniach programistycznych w 4 językach: Javie, Pythonie, C# i JavaScripcie. Wynik? W 45% przypadków wygenerowany kod zawierał podatność z listy OWASP Top 10. Prawie co drugi fragment. C# nie był tu chlubnym wyjątkiem: Python, C# i JavaScript wypadły w przedziale 38-45% wpadek, a najgorzej Java - ponad 70%.

Co ważne, to nie jest problem "starych modeli". Nowsze modele piszą kod, który częściej się kompiluje, ale wcale nie jest bezpieczniejszy. Składnia się poprawia, bezpieczeństwo stoi w miejscu.

Dlatego code review kodu od AI to nie opcja, tylko konieczność. Pytanie brzmi: na co konkretnie patrzeć?

Zasada #0: traktuj AI jak juniora z nieskończoną pewnością siebie


Zanim punkt pierwszy, jedna zasada, która ustawia wszystkie pozostałe. Traktuj AI jak juniora, który:

• pisze bardzo szybko i zna wszystkie wzorce z internetu,
• nie zna Twojego projektu,
• nigdy, przenigdy nie powie "nie wiem" - zawsze coś wygeneruje, i to pewnym tonem.

A na końcu to Ty klikasz "merge". To Ty podpisujesz się pod tym pull requestem, nie AI. Z takim nastawieniem przejdźmy do listy. Przy każdym punkcie pokażę, jak kod zwykle generuje AI, jak powinien wyglądać i co konkretnie sprawdzam.

#1 Zmyślone API i nieistniejące pakiety NuGet


AI potrafi z pełnym przekonaniem użyć metody, która nie istnieje. Przykład:

var response = await client.GetWithRetryAsync(url, retryCount: 3);

/* error CS1061: 'HttpClient' does not contain a definition for 'GetWithRetryAsync' */

Brzmi sensownie, prawda? Problem w tym, że HttpClient nie ma takiej metody. Nigdy nie miał. AI ją wymyśliło, bo "pasowała statystycznie".

To samo dzieje się z pakietami NuGet: AI potrafi polecić pakiet, którego nie ma. Albo pomieszać wersje i użyć metody z .NET 8 w projekcie, który stoi na .NET Framework.

Jeśli potrzebujesz retry, sięgnij po rozwiązanie, które istnieje naprawdę - AddStandardResilienceHandler z oficjalnego pakietu Microsoftu:

/* pakiet Microsoft.Extensions.Http.Resilience */
builder.Services.AddHttpClient<CatalogClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.AddStandardResilienceHandler(); /* retry, timeouty i circuit breaker z rozsądnymi ustawieniami domyślnymi */

Co sprawdzam:

• czy kod w ogóle się kompiluje - brzmi banalnie, ale ludzie naprawdę wklejają kod z czatu bez kompilacji,
• czy każdy pakiet istnieje na nuget.org i kto jest jego autorem,
• czy użyte API pasuje do wersji .NET w Twoim projekcie.

Slopsquatting: gdy halucynacja AI staje się atakiem


Punkt pierwszy ma jeszcze drugie dno, ważniejsze, niż się wydaje. Istnieje typ ataku na łańcuch dostaw oprogramowania, który nazywa się slopsquatting. Działa tak:

1. AI regularnie halucynuje te same nazwy nieistniejących pakietów.
2. Atakujący o tym wiedzą, więc publikują pod tymi nazwami prawdziwe pakiety - tylko że ze złośliwym kodem w środku.
3. Prosisz AI o kod, a AI podaje Ci taki pakiet.
4. Odruchowo wpisujesz dotnet add package... i malware właśnie ląduje w Twoim projekcie.

To nie jest teoria. W badaniu zaprezentowanym na konferencji USENIX Security 2025 naukowcy przeanalizowali 576 tysięcy próbek kodu wygenerowanych przez 16 popularnych modeli. Wynik: 19,7% pakietów proponowanych przez AI w ogóle nie istniało. Prawie co piąty pakiet! Co gorsza, te halucynacje są powtarzalne. Gdy badacze uruchomili ponownie, po 10 razy, zapytania, które wcześniej dały zmyślony pakiet, w 43% przypadków ta sama nazwa wracała za każdym razem. I to właśnie ta przewidywalność sprawia, że atak się opłaca. Badanie dotyczyło Pythona i JavaScriptu, ale mechanizm działa dokładnie tak samo w NuGecie.

Dlatego zasada jest prosta: każdy pakiet, którego nie znasz, sprawdzasz ręcznie. Autor, liczba pobrań, link do repozytorium. 30 sekund, które mogą uratować całą firmę.

#2 SQL injection i sekrety w kodzie


Bezpieczeństwo, czyli klasyka. Oto kod, który AI naprawdę potrafi wygenerować:

/* dane od użytkownika wklejone prosto do zapytania */
var sql = $"SELECT * FROM Users WHERE Email = '{email}'";
using var command = new SqlCommand(sql, connection);

/* sekret na sztywno w kodzie */
private const string PaymentApiKey = "a8f3c2d9-e71b-4f60-9c2d-1b7e3f8a0d42";

Pierwsza część to podręcznikowy SQL injection. Wystarczy, że ktoś wpisze w pole e-mail coś w rodzaju ' OR 1=1 --, a zapytanie zwróci wszystkich użytkowników. Podatność stara jak świat, a AI wciąż ją produkuje, bo nauczyło się jej z milionów starych tutoriali.

Niżej klucz API wklejony na sztywno. Wystarczy 1 commit i sekret siedzi w historii repozytorium na zawsze - nawet jeśli potem usuniesz go z kodu. Jeśli to już się stało, sam commit usuwający klucz nic nie da: taki klucz trzeba unieważnić i wygenerować nowy.

Jak ma być? Zapytanie sparametryzowane, a sekret w konfiguracji:

const string sql = "SELECT * FROM Users WHERE Email = @Email";
using var command = new SqlCommand(sql, connection);
command.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = email;

/* sekret z konfiguracji: user secrets lokalnie, zmienne środowiskowe albo Key Vault na produkcji */
var apiKey = configuration["Payments:ApiKey"];

Co sprawdzam:

• każde zapytanie z parametrami,
• zero sekretów w kodzie,
• walidacja wszystkiego, co przychodzi od użytkownika.

#3 Połknięte wyjątki: catch + return null i throw ex


Obsługa błędów to miejsce, w którym AI uwielbia 2 antywzorce. Pierwszy wygląda tak:

public async Task<PaymentResult?> ChargeAsync(Order order)
{
try
{
return await _paymentGateway.ChargeAsync(order.Id, order.Total);
}
catch (Exception)
{
return null;
}
}

Wygląda "bezpiecznie" - nic się nie wywali. Tylko że płatność właśnie zniknęła po cichu i nikt się o tym nie dowie. Aż zadzwoni klient.

Drugi antywzorzec to throw ex zamiast samego throw:

catch (Exception ex)
{
_logger.LogError(ex.Message);
throw ex; /* stack trace zaczyna się teraz w tej linijce */
}

Różnica 1 słowa, a throw ex nadpisuje stack trace: ślad błędu zaczyna się w miejscu ponownego rzucenia, a nie tam, gdzie problem naprawdę wystąpił. Debugowanie potem to wróżenie z fusów. Poprawna wersja łapie konkretny wyjątek, loguje go z kontekstem i rzuca dalej bez utraty śladu:

catch (PaymentGatewayException ex)
{
_logger.LogError(ex, "Obciążenie karty dla zamówienia {OrderId} nie powiodło się", order.Id);
throw;
}

Co sprawdzam:

• żadnych pustych catchy ani takich, które po cichu zamieniają błąd w null,
• łapiemy konkretny typ wyjątku, nie Exception,
• throw bez ex,
• każdy błąd zostawia ślad w logach - z kontekstem, na przykład z ID zamówienia.

#4 Pułapki async/await: async void, .Result i brak CancellationToken


Asynchroniczność to ulubione pole minowe programistów .NET - i AI chętnie po nim chodzi:

public async void SendNotifications(List<Order> orders)
{
foreach (var order in orders)
await _emailService.SendAsync(order.CustomerEmail);
}

public IActionResult Index()
{
var orders = _orderService.GetOrdersAsync().Result;
return View(orders);
}

async void. Jeśli z takiej metody poleci wyjątek, nie masz go gdzie złapać. Aplikacja potrafi się wywalić bez sensownego śladu.

.Result i .Wait(). Blokowanie taska to klasyczny przepis na deadlock - szczególnie w starszym ASP.NET i w aplikacjach desktopowych (WinForms, WPF). AI to generuje, bo internet sprzed lat jest zasypany takim kodem.

Brak CancellationToken. O tym AI zapomina notorycznie. Jeśli requesty się urywają (użytkownik zamknął kartę, klient przerwał połączenie), a operacje w tle i tak lecą dalej - to jest dokładnie ten problem. Poprawna wersja:

public async Task SendNotificationsAsync(List<Order> orders, CancellationToken ct)
{
foreach (var order in orders)
await _emailService.SendAsync(order.CustomerEmail, ct);
}

/* ASP.NET Core sam przekaże token powiązany z requestem */
public async Task<IActionResult> Index(CancellationToken ct)
{
var orders = await _orderService.GetOrdersAsync(ct);
return View(orders);
}

/* w serwisie token idzie dalej - aż do EF Core */
public Task<List<Order>> GetOrdersAsync(CancellationToken ct)
=> _context.Orders.ToListAsync(ct);

Co sprawdzam:

• async void tylko w event handlerach,
• zero .Result i .Wait(),
• token przekazany przez całą ścieżkę wywołań, a nie tylko obecny w sygnaturze.

#5 HttpClient w pętli i niezwolnione zasoby


Spójrz na ten kod:

foreach (var url in urls)
{
using var client = new HttpClient();
var html = await client.GetStringAsync(url);
/* ... */
}

Jest nawet using, wygląda porządnie. I to jest właśnie pułapka. Zamknięte połączenia i tak wiszą w systemie w stanie TIME_WAIT. Przy większym ruchu kończą się porty i produkcja nagle rzuca błędami połączeń. Klasyczne socket exhaustion: wszystko działa, działa... aż przestaje.

Rozwiązanie od lat jest jedno: IHttpClientFactory. Rejestrujesz klienta przez AddHttpClient, wstrzykujesz go do klasy i tyle - fabryka zarządza cyklem życia połączeń za Ciebie:

builder.Services.AddHttpClient<PriceChecker>();

public class PriceChecker
{
private readonly HttpClient _client;

public PriceChecker(HttpClient client)
{
_client = client;
}

public Task<string> GetPageAsync(string url, CancellationToken ct)
=> _client.GetStringAsync(url, ct);
}

Co sprawdzam:

• HttpClient tylko przez fabrykę,
• using na strumieniach i połączeniach do bazy,
• czy odpinamy event handlery - zapomniana subskrypcja zdarzenia to jedna z najczęstszych przyczyn wycieków pamięci w .NET.

#6 Wydajność: EF Core, ToList w złym miejscu i N+1


Tu ciekawostka: poniższy kod zadziała. Na Twojej maszynie, na testowej bazie z 50 rekordami - hulaj dusza:

var orders = await _context.Orders.ToListAsync(); /* cała tabela ląduje w RAM-ie */

var lastOrders = orders
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt)
.Take(10)
.ToList();

Problem polega na tym, że ToListAsync na samym początku ściąga całą tabelę do pamięci, a filtrowanie odbywa się w RAM-ie. Na produkcji, przy 100 tysiącach zamówień, jest pozamiatane. AI często nie rozróżnia, co poleci do SQL-a, a co wykona się w pamięci.

Wersja poprawna: filtr, sortowanie, Take i projekcja przed materializacją. 1 zapytanie i tylko potrzebne kolumny:

var lastOrders = await _context.Orders
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt)
.Take(10)
.Select(o => new OrderSummaryDto(o.Id, o.Total, o.CreatedAt))
.ToListAsync(ct);

Jeśli zamiast projekcji potrzebujesz całych encji tylko do odczytu, dodaj AsNoTracking(). EF Core nie będzie ich wtedy śledził w change trackerze, co oszczędza pamięć i czas. Przy projekcji do DTO, jak wyżej, śledzenia i tak nie ma.

Druga klasyka to problem N+1, czyli zapytania w pętli:

var orders = await _context.Orders.Where(o => o.Status == OrderStatus.New).ToListAsync(ct);

foreach (var order in orders)
{
/* osobne zapytanie do bazy dla każdego zamówienia */
var customer = await _context.Customers.FirstAsync(c => c.Id == order.CustomerId, ct);
}

100 zamówień = 101 zapytań do bazy. Rozwiązaniem jest Include albo projekcja, która pobierze dane klienta w tym samym zapytaniu.

Co sprawdzam:

• gdzie stoi ToList, czyli co idzie do SQL-a, a co do RAM-u,
• czy nie ma zapytań w pętli (N+1),
• czy odczyty encji mają AsNoTracking.

#7 Edge case'y: null, kultura przy parsowaniu i daty


3 rzeczy, które AI pomija hurtowo:

var total = order.Items.Sum(i => i.Price);
var price = decimal.Parse(input);
order.CreatedAt = DateTime.Now;

Null. Items może być nullem - i mamy NullReferenceException w środku nocy.

Parsowanie. Tu jest smaczek: AI uczyło się głównie na amerykańskim kodzie, a decimal.Parse bez jawnej kultury korzysta z ustawień regionalnych maszyny, na której działa aplikacja. Wartość "9,99" na polskim Windowsie da 9,99. Ale na serwerze z ustawieniami en-US przecinek jest separatorem tysięcy, więc wynik to... 999. Bez żadnego wyjątku, cena po prostu rośnie stukrotnie. W drugą stronę jest niewiele lepiej: "9.99" przy polskiej kulturze kończy się FormatException. Dlatego parsowanie zawsze z jawną kulturą.

Daty. DateTime.Now to czas lokalny serwera. Przenosisz aplikację do innego regionu w chmurze i czasy zaczynają się rozjeżdżać. W bazie trzymamy UTC. Kropka.

Wersja poprawna:

var total = order.Items?.Sum(i => i.Price) ?? 0m;

/* kultura użytkownika dla tego, co wpisuje człowiek; CultureInfo.InvariantCulture dla danych z API i plików */
if (!decimal.TryParse(input, NumberStyles.Number, CultureInfo.GetCultureInfo("pl-PL"), out var price))
{
return BadRequest("Nieprawidłowa cena");
}

order.CreatedAt = DateTime.UtcNow;

Co sprawdzam:

• nulle i puste kolekcje,
• jawną kulturę przy każdym parsowaniu,
• UtcNow zamiast Now,
• wartości brzegowe: 0, -1, pusty string.

#8 Czy kod pasuje do Twojego projektu?


Ten punkt jest inny niż poprzednie, bo nie chodzi w nim o błąd w kodzie. Kod może być w 100% poprawny... i dalej być zły. AI nie zna Twojego projektu - widzi dokładnie tyle, ile mu pokażesz. Dlatego zadaję 4 pytania kontrolne:

• Czy to już u nas nie istnieje? AI uwielbia pisać od zera helper, który macie w projekcie od 3 lat.

• Czy trzyma nasze konwencje? Na przykład: my zwracamy Result, a AI rzuca wyjątkami.

• Czy nie łamie warstw? DbContext wstrzyknięty prosto do kontrolera to klasyk.

• Czy używa naszych bibliotek? Zamiast wymyślać własny mapper, własną walidację czy własne logowanie.

Pro tip: dawaj AI kontekst. Wklej przykładowy plik z projektu, a reguły zespołu trzymaj w repozytorium: w pliku z instrukcjami dla Copilota (.github/copilot-instructions.md), w CLAUDE.md dla Claude Code - w czymkolwiek, co czyta Twoje narzędzie. Wystarczy kilka konkretnych zdań, np.:

# Zasady kodu w naszym projekcie
- Serwisy zwracają Result<T>, nie rzucają wyjątków dla błędów biznesowych.
- Kontrolery nie używają DbContext - tylko serwisów z warstwy aplikacji.
- Mapowanie wyłącznie przez istniejący mapper, bez ręcznych helperów.
- Każda metoda async przyjmuje CancellationToken i przekazuje go dalej.

Im więcej kontekstu dostanie AI, tym mniej Ty poprawiasz.

#9 Testy, które nic nie testują


AI chętnie generuje testy i to jest super... dopóki nie przeczytasz, co one testują. Spójrz:

[Fact]
public async Task SaveOrder_ReturnsTrue()
{
var repository = new Mock<IOrderRepository>();
repository.Setup(r => r.SaveAsync(It.IsAny<Order>())).ReturnsAsync(true);
var service = new OrderService(repository.Object);

var result = await service.SaveOrderAsync(new Order());

Assert.True(result); /* sprawdzamy tylko to, co sami ustawiliśmy w mocku */
}

Konfigurujemy mocka, żeby SaveAsync zwracał true. Wywołujemy serwis. I sprawdzamy... że wyszło true. Ten test nie testuje żadnej logiki, tylko to, czy mock działa. Zielony ptaszek jest, wartość - zero.

Sensowny test sprawdza zachowanie, najlepiej poza happy pathem:

[Fact]
public async Task SaveOrder_Throws_WhenTotalIsNegative()
{
var repository = new Mock<IOrderRepository>();
var service = new OrderService(repository.Object);
var order = new Order { Total = -10m };

await Assert.ThrowsAsync<ArgumentException>(() => service.SaveOrderAsync(order));
repository.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Never);
}

Test mutacyjny dla ubogich. To mój ulubiony trik: celowo zepsuj logikę - zakomentuj linijkę, odwróć warunek - i odpal testy. Jeśli dalej są zielone... to nie są testy. To dekoracja. Pełnoprawną, automatyczną wersją tego triku jest testowanie mutacyjne (w .NET np. narzędziem Stryker.NET), ale na code review w zupełności wystarczy wersja ręczna.

Co sprawdzam:

• edge case'y, nie tylko happy path,
• asserty na wyniku i zachowaniu, nie na setupie mocka,
• nazwy, które mówią CO i KIEDY - jak SaveOrder_Throws_WhenTotalIsNegative,
• czy testy czerwienieją, gdy zepsuję logikę.

#10 Overengineering, czyli kod, który tylko wygląda profesjonalnie


AI jest trenowane tak, żeby kod wyglądał profesjonalnie. Efekt? Na jedną implementację dostajesz interfejs, fabrykę i providera:

public interface IDiscountCalculator { decimal Calculate(Order order); }
public interface IDiscountCalculatorFactory { IDiscountCalculator Create(); }
public class DiscountCalculatorFactory : IDiscountCalculatorFactory { /* ... */ }
public class DiscountCalculatorProvider { /* ... */ }

#region Fields
private int _count;
private string _name;
#endregion

/* zwiększ licznik o jeden */
_count++;

Do tego komentarze opisujące oczywistości ("zwiększ licznik o jeden" - serio?) i region na 2 pola.

Każda linijka kodu to koszt. Ktoś będzie ją czytał, utrzymywał i debugował. Za rok. O 3 w nocy.

Test "usuń 1/3". Zadaj sobie pytanie: czy mogę usunąć 1/3 tego kodu i wszystko dalej działa? Jeśli tak, ta 1/3 była zbędna. Abstrakcje dodajemy wtedy, kiedy są potrzebne, a nie "na zapas" - YAGNI (You Aren't Gonna Need It). Często wystarczy po prostu:

public class DiscountCalculator
{
public decimal Calculate(Order order)
=> order.Total >= 500m ? order.Total * 0.05m : 0m;
}

Gdy pojawi się druga implementacja, interfejs wydzielisz w 2 minuty. Do tego czasu jest tylko kodem, który trzeba czytać.

Cała checklista: 10 punktów w jednej tabeli


To cała dziesiątka. Wygląda na dużo? Na początku tak. Ale po tygodniu robisz to automatycznie - oko samo łapie .Result i samo szuka ToList w złym miejscu.

#ObszarCzerwona flaga w kodzie od AICo sprawdzam
1Zmyślone API i pakietymetoda, której nie kojarzysz; nieznany pakiet NuGetkompilacja, pakiet na nuget.org (autor, pobrania, repozytorium), API zgodne z wersją .NET
2SQL injection i sekrety$"SELECT ... '{email}'", klucz API w stałejparametry w każdym zapytaniu, sekrety w konfiguracji, walidacja wejścia
3Wyjątkicatch (Exception) { return null; }, throw exkonkretny typ wyjątku, log z kontekstem, throw bez ex
4async/awaitasync void, .Result, .Wait(), brak tokenaasync void tylko w event handlerach, CancellationToken przez całą ścieżkę
5Zasobynew HttpClient() w pętliIHttpClientFactory, using na strumieniach i połączeniach, odpinanie eventów
6EF Core i LINQToListAsync() na całej tabeli, zapytanie w pętlifiltr, Take i Select przed materializacją, brak N+1, AsNoTracking przy odczycie
7Edge case'ybrak null-checka, decimal.Parse(input), DateTime.Nownull i puste kolekcje, jawna kultura, UtcNow, wartości 0 / -1 / pusty string
8Zgodność z projektemnowy helper, własny mapper, DbContext w kontrolerzeczy już istnieje, konwencje, warstwy, nasze biblioteki
9Testyassert na wartości ustawionej w mockuedge case'y, asserty na zachowaniu, nazwy CO i KIEDY, "zepsuj logikę"
10Overengineeringinterfejs + fabryka na 1 implementację, komentarze-oczywistościtest "usuń 1/3", YAGNI


I jeszcze 1 ważna rzecz. Cała ta lista łapie błędy na wyjściu. Ale możesz też poprawić wejście - im lepszy prompt, tym mniej masz potem do poprawiania. Wrócę do tego w podsumowaniu.

Złota zasada: git blame nie pokaże "Copilot"


Na koniec jedna rzecz, którą warto zapamiętać, nawet jeśli zapomnisz całą resztę:

git blame nie pokaże "Copilot". Pokaże Ciebie.

AI może pisać kod. Ale odpowiedzialność za to, co idzie na produkcję, jest Twoja. I dobrze - bo właśnie to odróżnia programistę od generatora kodu.

Podsumowanie


Kod od AI wygląda dobrze i właśnie dlatego trzeba go czytać uważniej niż kod od juniora, a nie mniej uważnie. W skrócie:

• 45% kodu od AI w badaniu Veracode zawierało podatność z OWASP Top 10 - review to konieczność, nie opcja.
• Traktuj AI jak juniora z nieskończoną pewnością siebie: szybki, oczytany, nie zna Twojego projektu i nigdy nie mówi "nie wiem".
• Kompiluj i weryfikuj każdy nieznany pakiet - slopsquatting to realny atak.
• Bezpieczeństwo: parametry w zapytaniach, sekrety w konfiguracji.
• Niezawodność: żadnych połkniętych wyjątków, żadnego .Result ani async void, HttpClient przez fabrykę.
• Wydajność: ToList na końcu zapytania, nie na początku.
• Edge case'y: null, jawna kultura, UTC.
• Kontekst projektu, sensowne testy i YAGNI.

Merge klikasz Ty - i to Twoje nazwisko pokaże git blame.

Jeśli chcesz poprawić też wejście, mam coś dla Ciebie. Przygotowałem darmowy PDF: 25 promptów AI dla programistów C#/.NET, które odwalają za Ciebie refaktoryzację, testy jednostkowe, debugowanie, code review i pracę z legacy. 8 stron, wszystko przetestowane na prawdziwym kodzie C#/.NET. Kopiujesz, wklejasz, podmieniasz nazwy w nawiasach i widzisz różnicę w 5 minut. Działa w Claude Code, Copilocie, Cursorze i ChatGPT - a im lepszy prompt, tym mniej czerwonych flag na code review.

Odbierzesz go tutaj: modestprogrammer.pl/25-promptow. Zostawiasz maila i PDF od razu ląduje w Twojej skrzynce. Przy okazji dołączasz do społeczności ponad 10 000 programistów .NET: raz w tygodniu jeden konkretny mail - newsy ze świata .NET i AI, rzeczy, które sam aktualnie testuję, i od czasu do czasu rabaty na moje kursy, dostępne tylko tam. Zero spamu - wypisujesz się jednym kliknięciem.

Powodzenia na najbliższym code review :)

Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
CTO & Founder - FindSolution.pl
Programista C#/.NET. Specjalizuje się w Blazor, ASP.NET Core, ASP.NET MVC, ASP.NET Web API, WPF oraz Windows Forms.
Autor bloga ModestProgrammer.pl
Dodaj komentarz
© Copyright 2026 modestprogrammer.pl | Sztuczna Inteligencja | Regulamin | Polityka prywatności. Design by Kazimierz Szpin. Wszelkie prawa zastrzeżone.