.NET 10: Een REST API bouwen met ASP.NET Core
Volledige gids voor het bouwen van een professionele REST API met .NET 10 en ASP.NET Core. Controllers, Entity Framework Core, ingebouwde validatie en best practices.

.NET 10 is de huidige Long-Term Support (LTS) release en brengt native validatie voor Minimal APIs, standaard OpenAPI 3.1-ondersteuning en aanzienlijke performance-verbeteringen. ASP.NET Core combineert C# met een modulaire architectuur die geschikt is voor enterprise-applicaties. Deze gids behandelt het bouwen van een productie-klare REST API, van de initiele setup tot deployment.
.NET 10 wordt ondersteund tot eind 2028. Ingebouwde validatie voor Minimal APIs, OpenAPI 3.1-ondersteuning en Native AOT-verbeteringen maken het de optimale keuze voor nieuwe API-projecten.
Projectconfiguratie met .NET 10 CLI
Het aanmaken van een ASP.NET Core API-project gebeurt via de .NET CLI, die een geoptimaliseerde projectstructuur genereert. De configuratie van essentiele NuGet-pakketten legt de basis voor de ontwikkeling.
# 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.AspNetCoreDeze commando's maken een API-project aan met afhankelijkheden voor Entity Framework Core en Swagger-documentatie. FluentValidation is niet langer nodig voor basisvalidatie in .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();De AddValidation()-methode activeert de ingebouwde validatie die is geintroduceerd in ASP.NET Core 10, waardoor third-party bibliotheken zoals FluentValidation voor veelvoorkomende scenario's overbodig worden.
Datamodellen en Entity Framework Core 10
Modellen vertegenwoordigen de business-entiteiten van de applicatie. Entity Framework Core verzorgt de object-relationele mapping met fluent configuratie en slimme conventies.
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; }
}Het required keyword zorgt ervoor dat essentiele properties altijd geinitialiseerd worden bij het aanmaken.
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);
});
}
}De Fluent API-configuratie biedt nauwkeurige controle over het databaseschema dat door EF Core-migraties wordt gegenereerd.
Migraties versioneren het databaseschema. Voer dotnet ef migrations add InitialCreate uit, gevolgd door dotnet ef database update om de wijzigingen toe te passen.
DTO's en Ingebouwde Validatie in ASP.NET Core 10
DTO's (Data Transfer Objects) scheiden domeinmodellen van de data die via de API wordt blootgesteld. ASP.NET Core 10 introduceert ingebouwde validatie met DataAnnotations, waardoor third-party bibliotheken in de meeste gevallen overbodig worden.
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
);Het gebruik van C# records maakt DTO's onveranderlijk en beknopt, met automatische waardegelijkheid. De ingebouwde validatie retourneert automatisch gestructureerde 400-fouten wanneer validatie faalt. Voor sollicitatievoorbereiding over ASP.NET Core-patterns, zie de gids ASP.NET Core interviewvragen.
Klaar om je .NET gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Business Services en Abstractielaag
De servicelaag kapselt bedrijfslogica en databasebewerkingen in, wat testen en onderhoud vergemakkelijkt.
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;
}
}Het gebruik van ExecuteDeleteAsync verbetert de performance doordat het laden van de entiteit voor verwijdering wordt overgeslagen. Voor geavanceerde patterns, zie de gids Clean Architecture met .NET.
API Controllers en REST Endpoints
Controllers stellen REST-endpoints beschikbaar en orchestreren aanroepen naar business services met passende HTTP-statuscode-afhandeling.
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();
}
}De ProducesResponseType-attributen documenteren mogelijke responscodes voor automatische Swagger-documentatiegeneratie.
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;
}Het gebruik van constraints zoals {id:int} voorkomt routing-conflicten en retourneert automatisch een 404 als het formaat niet klopt.
Globale Foutafhandelingsmiddleware
Een foutafhandelingsmiddleware centraliseert de verwerking van uitzonderingen voor consistente en veilige responses.
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 configurationConfiguratie en Omgevingsvariabelen
Geexternaliseerde configuratie maakt het mogelijk de applicatie aan te passen aan verschillende omgevingen zonder codewijzigingen.
{
"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;
}
}Unit Tests met xUnit
Unit tests valideren het gedrag van services en controllers in isolatie.
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));
}
}Tests worden uitgevoerd met dotnet test vanuit de projectroot. Voor meer over EF Core performance-patterns, inclusief teststrategieen, zie de gespecialiseerde gids.
Sources
- What's new in ASP.NET Core 10 - Ingebouwde validatie, OpenAPI 3.1-ondersteuning
- What's new in .NET 10 - LTS release details, C# 14-functies
- Entity Framework Core documentation - EF Core 10-updates
- Announcing .NET 10 - Officiele release-aankondiging
Checklist voor Productie .NET API's
.NET 10 met ASP.NET Core biedt een compleet en performant ecosysteem voor het bouwen van professionele REST API's. De combinatie van ingebouwde validatie, Entity Framework Core voor datatoegang en native dependency injection maakt het mogelijk onderhoudbare en testbare applicaties te bouwen.
- DTO's scheiden van domeinmodellen
- Servicelaag voor bedrijfslogica implementeren
- Ingebouwde validatie met DataAnnotations gebruiken (.NET 10+)
- Globale foutafhandelingsmiddleware configureren
- Configuratie externaliseren met IOptions
- Unit tests voor services schrijven
- API documenteren met OpenAPI 3.1
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
De gelaagde architectuur (Controllers, Services, Repository/DbContext) bevordert de scheiding van verantwoordelijkheden en vergemakkelijkt de doorontwikkeling van de applicatie. .NET 10-functies zoals ingebouwde validatie, standaard OpenAPI 3.1-ondersteuning en verbeterde Native AOT-ondersteuning moderniseren API-ontwikkeling en verbeteren tegelijkertijd de performance.
Zie jij de bug in .NET?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 22 augustus 2026
Tags
Delen
Gerelateerde artikelen

Clean Architecture met .NET: Praktische Gids
Beheers Clean Architecture in .NET met C#. Leer de SOLID-principes, laagscheiding en implementatiepatronen voor onderhoudbare applicaties.

C# en .NET Interviewvragen: Complete Gids 2026
De 25 meest voorkomende C# en .NET interviewvragen. LINQ, async/await, dependency injection, Entity Framework en best practices met gedetailleerde antwoorden.

.NET 10 in 2026: Nieuwe Features, Native AOT en C# 14 voor Sollicitatiepreparatie
.NET 10 verschijnt als Long-Term Support release met Native AOT-verbeteringen, C# 14 extension members, de field keyword en file-based apps. Een complete gids over nieuwe features, prestatiewinst en interview-ready kennis voor .NET-ontwikkelaars in 2026.