C# C# Podstawy Q01–Q10
01 Jaka jest różnica między class a struct w C#? łatwe +

class to typ referencyjny — obiekt żyje na stercie (heap), zmienna przechowuje referencję. struct to typ wartościowy — dane leżą na stosie (stack) lub inline w obiekcie-rodzicu, kopiowane przez wartość przy przypisaniu.

// class — kopiujemy referencję
Point a = new Point(1, 2);  // Point to class
Point b = a;
b.X = 99;
Console.WriteLine(a.X); // 99 — ten sam obiekt

// struct — kopiujemy wartość
PointS c = new PointS(1, 2);  // PointS to struct
PointS d = c;
d.X = 99;
Console.WriteLine(c.X); // 1 — niezależna kopia
Kiedy struct? Małe, immutable dane (Vector2, Color, DateTime). Unikaj struct większych niż ~16 bajtów lub mutowalnych — kopiowanie jest kosztowne i błędogenne.
02 Czym jest nullable reference type i jak działa w C# 8+? łatwe +

Od C# 8 typy referencyjne są domyślnie non-nullable. Kompilator ostrzega, gdy możliwy jest NullReferenceException. Dodanie ? jawnie oznacza, że zmienna może być null.

#nullable enable
string  name = null;   // ⚠️ warning CS8600
string? name = null;   // OK — świadomie nullable

void Greet(string? name)
{
    // null-forgiving operator — "wiem co robię"
    Console.WriteLine(name!.Length);

    // lepiej — null check
    if (name is not null)
        Console.WriteLine(name.Length);
}
W projekcie włącz <Nullable>enable</Nullable> w .csproj. Eliminuje całą klasę błędów runtime zanim trafią na produkcję.
03 Co to jest async/await i jak działa pod spodem? średnie +

async/await to syntactic sugar na state machine generowaną przez kompilator. Przy pierwszym await, który nie jest jeszcze gotowy, metoda zwraca kontrolę do wywołującego. Gdy operacja I/O się kończy, kontynuacja uruchamia się (domyślnie) na tym samym SynchronizationContext.

// Złe — blokuje wątek
string json = httpClient.GetStringAsync(url).Result; // deadlock ryzyko!

// Dobre
string json = await httpClient.GetStringAsync(url);

// Bez potrzeby powrotu do kontekstu (np. w library code)
string json = await httpClient.GetStringAsync(url)
                              .ConfigureAwait(false);
Pułapka: nigdy nie mieszaj .Result / .Wait() z async w ASP.NET — prowadzi do deadlocka. Jeśli async, to async cały callstack.
04 Różnica między IEnumerable<T>, ICollection<T> a IList<T>? łatwe +

IEnumerable<T> — tylko do odczytu, leniwa iteracja, brak informacji o Count. ICollection<T> dodaje Count, Add, Remove. IList<T> dodaje dostęp indeksowany i Insert/RemoveAt.

// Zasada: używaj najwęższego interfejsu wystarczającego do celu
void Process(IEnumerable<Order> orders)  { /* tylko iteracja */ }
void Fill(ICollection<Order> orders)     { orders.Add(new Order()); }
void Swap(IList<Order> orders, int i, int j) { (orders[i], orders[j]) = (orders[j], orders[i]); }
Wzorzec: parametry metod deklaruj jako IEnumerable (najszerszy kontrakt), pola prywatne jako List<T> (konkretna implementacja).
05 Co to jest LINQ i jaka jest różnica między Select, Where a FirstOrDefault? łatwe +

LINQ to deklaratywny mechanizm zapytań do dowolnych źródeł danych (kolekcje, bazy, XML). Operatory są leniwe — wykonują się przy pierwszej iteracji.

var orders = new List<Order> { ... };

// Where — filtruje, zwraca IEnumerable
var active = orders.Where(o => o.IsActive);

// Select — projekcja/transformacja każdego elementu
var ids = orders.Select(o => o.Id);

// FirstOrDefault — zwraca pierwszy lub null (nie rzuca wyjątku)
var order = orders.FirstOrDefault(o => o.Id == 5);
// vs First() — rzuca InvalidOperationException gdy brak elementu
Na rozmowie: pokaż świadomość deferred execution — LINQ query nie wykonuje się przy deklaracji, tylko przy enumeracji (foreach, ToList(), Count()).
06 Czym jest record w C# 9+ i kiedy go używać? średnie +

record to typ referencyjny z wbudowaną semantyką wartości: auto-generowane Equals, GetHashCode, ToString oparte na właściwościach, nie referencji. Kompilator generuje też with-expression dla niemutowalnych kopii.

record Person(string FirstName, string LastName);

var p1 = new Person("Jan", "Kowalski");
var p2 = new Person("Jan", "Kowalski");

Console.WriteLine(p1 == p2);  // true — porównanie wg wartości

var p3 = p1 with { LastName = "Nowak" }; // niemutowalna kopia

// record struct (C# 10) — value type + value semantics
record struct Point(double X, double Y);
Kiedy record? DTO, Value Objects w DDD, odpowiedzi z API, dane konfiguracyjne — wszędzie, gdzie zależy Ci na niemutowalności i porównaniu wg treści.
07 Co to jest delegate, Action, Func i Predicate? średnie +

delegate to typowany wskaźnik na metodę. Action<T>, Func<T,TResult>, Predicate<T> to gotowe delegaty z .NET — nie trzeba deklarować własnych.

// Func — zwraca wartość, ostatni param to typ zwracany
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3)); // 5

// Action — nie zwraca wartości (void)
Action<string> log = msg => Console.WriteLine(msg);

// Predicate — przyjmuje T, zwraca bool
Predicate<int> isEven = n => n % 2 == 0;

// Closures — lambda "zamyka" zmienną z zewnętrznego scope
int threshold = 10;
Func<int, bool> aboveThreshold = n => n > threshold;
threshold = 20; // zmiana wpływa na closure!
08 Jak działa garbage collector w .NET i co to są generacje? średnie +

GC w .NET jest generacyjny — obiekty podzielone na Gen0, Gen1, Gen2 + Large Object Heap (LOH, ≥85KB). Gen0 jest zbierana najczęściej i najszybciej (krótko żyjące obiekty). Przeżałe trafiają do Gen1, a potem Gen2 (długo żyjące).

// Obiekty NIGDY nie powinny implementować finalizer bez IDisposable
// Finalizer opóźnia zwolnienie o przynajmniej jeden GC cycle

class Resource : IDisposable
{
    private bool _disposed;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // bez tego finalizer i tak się odpali
    }

    protected virtual void Dispose(bool disposing) { /* cleanup */ }
    ~Resource() => Dispose(false); // finalizer — ostatnia deska ratunku
}
Praktyka: używaj using / await using dla IDisposable. Unikaj wymuszania GC (GC.Collect()) — prawie zawsze błąd projektowy.
09 Czym różni się string od StringBuilder? łatwe +

string jest niemutowalny — każda konkatenacja tworzy nowy obiekt na stercie. W pętlach prowadzi to do O(n²) alokacji. StringBuilder mutuje wewnętrzny bufor — alokacja O(n).

// Złe — tworzy n nowych stringów
string result = "";
foreach (var item in items)
    result += item + ", "; // 100 iteracji = ~100 alokacji

// Dobre
var sb = new StringBuilder();
foreach (var item in items)
    sb.Append(item).Append(", ");
string result = sb.ToString();

// Jeszcze lepiej (C# 10+) — interpolacja ze string.Create lub Span
// Dla małej liczby konkatenacji (2-3) += jest w porządku
10 Co to jest pattern matching w C# i jakie są jego formy? średnie +

Pattern matching pozwala testować kształt danych w is, switch i switch expression. C# 9/10 dodał relational, logical i property patterns.

object obj = new Order { Status = "Active", Amount = 150 };

// Type pattern + property pattern
if (obj is Order { Status: "Active", Amount: > 100 } order)
    Console.WriteLine($"Duże zamówienie: {order.Amount}");

// Switch expression (C# 8+)
string label = order.Amount switch
{
    < 50         => "małe",
    >= 50 and < 200 => "średnie",
    >= 200       => "duże",
    _            => "nieznane"
};
OOP OOP & SOLID Q11–Q20
11 Wyjaśnij cztery filary OOP na konkretnych przykładach. łatwe +

Enkapsulacja — ukrywanie stanu, dostęp przez publiczne API. Dziedziczenie — współdzielenie kodu przez hierarchię klas. Polimorfizm — ten sam interfejs, różne zachowanie. Abstrakcja — modelowanie esencji, ukrywanie szczegółów.

// Polimorfizm — kluczowy na rozmowie
abstract class Shape
{
    public abstract double Area();
}

class Circle(double radius) : Shape
{
    public override double Area() => Math.PI * radius * radius;
}

class Rectangle(double w, double h) : Shape
{
    public override double Area() => w * h;
}

// Polimorficzne wywołanie — nie obchodzi nas konkretny typ
List<Shape> shapes = [new Circle(5), new Rectangle(3, 4)];
double totalArea = shapes.Sum(s => s.Area()); // 98.54...
12 Co to jest zasada SRP (Single Responsibility Principle)? łatwe +

Klasa powinna mieć jeden powód do zmiany — jeden obszar odpowiedzialności. “Jeden powód” to jeden aktor biznesowy, który może zażądać zmiany logiki.

// Naruszenie SRP — OrderService robi za dużo
class OrderService
{
    public void PlaceOrder(Order order) { /* logika zamówień */ }
    public void SendEmail(Order order)  { /* wysyłka maila */ } // ← osobna odpowiedzialność
    public void SaveToCsv(Order order)  { /* eksport danych */ } // ← osobna odpowiedzialność
}

// Po SRP
class OrderService    { public void PlaceOrder(Order order) { ... } }
class EmailService    { public void SendConfirmation(Order order) { ... } }
class OrderExporter   { public void ExportToCsv(IEnumerable<Order> orders) { ... } }
Praktyczna wskazówka: jeśli opisujesz klasę słowem “i” (np. “zapisuje dane I wysyła maila”), to SRP jest naruszone.
13 Czym jest Open/Closed Principle i jak go stosować w praktyce? średnie +

Kod powinien być otwarty na rozszerzenie, zamknięty na modyfikację. Nowe zachowania dodajemy przez nowe klasy/implementacje, a nie przez zmienianie istniejącego kodu.

// Naruszenie — każdy nowy discount wymaga zmiany metody
decimal Calculate(Order o, string discountType)
{
    if (discountType == "vip") return o.Total * 0.9m;
    if (discountType == "promo") return o.Total * 0.85m;
    // ... kolejne if'y przy każdej nowej promocji
    return o.Total;
}

// Po OCP — nowy discount = nowa klasa, stary kod niezmieniony
interface IDiscountPolicy
{
    decimal Apply(decimal total);
}

class VipDiscount : IDiscountPolicy
    { public decimal Apply(decimal t) => t * 0.9m; }

class PromoDiscount : IDiscountPolicy
    { public decimal Apply(decimal t) => t * 0.85m; }

class OrderService(IDiscountPolicy discount)
{
    public decimal Calculate(Order o) => discount.Apply(o.Total);
}
14 Co to jest Dependency Inversion Principle i czym różni się od Dependency Injection? średnie +

DIP (zasada) — moduły wysokiego poziomu nie zależą od modułów niskiego poziomu; obie strony zależą od abstrakcji. DI (technika) — mechanizm dostarczania zależności z zewnątrz zamiast tworzenia ich wewnątrz klasy. DI to sposób realizacji DIP.

// Naruszenie DIP — OrderService tworzy konkretną zależność
class OrderService
{
    private readonly SqlOrderRepository _repo = new SqlOrderRepository(); // tight coupling!
}

// DIP + DI — zależność od interfejsu, wstrzykiwana z zewnątrz
class OrderService(IOrderRepository repo)  // constructor injection
{
    public async Task<Order> GetOrder(int id) => await repo.GetByIdAsync(id);
}

// Rejestracja w IoC container (ASP.NET Core)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
Na rozmowie: DIP to zasada projektowa (SOLID), DI to wzorzec implementacyjny. IoC Container (np. wbudowany w ASP.NET) to narzędzie automatyzujące DI.
15 Jaka jest różnica między abstract class a interface? Kiedy użyć którego? łatwe +

Klasa abstrakcyjna może mieć stan, konstruktory i konkretne metody — wyraża relację “jest”. Interfejs definiuje kontrakt zachowania — wyraża relację “potrafi”. Klasa może implementować wiele interfejsów, ale dziedziczyć tylko z jednej klasy.

// Abstract class — wspólna baza + shared behavior
abstract class Animal
{
    public string Name { get; }
    protected Animal(string name) { Name = name; }
    public abstract void Speak();          // musi być nadpisana
    public void Breathe() { /* wspólna */ } // dziedziczona
}

// Interface — zdolność/kontrakt (C# 8+ pozwala na default implementations)
interface ISerializable
{
    string Serialize();
}

// Klasa psa: jest zwierzęciem ORAZ potrafi być serializowana
class Dog : Animal, ISerializable
{
    public Dog(string name) : base(name) { }
    public override void Speak() => Console.WriteLine("Woof!");
    public string Serialize() => JsonSerializer.Serialize(this);
}
16 Co to jest LSP (Liskov Substitution Principle) i jak sprawdzić, czy jest naruszone? średnie +

Obiekt podklasy powinien być w stanie zastąpić obiekt klasy bazowej bez zmiany poprawności programu. Naruszenie objawia się rzucaniem wyjątków w nadpisanych metodach lub zawężaniem kontraktu.

// Klasyczne naruszenie LSP — Rectangle/Square
class Rectangle
{
    public virtual int Width  { get; set; }
    public virtual int Height { get; set; }
    public int Area => Width * Height;
}

class Square : Rectangle
{
    public override int Width  { set { base.Width = base.Height = value; } }
    public override int Height { set { base.Width = base.Height = value; } }
}

// Test ujawniający naruszenie
void TestArea(Rectangle r)
{
    r.Width  = 4;
    r.Height = 5;
    Debug.Assert(r.Area == 20); // ✅ dla Rectangle, ❌ dla Square (25)
}

// Rozwiązanie: nie dziedzicz Square z Rectangle — różne kontrakty
Test LSP: czy mogę zastąpić klasę bazową podklasą bez modyfikacji testów jednostkowych dla klasy bazowej?
17 Co to jest ISP (Interface Segregation Principle)? łatwe +

Klient nie powinien być zmuszony do zależności od metod, których nie używa. Duże interfejsy rozbijamy na mniejsze, specyficzne.

// Naruszenie ISP — "fat interface"
interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

// Robot implementuje interfejs, ale nie je i nie śpi
class Robot : IWorker
{
    public void Work() { /* OK */ }
    public void Eat()  => throw new NotImplementedException(); // zapach kodu!
    public void Sleep()=> throw new NotImplementedException();
}

// Po ISP — segregacja interfejsów
interface IWorkable  { void Work(); }
interface IEatable   { void Eat(); }
interface ISleepable { void Sleep(); }

class Human : IWorkable, IEatable, ISleepable { /* implementuje wszystkie */ }
class Robot : IWorkable                        { /* implementuje tylko Work */ }
18 Czym jest kompozycja vs dziedziczenie? Kiedy preferować jedno nad drugim? średnie +

Dziedziczenie (is-a) tworzy silne powiązanie hierarchiczne. Kompozycja (has-a) składa obiekt z mniejszych, wymiennych części. Zasada GoF: “favor composition over inheritance”.

// Dziedziczenie — OK gdy relacja "jest" jest prawdziwa i stabilna
class ElectricCar : Car { /* ElectricCar "jest" Car */ }

// Kompozycja — elastyczność, łatwość testowania
class OrderProcessor
{
    private readonly IPaymentGateway _payment;
    private readonly IInventoryService _inventory;
    private readonly IEmailService _email;

    // Wstrzykiwane przez konstruktor — łatwo podmienić w testach
    public OrderProcessor(IPaymentGateway p, IInventoryService i, IEmailService e)
        => (_payment, _inventory, _email) = (p, i, e);
}
Czerwona flaga: głęboka hierarchia dziedziczenia (3+ poziomy) prawie zawsze sygnalizuje problem projektowy. Spłaszcz hierarchię i użyj kompozycji.
19 Co to jest sealed class i kiedy jej użyć? łatwe +

sealed blokuje dziedziczenie z klasy (lub nadpisywanie metody wirtualnej). Daje kompilatorowi i JIT więcej możliwości optymalizacji (devirtualization), dokumentuje intencję projektową i chroni przed nieoczekiwanym rozszerzaniem.

sealed class SqlOrderRepository : IOrderRepository
{
    // Nikt nie może dziedziczyć z tej klasy
    // JIT może wywołać metody bezpośrednio (devirtualization)
}

// Sealed override — blokuje dalsze nadpisywanie
class Base    { public virtual void Do() { } }
class Derived : Base { public sealed override void Do() { } }
// class Further : Derived { public override void Do() { } } // błąd kompilacji

// Wzorzec: domain exceptions często sealed
sealed class OrderNotFoundException(int orderId)
    : Exception($"Order {orderId} not found.");
20 Czym jest override vs new w kontekście metod? średnie +

override — nadpisuje wirtualną metodę klasy bazowej. Wywołanie przez referencję bazową uruchamia wersję z klasy pochodnej (polimorfizm runtime). new — ukrywa metodę bazową. Wywołanie przez referencję bazową uruchamia wersję bazową.

class Animal   { public virtual void Speak()  => Console.Write("..."); }
class DogA : Animal { public override void Speak() => Console.Write("Woof!"); }
class DogB : Animal { public new     void Speak() => Console.Write("Woof!"); }

Animal a1 = new DogA();
Animal a2 = new DogB();

a1.Speak(); // "Woof!" — override: polimorfizm działa
a2.Speak(); // "..."   — new: metoda bazowa ukryta, nie nadpisana

DogB d = new DogB();
d.Speak();  // "Woof!" — przez konkretny typ działa wersja z DogB
Pułapka: new zamiast override łamie polimorfizm. Użyj new tylko świadomie, gdy celowo chcesz przesłonić metodę bez polimorficznego zachowania.
API ASP.NET Core & Web API Q21–Q30
21 Jaki jest cykl życia żądania HTTP w ASP.NET Core? Co to jest middleware? średnie +

Żądanie przechodzi przez pipeline middleware — łańcuch komponentów, z których każdy może przetworzyć żądanie i/lub odpowiedź, albo przekazać dalej (next()).

// Kolejność middleware ma krytyczne znaczenie
app.UseExceptionHandler("/error"); // musi być pierwszy — łapie błędy z dalszych
app.UseHttpsRedirection();
app.UseAuthentication();            // kim jesteś?
app.UseAuthorization();             // czy masz dostęp?
app.UseRateLimiter();
app.MapControllers();               // routing + akcje

// Własny middleware
public class TimingMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext ctx)
    {
        var sw = Stopwatch.StartNew();
        await next(ctx);             // wywołanie reszty pipeline
        sw.Stop();
        ctx.Response.Headers["X-Elapsed-Ms"] = sw.ElapsedMilliseconds.ToString();
    }
}
22 Różnica między Transient, Scoped i Singleton w DI? łatwe +

Transient — nowa instancja za każdym razem. Scoped — jedna instancja na żądanie HTTP. Singleton — jedna instancja przez całe życie aplikacji.

// Rejestracja
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();  // bezstanowe usługi
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>(); // DbContext, Unit of Work
builder.Services.AddSingleton<IConfiguration>(config);            // cache, konfiguracja

// PUŁAPKA: Captive Dependency
// Singleton NIGDY nie powinien zależeć od Scoped/Transient
// IoC container rzuci InvalidOperationException lub będzie leak
class BadSingleton(IScopedService scoped) { } // ❌ scoped "uwięziony" w singleton
Captive Dependency — najczęstszy błąd DI na rozmowie. Singleton wstrzykuje Scoped → Scoped żyje tak długo jak Singleton, czyli za długo. DbContext w Singleton to klasyczny przykład.
23 Jak zwracać właściwe kody HTTP z kontrolera Web API? łatwe +

Używamy IActionResult / ActionResult<T> i pomocniczych metod bazowego ControllerBase.

[ApiController]
[Route("api/[controller]")]
public class OrdersController(IOrderService service) : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<ActionResult<OrderDto>> Get(int id)
    {
        var order = await service.GetByIdAsync(id);
        if (order is null)
            return NotFound(new { message = $"Order {id} not found." }); // 404

        return Ok(order); // 200 + JSON
    }

    [HttpPost]
    public async Task<ActionResult<OrderDto>> Create(CreateOrderRequest req)
    {
        var order = await service.CreateAsync(req);
        return CreatedAtAction(nameof(Get), new { id = order.Id }, order); // 201
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        await service.DeleteAsync(id);
        return NoContent(); // 204
    }
}
24 Co to jest walidacja modelu w ASP.NET Core i jak ją rozszerzyć? łatwe +

[ApiController] automatycznie zwraca 400 z detalami gdy ModelState.IsValid == false. Data Annotations to proste dekoratory — dla złożonej walidacji używamy FluentValidation.

// Data Annotations — proste, built-in public class CreateOrderRequest { [Required] [StringLength(100, MinimumLength = 3)] public string ProductName { get; set; } = “”; [Range(1, 10000)] public decimal Amount { get; set; } } // FluentValidation — kompleksowa, testowalna walidacja public class CreateOrderValidator : AbstractValidator<CreateOrderRequest> { public CreateOrderValidator() { RuleFor(x => x.ProductName) .NotEmpty().WithMessage(“Nazwa produktu jest wymagana”) .Length(3, 100); RuleFor(x => x.Amount) .GreaterThan(0) .LessThanOrEqualTo(10000); } } // Rejestracja builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderValidator>();
25 Jak działa autentykacja JWT w ASP.NET Core? średnie +

JWT (JSON Web Token) składa się z header.payload.signature. Serwer generuje token po logowaniu; klient wysyła go w nagłówku Authorization: Bearer <token>. ASP.NET Core weryfikuje podpis i claims automatycznie.

// Program.cs builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { opt.TokenValidationParameters = new() { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = config[“Jwt:Issuer”], ValidAudience = config[“Jwt:Audience”], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(config[“Jwt:Key”]!)) }; }); // Generowanie tokenu var token = new JwtSecurityToken( issuer: config[“Jwt:Issuer”], audience: config[“Jwt:Audience”], claims: [new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())], expires: DateTime.UtcNow.AddHours(1), signingCredentials: credentials);
26 Co to jest IHostedService i BackgroundService? średnie +

IHostedService to interfejs dla usług startujących razem z aplikacją. BackgroundService to klasa bazowa upraszczająca implementację long-running workerów.

public class OrderCleanupWorker(IServiceScopeFactory scopeFactory, ILogger<OrderCleanupWorker> logger)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = scopeFactory.CreateScope(); // Scoped service w Singleton!
            var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();

            await repo.DeleteExpiredAsync();
            logger.LogInformation("Cleanup completed at {time}", DateTimeOffset.UtcNow);

            await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
        }
    }
}

// Rejestracja
builder.Services.AddHostedService<OrderCleanupWorker>();
Wzorzec: BackgroundService jest Singletonem — zależności Scoped wstrzykuj przez IServiceScopeFactory, nie bezpośrednio.
27 Co to jest Options Pattern i dlaczego lepszy od IConfiguration? średnie +

Options Pattern silnie typuje konfigurację, umożliwia walidację przy starcie i ułatwia testowanie — nie przekazujemy surowego IConfiguration w głąb kodu.

// appsettings.json // { “Email”: { “Host”: “smtp.gmail.com”, “Port”: 587 } } public class EmailOptions { public const string Section = “Email”; [Required] public string Host { get; set; } = “”; [Range(1, 65535)] public int Port { get; set; } } // Program.cs builder.Services .AddOptions<EmailOptions>() .BindConfiguration(EmailOptions.Section) .ValidateDataAnnotations() .ValidateOnStart(); // błąd konfiguracji = crash przy starcie, nie przy pierwszym użyciu // Wstrzykiwanie class EmailService(IOptions<EmailOptions> opts) { // IOptions — stała wartość (nie reload) // IOptionsSnapshot — reload per-request (Scoped) // IOptionsMonitor — reload on-the-fly (Singleton) }
28 Jak działa Problem Details i jak ujednolicić obsługę błędów w API? średnie +

RFC 9457 (Problem Details) to standard odpowiedzi błędów API. ASP.NET Core 8 wbudował globalny exception handler zastępując middleware z try/catch.

// Program.cs (ASP.NET Core 8+)
builder.Services.AddProblemDetails();
app.UseExceptionHandler();  // globalny handler

// Własny handler
app.UseExceptionHandler(x => x.Run(async ctx =>
{
    var ex = ctx.Features.Get<IExceptionHandlerFeature>()?.Error;
    var pd = ex switch
    {
        NotFoundException e => new ProblemDetails { Status = 404, Title = e.Message },
        ValidationException e => new ProblemDetails { Status = 422, Title = "Validation failed" },
        _ => new ProblemDetails { Status = 500, Title = "Internal server error" }
    };
    ctx.Response.StatusCode = pd.Status ?? 500;
    await ctx.Response.WriteAsJsonAsync(pd);
}));

// Odpowiedź zgodna z RFC 9457:
// { "type": "...", "title": "...", "status": 404, "detail": "..." }
29 Co to są Minimal APIs i kiedy użyć zamiast kontrolerów? łatwe +

Minimal APIs (ASP.NET 6+) rejestrują endpointy bezpośrednio w Program.cs bez kontrolerów. Mniej boilerplate, lepsza wydajność, świetne dla microservices i małych API.

// Minimal API var app = builder.Build(); app.MapGet(“/api/orders/{id}”, async (int id, IOrderService svc) => { var order = await svc.GetByIdAsync(id); return order is null ? Results.NotFound() : Results.Ok(order); }) .WithName(“GetOrder”) .WithOpenApi() .RequireAuthorization(); // Grupowanie (C# 12) var orders = app.MapGroup(“/api/orders”).RequireAuthorization(); orders.MapGet(“/”, (IOrderService s) => s.GetAllAsync()); orders.MapPost(“/”, async (CreateOrderRequest req, IOrderService s) => { … }); orders.MapDelete(“/{id}”, async (int id, IOrderService s) => { … });
Kiedy kontroler? Duże API z wieloma akcjami, filtrami, złożoną logiką routingu. Kiedy Minimal API? Proste CRUD, microservice, prototyp — mniej plików, szybciej.
30 Co to jest CORS i jak go skonfigurować w ASP.NET Core? łatwe +

CORS (Cross-Origin Resource Sharing) — przeglądarka blokuje żądania JS do innej domeny. Serwer musi jawnie zezwolić w nagłówkach odpowiedzi. Konfiguracja zbyt restrykcyjna = błędy frontendu. Zbyt luźna = ryzyko bezpieczeństwa.

// Rejestracja polityk
builder.Services.AddCors(opt =>
{
    opt.AddPolicy("Frontend", policy =>
        policy.WithOrigins("https://app.dev-hobby.pl", "http://localhost:4200")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials()); // wymagane gdy używasz cookies/Authorization

    opt.AddPolicy("Public", policy =>
        policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});

// Middleware — MUSI być przed UseRouting/MapControllers
app.UseCors("Frontend");

// Na poziomie endpointu
app.MapGet("/public", handler).RequireCors("Public");
Pułapka: AllowAnyOrigin() + AllowCredentials() = błąd runtime. Przeglądarki nie wysyłają credentials do wildcard origin.
DB Entity Framework Core & Bazy Danych Q31–Q37
31 Co to jest N+1 problem w EF Core i jak go unikać? średnie +

N+1 — zamiast jednego zapytania z JOIN, wykonujemy 1 zapytanie na listę + N zapytań dla każdego elementu. Demoluje wydajność przy dużych danych.

// Problem — N+1: 1 query na Orders + 1 query PER order
var orders = await ctx.Orders.ToListAsync();
foreach (var order in orders)
    Console.WriteLine(order.Customer.Name); // lazy load → dodatkowy SELECT!

// Fix 1 — Eager Loading (Include)
var orders = await ctx.Orders
    .Include(o => o.Customer)
    .Include(o => o.Items)
    .ToListAsync(); // JEDEN SQL z JOIN

// Fix 2 — Select (projekcja) — pobierz tylko to co potrzebne
var dtos = await ctx.Orders
    .Select(o => new OrderDto(o.Id, o.Customer.Name, o.Items.Count))
    .ToListAsync();

// Fix 3 — Split query (dla złożonych Include'ów)
var orders = await ctx.Orders
    .Include(o => o.Items).ThenInclude(i => i.Product)
    .AsSplitQuery()  // osobne SQL zamiast kartezjańskiego JOIN
    .ToListAsync();
Jak wykryć? Włącz logowanie SQL w development: optionsBuilder.LogTo(Console.WriteLine). Zobaczysz każdy SELECT.
32 Czym są migracje w EF Core i jak zarządzać nimi na produkcji? łatwe +

Migracje to kod C# opisujący zmiany schematu DB. Generowane automatycznie z modelu, wykonywane przez EF lub ręcznie jako skrypt SQL.

// CLI dotnet ef migrations add AddOrderStatusColumn dotnet ef database update // Generowanie SQL (bezpieczne na produkcji — nie dotykasz bezpośrednio) dotnet ef migrations script –idempotent -o migration.sql // Automatyczne migracje przy starcie (dev/staging) // NIE ROBIMY TEGO NA PRODUKCJI — brak kontroli, ryzyko using var scope = app.Services.CreateScope(); await scope.ServiceProvider .GetRequiredService<AppDbContext>() .Database.MigrateAsync();
Produkcja: zawsze generuj idempotent script, review przez DBA, wykonaj w transaction, backup przed migracją. Nigdy MigrateAsync() bezpośrednio na prod.
33 Co to jest AsNoTracking() i kiedy go używać? łatwe +

Domyślnie EF Core śledzi zmiany w załadowanych encjach (Change Tracker). AsNoTracking() wyłącza śledzenie — szybsze zapytania, mniejsze zużycie pamięci. Używaj dla read-only operacji.

// Read-only — nie będziemy zapisywać zmian var orders = await ctx.Orders .AsNoTracking() .Where(o => o.IsActive) .ToListAsync(); // ~30% szybciej, mniej RAM // Alternatywa: globalne ustawienie dla całego kontekstu (np. w query handler) ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // Tracking potrzebny tylko gdy: var order = await ctx.Orders.FindAsync(id); // ← tracked order.Status = “Completed”; await ctx.SaveChangesAsync(); // EF wykrywa zmianę automatycznie
34 Jak działa DbContext i dlaczego powinien być Scoped? średnie +

DbContext nie jest thread-safe i reprezentuje Unit of Work — śledzenie zmian w ramach jednej operacji biznesowej. Scoped = jeden kontekst na żądanie HTTP.

// Rejestracja builder.Services.AddDbContext<AppDbContext>(opt => opt.UseSqlServer(connectionString)); // Domyślnie Scoped — jeden DbContext na żądanie HTTP // NIE używaj Singleton — współdzielony stan między żądaniami // NIE używaj Transient — każde repository dostaje inny kontekst (brak UoW) // Repository w tej samej Scope — ten sam DbContext! public class OrderRepository(AppDbContext ctx) : IOrderRepository { } public class CustomerRepository(AppDbContext ctx) : ICustomerRepository { } // ctx w obu = ten sam obiekt dzięki Scoped lifetime
35 Co to jest Repository Pattern i czy zawsze warto go stosować z EF Core? średnie +

Repository abstrakcjonizuje dostęp do danych. Ułatwia testowanie (mock repository), pozwala zmienić ORM bez dotykania logiki biznesowej. Kontrowersja: DbContext sam w sobie jest już Unit of Work + Repository pattern.

// Proste repository public interface IOrderRepository { Task<Order?> GetByIdAsync(int id, CancellationToken ct = default); Task<List<Order>> GetActiveAsync(CancellationToken ct = default); void Add(Order order); void Remove(Order order); } public class SqlOrderRepository(AppDbContext ctx) : IOrderRepository { public Task<Order?> GetByIdAsync(int id, CancellationToken ct) => ctx.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == id, ct); public void Add(Order order) => ctx.Orders.Add(order); // SaveChangesAsync wywoływany przez Unit of Work / Service layer }
Kompromis: dla małych projektów bezpośredni DbContext w serwisach jest OK. Dla dużych — Repository + Unit of Work daje testowalność i izolację warstwy danych.
36 Kiedy użyć raw SQL zamiast LINQ w EF Core? średnie +

LINQ jest pierwszym wyborem. Raw SQL stosujemy gdy: LINQ generuje nieefektywny zapytanie, potrzebujemy zaawansowanych feature’ów SQL (CTE, window functions), lub optymalizacji wydajności krytycznej ścieżki.

// EF Core 7+ — FromSql (parametryzowane, bezpieczne na SQL Injection) var orders = await ctx.Orders .FromSql($”SELECT * FROM Orders WHERE CustomerId = {customerId}”) .ToListAsync(); // ExecuteSqlRaw — DML (INSERT/UPDATE/DELETE) lub DDL await ctx.Database.ExecuteSqlRawAsync( “UPDATE Orders SET Status = {0} WHERE CreatedAt < {1}", "Archived", DateTime.UtcNow.AddYears(-2)); // Dapper — gdy potrzebujesz pełnej kontroli SQL + projekcji na arbitrary DTO using var conn = ctx.Database.GetDbConnection(); var dtos = await conn.QueryAsync<OrderSummaryDto>( "SELECT o.Id, c.Name, SUM(i.Price) AS Total FROM Orders o ...");
Nigdy nie konkatenuj stringa z danymi użytkownika w SQL. Zawsze parametryzuj — EF robi to automatycznie przy $"...{variable}" w FromSql.
37 Co to jest optymistyczne i pesymistyczne blokowanie (concurrency)? trudne +

Optimistic Locking — zakładamy brak konfliktu, sprawdzamy przy zapisie przez RowVersion/ETag. Pessimistic Locking — blokujemy rekord na czas transakcji (SELECT FOR UPDATE). EF Core wspiera optimistic przez [Timestamp].

// Optimistic Concurrency w EF Core public class Order { public int Id { get; set; } [Timestamp] // EF doda WHERE RowVersion = @original w UPDATE public byte[] RowVersion { get; set; } = null!; } // Obsługa konfliktu try { await ctx.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var dbValues = await entry.GetDatabaseValuesAsync(); // Decyzja: reject, merge, overwrite — zależy od logiki biznesowej throw new ConflictException(“Zamówienie zostało zmienione przez innego użytkownika.”); }
ARC Wzorce projektowe & Architektura Q38–Q43
38 Wyjaśnij wzorzec Repository + Unit of Work. średnie +

Repository izoluje dostęp do danych. Unit of Work grupuje wiele operacji w jedną atomową transakcję — albo wszystkie się udają, albo wszystkie się cofają.

public interface IUnitOfWork : IDisposable { IOrderRepository Orders { get; } ICustomerRepository Customers { get; } Task<int> SaveChangesAsync(CancellationToken ct = default); } // Implementacja oparta na DbContext (DbContext jest już UoW) public class EfUnitOfWork(AppDbContext ctx) : IUnitOfWork { public IOrderRepository Orders { get; } = new SqlOrderRepository(ctx); public ICustomerRepository Customers { get; } = new SqlCustomerRepository(ctx); public Task<int> SaveChangesAsync(CancellationToken ct) => ctx.SaveChangesAsync(ct); public void Dispose() => ctx.Dispose(); } // Użycie w serwisie async Task PlaceOrder(CreateOrderRequest req) { var customer = await uow.Customers.GetByIdAsync(req.CustomerId); var order = Order.Create(customer, req.Items); uow.Orders.Add(order); await uow.SaveChangesAsync(); // jeden commit }
39 Co to jest wzorzec Mediator i jak go używać z MediatR? średnie +

Mediator oddziela nadawców od odbiorców — żądania/zdarzenia przechodzą przez centralny bus. MediatR + CQRS to fundament Clean Architecture w .NET.

// Command public record CreateOrderCommand(int CustomerId, List<OrderItem> Items) : IRequest<OrderDto>; // Handler public class CreateOrderHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler<CreateOrderCommand, OrderDto> { public async Task<OrderDto> Handle(CreateOrderCommand cmd, CancellationToken ct) { var order = Order.Create(cmd.CustomerId, cmd.Items); uow.Orders.Add(order); await uow.SaveChangesAsync(ct); return mapper.Map<OrderDto>(order); } } // Controller — cienki, deleguje do Mediatora [HttpPost] public Task<ActionResult<OrderDto>> Create( CreateOrderCommand cmd, IMediator mediator) => mediator.Send(cmd);
40 Co to jest Clean Architecture i jakie są warstwy? trudne +

Clean Architecture (Uncle Bob) organizuje kod w koncentryczne warstwy z Dependency Rule: zależności wskazują tylko do środka. Wewnętrzne warstwy nie wiedzą o zewnętrznych.

// Warstwy (od środka): // Domain (Entities, Value Objects, Domain Events) — zero zależności zewnętrznych // Application (Use Cases, Commands, Queries, Interfaces) — zależy od Domain // Infrastructure (EF, Email, Storage) — implementuje interfejsy z Application // Presentation (Controllers, Minimal API) — zależy od Application // Przykład struktury projektu: // src/ // MyApp.Domain/ Entities/Order.cs, Events/OrderPlaced.cs // MyApp.Application/ Orders/Commands/CreateOrderCommand.cs // Orders/Queries/GetOrderQuery.cs // Interfaces/IOrderRepository.cs // MyApp.Infrastructure/ Persistence/SqlOrderRepository.cs // Email/SmtpEmailSender.cs // MyApp.Api/ Controllers/OrdersController.cs // Dependency Rule — Infrastructure MOŻE zależeć od Application: class SqlOrderRepository : IOrderRepository // IOrderRepository żyje w Application!
Na rozmowie: nie musisz cytować nazwy “Clean Architecture” — wystarczy że rozumiesz Dependency Rule i potrafisz uzasadnić dlaczego logika biznesowa nie powinna wiedzieć o SQL ani HTTP.
41 Co to jest wzorzec Factory i kiedy go używać? łatwe +

Factory enkapsuluje logikę tworzenia obiektów. Używamy gdy: tworzenie jest złożone, musimy wybrać konkretny typ w runtime, lub chcemy ukryć szczegóły konstruktora.

// Factory Method w klasie domenowej public class Order { private Order() { } // prywatny konstruktor — wymuszamy użycie Factory public static Order Create(Customer customer, IEnumerable<OrderItem> items) { if (!customer.IsActive) throw new DomainException(“Nieaktywny klient”); if (!items.Any()) throw new DomainException(“Brak pozycji”); return new Order { CustomerId = customer.Id, Items = items.ToList(), Status = OrderStatus.Pending, CreatedAt = DateTime.UtcNow }; } } // Abstract Factory — różne implementacje w zależności od środowiska public interface INotificationFactory { IEmailSender CreateEmailSender(); ISmsSender CreateSmsSender(); }
42 Czym jest wzorzec Decorator i gdzie się go stosuje w .NET? średnie +

Decorator owija istniejący obiekt dodając nowe zachowanie bez modyfikowania klasy. Cross-cutting concerns: caching, logowanie, retry, timing.

// Dekorator dodający cache do repository public class CachedOrderRepository( IOrderRepository inner, IMemoryCache cache) : IOrderRepository { public async Task<Order?> GetByIdAsync(int id, CancellationToken ct) { var key = $”order_{id}”; if (cache.TryGetValue(key, out Order? cached)) return cached; var order = await inner.GetByIdAsync(id, ct); if (order is not null) cache.Set(key, order, TimeSpan.FromMinutes(5)); return order; } // pozostałe metody delegują do inner } // Rejestracja w DI builder.Services.AddScoped<SqlOrderRepository>(); builder.Services.AddScoped<IOrderRepository>(sp => new CachedOrderRepository( sp.GetRequiredService<SqlOrderRepository>(), sp.GetRequiredService<IMemoryCache>()));
43 Co to jest Result Pattern i kiedy lepszy od wyjątków? trudne +

Result Pattern jawnie komunikuje sukces/błąd przez typ zwracany, zamiast rzucać wyjątki dla oczekiwanych scenariuszy. Wyjątki są kosztowne i utrudniają śledzenie flow.

// Prosty Result type public class Result<T> { public bool IsSuccess { get; } public T? Value { get; } public string? Error { get; } private Result(T value) { IsSuccess = true; Value = value; } private Result(string error) { IsSuccess = false; Error = error; } public static Result<T> Ok(T value) => new(value); public static Result<T> Fail(string msg) => new(msg); } // Użycie w serwisie public async Task<Result<OrderDto>> CreateOrderAsync(CreateOrderRequest req) { var customer = await repo.GetCustomerAsync(req.CustomerId); if (customer is null) return Result<OrderDto>.Fail($”Customer {req.CustomerId} not found”); if (!customer.HasCreditLimit(req.Total)) return Result<OrderDto>.Fail(“Przekroczony limit kredytowy”); // … tworzenie zamówienia return Result<OrderDto>.Ok(dto); } // Kontroler var result = await service.CreateOrderAsync(req); return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
Kiedy wyjątki? Sytuacje naprawdę wyjątkowe (brak połączenia z DB, błąd IO). Kiedy Result? Oczekiwane błędy biznesowe (brak encji, naruszenie reguły).
TEST Testowanie Q44–Q47
44 Co to jest test jednostkowy vs integracyjny vs e2e? Kiedy który? łatwe +

Unit test — testuje izolowaną jednostkę kodu, zależności mockowane. Szybki, deterministyczny. Integration test — testuje współpracę komponentów (np. kod + DB). E2E — symuluje użytkownika przez cały system (Playwright, Selenium).

// Unit test — xUnit + FluentAssertions + NSubstitute/Moq public class OrderServiceTests { private readonly IOrderRepository _repo = Substitute.For<IOrderRepository>(); private readonly OrderService _sut; public OrderServiceTests() => _sut = new OrderService(_repo); [Fact] public async Task CreateOrder_WhenCustomerInactive_ThrowsDomainException() { // Arrange var customer = new Customer { IsActive = false }; _repo.GetCustomerAsync(Arg.Any<int>()).Returns(customer); // Act var act = () => _sut.CreateOrderAsync(new CreateOrderRequest()); // Assert await act.Should().ThrowAsync<DomainException>() .WithMessage(“*nieaktywny*”); } }
Test Pyramid: wiele unit testów (fast, cheap), mniej integracyjnych, bardzo mało e2e (slow, expensive). Złota proporcja: 70/20/10.
45 Co to jest mockowanie i jaka jest różnica między Mock, Stub a Fake? średnie +

Stub — zwraca predefiniowane dane, nie weryfikuje wywołań. Mock — weryfikuje, że metoda została wywołana z właściwymi argumentami. Fake — działająca uproszczona implementacja (np. in-memory DB).

using NSubstitute; var repo = Substitute.For<IOrderRepository>(); // Stub — co zwrócić repo.GetByIdAsync(1).Returns(new Order { Id = 1 }); // Weryfikacja wywołania (mock behavior) await svc.DeleteOrderAsync(1); await repo.Received(1).DeleteAsync(Arg.Is<Order>(o => o.Id == 1)); // Fake — in-memory implementacja na potrzeby testów public class InMemoryOrderRepository : IOrderRepository { private readonly List<Order> _orders = []; public Task<Order?> GetByIdAsync(int id, CancellationToken _) => Task.FromResult(_orders.FirstOrDefault(o => o.Id == id)); public void Add(Order o) => _orders.Add(o); }
46 Jak testować Web API integracyjnie w ASP.NET Core? trudne +

WebApplicationFactory<T> uruchamia in-process prawdziwy serwer ASP.NET Core — pełny pipeline middleware, DI, routing. Bez mockowania HTTP. Baza danych wymieniona na in-memory lub Testcontainers.

public class OrdersApiTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client = factory .WithWebHostBuilder(builder => builder.ConfigureServices(svc => svc.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase(“TestDb”)))) .CreateClient(); [Fact] public async Task GET_Orders_ReturnsOk() { var response = await _client.GetAsync(“/api/orders”); response.StatusCode.Should().Be(HttpStatusCode.OK); var orders = await response.Content.ReadFromJsonAsync<List<OrderDto>>(); orders.Should().NotBeNull(); } }
47 Co to jest AAA (Arrange-Act-Assert) i jak pisać czytelne testy? łatwe +

AAA to struktura testu: przygotuj dane → wykonaj akcję → zweryfikuj wynik. Czytelny test = dokumentacja zachowania kodu.

// Wzorcowa struktura testu [Theory] [InlineData(100, 0.1, 90)] // kwota, rabat, oczekiwany wynik [InlineData(200, 0.2, 160)] public void ApplyDiscount_ReturnsCorrectTotal( decimal total, double discount, decimal expected) { // Arrange var order = new Order { Total = total }; var policy = new PercentageDiscount(discount); // Act var result = policy.Apply(order.Total); // Assert result.Should().Be(expected); } // Nazewnictwo: MethodName_Scenario_ExpectedBehavior // void PlaceOrder_WhenInsufficientStock_ThrowsOutOfStockException()
FluentAssertions poprawiają czytelność i komunikaty błędów: result.Should().Be(90) zamiast Assert.Equal(90, result).
GIT Git, DevOps & Dobre praktyki Q48–Q50
48 Jaka jest różnica między git merge a git rebase? średnie +

merge łączy gałęzie zachowując historię (merge commit). rebase przepisuje historię tak, jakby branch był stworzony z aktualnego HEAD — linearna, czysta historia.

// Merge — zachowuje historię, bezpieczny git checkout main git merge feature/orders # Tworzy merge commit, widoczny w historii // Rebase — czysta liniowa historia git checkout feature/orders git rebase main # Przenosi commity feature na wierzchołek main # Potem fast-forward merge # Złota zasada rebase: # NIE rebasuj commitów już opublikowanych (push) na współdzielone branche # Rebase tylko na prywatnych/lokalnych branchach
W praktyce: rebase feature branch na main przed PR = czysta historia. Merge na main = zawsze (zachowuje punkt integracji).
49 Co to jest CI/CD i jak wygląda typowy pipeline w .NET? średnie +

CI (Continuous Integration) — automatyczna weryfikacja kodu przy każdym push (build + testy). CD (Continuous Delivery/Deployment) — automatyczne wdrożenie do środowisk po przejściu CI.

# .github/workflows/ci.yml — przykład GitHub Actions name: CI/CD on: [push, pull_request] jobs: build-and-test: runs-on: ubuntu-latest steps: – uses: actions/checkout@v4 – uses: actions/setup-dotnet@v4 with: { dotnet-version: ‘9.x’ } – name: Restore run: dotnet restore – name: Build run: dotnet build –no-restore –configuration Release – name: Test run: dotnet test –no-build –configuration Release –logger trx – name: Publish if: github.ref == ‘refs/heads/main’ run: dotnet publish -c Release -o ./publish – name: Deploy to Azure if: github.ref == ‘refs/heads/main’ uses: azure/webapps-deploy@v3 with: app-name: my-dotnet-app publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
50 Jak podejść do code review — co sprawdzasz i jak dawać feedback? łatwe +

Code review to nie poszukiwanie błędów — to dzielenie się wiedzą i podnoszenie jakości zespołu. Dobry review patrzy na czytelność, testy, bezpieczeństwo i zgodność z architekturą.

// Checklist — co sprawdzam w review: // ✅ Czy kod jest czytelny bez komentarza? (Self-documenting code) // ✅ Czy nazwy metod/zmiennych wyrażają intencję? // ✅ Czy są testy — i czy testują właściwe scenariusze? // ✅ Czy brak N+1, nieobsłużonych wyjątków, SQL injection? // ✅ Czy nie ma zbędnych zależności i over-engineering? // ✅ Czy zmiana jest zgodna z architekturą projektu? // ✅ Czy secrets/klucze nie trafiły do kodu? // Jak dawać feedback: // ❌ “To jest złe.” // ✅ “Rozważyłbym tu IEnumerable zamiast List — zewnętrzny kod // nie powinien modyfikować wewnętrznej kolekcji. Co sądzisz?” // Prefix komentarzy (Conventional Comments): // nit: drobiazg, opcjonalne // suggestion: propozycja, nie blokuje // issue: rzeczywisty problem, blokuje merge // question: pytanie, nie ocena
Na rozmowie: pokaż, że traktujesz review jako rozmowę, nie osąd. Pytasz “dlaczego” zanim zaproponujesz zmianę. To dojrzałość teamplayera.