TOP 50 Pytań Rekrutacyjnych
Junior .NET Developer
Zebrane z realnych rozmów kwalifikacyjnych. Każde pytanie ma wzorcową odpowiedź — taką, której rekruter naprawdę szuka.
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
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);
}
<Nullable>enable</Nullable> w .csproj. Eliminuje całą klasę błędów runtime zanim trafią na produkcję.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);
.Result / .Wait() z async w ASP.NET — prowadzi do deadlocka. Jeśli async, to async cały callstack.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]); }
IEnumerable (najszerszy kontrakt), pola prywatne jako List<T> (konkretna implementacja).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
foreach, ToList(), Count()).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);
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!
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
}
using / await using dla IDisposable. Unikaj wymuszania GC (GC.Collect()) — prawie zawsze błąd projektowy.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
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"
};
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...
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) { ... } }
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);
}
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>();
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);
}
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
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 */ }
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);
}
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.");
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
new zamiast override łamie polimorfizm. Użyj new tylko świadomie, gdy celowo chcesz przesłonić metodę bez polimorficznego zachowania.Żą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();
}
}
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
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
}
}
[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>();
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);
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>();
IServiceScopeFactory, nie bezpośrednio.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)
}
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": "..." }
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) => { … });
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");
AllowAnyOrigin() + AllowCredentials() = błąd runtime. Przeglądarki nie wysyłają credentials do wildcard origin.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();
optionsBuilder.LogTo(Console.WriteLine). Zobaczysz każdy SELECT.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();
MigrateAsync() bezpośrednio na prod.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
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
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
}
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 ...");
$"...{variable}" w FromSql.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.”);
}
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
}
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);
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!
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();
}
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>()));
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);
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*”);
}
}
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);
}
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();
}
}
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()
result.Should().Be(90) zamiast Assert.Equal(90, result).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
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 }}
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
