🔥 Darmowa Roadmapa .NET — kompletny plan nauki do pierwszej pracy. Pobierz i dołącz do Listy VIP →

10 pytań rekrutacyjnych Junior .NET Developer

10 pytań rekrutacyjnych Junior .NET Developer

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 testy są zielone.


Spis treści

  1. Interface vs Abstract class
  2. Dependency Injection
  3. Record vs Class
  4. async/await i ConfigureAwait
  5. LINQ i lazy evaluation
  6. Value types vs Reference types
  7. Generics i constraints
  8. Wyjątki vs kody błędów
  9. IDisposable i using
  10. SRP — Single Responsibility Principle

1. Interface vs Abstract class

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

InterfaceAbstract class
„Mogę to zrobić” — kontrakt„Jestem tym” — wspólna tożsamość
Klasy mają różne origin, to samo zachowanieKlasy są tym samym, różnią się szczegółami
Łatwe mockowanie w testachWymaga 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

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 życiaKiedy nowa instancja?Typowe użycie
TransientPrzy każdym żądaniu o serwis (resolve)Lekkie serwisy bezstanowe
ScopedJedna instancja na HTTP requestDbContext, serwisy z danymi żądania
SingletonJedna instancja na cały czas życia aplikacjiCache, 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 recordUżyj class
DTO — dane między warstwamiEncje domenowe (tożsamość przez ID)
Komendy i zapytania CQRSSerwisy, repozytoria, kontrolery
Value Objects w DDD (Money, Email)Cokolwiek, co zmienia stan w czasie
Eventy domenoweHandlery, 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

Async avait flow

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

  1. async all the way — jeśli jeden poziom jest async, całe call stack powinno być async.
  2. Nigdy async void — łyka wyjątki bez śladu. Wyjątek: event handlers w WPF/WinForms.
  3. 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

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?

Kryteriastruct ✅class ✅
RozmiarMały (< 16 bajtów)Dowolny
MutacjaImmutable po stworzeniuZmienia stan w czasie
TworzenieMiliony instancji (brak presji GC)Normalna liczba
DziedziczenieNie potrzebujeszPotrzebujesz
SemantykaValue 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

Srp

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ą

List

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ż

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#.

Mariusz Jurczenko
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

1 comment

Dodaj komentarz

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