Idziesz na rozmowę rekrutacyjną na stanowisko Junior .NET Developer i chcesz wiedzieć, czego się spodziewać? Dobrze trafiłeś.
Poniżej znajdziesz 10 pytań, które padają na niemal każdej rozmowie — od interfejsów i Dependency Injection, przez async/await, aż po zasadę SRP. Do każdego pytania dostajesz:
- ❌ Złą odpowiedź — to, co mówi większość kandydatów (poprawne, ale niepełne).
- ✅ Wzorcową odpowiedź — z działającym kodem C# i testami xUnit.
- 💡 Wskazówkę — zdanie, które robi różnicę w oczach rekrutera.
📥 Cały kod źródłowy (solution Visual Studio) pobierzesz z repozytorium GitHub. Uruchom dotnet test i sprawdź, że wszystkie 22 testy są zielone.
Spis treści
- Interface vs Abstract class
- Dependency Injection
- Record vs Class
- async/await i ConfigureAwait
- LINQ i lazy evaluation
- Value types vs Reference types
- Generics i constraints
- Wyjątki vs kody błędów
- IDisposable i using
- SRP — Single Responsibility Principle
1. Interface vs Abstract class

Pytanie rekrutera: Czym różni się interface od abstract class? Kiedy użyjesz jednego, a kiedy drugiego?
❌ Zła odpowiedź
„Interface nie ma implementacji, abstract class może mieć.”
Technicznie poprawne — ale rekruter zaznacza w notatce: „zna definicję, nie rozumie zastosowania”. Brakuje odpowiedzi na kluczowe pytanie: kiedy co wybrać?
✅ Wzorcowa odpowiedź
Interface definiuje KONTRAKT — mówi co coś robi, nie jak. Używaj go, kiedy różne, niepowiązane klasy muszą spełnić ten sam kontrakt:
// KONTRAKT — różne klasy, ten sam interfejs
public interface IEmailService
{
Task SendAsync(string to, string subject, string body);
}
// Dwie niezależne implementacje — łączy je kontrakt, nie hierarchia
public class SmtpEmailService : IEmailService
{
public Task SendAsync(string to, string subject, string body)
{
Console.WriteLine($"[SMTP] {to}: {subject}");
return Task.CompletedTask;
}
}
public class SendGridEmailService : IEmailService
{
public Task SendAsync(string to, string subject, string body)
{
Console.WriteLine($"[SendGrid] {to}: {subject}");
return Task.CompletedTask;
}
}
Abstract class definiuje SZKIELET — wspólne zachowanie plus wymuszony override. Używaj, gdy klasy są ze sobą powiązane i dzielą wspólny kod:
// SZKIELET — wspólny kod + wymuszony override
public abstract class BaseRepository<T> where T : class
{
protected readonly List<T> _store = new(); // wspólne dla każdego repo
public abstract T? GetById(int id); // wymuszony override
public abstract void Add(T entity); // wymuszony override
public virtual IReadOnlyList<T> GetAll() // gotowa implementacja
=> _store.AsReadOnly();
public void AddRange(IEnumerable<T> entities) // wspólna logika
{
foreach (var e in entities) Add(e);
}
}
Reguła do zapamiętania
| Interface | Abstract class |
| „Mogę to zrobić” — kontrakt | „Jestem tym” — wspólna tożsamość |
| Klasy mają różne origin, to samo zachowanie | Klasy są tym samym, różnią się szczegółami |
| Łatwe mockowanie w testach | Wymaga konkretnej implementacji |
💡 Wskazówka na rozmowę:
„W C# 8 interface może mieć domyślną implementację — ważne przy rozszerzaniu API bez łamania istniejących klas. W nowoczesnym C# preferuję interface plus composition over inheritance.”
📖 Zobacz też: Wzorzec repozytorium w C# — jak poprawnie zaimplementować Repository Pattern
2. Dependency Injection

Pytanie rekrutera: Co to jest Dependency Injection i jakie są czasy życia serwisów w ASP.NET Core?
❌ Zła odpowiedź
„DI to wzorzec projektowy, gdzie przekazujemy zależności przez konstruktor zamiast tworzyć je w klasie.”
Poprawne, ale brakuje: dlaczego? Co zyskujemy? Jak wygląda to w ASP.NET Core?
✅ Wzorcowa odpowiedź — najpierw problem
// ❌ BEZ DI — tight coupling, brak testowalności
public class OrderServiceBad
{
// Klasa SAMA tworzy swoje zależności
// Nie można podmienić bez zmiany kodu
// Nie można przetestować bez bazy i SMTP
private readonly SqlOrderRepositoryBad _repository = new();
private readonly SmtpEmailServiceBad _email = new();
public void PlaceOrder(int userId, decimal amount)
{
_repository.Save(userId, amount);
_email.Send(userId, amount);
}
}
✅ Wzorcowa odpowiedź — rozwiązanie z DI
// Krok 1: Zdefiniuj interfejsy (kontrakty)
public interface IOrderRepository
{
void Save(int userId, decimal amount);
IReadOnlyList<(int UserId, decimal Amount)> GetAll();
}
public interface IEmailNotifier
{
void SendConfirmation(int userId, decimal amount);
}
// Krok 2: Serwis zależy od INTERFEJSÓW, nie od klas
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailNotifier _notifier;
// ✅ Konstruktor przyjmuje interfejsy
// Można przekazać SqlRepo, MongoRepo, InMemoryRepo — bez zmian w kodzie
public OrderService(IOrderRepository repository, IEmailNotifier notifier)
{
_repository = repository;
_notifier = notifier;
}
public void PlaceOrder(int userId, decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be > 0");
_repository.Save(userId, amount);
_notifier.SendConfirmation(userId, amount);
}
}
Trzy czasy życia w ASP.NET Core
To pytanie prawie zawsze pada jako follow-up. Jeśli sam wspomnisz czasy życia bez pytania — rekruter robi notatkę:
| Czas życia | Kiedy nowa instancja? | Typowe użycie |
| Transient | Przy każdym żądaniu o serwis (resolve) | Lekkie serwisy bezstanowe |
| Scoped | Jedna instancja na HTTP request | DbContext, serwisy z danymi żądania |
| Singleton | Jedna instancja na cały czas życia aplikacji | Cache, konfiguracja — musi być thread-safe! |
// Rejestracja w Program.cs (ASP.NET Core)
builder.Services.AddTransient<IEmailNotifier, SmtpEmailNotifier>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddSingleton<ICacheService, InMemoryCacheService>();
Test dowodzący wartość DI
[Fact]
public void OrderService_PlaceOrder_SavesAndSendsEmail()
{
// Test BEZ bazy danych i BEZ serwera SMTP — to jest wartość DI
var repo = new InMemoryOrderRepository();
var notifier = new FakeEmailNotifier();
var service = new OrderService(repo, notifier);
service.PlaceOrder(userId: 1, amount: 299.99m);
Assert.Single(repo.GetAll()); // zapis nastąpił
Assert.Single(notifier.SentEmails); // email wysłany
}
💡 Wskazówka:
Najczęstszą pułapką jest wstrzyknięcie serwisu Scoped do Singletona — Scoped zostanie de facto Singletonem (tzw. captive dependency). ASP.NET Core w trybie Development rzuci wyjątek, jeśli to wykryje.
📖 Zobacz też: Dependency Injection w ASP.NET Core
3. Record vs Class
Pytanie rekrutera: Kiedy użyjesz record zamiast class?
❌ Zła odpowiedź
„Record jest immutable, a class jest mutable.”
Niekompletne. Record to znacznie więcej niż immutability — a class też może być immutable.
✅ Wzorcowa odpowiedź — value semantics
Kluczowa różnica to tożsamość:
// CLASS — tożsamość przez REFERENCJĘ
var a1 = new AddressClass { Street = "Marszałkowska 1", City = "Warszawa" };
var a2 = new AddressClass { Street = "Marszałkowska 1", City = "Warszawa" };
Console.WriteLine(a1 == a2); // FALSE — dwa różne obiekty
// RECORD — tożsamość przez WARTOŚĆ (value semantics)
var r1 = new Address("Marszałkowska 1", "Warszawa");
var r2 = new Address("Marszałkowska 1", "Warszawa");
Console.WriteLine(r1 == r2); // TRUE — te same wartości!
// with-expression: kopia z jedną zmianą (immutability)
var r3 = r1 with { City = "Kraków" };
// r1 niezmieniony, r3 to nowy obiekt
Kiedy co wybrać?
| Użyj record | Użyj class |
| DTO — dane między warstwami | Encje domenowe (tożsamość przez ID) |
| Komendy i zapytania CQRS | Serwisy, repozytoria, kontrolery |
| Value Objects w DDD (Money, Email) | Cokolwiek, co zmienia stan w czasie |
| Eventy domenowe | Handlery, middleware |
Value Object z rekordem — produkcyjny przykład
public record Money
{
public decimal Amount { get; init; }
public string Currency { get; init; }
public Money(decimal amount, string currency)
{
if (amount <= 0)
throw new ArgumentException("Amount must be > 0", nameof(amount));
if (string.IsNullOrWhiteSpace(currency))
throw new ArgumentException("Currency required", nameof(currency));
Amount = amount;
Currency = currency;
}
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException(
$"Cannot add {Currency} and {other.Currency}");
return this with { Amount = Amount + other.Amount };
}
}
💡 Reguła: Jeśli dwa obiekty z tymi samymi danymi powinny być równe — record. Jeśli obiekt ma tożsamość niezależną od swoich pól (ID w bazie) — class.
📖 Zobacz też: Value Object w DDD — implementacja w C# z rekordem
4. async/await i ConfigureAwait

Pytanie rekrutera: Co to jest async/await? Co się stanie bez ConfigureAwait(false)?
❌ Zła odpowiedź
„async/await służy do asynchronicznego programowania, żeby aplikacja się nie blokowała.”
Poprawne, ale płytkie. Rekruter chce wiedzieć o mechanizmie zwalniania wątku, SynchronizationContext i praktycznych pułapkach.
✅ Wzorcowa odpowiedź — co naprawdę robi await
Kiedy wykonanie dochodzi do await, wątek jest zwalniany z powrotem do puli wątków. Nie czeka, nie blokuje się — wraca do puli i może obsłużyć inne żądanie HTTP.
Gdy operacja I/O się zakończy (np. odpowiedź z bazy), system bierze wolny wątek z puli (niekoniecznie ten sam) i kontynuuje wykonanie od linijki po await.
Dlatego async nie przyspiesza jednego żądania — ale pozwala obsłużyć więcej żądań jednocześnie, bo wątki nie marnują się na czekanie.
public async Task<string> GetOrderAsync(int orderId)
{
// ↓ Tutaj wątek jest ZWALNIANY — może obsłużyć inne żądania
var response = await _http.GetAsync($"/api/orders/{orderId}");
// ↓ Tutaj wątek WRACA (niekoniecznie ten sam) i kontynuuje
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
Deadlock — klasyczna pułapka
// ❌ DEADLOCK w WPF/WinForms/ASP.NET Classic!
public static string GetDataBad()
{
// Krok 1: .Result BLOKUJE bieżący wątek
// Krok 2: await kończy operację I/O
// Krok 3: SynchronizationContext chce wrócić na zablokowany wątek
// Krok 4: DEADLOCK — wątek czeka na Task, Task czeka na wątek
return FetchDataAsync().Result;
}
// ✅ ConfigureAwait(false) — kontynuuj na DOWOLNYM wątku z puli
public static async Task<string> GetDataGoodAsync()
{
return await FetchDataAsync().ConfigureAwait(false);
}
Uwaga:
W ASP.NET Core nie ma SynchronizationContext, więc deadlock z .Result technicznie nie wystąpi. Ale .Result i tak blokuje wątek z puli — pod obciążeniem zabije throughput Twojej aplikacji.
Trzy żelazne reguły async
- async all the way — jeśli jeden poziom jest async, całe call stack powinno być async.
- Nigdy async void — łyka wyjątki bez śladu. Wyjątek: event handlers w WPF/WinForms.
- Task.WhenAll zamiast sequential await — równoległe operacje I/O.
// ✅ Równoległe pobieranie — nie czekaj na jedno, żeby zacząć drugie
public async Task<(string Orders, string Users)> FetchBothAsync()
{
var ordersTask = FetchOrdersAsync();
var usersTask = FetchUsersAsync();
await Task.WhenAll(ordersTask, usersTask);
return (await ordersTask, await usersTask);
}
💡 Wskazówka na rozmowę:
„CancellationToken — zawsze przyjmuję w sygnaturze metody async i zawsze przekazuję dalej. Jeśli o tym wspomnisz bez pytania, rekruter wie, że pisałeś produkcyjny kod.”
📖 Zobacz też: async/await w C# — kompletny przewodnik z pułapkami
5. LINQ i lazy evaluation
Pytanie rekrutera: Czym jest LINQ? Jak działa lazy evaluation?
❌ Zła odpowiedź
„LINQ to zapytania do kolekcji, jak SQL.”
Brakuje tego, co najważniejsze: lazy evaluation (odroczone wykonanie) i pułapka multiple enumeration.
✅ Wzorcowa odpowiedź
Kiedy piszesz .Where().OrderBy(), żadna iteracja się nie odbywa. Tworzysz plan zapytania — obiekt, który pamięta: „gdy ktoś mnie poprosi o elementy, przefiltruję i posortuję”.
Dopiero kiedy wywołasz metodę wymagającą danych — plan się wykonuje. To jest deferred execution:
var products = GetProducts();
// ↓ LAZY — zapytanie NIE wykonuje się tutaj. To tylko definicja.
var expensiveQuery = products
.Where(p => p.Price > 100)
.OrderBy(p => p.Name);
Console.WriteLine("Zapytanie zdefiniowane — ale jeszcze nic się nie stało");
// ↓ MATERIALIZACJA — dopiero tutaj zapytanie się wykonuje
var result = expensiveQuery.ToList();
Pułapka: multiple enumeration
// ❌ Multiple enumeration — w EF Core to DWIE kwerendy SQL!
var filtered = products.Where(p => p.Price > 100);
var count = filtered.Count(); // 1. zapytanie do bazy
var first = filtered.First(); // 2. zapytanie — przy zmianach w DB może dać inny wynik!
// ✅ Zmaterializuj raz — potem operuj na liście w pamięci
var filteredList = products.Where(p => p.Price > 100).ToList();
var count2 = filteredList.Count; // tylko pamięć — brak zapytania SQL
var first2 = filteredList.First(); // szybko i bezpiecznie
Operatory materializujące (kończą lazy evaluation)
💡 Wskazówka:
„Na rozmowie pokaż, że znasz różnicę LINQ-to-Objects vs LINQ-to-Entities. To samo Where() w pamięci to delegat C#, ale w EF Core to wyrażenie, które przekłada się na SQL. Nie każdy operator jest wspierany po stronie bazy.”
📖 Zobacz też: EF Core — N+1 problem i jak go unikać z Include
6. Value types vs Reference types

Pytanie rekrutera: Czym różnią się value types od reference types?
❌ Zła odpowiedź
„Value types są na stosie, reference types na stercie.”
To jest uproszczenie. Value type jako pole klasy trafia na stertę razem z obiektem nadrzędnym. Kluczowa różnica to semantyka kopiowania, nie lokalizacja w pamięci.
✅ Wzorcowa odpowiedź
// VALUE TYPE — struct
// Każda zmienna ma WŁASNĄ KOPIĘ danych
var s1 = new PointStruct { X = 1, Y = 2 };
var s2 = s1; // KOPIA — niezależna od s1
s2.X = 99;
Console.WriteLine(s1.X); // 1 — oryginał NIEZMIENIONY
// REFERENCE TYPE — class
// Zmienne współdzielą TEN SAM obiekt na stercie
var r1 = new PointClass { X = 1, Y = 2 };
var r2 = r1; // TA SAMA referencja — r2 wskazuje na r1
r2.X = 99;
Console.WriteLine(r1.X); // 99 — oryginał ZMIENIONY!
Kiedy struct zamiast class?
| Kryteria | struct ✅ | class ✅ |
| Rozmiar | Mały (< 16 bajtów) | Dowolny |
| Mutacja | Immutable po stworzeniu | Zmienia stan w czasie |
| Tworzenie | Miliony instancji (brak presji GC) | Normalna liczba |
| Dziedziczenie | Nie potrzebujesz | Potrzebujesz |
| Semantyka | Value semantics (Point, Color) | Tożsamość przez referencję |
💡 Reguła:
Przy value type zmiana jednej zmiennej nie dotyka drugiej. Przy reference type wiele zmiennych wskazuje na ten sam obiekt — zmiana przez jedną jest widoczna przez wszystkie.
📖 Zobacz też: Boxing i unboxing w C# — ukryty koszt wydajności
7. Generics i constraints
Pytanie rekrutera: Co to są Generics? Do czego służą constraints?
❌ Zła odpowiedź
„Generics to coś jak szablony — można użyć dowolnego typu zamiast pisać osobną klasę dla każdego.”
Kierunek dobry, ale brakuje wyjaśnienia dlaczego constraints są potrzebne i co dają kompilatorowi.
✅ Wzorcowa odpowiedź — problem bez Generics
// ❌ Bez Generics — osobna klasa dla każdego typu
public class IntStack
{
private readonly List<int> _items = new();
public void Push(int item) => _items.Add(item);
public int Pop() { /* ... */ }
}
// Chcesz Stack<string>? Piszesz kolejną klasę.
// Albo używasz object — tracisz type safety + masz boxing.
✅ Z Generics — jeden kod, wiele typów
public class Stack<T>
{
private readonly List<T> _items = new();
public void Push(T item) => _items.Add(item);
public T Pop()
{
if (_items.Count == 0)
throw new InvalidOperationException("Stack is empty.");
var item = _items[^1];
_items.RemoveAt(_items.Count - 1);
return item;
}
public int Count => _items.Count;
}
// Ten sam kod — dwa różne typy, pełne type safety
var intStack = new Stack<int>();
var stringStack = new Stack<string>();
Constraints — mówią kompilatorowi, co T potrafi
Bez constraints kompilator wie o T tyle co o object — prawie nic. Constraints odbllokowują możliwości:
public class Repository<T> where T : class, IEntity, new()
{
// where T : class → T jest typem referencyjnym (może być null)
// where T : IEntity → T ma właściwość Id — mogę po niej szukać
// where T : new() → T ma bezparametrowy konstruktor — mogę zrobić new T()
private readonly List<T> _store = new();
public T? FindById(int id) => _store.FirstOrDefault(e => e.Id == id);
public T CreateNew() => new T(); // możliwe TYLKO dzięki where T : new()
}
Result<T> — Generics w produkcyjnym kodzie
public class Result<T>
{
public T? Value { get; private init; }
public string? Error { get; private init; }
public bool IsSuccess => Error is null;
public static Result<T> Ok(T value) => new() { Value = value };
public static Result<T> Fail(string e) => new() { Error = e };
// Map — łańcuchowanie operacji (jak w funkcyjnym programowaniu)
public Result<TNew> Map<TNew>(Func<T, TNew> mapper) =>
IsSuccess ? Result<TNew>.Ok(mapper(Value!)) : Result<TNew>.Fail(Error!);
}
// Użycie:
var result = Result<int>.Ok(5)
.Map(x => x * 2)
.Map(x => $"Wynik: {x}");
// result.Value == "Wynik: 10"
💡 Wskazówka:
„Na rozmowie wspomnij o covariance/contravariance (out/in). IEnumerable<out T> jest kowariantny — możesz przypisać IEnumerable<Dog> do IEnumerable<Animal>. To pokazuje głębsze rozumienie Generics.”
📖 Zobacz też: Result Pattern w C# — obsługa błędów bez wyjątków
8. Wyjątki vs kody błędów
Pytanie rekrutera: Kiedy używać wyjątków, a kiedy zwracać kody błędów / Result?
❌ Zła odpowiedź
„Wyjątki służą do obsługi błędów.”
Rekruter chce wiedzieć: kiedy rzucać a kiedy zwracać Result / null / bool.
✅ Wzorcowa odpowiedź — granica jest prosta
Zadaj sobie pytanie: czy caller mógł temu zapobiec?
- TAK → rzuć wyjątek. To BUG w kodzie callera.
- NIE → zwróć null, Result.Fail() lub bool TryGet(…). To normalny stan biznesowy.
public class ExceptionGuidelines
{
// ✅ Rzucaj: caller przekazał null — to BUG callera
public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order);
if (order.Amount <= 0)
throw new ArgumentOutOfRangeException(
nameof(order.Amount), "Amount must be > 0");
if (order.Status == OrderStatus.Cancelled)
throw new InvalidOperationException(
$"Cannot process cancelled order #{order.Id}");
}
// ✅ Zwracaj null: brak zamówienia to normalny case, nie bug
public Order? FindOrder(int id)
=> _orders.FirstOrDefault(o => o.Id == id);
}
Własna hierarchia wyjątków
// Bazowy wyjątek domenowy
public class DomainException(string message) : Exception(message);
// Wyjątek z kontekstem — OrderId jest dostępny bez parsowania Message
public class OrderNotFoundException(int orderId)
: DomainException($"Order #{orderId} not found.")
{
public int OrderId { get; } = orderId;
}
Zasady łapania wyjątków
- ✅ Łap selektywnie — tylko to, co umiesz obsłużyć.
- ✅ Zawsze loguj — catch bez logowania to zamiatanie pod dywan.
- ❌ Nigdy pusty catch {} — najgorszy antywzorzec w C#.
- ❌ Nigdy throw new Exception(“błąd”) — brak kontekstu, brak typu.
💡 Wskazówka:
„Wyjątki są kosztowne (~100x wolniejsze niż return). W hot path (walidacja, parsowanie) nigdy nie steruj flow wyjątkami. Dlatego istnieje int.TryParse() zamiast int.Parse() + catch.”
📖 Zobacz też: Global Exception Handling w ASP.NET Core — middleware i ProblemDetails
9. IDisposable i using
Pytanie rekrutera: Co to jest IDisposable? Do czego służy using?
❌ Zła odpowiedź
„IDisposable to interfejs z metodą Dispose(), a using automatycznie ją wywołuje.”
Poprawne, ale brakuje dlaczego to jest potrzebne: GC zarządza pamięcią zarządzaną automatycznie. Ale zasoby niezarządzane — pliki, połączenia DB, sockety, handlery — trzeba zwalniać ręcznie.
✅ Wzorcowa odpowiedź — implementacja wzorca Dispose
public class DatabaseConnection : IDisposable
{
private bool _disposed = false;
public DatabaseConnection(string connectionString)
{
Console.WriteLine($"[DB] Connection opened");
}
public void ExecuteQuery(string sql)
{
// ✅ Po Dispose obiekt INFORMUJE o złym użyciu
ObjectDisposedException.ThrowIf(_disposed, nameof(DatabaseConnection));
Console.WriteLine($"[DB] Executing: {sql}");
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this); // Finalizator niepotrzebny
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Zwolnij zasoby zarządzane (inne IDisposable)
Console.WriteLine("[DB] Connection closed.");
}
// Zwolnij zasoby niezarządzane (handles, native memory)
_disposed = true;
}
}
using — gwarancja zwolnienia zasobów
// ✅ using statement — Dispose() GWARANTOWANY, nawet gdy wyjątek
using var conn = new DatabaseConnection("Server=localhost;...");
conn.ExecuteQuery("SELECT * FROM Orders");
// Dispose() wywoła się automatycznie na końcu scope
// ❌ BEZ using — connection leak!
var badConn = new DatabaseConnection("Server=localhost;...");
badConn.ExecuteQuery("SELECT 1");
// Dispose() NIGDY nie zostanie wywołany (chyba że finalizator — za późno)
Test potwierdzający
[Fact]
public void DatabaseConnection_AfterDispose_ThrowsObjectDisposed()
{
var conn = new DatabaseConnection("Server=test;...");
conn.Dispose();
// Po Dispose — jasna informacja zamiast cichego crasha
Assert.Throws<ObjectDisposedException>(
() => conn.ExecuteQuery("SELECT 1"));
}
💡 Wskazówka: „IAsyncDisposable i await using (C# 8+) służą do asynchronicznego cleanup — np. zamykanie async połączenia DB, flush bufora. DbContext w EF Core implementuje oba interfejsy. W kontrolerze async używasz await using, w teście synchronicznym — zwykłe using.”
📖 Zobacz też: DbContext w EF Core — czasy życia, Dispose i pułapki
10. SRP — Single Responsibility Principle

Pytanie rekrutera: Opisz zasadę SRP i podaj przykład jej złamania.
❌ Zła odpowiedź
„Klasa powinna robić tylko jedną rzecz.”
Zbyt ogólne. Rekruter pyta: „A co to znaczy ‘jedna rzecz’?” Lepsza definicja: klasa powinna mieć tylko jeden powód do zmiany.
✅ Wzorcowa odpowiedź — przykład złamania SRP
// ❌ SRP ZŁAMANE — 4 powody do zmiany, 4 aktorów
public class UserServiceBad
{
public void Register(string email, string password)
{
// 1. Walidacja → zmienia: dział UX
if (!email.Contains('@'))
throw new ArgumentException("Invalid email");
// 2. Hashowanie hasła → zmienia: zespół security
var hash = Convert.ToBase64String(
SHA256.HashData(Encoding.UTF8.GetBytes(password)));
// 3. Zapis do bazy → zmienia: DBA
Console.WriteLine($"INSERT INTO Users ('{email}', '{hash}')");
// 4. Wysyłka emaila → zmienia: marketing
Console.WriteLine($"Welcome email sent to {email}");
}
}
Pytanie testowe: Kto zmienia tę klasę? — dział UX, DBA, marketing, security. Czterech aktorów = SRP złamane.
✅ Rozwiązanie — jeden powód do zmiany na klasę
// Każda klasa odpowiada JEDNEMU aktorowi
public class UserValidator { /* zmienia: UX */ }
public class Sha256PasswordHasher : IPasswordHasher { /* zmienia: security */ }
public class SqlUserRepository : IUserRepository { /* zmienia: DBA */ }
public class SmtpWelcomeEmailSender : IWelcomeEmailSender { /* zmienia: marketing */ }
// ✅ Orkiestrator — zmienia się TYLKO gdy zmienia się FLOW rejestracji
public class UserRegistrationService(
UserValidator validator,
IPasswordHasher hasher,
IUserRepository repository,
IWelcomeEmailSender emailSender)
{
public async Task RegisterAsync(RegisterUserCommand cmd)
{
validator.Validate(cmd);
if (repository.Exists(cmd.Email))
throw new InvalidOperationException($"User '{cmd.Email}' already exists.");
var hash = hasher.Hash(cmd.Password);
repository.Save(cmd.Email, hash);
await emailSender.SendAsync(cmd.Email);
}
}
Praktyczne sygnały złamania SRP
- ⚠️ Metoda ma więcej niż 20–30 linii.
- ⚠️ Klasa ma więcej niż 3–4 zależności w konstruktorze.
- ⚠️ Komentarze „Część 1: walidacja // Część 2: zapis // Część 3: email”.
- ⚠️ Trudno napisać unit test bez mockowania 4+ klas.
- ⚠️ Pytanie „kto zmienia tę klasę?” ma więcej niż jedną odpowiedź.
💡 Wskazówka: „SRP nie znaczy, że klasa ma jedną metodę — znaczy, że wszystkie jej metody zmieniają się z tego samego powodu. UserValidator może mieć 10 metod walidacji — ale wszystkie zmienia ten sam aktor (UX).”
📖 Zobacz też: SOLID w praktyce — 5 zasad z kodem C# i przykładami
Checklist: przed rozmową rekrutacyjną

Przed wejściem do pokoju (lub na Zooma) sprawdź, czy potrafisz odpowiedzieć na każde pytanie z przykładem kodu i uzasadnieniem:
- [ ] Interface vs Abstract — wiesz kiedy co wybrać, nie tylko czym się różnią.
- [ ] DI — potrafisz wymienić 3 korzyści i 3 czasy życia.
- [ ] Record vs Class — rozumiesz value semantics i potrafisz podać with expression.
- [ ] async/await — wiesz co robi await z wątkiem i czemu async void jest zły.
- [ ] LINQ — wyjaśniasz lazy evaluation i pułapkę multiple enumeration.
- [ ] Value vs Reference — demonstrujesz kopiowanie vs współdzielenie referencji.
- [ ] Generics — wyjaśniasz constraints i co dają kompilatorowi.
- [ ] Wyjątki — wiesz kiedy throw a kiedy return null / Result.
- [ ] IDisposable — implementujesz wzorzec Dispose i wyjaśniasz using.
- [ ] SRP — podajesz przykład złamania i naprawy z pytaniem „kto zmienia tę klasę?”.
Pobierz kod i ćwicz
Cały kod z tego artykułu jest dostępny jako solution Visual Studio z 22 testami xUnit:
git clone https://github.com/mariuszjurczenko/DevHobby.RecruitmentQA.git
cd DevHobby.RecruitmentQA
dotnet test
Wszystkie testy zielone = wzorcowy kod działa. Eksperymentuj — zmień coś w kodzie, zobacz czy testy dalej przechodzą.
Co dalej?
Te 10 pytań to fundament — ale na rozmowie rekrutacyjnej może paść dużo więcej. Jeśli chcesz systemowo przygotować się do pierwszej pracy jako .NET Developer:
🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy, z konkretnym planem co robić na każdym etapie.
🎓 Skorzystaj z 7 dni bezpłatnego dostępu do kursów — przejdź przez każdy krok z kursami wideo i code review.
🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Które pytanie było dla Ciebie najtrudniejsze? Napisz w komentarzu — czytam każdy! 💬
Zobacz też
- SOLID w C# — 5 zasad na 5 przykładach z prawdziwego projektu
- Clean Architecture w ASP.NET Core
- 50 pytań rekrutacyjnych Junior .NET — z wzorcowymi odpowiedziami
- Dependency Injection w ASP.NET Core
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
👉 sprawdź moje kursy .NET i C#.

