ASP.NET Core Minimal API, .NET 6 ile tanıtıldı ve her sürümde controller tabanlı yaklaşıma daha iyi bir alternatif haline geldi. .NET 8 ile birlikte artık büyük ölçekli uygulamalar için de tercih edilebilir olgunluğa ulaştı.
Neden Minimal API?
- Daha az overhead: Controller pipeline'ından kaçındığı için request başına maliyet düşük
- Daha az kod: Attribute, base class, action naming kuralı yok
- Açık bağımlılıklar: Her endpoint'in neye ihtiyaç duyduğu imzadan okunabilir
- Test kolaylığı: Lambda olduğu için unit test çok daha basit
Proje Kurulumu
var builder = WebApplication.CreateBuilder(args);
// Servisler
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddValidatorsFromAssemblyContaining<Program>(); // FluentValidation
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "Product API", Version = "v1" });
c.EnableAnnotations();
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Endpoint'leri grupla
app.MapProductEndpoints();
app.Run();
MapGroup ile Modüler Endpoint Tanımı
// ProductEndpoints.cs — extension method ile gruplanmış endpoint'ler
public static class ProductEndpoints
{
public static void MapProductEndpoints(this WebApplication app)
{
var group = app
.MapGroup("/api/v1/products")
.WithTags("Products")
.WithOpenApi()
.AddEndpointFilter<ValidationFilter>();
group.MapGet("/", GetAll) .WithName("GetAllProducts");
group.MapGet("/{id:int}", GetById) .WithName("GetProductById");
group.MapPost("/", Create) .WithName("CreateProduct");
group.MapPut("/{id:int}", Update) .WithName("UpdateProduct");
group.MapDelete("/{id:int}", Delete) .WithName("DeleteProduct");
}
static async Task<IResult> GetAll(
IProductService svc,
[AsParameters] ProductQuery query) // ?page=1&size=20&category=electronics
{
var result = await svc.GetAllAsync(query);
return Results.Ok(result);
}
static async Task<IResult> GetById(int id, IProductService svc)
{
var product = await svc.GetByIdAsync(id);
return product is not null
? Results.Ok(product)
: Results.NotFound(new { Error = $"Ürün bulunamadı: {id}" });
}
static async Task<IResult> Create(
CreateProductRequest request,
IProductService svc,
LinkGenerator links,
HttpContext ctx)
{
var product = await svc.CreateAsync(request);
var uri = links.GetUriByName(ctx, "GetProductById", new { id = product.Id });
return Results.Created(uri, product);
}
static async Task<IResult> Update(int id, UpdateProductRequest request, IProductService svc)
{
var updated = await svc.UpdateAsync(id, request);
return updated ? Results.NoContent() : Results.NotFound();
}
static async Task<IResult> Delete(int id, IProductService svc)
{
var deleted = await svc.DeleteAsync(id);
return deleted ? Results.NoContent() : Results.NotFound();
}
}
Endpoint Filter ile Merkezi Validation
Her endpoint'e ayrı validation kodu yazmak yerine, filter ile bunu merkezi hale getirebilirsiniz:
// Tüm gruba uygulanan validation filter
public class ValidationFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// FluentValidation: request body'deki ilk IValidator<T>'yi bul ve çalıştır
foreach (var arg in context.Arguments)
{
if (arg is null) continue;
var validatorType = typeof(IValidator<>).MakeGenericType(arg.GetType());
var validator = context.HttpContext.RequestServices.GetService(validatorType) as IValidator;
if (validator is null) continue;
var ctx = new ValidationContext<object>(arg);
var result = await validator.ValidateAsync(ctx);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
}
return await next(context);
}
}
// Validator tanımı
public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest>
{
public CreateProductRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Ürün adı zorunludur")
.MaximumLength(100).WithMessage("Ürün adı 100 karakterden uzun olamaz");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Fiyat sıfırdan büyük olmalıdır");
RuleFor(x => x.Stock)
.GreaterThanOrEqualTo(0).WithMessage("Stok negatif olamaz");
}
}
Global Hata Yönetimi
// Problem Details (RFC 7807) uyumlu hata yanıtı
app.UseExceptionHandler(errApp =>
{
errApp.Run(async ctx =>
{
var feature = ctx.Features.Get<IExceptionHandlerFeature>();
var ex = feature?.Error;
ctx.Response.ContentType = "application/problem+json";
ctx.Response.StatusCode = ex switch
{
NotFoundException => 404,
ValidationException => 400,
UnauthorizedException => 401,
_ => 500
};
await ctx.Response.WriteAsJsonAsync(new
{
type = $"https://httpstatuses.io/{ctx.Response.StatusCode}",
title = ex?.Message ?? "Beklenmedik bir hata oluştu",
status = ctx.Response.StatusCode,
traceId = ctx.TraceIdentifier
});
});
});
AsParameters ile Query String Binding
// [AsParameters] sayfa/filtre parametrelerini tek record'a toplar
public record ProductQuery(
[property: FromQuery] int Page = 1,
[property: FromQuery] int Size = 20,
[property: FromQuery] string? Category = null,
[property: FromQuery] string? Search = null,
[property: FromQuery] string OrderBy = "name");
// Endpoint imzası temiz kalır
static async Task<IResult> GetAll(IProductService svc, [AsParameters] ProductQuery query)
=> Results.Ok(await svc.GetAllAsync(query));
Test Edilebilirlik
// WebApplicationFactory ile integration test
public class ProductApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task CreateProduct_ValidRequest_Returns201()
{
var client = factory.CreateClient();
var request = new CreateProductRequest("Test Ürün", 99.99m, 10, "electronics");
var response = await client.PostAsJsonAsync("/api/v1/products", request);
response.StatusCode.Should().Be(HttpStatusCode.Created);
response.Headers.Location.Should().NotBeNull();
}
[Fact]
public async Task CreateProduct_InvalidPrice_Returns400()
{
var client = factory.CreateClient();
var request = new CreateProductRequest("Test", -1m, 0, "");
var response = await client.PostAsJsonAsync("/api/v1/products", request);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}
Minimal API, doğru yapılandırıldığında controller tabanlı yaklaşım kadar organize ve daha performanslı bir servis sunabilir. MapGroup, endpoint filter ve [AsParameters] kombinasyonu büyük projelerde bile kodu yönetilebilir tutar.