مقالات › ASP.NET Core
Minimal API یا Controller در ASP.NET Core؛ کدام را انتخاب کنیم؟
مقایسهی عملی Minimal API و کنترلرهای MVC در ASP.NET Core: نحو، سازماندهی با Route Group، اعتبارسنجی، فیلترها، OpenAPI، تست و کارایی، همراه با راهنمای روشن برای انتخاب مناسب پروژهی شما.
از .NET 6 به بعد، ASP.NET Core دو روش برای ساخت HTTP API ارائه میدهد: کنترلرهای کلاسیک (MVC) و Minimal API. هر دو بهطور کامل پشتیبانی میشوند، روی همان سیستم مسیریابی و میانافزار اجرا میشوند و میتوانند در یک پروژه کنار هم باشند. سؤال این نیست که «کدام بهتر است»، بلکه این است که «کدام برای این پروژه و این تیم مناسبتر است». این مقاله آنها را کنار هم مقایسه میکند.
یک Endpoint با دو سبک
کنترلر:
[ApiController]
[Route("api/[controller]")]
public class ProductsController(IProductService products) : ControllerBase
{
[HttpGet("{id:int}")]
[ProducesResponseType<ProductDto>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProductDto>> Get(int id, CancellationToken ct)
{
var product = await products.GetAsync(id, ct);
return product is null ? NotFound() : Ok(product);
}
[HttpPost]
public async Task<ActionResult<ProductDto>> Create(CreateProductRequest request, CancellationToken ct)
{
var created = await products.CreateAsync(request, ct);
return CreatedAtAction(nameof(Get), new { id = created.Id }, created);
}
}
builder.Services.AddControllers();
app.MapControllers();
Minimal API:
app.MapGet("/api/products/{id:int}", async Task<Results<Ok<ProductDto>, NotFound>> (
int id, IProductService products, CancellationToken ct) =>
{
var product = await products.GetAsync(id, ct);
return product is null ? TypedResults.NotFound() : TypedResults.Ok(product);
});
app.MapPost("/api/products", async (CreateProductRequest request, IProductService products, CancellationToken ct) =>
{
var created = await products.CreateAsync(request, ct);
return TypedResults.Created($"/api/products/{created.Id}", created);
});
در Minimal API سرویسها مستقیماً به پارامترهای Handler تزریق میشوند و منبع Binding خودکار تشخیص داده میشود: مقادیر مسیر، Query String، بدنه (برای نوعهای پیچیده) و سرویسهای DI.
سازماندهی Minimal API
انتقاد رایج — «همهچیز در Program.cs جمع میشود» — با Route Group و متدهای Extension حل میشود:
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products")
.WithTags("Products")
.RequireAuthorization();
group.MapGet("/", GetAll).AllowAnonymous();
group.MapGet("/{id:int}", GetById).WithName("GetProduct");
group.MapPost("/", Create).RequireAuthorization("Admin");
group.MapDelete("/{id:int}", Delete).RequireAuthorization("Admin");
return group;
}
private static async Task<Ok<List<ProductDto>>> GetAll(IProductService products, CancellationToken ct) =>
TypedResults.Ok(await products.GetAllAsync(ct));
private static async Task<Results<Ok<ProductDto>, NotFound>> GetById(int id, IProductService products, CancellationToken ct) =>
await products.GetAsync(id, ct) is { } p ? TypedResults.Ok(p) : TypedResults.NotFound();
private static async Task<Created<ProductDto>> Create(CreateProductRequest request, IProductService products, CancellationToken ct)
{
var created = await products.CreateAsync(request, ct);
return TypedResults.Created($"/api/products/{created.Id}", created);
}
private static async Task<NoContent> Delete(int id, IProductService products, CancellationToken ct)
{
await products.DeleteAsync(id, ct);
return TypedResults.NoContent();
}
}
// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();
گروهها پیشوند مسیر، Tag، Authorization، Rate Limiting، CORS و فیلترها را به اشتراک میگذارند؛ مشابه Attributeهای سطح کنترلر. بسیاری از تیمها کد را بر اساس ویژگی سازمان میدهند (Features/Products/ProductEndpoints.cs) که با معماری Vertical Slice هماهنگ است.
مقایسهی امکانات
| امکان | Controller | Minimal API |
|---|---|---|
| مسیریابی، احراز هویت، CORS، Rate Limiting | ✅ با Attribute | ✅ با متدهای Fluent و گروه |
| اعتبارسنجی خودکار مدل (DataAnnotations) | ✅ [ApiController] خودکار 400 برمیگرداند |
✅ داخلی از .NET 10 (AddValidation())؛ قبل از آن با فیلتر یا FluentValidation |
| فیلترها | Action/Resource/Exception Filter | Endpoint Filter (AddEndpointFilter) |
| سفارشیسازی Model Binding | غنی (Model Binder، [FromForm] و...) |
قراردادهای BindAsync/TryParse و [AsParameters] |
| OpenAPI | ✅ | ✅ (TypedResults نوع پاسخ را خودکار مشخص میکند) |
| Native AOT | ❌ | ✅ |
| View و Razor | ✅ | ❌ (فقط API) |
| تشریفات کد | بیشتر (کلاس، Attribute) | کمتر |
اعتبارسنجی
با [ApiController]، DataAnnotationهای نامعتبر خودکار پاسخ 400 از نوع ValidationProblemDetails برمیگردانند. برای Minimal API در .NET 10 به بعد:
builder.Services.AddValidation();
public record CreateProductRequest(
[Required, MaxLength(120)] string Name,
[Range(0, 1_000_000_000)] decimal Price);
در نسخههای قبلی (یا برای قوانین پیچیده) از یک Endpoint Filter همراه FluentValidation استفاده کنید:
public class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (argument is null) return TypedResults.BadRequest();
var result = await validator.ValidateAsync(argument);
return result.IsValid
? await next(context)
: TypedResults.ValidationProblem(result.ToDictionary());
}
}
group.MapPost("/", Create).AddEndpointFilter<ValidationFilter<CreateProductRequest>>();
TypedResults و OpenAPI
برگرداندن TypedResults همراه یک Union از نوع Results<...> پاسخهای ممکن را بخشی از امضای متد میکند. تولیدکنندهی OpenAPI آنها را میخواند و مستندات بدون Attributeهای ProducesResponseType دقیق میماند؛ Unit Testها هم میتوانند نوع دقیق نتیجه را بررسی کنند:
var result = await ProductEndpoints.GetById(99, fakeService, default);
Assert.IsType<NotFound>(result.Result);
کارایی
Minimal API سربار کمتری دارد (بدون ساخت کنترلر و بدون Pipeline فیلترهای Action، مگر اینکه فیلتر اضافه کنید) و از Native AOT برای شروع سریعتر و مصرف حافظهی کمتر پشتیبانی میکند. در بنچمارکها بهطور محسوسی سریعتر است؛ اما در برنامههای معمولی که درخواستها به دیتابیس میرسند، این تفاوت در مقایسه با زمان کوئری ناچیز است. بر اساس ساختار و ترجیح تیم انتخاب کنید، نه میکروبنچمارک؛ مگر سرویسهای پرترافیک یا Serverless بسازید که زمان شروع در آنها مهم است.
چه زمانی Controller را انتخاب کنیم
- کدبیسهای بزرگ موجود که روی MVC ساخته شدهاند.
- تیمهایی که به قراردادها و Attributeهای MVC عادت دارند.
- به امکانات خاص MVC نیاز دارید: Model Binding سفارشی پیچیده، زیرساخت مبتنی بر
IActionFilter، Content Negotiation با Formatterها یا Razor View در همان برنامه. - Endpointهای زیادی که رفتار مشترک فراوانی دارند و تیم ترجیح میدهد آن را با Attribute بیان کند.
چه زمانی Minimal API را انتخاب کنیم
- APIها و میکروسرویسهای جدید.
- معماری Vertical Slice یا پوشهبندی بر اساس ویژگی.
- سرویسهای کوچک، Webhookها، BFF (Backend for Frontend)، Serverless یا Native AOT.
- تیمهایی که کد صریح و کمجادو را ترجیح میدهند.
ترکیب هر دو
هیچ چیز مانع استفادهی همزمان از هر دو در یک برنامه نیست:
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers(); // API مدیریت موجود
app.MapGroup("/api/v2").MapProductEndpoints(); // Endpointهای جدید
app.MapPost("/webhooks/payment", HandlePaymentWebhook);
این کار مهاجرت تدریجی را بیدردسر میکند.
جمعبندی
کنترلرها و Minimal API پایهی یکسانی دارند: مسیریابی، میانافزار، DI، احراز هویت و OpenAPI. کنترلرها قراردادهای آشنا و غنیترین امکانات MVC را ارائه میدهند؛ Minimal API تشریفات کمتر، نتایج تایپشده، Endpoint Filter، Route Group و Native AOT. Minimal API را با Route Group و فایلهای مبتنی بر ویژگی سازمان دهید، ورودی را با اعتبارسنجی داخلی یا فیلترها بررسی کنید و در دوران مهاجرت بدون نگرانی هر دو سبک را ترکیب کنید.
پرسشهای متداول
آیا Minimal API فقط برای پروژههای کوچک است؟
خیر. با Route Group و سازماندهی بر اساس ویژگی، برای APIهای بزرگ هم مقیاسپذیر است. «Minimal» به تشریفات کد اشاره دارد، نه توانایی.
آیا کنترلرها منسوخ خواهند شد؟
نشانهای از این موضوع نیست. هر دو درجهیک هستند و همچنان بهبود میگیرند.
آیا میتوان MediatR یا Clean Architecture را با Minimal API به کار برد؟
بله. Endpointها به آداپتورهای نازکی تبدیل میشوند که یک Command یا Query میفرستند؛ دقیقاً مثل کنترلرهای نازک.