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 nadrzędnym, 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 na stercie // 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, niemutowalne dane: Vector2, Color, DateTime. Unikaj struct większych niż ~16 bajtów lub mutowalnych — kopiowanie jest kosztowne i podatne na błędy.
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 — “kompilatorze, wiem co robię” Console.WriteLine(name!.Length); // lepiej — jawny 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 lukier składniowy (syntactic sugar) na maszynę stanów generowaną przez kompilator. Przy pierwszym await, który nie jest 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 i grozi deadlockiem string json = httpClient.GetStringAsync(url).Result; // ❌ deadlock ryzyko! // Dobre string json = await httpClient.GetStringAsync(url); // Bez powrotu do kontekstu (np. w kodzie bibliotecznym) 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 przez cały callstack.
04 Różnica między IEnumerable<T>, ICollection<T> a IList<T>? łatwe +

IEnumerable<T> — tylko do odczytu, leniwa iteracja, brak Count. ICollection<T> dodaje Count, Add, Remove. IList<T> dodaje dostęp indeksowany oraz 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<T> (najwęższy wystarczający kontrakt — nie wymuszasz konkretnej kolekcji na wywołującym), 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 danych, XML). Operatory są leniwe — wykonują się dopiero przy pierwszej enumeracji.

var orders = new List<Order> { … }; // Where — filtruje, zwraca IEnumerable<T> var active = orders.Where(o => o.IsActive); // Select — projekcja/transformacja każdego elementu var ids = orders.Select(o => o.Id); // FirstOrDefault — zwraca pierwszy pasujący 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 i ToString oparte na właściwościach, nie referencji. Kompilator generuje też wyrażenie with do tworzenia 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 właściwości var p3 = p1 with { LastName = “Nowak” }; // niemutowalna kopia z nowym nazwiskiem // record struct (C# 10) — typ wartościowy + semantyka wartości record struct Point(double X, double Y);
Kiedy record? DTO, Value Objects w DDD, odpowiedzi z API, dane konfiguracyjne — wszędzie tam, gdzie zależy 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, wbudowane delegaty — nie trzeba deklarować własnych.

// Func — zwraca wartość; ostatni parametr generyczny 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; // Closure — lambda “zamyka” zmienną z zewnętrznego scope int threshold = 10; Func<int, bool> above = n => n > threshold; threshold = 20; // uwaga: zmiana wpływa na zachowanie 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 oraz Large Object Heap (LOH, ≥85 KB). Gen0 jest zbierana najczęściej i najszybciej (krótko żyjące obiekty). Obiekty, które przeżyją kolekcję, trafiają do Gen1, a następnie do Gen2.

// Finalizer bez IDisposable to antypattern — opóźnia zwolnienie o co najmniej jeden cykl GC class Resource : IDisposable { private bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); // bez tego finalizer odpali się ponownie } protected virtual void Dispose(bool disposing) { /* cleanup */ } ~Resource() => Dispose(false); // finalizer — ostatnia deska ratunku }
Praktyka: używaj using / await using dla IDisposable. Unikaj wymuszania kolekcji (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 alokacji O(n²). StringBuilder mutuje wewnętrzny bufor — alokacja O(n).

// Złe — tworzy n nowych obiektów string 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(); // Dla 2–3 konkatenacji += jest całkowicie w porządku // C# 10+: rozważ string.Create() lub Span<char> dla hot paths
10 Co to jest pattern matching w C# i jakie są jego formy? średnie +

Pattern matching pozwala testować kształt i wartości danych w wyrażeniach is, switch i switch expression. C# 9/10 dodał wzorce relacyjne, logiczne i wzorce właściwości.

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 domeny, ukrywanie szczegółów implementacji.

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 — konkretny typ nie ma znaczenia List<Shape> shapes = [new Circle(5), new Rectangle(3, 4)]; double total = 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 — każda klasa ma jeden powód do zmiany 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”), 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, nie przez zmianę istniejącego kodu.

// Naruszenie — każdy nowy typ rabatu wymaga modyfikacji 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 rabat = 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. DI jest sposobem 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 kontenerze IoC (ASP.NET Core) builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
Na rozmowie: DIP to zasada projektowa (SOLID), DI to wzorzec implementacyjny. Kontener IoC 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 + współdzielone zachowanie abstract class Animal { public string Name { get; } protected Animal(string name) { Name = name; } public abstract void Speak(); // wymaga nadpisania public void Breathe() { /* wspólna */ } // dziedziczona } // Interface — zdolność/kontrakt (C# 8+ pozwala na domyślne implementacje) interface ISerializable { string Serialize(); } // Pies “jest” zwierzęciem ORAZ “potrafi” być serializowany 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 (zwraca 25) } // Rozwiązanie: nie dziedzicz Square z Rectangle — mają różne kontrakty
Test LSP: czy mogę zastąpić klasę bazową podklasą bez modyfikowania testów jednostkowych napisanych 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, wyspecjalizowane.

// Naruszenie ISP — “fat interface” interface IWorker { void Work(); void Eat(); void Sleep(); } // Robot implementuje interfejs, ale nie je ani nie śpi class Robot : IWorker { public void Work() { /* OK */ } public void Eat() => throw new NotImplementedException(); // code smell! 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 z GoF: “favor composition over inheritance”.

// Dziedziczenie — OK gdy relacja “jest” jest prawdziwa i stabilna class ElectricCar : Car { /* ElectricCar “jest” Car */ } // Kompozycja — elastyczność, łatwość testowania i podmiany zależności class OrderProcessor { private readonly IPaymentGateway _payment; private readonly IInventoryService _inventory; private readonly IEmailService _email; 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 zastosuj kompozycję.
19 Co to jest klasa sealed i kiedy jej używać? ł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 wywołuje metody bezpośrednio (devirtualization = szybciej). } // sealed override — blokuje dalsze nadpisywanie class Base { public virtual void Do() { } } class Derived : Base { public sealed override void Do() { } } // class Further : Derived { override void Do() } // ← błąd kompilacji // Wzorzec: wyjątki domenowe 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 nie jest nadpisana, tylko ukryta DogB d = new DogB(); d.Speak(); // “Woof!” — przez konkretny typ działa wersja z DogB
Pułapka: new zamiast override łamie polimorfizm. Używaj new tylko świadomie, gdy celowo chcesz ukryć 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 przez wywołanie next().

// Kolejność middleware ma krytyczne znaczenie! app.UseExceptionHandler(“/error”); // musi być pierwszy 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); 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 przy każdym pobraniu z kontenera. Scoped — jedna instancja na żądanie HTTP. Singleton — jedna instancja przez całe życie aplikacji.

builder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); // bezstanowe builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>(); // DbContext, UoW builder.Services.AddSingleton<IConfiguration>(config); // cache, konfiguracja // PUŁAPKA: Captive Dependency // Singleton NIE powinien bezpośrednio zależeć od Scoped/Transient. // Kontener IoC rzuci InvalidOperationException lub Scoped będzie wyciekał. class BadSingleton(IScopedService scoped) { } // ❌ Scoped “uwięziony” w Singleton
Captive Dependency — najczęstszy błąd DI na rozmowach. Singleton wstrzykuje Scoped → Scoped żyje tak długo jak Singleton. DbContext w Singleton to klasyczny przykład wycieku.
23 Jak zwracać właściwe kody HTTP z kontrolera Web API? łatwe +

Używamy IActionResult / ActionResult<T> i pomocniczych metod z 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 atrybuty wbudowane w .NET — dla złożonej walidacji używamy FluentValidation.

// Data Annotations — proste, wbudowane 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().Length(3, 100); RuleFor(x => x.Amount).GreaterThan(0).LessThanOrEqualTo(10000); } } builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderValidator>();
25 Jak działa autentykacja JWT w ASP.NET Core? średnie +

JWT (JSON Web Token) składa się z trzech części: header.payload.signature. Serwer generuje token po zalogowaniu użytkownika; 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 (np. w LoginHandler) var credentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config[“Jwt:Key”]!)), SecurityAlgorithms.HmacSha256); 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); string tokenString = new JwtSecurityTokenHandler().WriteToken(token);
26 Co to jest IHostedService i BackgroundService? średnie +

IHostedService to interfejs dla usług startujących i zatrzymujących się razem z aplikacją. BackgroundService to klasa bazowa upraszczająca implementację długo działających 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(); var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>(); await repo.DeleteExpiredAsync(); logger.LogInformation(“Cleanup at {time}”, DateTimeOffset.UtcNow); await Task.Delay(TimeSpan.FromHours(1), stoppingToken); } } } builder.Services.AddHostedService<OrderCleanupWorker>();
Wzorzec: hosted services są rejestrowane jako Singleton — zależności Scoped (jak DbContext) wstrzykuj przez IServiceScopeFactory, nigdy bezpośrednio.
27 Co to jest Options Pattern i dlaczego jest 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; } } builder.Services .AddOptions<EmailOptions>() .BindConfiguration(EmailOptions.Section) .ValidateDataAnnotations() .ValidateOnStart(); // błędna konfiguracja = crash przy starcie, nie przy pierwszym użyciu class EmailService(IOptions<EmailOptions> opts) { // IOptions — stała wartość, nie reaguje na zmiany pliku // 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 HTTP API. ASP.NET Core 8 wprowadził wbudowany globalny exception handler, zastępując ręczne middleware z try/catch.

// Program.cs (ASP.NET Core 8+) builder.Services.AddProblemDetails(); app.UseExceptionHandler(); // globalny 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ć ich zamiast kontrolerów? łatwe +

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

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 endpointów (ASP.NET Core 7+) 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 i filtrami. Kiedy Minimal API? Proste CRUD, mikrousługa, prototyp — mniej plików, szybszy start.
30 Co to jest CORS i jak go skonfigurować w ASP.NET Core? łatwe +

CORS (Cross-Origin Resource Sharing) — przeglądarka blokuje żądania JavaScript do innej domeny. Serwer musi jawnie zezwolić przez nagłówki odpowiedzi.

builder.Services.AddCors(opt => { opt.AddPolicy(“Frontend”, policy => policy.WithOrigins(“https://app.dev-hobby.pl”, “http://localhost:4200”) .AllowAnyMethod().AllowAnyHeader() .AllowCredentials()); // wymagane przy cookies/Authorization header opt.AddPolicy(“Public”, policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); }); // Middleware — MUSI być przed MapControllers app.UseCors(“Frontend”); app.MapGet(“/public”, handler).RequireCors(“Public”);
Pułapka: AllowAnyOrigin() + AllowCredentials() = błąd runtime. Przeglądarki nie wysyłają danych uwierzytelniających do wildcard origin.
DB Entity Framework Core & Bazy Danych Q31–Q37
31 Co to jest problem N+1 w EF Core i jak go unikać? średnie +

Problem N+1 — zamiast jednego zapytania z JOIN, wykonujemy 1 zapytanie po listę + N dodatkowych zapytań dla każdego elementu. Demoluje wydajność przy większych danych.

// Problem — N+1: 1 query po Orders + 1 query PER zamówienie 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: 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 bazy danych. Generowane automatycznie z modelu, mogą być wykonywane przez EF lub ręcznie jako skrypt SQL.

dotnet ef migrations add AddOrderStatusColumn dotnet ef database update // Generowanie skryptu SQL (bezpieczne na produkcji) dotnet ef migrations script –idempotent -o migration.sql // Automatyczne migracje przy starcie — TYLKO dev/staging, NIE na produkcji! using var scope = app.Services.CreateScope(); await scope.ServiceProvider .GetRequiredService<AppDbContext>() .Database.MigrateAsync();
Produkcja: generuj idempotent script, przejrzyj z DBA, wykonaj w transakcji, zrób backup przed migracją. Nigdy MigrateAsync() bezpośrednio na produkcji.
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. Stosuj dla operacji tylko do odczytu.

// Read-only — śledzenie niepotrzebne var orders = await ctx.Orders .AsNoTracking() .Where(o => o.IsActive) .ToListAsync(); // ~30% szybciej, mniejszy footprint pamięci // Globalne ustawienie dla całego kontekstu ctx.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // Tracking jest potrzebny gdy zapisujesz zmiany: var order = await ctx.Orders.FindAsync(id); // ← tracked order.Status = “Completed”; await ctx.SaveChangesAsync();
34 Jak działa DbContext i dlaczego powinien być Scoped? średnie +

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

// Domyślnie Scoped builder.Services.AddDbContext<AppDbContext>(opt => opt.UseSqlServer(connectionString)); // NIE używaj Singleton — współdzielony stan między równoległymi żądaniami // NIE używaj Transient — każde repository dostaje inny kontekst (brak UoW) // Scoped gwarantuje ten SAM DbContext w całym żądaniu: 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 i pozwala zmienić ORM bez dotykania logiki biznesowej. Kontrowersja: DbContext sam realizuje już wzorce Unit of Work i 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łuje Unit of Work / Service layer }
Kompromis: dla małych projektów bezpośredni DbContext w serwisach jest akceptowalny. Dla dużych — Repository + Unit of Work dają 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 nieefektywne zapytanie, potrzebujemy zaawansowanych funkcji SQL (CTE, window functions) lub optymalizujemy krytyczną ścieżkę wydajnościową.

// EF Core 7+ — FromSql z interpolacją (parametryzowane, bezpieczne na SQL Injection) var orders = await ctx.Orders .FromSql($”SELECT * FROM Orders WHERE CustomerId = {customerId}”) .ToListAsync(); // ExecuteSqlRaw — DML (INSERT/UPDATE/DELETE) await ctx.Database.ExecuteSqlRawAsync( “UPDATE Orders SET Status = {0} WHERE CreatedAt < {1}”, “Archived”, DateTime.UtcNow.AddYears(-2)); // Dapper — pełna kontrola SQL + projekcja na dowolne 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 interpolacji $"...{variable}" w FromSql.
37 Co to jest optymistyczne i pesymistyczne blokowanie współbieżności? trudne +

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

public class Order { public int Id { get; set; } [Timestamp] // EF doda WHERE RowVersion = @original w UPDATE public byte[] RowVersion { get; set; } = null!; } try { await ctx.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var dbValues = await entry.GetDatabaseValuesAsync(); // Decyzja: odrzuć, scal lub nadpisz — zależy od reguł biznesowych 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 są cofane.

public interface IUnitOfWork : IDisposable { IOrderRepository Orders { get; } ICustomerRepository Customers { get; } Task<int> SaveChangesAsync(CancellationToken ct = default); } 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(); } 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 dla całej operacji }
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 ekosystemie .NET.

// Command (mutuje stan) public record CreateOrderCommand(int CustomerId, List<OrderItem> Items) : IRequest<OrderDto>; // Handler — cała logika biznesowa w jednym miejscu 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 logikę do Mediatora [HttpPost] public Task<ActionResult<OrderDto>> Create(CreateOrderCommand cmd, IMediator mediator) => mediator.Send(cmd);
40 Co to jest Clean Architecture i jakie są jej warstwy? trudne +

Clean Architecture (Robert C. Martin) organizuje kod w koncentryczne warstwy z regułą zależności (Dependency Rule): zależności wskazują wyłącznie do środka. Warstwy wewnętrzne nie wiedzą nic o zewnętrznych.

// Warstwy (od środka do zewnątrz): // Domain — Entities, Value Objects, Domain Events; zero zewnętrznych zależności // Application — Use Cases, Commands, Queries, Interfejsy; zależy od Domain // Infrastructure — EF Core, Email, Storage; implementuje interfejsy z Application // Presentation — Controllers, Minimal API; zależy od Application // src/ // MyApp.Domain/ Entities/Order.cs, Events/OrderPlaced.cs // MyApp.Application/ Orders/Commands/CreateOrderCommand.cs // Interfaces/IOrderRepository.cs // MyApp.Infrastructure/ Persistence/SqlOrderRepository.cs // MyApp.Api/ Controllers/OrdersController.cs // Infrastructure zależy od Application (nie odwrotnie): class SqlOrderRepository : IOrderRepository // IOrderRepository żyje w Application!
Na rozmowie: 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 lub zawiera reguły biznesowe, musimy wybrać konkretny typ w runtime, lub chcemy ukryć szczegóły konstruktora.

public class Order { private Order() { } // prywatny konstruktor — wymuszamy użycie metody fabrycznej 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 w zamówieniu”); return new Order { CustomerId = customer.Id, Items = items.ToList(), Status = OrderStatus.Pending, CreatedAt = DateTime.UtcNow }; } } // Abstract Factory — rodzina powiązanych obiektów public interface INotificationFactory { IEmailSender CreateEmailSender(); ISmsSender CreateSmsSender(); }
42 Czym jest wzorzec Decorator i gdzie jest stosowany w .NET? średnie +

Decorator owija istniejący obiekt, dodając nowe zachowanie bez modyfikowania klasy bazowej. Doskonały do cross-cutting concerns: caching, logowanie, retry, pomiar czasu.

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 dekoratora 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 jest lepszy od wyjątków? trudne +

Result Pattern jawnie komunikuje sukces lub błąd przez typ zwracany, zamiast rzucać wyjątki dla przewidywalnych scenariuszy. Wyjątki są kosztowne i utrudniają śledzenie przepływu sterowania.

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); } public async Task<Result<OrderDto>> CreateOrderAsync(CreateOrderRequest req) { var customer = await repo.GetCustomerAsync(req.CustomerId); if (customer is null) return Result<OrderDto>.Fail($”Klient {req.CustomerId} nie istnieje”); if (!customer.HasCreditLimit(req.Total)) return Result<OrderDto>.Fail(“Przekroczony limit kredytowy”); return Result<OrderDto>.Ok(dto); } // Controller 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 I/O. Kiedy Result? Oczekiwane błędy biznesowe: brak encji, naruszenie reguły domenowej.
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 + baza danych). E2E — symuluje użytkownika przez cały system (Playwright, Selenium).

// Unit test — xUnit + FluentAssertions + NSubstitute 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.GetByIdAsync(Arg.Any<int>()).Returns(customer); // Act var act = () => _sut.CreateOrderAsync(new CreateOrderRequest()); // Assert await act.Should().ThrowAsync<DomainException>() .WithMessage(“*nieaktywny*”); } }
Test Pyramid: dużo 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. baza in-memory).

using NSubstitute; var repo = Substitute.For<IOrderRepository>(); // Stub — konfiguracja zwracanej wartoś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 — działająca implementacja in-memory 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. Bazę danych wymieniamy na in-memory lub Testcontainers.

public class OrdersApiTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client = factory .WithWebHostBuilder(b => b.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 = żywa dokumentacja zachowania kodu.

[Theory] [InlineData(100, 0.1, 90)] // kwota, procent rabatu, 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); } // Konwencja nazewnictwa: 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 pełną historię (tworzy merge commit). rebase przepisuje historię tak, jakby branch był stworzony z aktualnego HEAD — liniowa, 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 # Złota zasada rebase: # NIE rebasuj commitów już opublikowanych na współdzielone branche! # Rebase stosuj 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 dla .NET 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 polowanie na błędy — to dzielenie się wiedzą i podnoszenie jakości całego zespołu. Dobry review patrzy na czytelność, testy, bezpieczeństwo i zgodność z architekturą.

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