Jeśli używasz AI do pisania kodu w C#, jest spora szansa, że ten kod jest gorszy, niż mógłby być. I nie, to nie jest wina GitHub Copilota, ChatGPT ani Claude'a. W 9 przypadkach na 10 winny jest sposób, w jaki z tych narzędzi korzystamy: jednozdaniowy prompt, zero kontekstu i pierwsza odpowiedź wklejona prosto do commita.
Pracuję z AI codziennie przy komercyjnych projektach .NET i u programistów widzę w kółko te same błędy. Dobra wiadomość jest taka, że każdy z nich naprawisz w kilka minut. Nie potrzebujesz do tego nowych narzędzi ani droższej subskrypcji, tylko innego sposobu pracy.
W tym artykule przejdziemy przez 7 najczęstszych błędów, przez które AI generuje słaby kod w C#, plus 1 bonusowy, o którym mało kto mówi. Przy każdym zobaczysz konkretny przykład: kod sprzed dekady, happy path, który wybucha na produkcji, decimal.Parse wywracający się na polskiej kulturze czy klasyczny problem N+1 w EF Core. Na koniec dostaniesz gotowy szablon prompta, który od razu podnosi jakość kodu z AI.
Wersje .NET: przykłady kodu są napisane pod .NET 8 i C# 12 (m.in. primary constructors) i działają tak samo w .NET 9 i 10. Same zasady nie zależą ani od wersji frameworka, ani od narzędzia - sprawdzą się w Copilocie, ChatGPT, Claude Code czy Cursorze. W promptach wpisuj po prostu wersję, której używasz w swoim projekcie.
Zasada nr 1: kod od AI to kod od juniora
Zanim przejdziemy do konkretów, jedna rzecz, która ustawia całą resztę: AI nie pisze słabego kodu. AI pisze taki kod, na jaki mu pozwalasz.
Najlepszy model mentalny, jaki znam: traktuj AI jak bardzo szybkiego juniora. Junior bez kontekstu, bez konkretnego zadania i bez code review napisze Ci byle co. Ten sam junior z kontekstem, jasno opisanym zadaniem i porządnym review napisze kod lepszy niż niejeden regular. Z AI jest dokładnie tak samo. Cały proces sprowadza się do 3 kroków:
1. Kontekst - co to za projekt, jaki stack, jakie konwencje.
2. Zadanie - jedna konkretna rzecz do zrobienia.
3. Review - sprawdzasz wynik, zanim trafi do repozytorium.
Wszystkie błędy, które za chwilę omówimy, to łamanie tej jednej zasady. Każdy w inny sposób, ale zawsze tej samej.
Błąd #1: zero kontekstu w promptach
Piszesz do AI: "Napisz mi serwis do obsługi zamówień w C#." I co dostajesz? Losową architekturę, wymyślone modele i kod, który nijak nie pasuje do Twojego projektu. A potem pada wniosek: "AI jest słabe".
Nie. AI po prostu nie czyta Ci w myślach. Taki prompt to jak rzucenie juniorowi zadania jednym zdaniem na korytarzu, bez dostępu do repozytorium. Trudno oczekiwać, że trafi w Twoją architekturę, skoro nigdy jej nie widział.
Prompt, który daje wyniki. Porównaj to jedno zdanie z promptem, w którym AI dostaje wszystko, czego potrzebuje: wersję .NET i bibliotek, architekturę, wklejony istniejący interfejs, jedno konkretne zadanie i listę wymagań:
Stack: .NET 8, C# 12, ASP.NET Core Web API, EF Core 8.
Architektura: Clean Architecture - warstwy Domain, Application, Infrastructure i API.
Logika biznesowa siedzi w serwisach w warstwie Application.
Istniejący interfejs:
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(int id, CancellationToken ct);
Task AddAsync(Order order, CancellationToken ct);
}
Zadanie: napisz klasę OrderService z metodą
CreateOrderAsync(CreateOrderRequest request, CancellationToken ct),
która tworzy nowe zamówienie i zapisuje je przez IOrderRepository.
Wymagania:
- zamówienie musi mieć co najmniej 1 pozycję, inaczej zwróć błąd walidacji,
- korzystaj tylko z IOrderRepository, nie z DbContext bezpośrednio,
- async z CancellationToken w każdej metodzie,
- bez dodatkowych bibliotek.Taki prompt piszesz 2 minuty dłużej, ale oszczędzasz pół godziny poprawek. AI nie musi zgadywać, jak nazywasz klasy, gdzie trzymasz logikę ani z czego ma korzystać - wszystko ma czarno na białym.
Zapamiętaj: AI wie o Twoim projekcie dokładnie tyle, ile mu powiesz. Ani słowa więcej.
Błąd #2: nie podajesz wersji .NET i C#
AI uczyło się na milionach linii kodu z internetu. A wiesz, jaki kod dominuje w internecie? Stary. Odpowiedzi ze Stack Overflow z 2014 roku, tutoriale do .NET Framework 4.5, wpisy na blogach, których nikt nie aktualizował od lat.
Dlatego bez podanej wersji potrafisz dostać WebClienta zamiast HttpClienta, Newtonsoft.Json zamiast System.Text.Json i kod synchroniczny tam, gdzie powinien być async. Tak może wyglądać kod, który AI wypluje bez żadnego kontekstu:
public class ProductService
{
public Product GetProduct(int id)
{
using (var client = new WebClient())
{
client.Headers.Add("Accept", "application/json");
var json = client.DownloadString("https://api.example.com/products/" + id);
var product = JsonConvert.DeserializeObject<Product>(json);
return product;
}
}
}Działa? Działa. Ale to kod sprzed dekady. WebClient jest w nowoczesnym .NET oznaczony jako przestarzały (ostrzeżenie SYSLIB0014 od .NET 6), wątek blokuje się na czas całego żądania, a do tego dochodzi zewnętrzna biblioteka do JSON-a, której w nowym projekcie w ogóle nie potrzebujesz.
A teraz ten sam kod po dopisaniu do prompta jednego zdania: "Używam .NET 8 i C# 12, pisz nowoczesny kod."
public class ProductService(HttpClient httpClient) /* HttpClient z IHttpClientFactory */
{
public Task<Product?> GetProductAsync(int id, CancellationToken ct)
=> httpClient.GetFromJsonAsync<Product>($"products/{id}", ct);
}Primary constructor, async z CancellationToken, wbudowane GetFromJsonAsync z System.Text.Json i jedna linijka logiki zamiast kilku. HttpClient przychodzi z kontenera DI (przez IHttpClientFactory), więc nie tworzysz go ręcznie przy każdym żądaniu.
Jedno zdanie w prompcie. Dekada różnicy w kodzie.
Najlepiej, żebyś nie musiał o tym zdaniu w ogóle pamiętać. Ja mam je zapisane na stałe w instrukcjach Copilota i w ustawieniach czatu - i polecam zrobić to samo. W Copilocie służy do tego plik .github/copilot-instructions.md w repozytorium (Claude Code ma swój odpowiednik: plik CLAUDE.md), a w ChatGPT czy Claude - instrukcje własne w ustawieniach. Wpisujesz raz i masz spokój przy każdym kolejnym prompcie.
Błąd #3: akceptujesz happy path
AI domyślnie pisze kod w wersji optymistycznej: API zawsze odpowiada, JSON zawsze się parsuje, nic nie jest nullem. Kod się kompiluje, demo działa, wszyscy zadowoleni. A potem jest piątek, 16:50, produkcja i NullReferenceException.
Zobacz ten kod. Pobiera kurs waluty z zewnętrznego API i na pierwszy rzut oka wygląda porządnie:
public class ExchangeRateService(HttpClient httpClient)
{
public async Task<decimal> GetExchangeRateAsync(string currency)
{
var response = await httpClient.GetAsync($"rates/{currency}");
var rate = await response.Content.ReadFromJsonAsync<RateDto>();
return rate!.Mid;
}
}Policzmy pułapki:
1. Brak sprawdzenia statusu HTTP. Gdy API zwróci błąd 500 ze stroną HTML zamiast JSON-a, deserializacja wybucha wyjątkiem, z którego trudno się domyślić, co naprawdę się stało.
2. rate może być nullem. Wystarczy, że API zwróci w treści samo "null", i masz kolejny wybuch, tym razem NullReferenceException. Wykrzyknik tylko ucisza ostrzeżenie kompilatora, niczego nie sprawdza.
3. Brak CancellationToken. Żądania nie da się anulować. Gdy użytkownik zamknie stronę albo API zacznie odpowiadać bardzo wolno, request i tak wisi - w najgorszym razie aż do domyślnego timeoutu HttpClienta, czyli 100 sekund.
4. Zero logów. Jak coś padnie, zostaje Ci zgadywanie.
4 pułapki w 3 linijkach logiki. I to jest kod, który programiści wklejają do projektów bez mrugnięcia okiem.
Poprawka: 1 zdanie w prompcie. Wystarczy dopisać: "Dodaj obsługę błędów: sprawdź status HTTP, obsłuż null, dodaj CancellationToken." I dostajesz coś takiego:
public class ExchangeRateService(
HttpClient httpClient,
ILogger<ExchangeRateService> logger)
{
public async Task<decimal?> GetExchangeRateAsync(string currency, CancellationToken ct)
{
using var response = await httpClient.GetAsync($"rates/{currency}", ct);
if (!response.IsSuccessStatusCode)
{
logger.LogWarning("API kursów zwróciło {StatusCode} dla {Currency}",
(int)response.StatusCode, currency);
return null;
}
var rate = await response.Content.ReadFromJsonAsync<RateDto>(ct);
if (rate is null)
{
logger.LogWarning("API kursów zwróciło pustą odpowiedź dla {Currency}", currency);
return null;
}
return rate.Mid;
}
}Status sprawdzony, log przy błędzie, token przekazywany wszędzie, a nullable zwracany świadomie - wywołujący widzi w sygnaturze, że kursu może nie być, i musi ten przypadek obsłużyć. To kod gotowy na produkcję, a nie na demo.
Mój ulubiony trick. Po wygenerowaniu kodu pytam AI wprost: "Co może pójść nie tak w tym kodzie na produkcji?" Zwykle samo znajduje własne błędy. Serio, to działa zaskakująco dobrze, a kosztuje Cię kilka sekund.
Błąd #4: za duże zadania naraz
"Napisz mi cały moduł płatności: integracja ze Stripe, baza danych, API, walidacja." Znasz to? Efekt jest zawsze ten sam - AI zaczyna halucynować:
• wymyśla metody, których nie ma w bibliotece,
• gubi spójność między klasami,
• pomija połowę wymagań.
To działa dokładnie jak z juniorem. Nie mówisz mu "napisz system", tylko dajesz mu taski. Zasada jest prosta: 1 prompt = 1 zadanie.
5 małych kroków zamiast 1 giga-prompta. Ten sam moduł płatności możesz zbudować tak:
1. Interfejs - same sygnatury metod z komentarzami, zero implementacji.
2. Twój review architektury - Twój moment kontroli, zanim powstanie jakikolwiek kod.
3. Implementacja jednej metody.
4. Testy do tej metody.
5. Kolejna metoda albo refactor - i tak dalej.
Pierwszy prompt może brzmieć na przykład tak: "Zaprojektuj interfejs IPaymentService dla płatności przez Stripe: utworzenie płatności, sprawdzenie statusu i zwrot. Same sygnatury z komentarzami XML, bez implementacji." Zanim pójdziesz dalej, sprawdzasz, czy taki kontrakt w ogóle pasuje do Twojego systemu. Poprawienie interfejsu na tym etapie to drobna zmiana. Poprawienie go, kiedy stoi na nim już kilka klas, to przepisywanie połowy modułu.
Każdy krok sprawdzasz, zanim błąd rozleje się na resztę. I tu jest paradoks: mniejsze prompty to szybsza praca, bo nie cofasz się o 3 kroki, żeby naprawić fundament.
Błąd #5: AI nie zna Twojego projektu (a Ty udajesz, że zna)
Efekt znasz: kod, który sam w sobie jest OK, ale w Twoim projekcie wygląda jak przeszczep. Inne nazewnictwo. Wyjątki tam, gdzie wszędzie indziej używasz wzorca Result. Duplikat helpera, który już istnieje 3 foldery dalej.
Błąd #1 dotyczył ogólnego kontekstu: stacku i architektury. Tu chodzi o coś więcej - o Twój konkretny kod i Twój styl. Rozwiązanie jest banalne: pokaż AI swój kod. Do prompta wklej 3 rzeczy:
• Encję albo model, którego dotyczy zadanie.
• Interfejsy, z którymi nowy kod ma współpracować.
• 1 przykładową klasę jako wzorzec stylu - z dopiskiem "pisz dokładnie w tym stylu".
To ostatnie zdanie robi więcej niż akapit opisu konwencji. Zamiast tłumaczyć, że używasz wzorca Result, primary constructors i określonego nazewnictwa metod, po prostu to pokazujesz. AI bardzo dobrze naśladuje wzorce, które ma przed oczami.
Pro tip dla użytkowników Copilota. Copilot widzi otwarte karty w edytorze i traktuje je jako kontekst. Zanim zaczniesz pisać, otwórz encję, interfejs i podobny serwis. Jakość podpowiedzi rośnie natychmiast, a Ty nie wykonujesz żadnej dodatkowej pracy.
Błąd #6: kompiluje się, więc działa
Kod się kompiluje, więc uznajesz, że działa. Tymczasem kompilacja to nie test.
Klasyk z polskich projektów. AI generuje coś takiego:
var amount = decimal.Parse(input); /* na pl-PL: "10.50" → FormatException */Na Twojej maszynie działa. Wdrażasz na serwer z kulturą pl-PL i nagle "10.50" wywala FormatException, bo polska kultura oczekuje przecinka jako separatora dziesiętnego. AI notorycznie zapomina o InvariantCulture, bo w przykładach z internetu prawie nikt go nie używa. Poprawka to jeden parametr:
var amount = decimal.Parse(input, CultureInfo.InvariantCulture);Każ AI pisać testy razem z kodem. Wracamy do zasady z początku: kod od AI traktujesz jak kod od juniora. A junior nie merguje bez testów. Dlatego razem z kodem zamawiaj testy: "Napisz testy xUnit. Uwzględnij edge case'y: null, pusty string, kultura pl-PL."
Przypadki wymieniaj konkretnie, bo samo "napisz testy" da Ci 3 testy happy path. Z dopiskiem o kulturze dostaniesz coś w tym stylu (zakładając, że parsowanie siedzi w metodzie AmountParser.Parse):
[Fact]
public void Parse_OnPolishCulture_ReadsDotAsDecimalSeparator()
{
CultureInfo.CurrentCulture = new CultureInfo("pl-PL");
var result = AmountParser.Parse("10.50");
Assert.Equal(10.50m, result);
}Na wersji bez InvariantCulture ten test wywali się od razu, jeszcze na Twojej maszynie, a nie dopiero na serwerze.
Level up: AI-TDD. Poziom wyżej to odwrócenie kolejności. Najpierw każ AI napisać testy, zatwierdź je, a dopiero potem poproś o implementację. Testy stają się Twoją specyfikacją - i nagle AI dokładnie wie, co ma powstać.
Błąd #7: bierzesz pierwszą odpowiedź
I błąd, który lubię najbardziej. Pierwsza odpowiedź AI to szkic. Draft. A większość programistów traktuje ją jak wersję finalną: kopiuj, wklej, commit, push.
Tymczasem AI jest zaskakująco dobre w krytykowaniu własnego kodu. Musisz tylko zapytać. Oto 5 pytań, które zadaję po każdym wygenerowanym fragmencie:
1. "Zrób code review tego kodu jak senior .NET developer. Bądź krytyczny." Dopisek o krytyczności jest ważny, bo bez niego AI grzecznie chwali.
2. "Jakie edge case'y pominąłeś?"
3. "Gdzie ten kod może rzucić wyjątek na produkcji?"
4. "Jak ten kod zachowa się przy 10 000 rekordów?" To pytanie łapie problemy wydajnościowe.
5. "Zrefaktoruj pod testowalność." Wyciąga zależności do interfejsów, dzięki czemu kod łatwiej przetestować.
Każde pytanie to kilkanaście sekund. A co potrafią wyłapać? Zobacz na przykładzie.
Przykład: N+1 w EF Core. Pierwsza wersja od AI: pobierz zamówienia, a potem w pętli dociągnij klienta dla każdego z nich:
var orders = await db.Orders
.Where(o => o.CreatedAt >= from)
.ToListAsync(ct);
var result = new List<OrderDto>();
foreach (var order in orders)
{
var customer = await db.Customers.FindAsync(order.CustomerId); /* zapytanie w pętli */
result.Add(new OrderDto(order.Id, order.Total, customer!.Name));
}Kod działa, testy przechodzą. Tylko że to klasyczny problem N+1: 1 zapytanie o zamówienia plus osobne zapytanie o klienta dla każdego z nich. 100 zamówień to nawet 101 zapytań do bazy. Na dev-bazie z 10 rekordami tego nie zobaczysz. Na produkcji API klęka.
Zadajesz jedno pytanie: "Jak ten kod zachowa się przy 10 000 rekordów?" I AI samo odpowiada: problem N+1, użyj Include:
var orders = await db.Orders
.Where(o => o.CreatedAt >= from)
.Include(o => o.Customer)
.ToListAsync(ct);
var result = orders
.Select(o => new OrderDto(o.Id, o.Total, o.Customer.Name))
.ToList();1 zapytanie zamiast 101. A jeśli potrzebujesz tylko kilku pól, możesz pójść krok dalej i zrobić projekcję przez Select bezpośrednio w zapytaniu - wtedy z bazy pobierasz wyłącznie te kolumny, których faktycznie używasz.
Jedna iteracja więcej. Kilkanaście sekund. A różnica taka, że nikt nie budzi Cię w nocy telefonem z produkcji.
Bonus: nie wklejaj sekretów do czatu
Na koniec błąd, o którym mało kto mówi, a powinien, bo na firmowych projektach to temat numer 1. Zanim wkleisz kod do prompta, zamień na placeholdery wszystko, co wrażliwe:
• connection stringi,
• klucze API,
• dane klientów.
Zamiast prawdziwego connection stringa wystarczy coś takiego: "Server=<SERWER>;Database=<BAZA>;User Id=<UŻYTKOWNIK>;Password=<HASŁO>". AI nie potrzebuje prawdziwego hasła do bazy, żeby napisać Ci repozytorium. A Ty na pewno nie chcesz tłumaczyć się działowi bezpieczeństwa, dlaczego produkcyjny connection string wylądował w zewnętrznym narzędziu.
Jeśli pracujesz w firmie, sprawdź też, jaką politykę używania AI macie w zespole: jakie narzędzia są dozwolone i na jakich kontach. To 2 minuty, które mogą Ci oszczędzić bardzo niezręcznej rozmowy.
Ściąga: 7 błędów i 7 poprawek
Wszystkie błędy w pigułce:
| Błąd | Poprawka |
| Zero kontekstu | podawaj stack i architekturę |
| Brak wersji .NET i C# | ".NET 8, C# 12" (albo Twoja wersja) w każdym prompcie, najlepiej na stałe w instrukcjach |
| Happy path | wymagaj obsługi błędów |
| Giga-zadania | 1 prompt = 1 zadanie |
| Obcy styl | wklejaj wzorzec swojego kodu |
| Brak testów | AI pisze testy razem z kodem (albo przed nim) |
| Pierwsza odpowiedź | zawsze minimum 1 iteracja |
| Bonus: sekrety w promptach | placeholdery zamiast prawdziwych danych |
Zwróć uwagę na jedno: żaden z tych punktów nie wymaga nowych narzędzi ani płatnych subskrypcji. To zmiana sposobu pracy, którą możesz wdrożyć dosłownie dziś, przy najbliższym tasku.
Szablon prompta do skopiowania
Obiecany szablon łączy wszystko, o czym była mowa, w 4 sekcjach:
KONTEKST
Stack: .NET 8, C# 12, EF Core 8
Architektura: [1 zdanie o Twojej]
WZORZEC STYLU
[wklej interfejs + 1 przykładową klasę]
ZADANIE
[jedna konkretna rzecz]
WYMAGANIA
- pełna obsługa błędów, nie happy path
- testy xUnit z edge case'ami
- na koniec: wypisz, co może pójść nie takKontekst to stack i architektura. Wzorzec stylu to Twój interfejs i jedna przykładowa klasa. Zadanie - jedna konkretna rzecz. A w wymaganiach mój ulubiony punkt na końcu: "wypisz, co może pójść nie tak". To zdanie wymusza na AI autorecenzję jeszcze przed Twoim review.
Jeśli Twój projekt stoi już na nowszej wersji, po prostu podmień pierwszą linijkę, np. na .NET 10, C# 14 i EF Core 10. Reszta szablonu zostaje bez zmian.
Podsumowanie
AI nie pisze słabego kodu - pisze taki, na jaki mu pozwalasz. Traktuj je jak bardzo szybkiego juniora i daj mu to, czego potrzebuje każdy junior:
• dawaj kontekst: stack, architekturę, istniejące interfejsy,
• podawaj wersję .NET i C#, najlepiej na stałe w instrukcjach narzędzia,
• wymagaj obsługi błędów, nie tylko happy path,
• dziel pracę na małe zadania: 1 prompt = 1 zadanie,
• pokazuj AI swój kod jako wzorzec stylu,
• wymagaj testów razem z kodem (albo przed nim),
• nigdy nie bierz pierwszej odpowiedzi - zawsze minimum 1 iteracja,
• nie wklejaj sekretów do czatu.
Nie próbuj wdrażać wszystkiego naraz. Wybierz jeden punkt i zastosuj go przy najbliższym zadaniu. Najlepiej zacznij od kontekstu w promptach, bo to daje najszybszy efekt.
A jeśli chcesz iść krok dalej, mam coś dla Ciebie. Spakowałem swoje sprawdzone prompty do darmowego PDF-a: 25 promptów AI dla programistów C#/.NET. 8 stron i 5 obszarów, w których AI oszczędza najwięcej czasu: refaktoryzacja, testy jednostkowe, debugowanie, code review oraz legacy i dokumentacja - po 5 promptów na każdy. Do tego 3 zasady pracy z AI, które spinają wszystko, o czym była mowa w tym artykule. Każdy prompt przetestowałem na prawdziwym kodzie i do każdego dopisałem krótką wskazówkę, dlaczego jest tak zbudowany - żebyś umiał pisać własne, a nie tylko kopiować moje. Działają w Claude Code, Copilocie, Cursorze i ChatGPT. Przykład? Prompt nr 9: test regresyjny przed poprawką. AI najpierw pisze czerwony test, który odtwarza buga, czeka na Twoją akceptację i dopiero potem naprawia.
Odbierzesz go tutaj: modestprogrammer.pl/25-promptow. Zostawiasz maila i PDF przychodzi od razu. Przy okazji dołączasz do społeczności ponad 10 000 programistów: raz w tygodniu jeden konkretny mail o .NET i AI, plus rabaty na moje kursy, których nie znajdziesz nigdzie indziej. Zero spamu - wypisujesz się jednym kliknięciem.
Powodzenia - i niech Twój następny prompt będzie dłuższy niż jedno zdanie :)