Mit .NET 6 hat Microsoft Minimal APIs eingeführt — eine schlanke Alternative zu den klassischen Controller-basierten APIs in ASP.NET Core. Statt Klassen mit [ApiController]-Attribut und Route-Annotationen werden Endpunkte direkt in Program.cs registriert. Das Ergebnis ist deutlich weniger Boilerplate, besonders für einfache Microservices.
Klassische Controller-API vs. Minimal API
Controller-basiert (klassisch)
// Controllers/ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetAll()
{
var products = await _productService.GetAllAsync();
return Ok(products);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<Product>> GetById(int id)
{
var product = await _productService.GetByIdAsync(id);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
public async Task<ActionResult<Product>> Create([FromBody] ProductDto dto)
{
var created = await _productService.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}
}
Minimal API (ab .NET 6)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();
// GET /api/products
app.MapGet("/api/products", async (IProductService service) =>
await service.GetAllAsync());
// GET /api/products/{id}
app.MapGet("/api/products/{id:int}", async (int id, IProductService service) =>
{
var product = await service.GetByIdAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
});
// POST /api/products
app.MapPost("/api/products", async (ProductDto dto, IProductService service) =>
{
var created = await service.CreateAsync(dto);
return Results.Created($"/api/products/{created.Id}", created);
});
// DELETE /api/products/{id}
app.MapDelete("/api/products/{id:int}", async (int id, IProductService service) =>
{
var deleted = await service.DeleteAsync(id);
return deleted ? Results.NoContent() : Results.NotFound();
});
app.Run();
Vollständiges Setup
Projektstruktur
MyApi/
├── Program.cs # Einstiegspunkt und Routing
├── Models/
│ ├── Product.cs # Entität
│ └── ProductDto.cs # Data Transfer Object (record)
├── Services/
│ ├── IProductService.cs
│ └── ProductService.cs
└── Data/
└── AppDbContext.cs # Entity Framework Context
Models als Records
// Models/Product.cs
public record Product(int Id, string Name, decimal Price, string Category);
// Models/ProductDto.cs — für POST/PUT Requests
public record ProductDto(
[Required] string Name,
[Range(0.01, double.MaxValue)] decimal Price,
[Required] string Category
);
Entity Framework mit SQLite (für Entwicklung)
// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}
public DbSet<Product> Products => Set<Product>();
}
// Program.cs — EF registrieren
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite("Data Source=products.db"));
Service-Layer
// Services/IProductService.cs
public interface IProductService
{
Task<IEnumerable<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(int id);
Task<Product> CreateAsync(ProductDto dto);
Task<bool> DeleteAsync(int id);
}
// Services/ProductService.cs
public class ProductService : IProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Product>> GetAllAsync()
=> await _context.Products.ToListAsync();
public async Task<Product?> GetByIdAsync(int id)
=> await _context.Products.FindAsync(id);
public async Task<Product> CreateAsync(ProductDto dto)
{
var product = new Product(0, dto.Name, dto.Price, dto.Category);
_context.Products.Add(product);
await _context.SaveChangesAsync();
return product;
}
public async Task<bool> DeleteAsync(int id)
{
var product = await _context.Products.FindAsync(id);
if (product is null) return false;
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return true;
}
}
Validierung und Fehlerbehandlung
Minimal APIs nutzen IValidator von FluentValidation oder die eingebauten DataAnnotations-Validierung:
// Program.cs mit Validierung
app.MapPost("/api/products", async (
[FromBody] ProductDto dto,
IProductService service,
HttpContext context) =>
{
// Manuelle Validierung mit DataAnnotations
var validationResults = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(
dto,
new ValidationContext(dto),
validationResults,
validateAllProperties: true
);
if (!isValid)
{
return Results.ValidationProblem(validationResults
.GroupBy(v => v.MemberNames.FirstOrDefault() ?? "")
.ToDictionary(g => g.Key, g => g.Select(v => v.ErrorMessage!).ToArray()));
}
var created = await service.CreateAsync(dto);
return Results.Created($"/api/products/{created.Id}", created);
});
Einfacher mit der Route-Group-Extension:
// Endpunkte in Gruppen organisieren (ab .NET 7)
var productsGroup = app.MapGroup("/api/products")
.WithTags("Products")
.WithOpenApi();
productsGroup.MapGet("/", GetAll);
productsGroup.MapGet("/{id:int}", GetById);
productsGroup.MapPost("/", Create).AddEndpointFilter<ValidationFilter<ProductDto>>();
OpenAPI / Swagger Integration
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ...
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// Einzelne Endpunkte dokumentieren
app.MapGet("/api/products", async (IProductService service) =>
await service.GetAllAsync())
.WithName("GetAllProducts")
.WithSummary("Alle Produkte abrufen")
.Produces<IEnumerable<Product>>(200)
.WithOpenApi();
Vergleich mit PHP/Symfony
Für PHP-Entwickler sind Minimal APIs vergleichbar mit einem Symfony-Controller, der nur Routes definiert:
// Symfony: Route-Definitionen im Controller
#[Route('/api/products')]
class ProductController extends AbstractController
{
#[Route('', methods: ['GET'])]
public function list(): JsonResponse { /* ... */ }
}
// C# Minimal API: Route-Definitionen direkt in Program.cs
app.MapGet("/api/products", async (IProductService service) => /* ... */);
Der Hauptunterschied: In Symfony ist ein Controller-Klasse der Organisationsrahmen, in Minimal APIs kann man entweder alles in Program.cs schreiben oder Methoden auslagern.
Wann Minimal APIs, wann Controller?
| Kriterium | Minimal APIs | Controller |
|---|---|---|
| Boilerplate | Minimal | Mehr |
| Microservices | Ideal | Akzeptabel |
| Große APIs | Unübersichtlich | Besser strukturiert |
| Filtern/Policies | Manuell | Attribute-basiert |
| Konventionen | Wenige | Viele |
| Testing | Leicht | Leicht |
Minimal APIs eignen sich für: kleine Microservices, Function-as-a-Service-Szenarien, Prototypen, APIs mit wenigen Endpunkten.
Controller eignen sich für: große APIs mit vielen Endpunkten, wenn Konventionen und Attribute-basierte Policies wichtig sind.
Fazit
Minimal APIs sind nicht der Ersatz für klassische Controller, sondern eine Ergänzung. Sie reduzieren Boilerplate erheblich und machen kleine .NET-APIs zugänglicher — besonders für Entwickler, die aus anderen Sprachen kommen. Für PHP-Entwickler, die C# kennenlernen möchten, sind Minimal APIs ein hervorragender Einstiegspunkt.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.