.NET 10: Budowanie REST API z ASP.NET Core
Kompletny przewodnik po tworzeniu profesjonalnego REST API z .NET 10 i ASP.NET Core. Kontrolery, Entity Framework Core, wbudowana walidacja i najlepsze praktyki.

.NET 10 to aktualne wydanie Long-Term Support (LTS), wprowadzające natywną walidację dla Minimal APIs, domyślne wsparcie OpenAPI 3.1 oraz znaczące usprawnienia wydajności. ASP.NET Core łączy moc języka C# z modularną architekturą idealną dla aplikacji korporacyjnych. Niniejszy przewodnik obejmuje tworzenie profesjonalnego REST API, od konfiguracji projektu po wdrożenie.
.NET 10 jest wspierany do końca 2028 roku. Wbudowana walidacja dla Minimal APIs, wsparcie OpenAPI 3.1 oraz ulepszenia Native AOT sprawiają, że jest to optymalny wybór dla nowych projektów API.
Konfiguracja projektu z .NET 10 CLI
Tworzenie projektu API w ASP.NET Core odbywa się za pomocą .NET CLI, które generuje zoptymalizowaną strukturę projektu. Konfiguracja niezbędnych pakietów NuGet stanowi fundament dalszej pracy.
# terminal
# Check installed .NET version
dotnet --version
# Expected: 10.0.x
# Create the API project
dotnet new webapi -n ProductApi -o ProductApi
cd ProductApi
# Add essential packages
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Swashbuckle.AspNetCorePowyższe komendy tworzą projekt API z zależnościami dla Entity Framework Core oraz dokumentacji Swagger. FluentValidation nie jest już wymagany do podstawowej walidacji w .NET 10.
using Microsoft.EntityFrameworkCore;
using ProductApi.Data;
using ProductApi.Services;
var builder = WebApplication.CreateBuilder(args);
// Configure Entity Framework Core with SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Register business services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<ICategoryService, CategoryService>();
// Configure controllers with built-in validation (.NET 10)
builder.Services.AddControllers();
builder.Services.AddValidation();
// Configure OpenAPI 3.1 (default in .NET 10)
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "Product API", Version = "v1" });
});
var app = builder.Build();
// Middleware pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();Metoda AddValidation() włącza wbudowaną walidację wprowadzoną w ASP.NET Core 10, eliminując potrzebę bibliotek zewnętrznych jak FluentValidation w typowych scenariuszach.
Modele danych i Entity Framework Core 10
Modele reprezentują encje biznesowe aplikacji. Entity Framework Core obsługuje mapowanie obiektowo-relacyjne z konfiguracją Fluent API i inteligentnymi konwencjami.
namespace ProductApi.Models;
public class Product
{
// Primary key with auto-increment
public int Id { get; set; }
// Required properties (non-nullable in C# 14)
public required string Name { get; set; }
public required string Description { get; set; }
// Price with decimal precision
public decimal Price { get; set; }
// Stock with default value
public int StockQuantity { get; set; } = 0;
// Product status
public bool IsActive { get; set; } = true;
// Relationship with Category (foreign key)
public int CategoryId { get; set; }
public Category? Category { get; set; }
// Automatic tracking dates
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}Słowo kluczowe required zapewnia, że istotne właściwości są zawsze inicjalizowane podczas tworzenia obiektu.
namespace ProductApi.Models;
public class Category
{
public int Id { get; set; }
public required string Name { get; set; }
// Slug for friendly URLs
public required string Slug { get; set; }
public string? Description { get; set; }
// Inverse navigation: list of products in this category
public ICollection<Product> Products { get; set; } = new List<Product>();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}using Microsoft.EntityFrameworkCore;
using ProductApi.Models;
namespace ProductApi.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
// DbSets for each entity
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Product entity configuration
modelBuilder.Entity<Product>(entity =>
{
// Index on name for fast search
entity.HasIndex(p => p.Name);
// Price precision: 18 digits, 2 decimals
entity.Property(p => p.Price)
.HasPrecision(18, 2);
// Relationship with Category
entity.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
});
// Category entity configuration
modelBuilder.Entity<Category>(entity =>
{
// Unique slug
entity.HasIndex(c => c.Slug).IsUnique();
// Maximum name length
entity.Property(c => c.Name).HasMaxLength(100);
});
}
}Konfiguracja Fluent API zapewnia precyzyjną kontrolę nad schematem bazy danych generowanym przez migracje EF Core.
Migracje wersjonują schemat bazy danych. Należy uruchomić dotnet ef migrations add InitialCreate, a następnie dotnet ef database update, aby zastosować zmiany.
DTO i wbudowana walidacja w ASP.NET Core 10
DTO (Data Transfer Objects) oddzielają modele domenowe od danych eksponowanych przez API. ASP.NET Core 10 wprowadza wbudowaną walidację z użyciem DataAnnotations, eliminując potrzebę bibliotek zewnętrznych w większości przypadków.
using System.ComponentModel.DataAnnotations;
namespace ProductApi.DTOs;
// DTO for product creation with DataAnnotations
public record CreateProductDto(
[Required(ErrorMessage = "Product name is required.")]
[MaxLength(200, ErrorMessage = "Name cannot exceed 200 characters.")]
string Name,
[Required(ErrorMessage = "Description is required.")]
[MinLength(10, ErrorMessage = "Description must contain at least 10 characters.")]
string Description,
[Range(0.01, 999999.99, ErrorMessage = "Price must be between 0.01 and 999,999.99.")]
decimal Price,
[Range(0, int.MaxValue, ErrorMessage = "Stock cannot be negative.")]
int StockQuantity,
[Required(ErrorMessage = "A valid category is required.")]
int CategoryId
);
// DTO for product update
public record UpdateProductDto(
[MaxLength(200)]
string? Name,
string? Description,
[Range(0.01, 999999.99)]
decimal? Price,
[Range(0, int.MaxValue)]
int? StockQuantity,
bool? IsActive
);
// DTO for response (read)
public record ProductDto(
int Id,
string Name,
string Description,
decimal Price,
int StockQuantity,
bool IsActive,
string CategoryName,
DateTime CreatedAt
);
// DTO for list with pagination
public record ProductListDto(
int Id,
string Name,
decimal Price,
int StockQuantity,
bool IsActive,
string CategoryName
);Użycie rekordów z C# sprawia, że DTO są niemutowalne i zwięzłe, z automatycznym porównywaniem wartości. Wbudowana walidacja automatycznie zwraca ustrukturyzowane błędy 400 w przypadku niepowodzenia walidacji. W kontekście przygotowania do rozmowy kwalifikacyjnej z ASP.NET Core, warto zapoznać się z pytaniami rekrutacyjnymi z ASP.NET Core.
Gotowy na rozmowy o .NET?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Warstwa serwisów i abstrakcja logiki biznesowej
Warstwa serwisów enkapsuluje logikę biznesową i operacje bazodanowe, ułatwiając testowanie i utrzymanie kodu.
using ProductApi.DTOs;
namespace ProductApi.Services;
public interface IProductService
{
// Retrieval with pagination
Task<(IEnumerable<ProductListDto> Items, int TotalCount)> GetAllAsync(
int page = 1,
int pageSize = 10,
string? search = null,
int? categoryId = null);
// Retrieval by ID
Task<ProductDto?> GetByIdAsync(int id);
// Creation
Task<ProductDto> CreateAsync(CreateProductDto dto);
// Update
Task<ProductDto?> UpdateAsync(int id, UpdateProductDto dto);
// Deletion
Task<bool> DeleteAsync(int id);
}using Microsoft.EntityFrameworkCore;
using ProductApi.Data;
using ProductApi.DTOs;
using ProductApi.Models;
namespace ProductApi.Services;
public class ProductService : IProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<(IEnumerable<ProductListDto> Items, int TotalCount)> GetAllAsync(
int page = 1,
int pageSize = 10,
string? search = null,
int? categoryId = null)
{
// Build base query
var query = _context.Products
.Include(p => p.Category)
.AsQueryable();
// Filter by text search
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(p =>
p.Name.Contains(search) ||
p.Description.Contains(search));
}
// Filter by category
if (categoryId.HasValue)
{
query = query.Where(p => p.CategoryId == categoryId.Value);
}
// Total count before pagination
var totalCount = await query.CountAsync();
// Apply pagination
var items = await query
.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(p => new ProductListDto(
p.Id,
p.Name,
p.Price,
p.StockQuantity,
p.IsActive,
p.Category!.Name))
.ToListAsync();
return (items, totalCount);
}
public async Task<ProductDto?> GetByIdAsync(int id)
{
// Retrieve with category inclusion
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
if (product == null) return null;
// Map to DTO
return new ProductDto(
product.Id,
product.Name,
product.Description,
product.Price,
product.StockQuantity,
product.IsActive,
product.Category?.Name ?? "Uncategorized",
product.CreatedAt);
}
public async Task<ProductDto> CreateAsync(CreateProductDto dto)
{
// Create entity
var product = new Product
{
Name = dto.Name,
Description = dto.Description,
Price = dto.Price,
StockQuantity = dto.StockQuantity,
CategoryId = dto.CategoryId
};
// Add and save
_context.Products.Add(product);
await _context.SaveChangesAsync();
// Load category for response
await _context.Entry(product)
.Reference(p => p.Category)
.LoadAsync();
return new ProductDto(
product.Id,
product.Name,
product.Description,
product.Price,
product.StockQuantity,
product.IsActive,
product.Category?.Name ?? "Uncategorized",
product.CreatedAt);
}
public async Task<ProductDto?> UpdateAsync(int id, UpdateProductDto dto)
{
// Retrieve existing entity
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
if (product == null) return null;
// Conditional field updates
if (!string.IsNullOrEmpty(dto.Name))
product.Name = dto.Name;
if (!string.IsNullOrEmpty(dto.Description))
product.Description = dto.Description;
if (dto.Price.HasValue)
product.Price = dto.Price.Value;
if (dto.StockQuantity.HasValue)
product.StockQuantity = dto.StockQuantity.Value;
if (dto.IsActive.HasValue)
product.IsActive = dto.IsActive.Value;
// Update modification date
product.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return new ProductDto(
product.Id,
product.Name,
product.Description,
product.Price,
product.StockQuantity,
product.IsActive,
product.Category?.Name ?? "Uncategorized",
product.CreatedAt);
}
public async Task<bool> DeleteAsync(int id)
{
// Direct deletion without prior loading (EF Core 7+)
var result = await _context.Products
.Where(p => p.Id == id)
.ExecuteDeleteAsync();
return result > 0;
}
}Użycie ExecuteDeleteAsync poprawia wydajność, eliminując konieczność ładowania encji przed usunięciem. Szczegóły zaawansowanych wzorców można znaleźć w przewodniku Clean Architecture z .NET.
Kontrolery API i endpointy REST
Kontrolery eksponują endpointy REST i orkiestrują wywołania serwisów biznesowych z odpowiednią obsługą kodów statusu HTTP.
using Microsoft.AspNetCore.Mvc;
using ProductApi.DTOs;
using ProductApi.Services;
namespace ProductApi.Controllers;
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
/// <summary>
/// Retrieves the list of products with pagination and filters.
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(PaginatedResponse<ProductListDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAll(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? search = null,
[FromQuery] int? categoryId = null)
{
// Validate pagination parameters
if (page < 1) page = 1;
if (pageSize < 1 || pageSize > 100) pageSize = 10;
var (items, totalCount) = await _productService.GetAllAsync(
page, pageSize, search, categoryId);
// Standardized paginated response
var response = new PaginatedResponse<ProductListDto>
{
Items = items,
Page = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
return Ok(response);
}
/// <summary>
/// Retrieves a product by its identifier.
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id)
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
{
return NotFound(new { message = $"Product with ID {id} not found." });
}
return Ok(product);
}
/// <summary>
/// Creates a new product.
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create([FromBody] CreateProductDto dto)
{
// Validation is automatic via AddValidation()
var product = await _productService.CreateAsync(dto);
// Returns 201 with the created resource URL
return CreatedAtAction(
nameof(GetById),
new { id = product.Id },
product);
}
/// <summary>
/// Updates an existing product.
/// </summary>
[HttpPut("{id:int}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Update(int id, [FromBody] UpdateProductDto dto)
{
var product = await _productService.UpdateAsync(id, dto);
if (product == null)
{
return NotFound(new { message = $"Product with ID {id} not found." });
}
return Ok(product);
}
/// <summary>
/// Deletes a product.
/// </summary>
[HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(int id)
{
var deleted = await _productService.DeleteAsync(id);
if (!deleted)
{
return NotFound(new { message = $"Product with ID {id} not found." });
}
// 204 No Content for successful deletion
return NoContent();
}
}Atrybuty ProducesResponseType dokumentują możliwe kody odpowiedzi dla automatycznego generowania dokumentacji Swagger.
namespace ProductApi.DTOs;
public class PaginatedResponse<T>
{
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int TotalPages { get; set; }
public bool HasPreviousPage => Page > 1;
public bool HasNextPage => Page < TotalPages;
}Użycie ograniczeń takich jak {id:int} zapobiega konfliktom routingu i automatycznie zwraca 404, jeśli format jest nieprawidłowy.
Globalna obsługa błędów
Middleware do obsługi błędów centralizuje przetwarzanie wyjątków, zapewniając spójne i bezpieczne odpowiedzi.
using System.Net;
using System.Text.Json;
namespace ProductApi.Middleware;
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
private readonly IHostEnvironment _env;
public ExceptionMiddleware(
RequestDelegate next,
ILogger<ExceptionMiddleware> logger,
IHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
// Continue pipeline
await _next(context);
}
catch (Exception ex)
{
// Log the error
_logger.LogError(ex, "An unhandled exception occurred");
// Prepare response
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
// Different response based on environment
var response = _env.IsDevelopment()
? new ErrorResponse(
StatusCode: context.Response.StatusCode,
Message: ex.Message,
Details: ex.StackTrace)
: new ErrorResponse(
StatusCode: context.Response.StatusCode,
Message: "An internal error occurred.",
Details: null);
// Serialize with camelCase options
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(response, options);
await context.Response.WriteAsync(json);
}
}
}
// DTO for error responses
public record ErrorResponse(int StatusCode, string Message, string? Details);
// Extension to register middleware
public static class ExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder app)
{
return app.UseMiddleware<ExceptionMiddleware>();
}
}var app = builder.Build();
// Exception middleware must be first
app.UseExceptionMiddleware();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// ... rest of configurationKonfiguracja i zmienne środowiskowe
Eksternalizacja konfiguracji pozwala dostosować aplikację do różnych środowisk bez zmian w kodzie.
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=ProductDb;User Id=sa;Password=YourPassword;TrustServerCertificate=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"ApiSettings": {
"DefaultPageSize": 10,
"MaxPageSize": 100,
"ApiVersion": "1.0"
}
}namespace ProductApi.Configuration;
public class ApiSettings
{
public int DefaultPageSize { get; set; } = 10;
public int MaxPageSize { get; set; } = 100;
public string ApiVersion { get; set; } = "1.0";
}builder.Services.Configure<ApiSettings>(
builder.Configuration.GetSection("ApiSettings"));
// Usage in a service
public class ProductService : IProductService
{
private readonly ApiSettings _settings;
public ProductService(IOptions<ApiSettings> settings)
{
_settings = settings.Value;
}
}Testy jednostkowe z xUnit
Testy jednostkowe weryfikują zachowanie serwisów i kontrolerów w izolacji.
using Microsoft.EntityFrameworkCore;
using ProductApi.Data;
using ProductApi.DTOs;
using ProductApi.Models;
using ProductApi.Services;
using Xunit;
namespace ProductApi.Tests;
public class ProductServiceTests
{
private AppDbContext CreateInMemoryContext()
{
// Configure in-memory database
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AppDbContext(options);
}
[Fact]
public async Task CreateAsync_ValidDto_ReturnsProductDto()
{
// Arrange
using var context = CreateInMemoryContext();
// Add test category
var category = new Category { Id = 1, Name = "Electronics", Slug = "electronics" };
context.Categories.Add(category);
await context.SaveChangesAsync();
var service = new ProductService(context);
var dto = new CreateProductDto(
Name: "Test Product",
Description: "Test Description",
Price: 99.99m,
StockQuantity: 10,
CategoryId: 1);
// Act
var result = await service.CreateAsync(dto);
// Assert
Assert.NotNull(result);
Assert.Equal("Test Product", result.Name);
Assert.Equal(99.99m, result.Price);
Assert.Equal("Electronics", result.CategoryName);
}
[Fact]
public async Task GetByIdAsync_NonExistent_ReturnsNull()
{
// Arrange
using var context = CreateInMemoryContext();
var service = new ProductService(context);
// Act
var result = await service.GetByIdAsync(999);
// Assert
Assert.Null(result);
}
[Fact]
public async Task DeleteAsync_ExistingProduct_ReturnsTrue()
{
// Arrange
using var context = CreateInMemoryContext();
var category = new Category { Id = 1, Name = "Test", Slug = "test" };
var product = new Product
{
Id = 1,
Name = "To Delete",
Description = "Will be deleted",
Price = 10.00m,
CategoryId = 1
};
context.Categories.Add(category);
context.Products.Add(product);
await context.SaveChangesAsync();
var service = new ProductService(context);
// Act
var result = await service.DeleteAsync(1);
// Assert
Assert.True(result);
Assert.Null(await context.Products.FindAsync(1));
}
}Testy uruchamia się poleceniem dotnet test z katalogu głównego projektu. Więcej informacji o wzorcach wydajności EF Core, w tym strategiach testowania, znajduje się w dedykowanym przewodniku.
Źródła
- What's new in ASP.NET Core 10 - Wbudowana walidacja, wsparcie OpenAPI 3.1
- What's new in .NET 10 - Szczegóły wydania LTS, funkcje C# 14
- Entity Framework Core documentation - Aktualizacje EF Core 10
- Announcing .NET 10 - Oficjalne ogłoszenie wydania
Lista kontrolna dla produkcyjnych API .NET
.NET 10 z ASP.NET Core zapewnia kompletny i wydajny ekosystem do budowania profesjonalnych REST API. Połączenie wbudowanej walidacji, Entity Framework Core do dostępu do danych oraz natywnego wstrzykiwania zależności umożliwia tworzenie aplikacji łatwych w utrzymaniu i testowaniu.
- Oddzielenie DTO od modeli domenowych
- Implementacja warstwy serwisów dla logiki biznesowej
- Użycie wbudowanej walidacji z DataAnnotations (.NET 10+)
- Konfiguracja globalnego middleware do obsługi błędów
- Eksternalizacja konfiguracji z IOptions
- Pisanie testów jednostkowych dla serwisów
- Dokumentowanie API za pomocą OpenAPI 3.1
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Warstwowa architektura (Controllers, Services, Repository/DbContext) promuje separację odpowiedzialności i ułatwia rozwój aplikacji. Funkcje .NET 10, takie jak wbudowana walidacja, domyślne wsparcie OpenAPI 3.1 oraz ulepszone Native AOT, modernizują tworzenie API przy jednoczesnej poprawie wydajności.
Znajdziesz błąd w .NET?
Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Autor:
Anthony Fillion-MailletZałożyciel SharpSkill
Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.
Zaktualizowano 22 sierpnia 2026
Tagi
Udostępnij
Powiązane artykuły

Clean Architecture w .NET: Praktyczny Przewodnik
Opanowanie Clean Architecture w .NET z C#. Poznanie zasad SOLID, separacji warstw i wzorców implementacyjnych dla aplikacji łatwych w utrzymaniu.

Pytania rekrutacyjne C# i .NET: Kompletny przewodnik 2026
17 najczesciej zadawanych pytan rekrutacyjnych z C# i .NET. LINQ, async/await, Dependency Injection, Entity Framework Core i ASP.NET Core ze szczegolowymi odpowiedziami.

Zarządzanie czasem życia DbContext w ASP.NET Core: Wydajność kontra bezpieczeństwo wątkowe w operacjach asynchronicznych
Kompleksowy przewodnik po zarządzaniu czasem życia DbContext w ASP.NET Core. Dowiedz się, kiedy stosować scoped vs transient, jak bezpiecznie obsługiwać operacje asynchroniczne oraz jak optymalizować wydajność za pomocą poolingu DbContext.