الحد الأدنى من واجهات برمجة التطبيقات في .NET: إنشاء واجهات برمجة تطبيقات HTTP خفيفة الوزن
إذا كنت تقوم ببناء واجهات برمجة التطبيقات باستخدام ASP.NET Core لفترة من الوقت، فمن المحتمل أنك على دراية كبيرة بالنهج القائم على وحدة التحكم: قم بإنشاء فئة وحدة تحكم، وقم بتزيينها بالسمات، وإدخال خدماتك من خلال المنشئ، وربط مساراتك. إنه يعمل بشكل جيد - ولكن في بعض الأحيان يبدو الأمر وكأنك تكتب الكثير من الاحتفالات لما يرقى إلى “قبول هذا الطلب، القيام بشيء ما، الرد على الرد”.
هذه هي بالضبط المشكلة التي تم تصميم Minimal APIs لحلها. تم تقديم الحد الأدنى من واجهات برمجة التطبيقات (APIs) في .NET 6، مما يتيح لك تحديد نقاط نهاية HTTP مع القليل جدًا من القواعد النموذجية. لا توجد وحدات تحكم، ولا سمات، ولا تلاعب بفئة بدء التشغيل - ما عليك سوى توجيه تعيينات المسار إلى المعالج بأسلوب نظيف وعملي.
في هذا المنشور، أريد أن أطلعك على كل ما تحتاج إلى معرفته حول Minimal APIs: بدءًا من نقطة النهاية الأولى وحتى تنظيم التطبيقات الكبيرة، والتعامل مع المصادقة، والتحقق من الصحة، ومستندات OpenAPI، واعتبارات الأداء. دعونا نصل الى ذلك.
لماذا الحد الأدنى من واجهات برمجة التطبيقات؟
قبل أن نبدأ، دعونا نتحدث عن لماذا تختار الحد الأدنى من واجهات برمجة التطبيقات بدلاً من النهج التقليدي القائم على وحدة التحكم.
وحدات التحكم رائعة عندما تحتاج إلى إطار عمل منظم ومحدد. إنها توفر لك ربط النماذج، والمرشحات، والتفاوض على المحتوى، والفصل الواضح بين الاهتمامات خارج الصندوق. بالنسبة لتطبيقات المؤسسات الكبيرة التي تضم العشرات من المطورين، يمكن أن يكون الاتساق الذي تفرضه وحدات التحكم مفيدًا حقًا.
الحد الأدنى من واجهات برمجة التطبيقات، من ناحية أخرى، يمكنك التألق عندما تريد:
- نموذج معياري أقل — لا توجد فئات لوحدات التحكم، ولا توجد سمات
[ApiController]، ولا يوجد تكوين منفصل لبدء التشغيل. - بدء تشغيل أسرع — عدد أقل من العمليات المستندة إلى الانعكاس في وقت التمهيد.
- نموذج عقلي أبسط — يتم تعيين المسار مباشرةً إلى المعالج. هذا كل شيء.
- متوافق مع الخدمات الصغيرة — عندما تحتوي واجهة برمجة التطبيقات لديك على 5 إلى 10 نقاط نهاية، قد يبدو إعداد وحدة التحكم الكاملة أمرًا مبالغًا فيه.
والخبر السار هو أن هذا ليس قرارًا إما أو. يمكنك مزج وحدات التحكم والحد الأدنى من واجهات برمجة التطبيقات في نفس المشروع. ولكن بمجرد أن تعتاد على الحد الأدنى من النهج، قد تجد نفسك تلجأ إليه في كثير من الأحيان أكثر مما تتوقع.
البدء
لنقم بإنشاء Minimal API من البداية. إذا كان لديك .NET SDK مثبتًا، فالأمر بسيط مثل:
dotnet new web -n MyMinimalApi
cd MyMinimalApi
يمنحك هذا Program.cs الذي يبدو كما يلي:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
هذا كل شيء. هذه واجهة برمجة تطبيقات عاملة. لا Startup.cs، ولا فئة وحدة التحكم، ولا تكوين التوجيه. تقوم بتشغيل dotnet run، ثم تضغط على http://localhost:5000، وستحصل على “Hello World!” خلف. البساطة هنا هي بيت القصيد.
لنجعلها أكثر فائدة قليلًا باستخدام واجهة برمجة تطبيقات المهام الكلاسيكية:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var todos = new List<Todo>();
app.MapGet("/todos", () => todos);
app.MapGet("/todos/{id:int}", (int id) =>
todos.FirstOrDefault(t => t.Id == id) is { } todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todos", (Todo todo) =>
{
todos.Add(todo);
return Results.Created($"/todos/{todo.Id}", todo);
});
app.MapDelete("/todos/{id:int}", (int id) =>
{
var todo = todos.FirstOrDefault(t => t.Id == id);
if (todo is null) return Results.NotFound();
todos.Remove(todo);
return Results.NoContent();
});
app.Run();
record Todo(int Id, string Title, bool IsComplete);
لدينا CRUD كامل في أقل من 30 سطرًا. هذه هي قوة النهج البسيط.
معالجات الطريق
معالجات المسار هي الوظائف التي يتم تنفيذها عندما يتطابق الطلب مع المسار. لديك عدة خيارات لتحديدها.
تعبيرات لامدا
النهج الأكثر شيوعًا، وما ستراه في معظم الأمثلة:
app.MapGet("/hello", () => "Hello!");
app.MapGet("/hello/{name}", (string name) => $"Hello, {name}!");
مجموعات الطريقة
للحصول على منطق أكثر تعقيدًا، يمكنك الإشارة إلى طريقة مُسمّاة:
app.MapGet("/products", GetProducts);
app.MapPost("/products", CreateProduct);
static IResult GetProducts(AppDbContext db)
=> Results.Ok(db.Products.ToList());
static IResult CreateProduct(Product product, AppDbContext db)
{
db.Products.Add(product);
db.SaveChanges();
return Results.Created($"/products/{product.Id}", product);
}
```هذا هو النهج المفضل لدي لأي شيء يتجاوز الخطوط الفردية. فهو يحافظ على قسم تعيين المسار الخاص بك نظيفًا وقابلاً للقراءة - يمكنك رؤية نقاط النهاية الموجودة بنظرة سريعة دون الخوض في تفاصيل التنفيذ.
### الوظائف المحلية
يمكنك أيضًا استخدام الدوال المحلية، وهو أمر مفيد عندما تريد إبقاء المعالجات قريبة من تعريفات المسار الخاصة بها:
```csharp
app.MapGet("/health", CheckHealth);
IResult CheckHealth()
{
// Some health check logic
return Results.Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow });
}
ربط المعلمة
أحد الأشياء التي أقدرها حقًا بشأن Minimal APIs هو مدى سهولة ربط المعلمات. يحدد إطار العمل مكان سحب القيم بناءً على السياق والنوع.
معلمات الطريق
app.MapGet("/users/{id:int}", (int id) => $"User {id}");
app.MapGet("/files/{*path}", (string path) => $"File: {path}"); // catch-all
معلمات سلسلة الاستعلام
app.MapGet("/search", (string? query, int page = 1, int pageSize = 20) =>
Results.Ok(new { query, page, pageSize }));
تصبح الأنواع الخالية معلمات اختيارية. تعمل القيم الافتراضية تمامًا كما تتوقع.
نص الطلب
app.MapPost("/orders", (Order order) =>
{
// 'order' is automatically deserialized from the JSON body
return Results.Created($"/orders/{order.Id}", order);
});
ربط الرأس والخدمة
app.MapGet("/protected", (
[FromHeader(Name = "X-Api-Key")] string apiKey,
[FromServices] ILogger<Program> logger) =>
{
logger.LogInformation("Request with API key: {Key}", apiKey[..4] + "****");
return Results.Ok("Authorized");
});
HttpContext وHttpRequest
عندما تحتاج إلى الوصول إلى المستوى الأدنى:
app.MapGet("/info", (HttpContext context) =>
{
var userAgent = context.Request.Headers.UserAgent.ToString();
var ip = context.Connection.RemoteIpAddress?.ToString();
return Results.Ok(new { userAgent, ip });
});
ربط مخصص باستخدام BindAsync
بالنسبة للأنواع المعقدة، يمكنك تنفيذ طريقة ثابتة BindAsync:
public record PaginationParams(int Page, int PageSize)
{
public static ValueTask<PaginationParams?> BindAsync(HttpContext context)
{
int.TryParse(context.Request.Query["page"], out var page);
int.TryParse(context.Request.Query["pageSize"], out var pageSize);
var result = new PaginationParams(
Page: page > 0 ? page : 1,
PageSize: pageSize > 0 ? Math.Min(pageSize, 100) : 20
);
return ValueTask.FromResult<PaginationParams?>(result);
}
}
app.MapGet("/items", (PaginationParams pagination, AppDbContext db) =>
{
var items = db.Items
.Skip((pagination.Page - 1) * pagination.PageSize)
.Take(pagination.PageSize)
.ToList();
return Results.Ok(items);
});
هذا قوي بشكل لا يصدق. يمكنك تحديد منطق الربط مرة واحدة وإعادة استخدامه عبر جميع نقاط النهاية الخاصة بك.
التحقق من الصحة
أحد المجالات التي لا يمنحك فيها الحد الأدنى من واجهات برمجة التطبيقات (APIs) قدرًا كبيرًا من خارج الصندوق مقارنةً بوحدات التحكم هو التحقق من صحة النموذج. لا يوجد تنفيذ تلقائي لـ [Required] أو [StringLength]. ولكن هناك أنماط نظيفة للتعامل معها.
التحقق اليدوي
أبسط طريقة — فقط قم بالتحقق من صحة المعالج:
app.MapPost("/users", (CreateUserRequest request) =>
{
if (string.IsNullOrWhiteSpace(request.Email))
return Results.BadRequest(new { Error = "Email is required" });
if (request.Name?.Length > 100)
return Results.BadRequest(new { Error = "Name must be 100 characters or less" });
// Create user...
return Results.Created($"/users/{request.Email}", request);
});
استخدام مكتبة التحقق من الصحة
بالنسبة لأي شيء غير تافه، أنصحك بالوصول إلى FluentValidation أو مكتبة مشابهة:
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserValidator()
{
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Age).InclusiveBetween(0, 150);
}
}
يمكنك توصيل هذا إلى نقاط النهاية الخاصة بك من خلال مرشحات نقطة النهاية، وهو ما يقودنا إلى القسم التالي.
مرشحات نقطة النهاية
تعد مرشحات نقطة النهاية واحدة من أفضل ميزات Minimal APIs. فكر فيها كبرامج وسيطة، ولكنها تستهدف نقاط نهاية محددة بدلاً من خط الأنابيب بأكمله. لقد تم تقديمها في .NET 7 وهي رائعة للاهتمامات الشاملة.
الفلتر الأساسي
app.MapPost("/todos", (Todo todo) =>
{
// Handle the request
return Results.Created($"/todos/{todo.Id}", todo);
})
.AddEndpointFilter(async (context, next) =>
{
var todo = context.GetArgument<Todo>(0);
if (string.IsNullOrEmpty(todo.Title))
{
return Results.BadRequest(new { Error = "Title is required" });
}
return await next(context);
});
مرشح التحقق من الصحة باستخدام FluentValidation
هنا يصبح الأمر قويًا حقًا - مرشح التحقق من الصحة القابل لإعادة الاستخدام:
public class ValidationFilter<T> : IEndpointFilter where T : class
{
private readonly IValidator<T> _validator;
public ValidationFilter(IValidator<T> validator)
{
_validator = validator;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var model = context.Arguments
.OfType<T>()
.FirstOrDefault();
if (model is null)
return Results.BadRequest(new { Error = "Request body is required" });
var result = await _validator.ValidateAsync(model);
if (!result.IsValid)
{
var errors = result.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
return Results.ValidationProblem(errors);
}
return await next(context);
}
}
// Usage
app.MapPost("/users", (CreateUserRequest request) =>
{
// Only reached if validation passes
return Results.Created($"/users/{request.Email}", request);
})
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>();
مرشح التسجيل
app.MapGet("/products", (AppDbContext db) => db.Products.ToList())
.AddEndpointFilter(async (context, next) =>
{
var logger = context.HttpContext
.RequestServices.GetRequiredService<ILogger<Program>>();
var sw = Stopwatch.StartNew();
var result = await next(context);
sw.Stop();
logger.LogInformation("Endpoint executed in {Elapsed}ms", sw.ElapsedMilliseconds);
return result;
});
يمكنك ربط مرشحات متعددة، ويتم تنفيذها بالترتيب الذي تمت إضافتها به - تمامًا مثل البرامج الوسيطة.
تكامل OpenAPI/Swagger
لم تعد وثائق API الجيدة اختيارية بعد الآن. لحسن الحظ، تتمتع Minimal APIs بدعم من الدرجة الأولى لـ OpenAPI من خلال الحزمة Microsoft.AspNetCore.OpenApi.
الإعداد الأساسي
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapGet("/todos", () => new List<Todo>())
.WithName("GetTodos")
.WithDescription("Retrieves all todo items")
.WithTags("Todos");
app.Run();
بيانات تعريف نقطة النهاية الغنية
يمكنك تقديم معلومات مفصلة حول نقاط النهاية الخاصة بك:
app.MapPost("/todos", (Todo todo) =>
{
return Results.Created($"/todos/{todo.Id}", todo);
})
.WithName("CreateTodo")
.WithDescription("Creates a new todo item")
.WithTags("Todos")
.Accepts<Todo>("application/json")
.Produces<Todo>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.ProducesValidationProblem();
استخدام WithOpenApi للتخصيص
للتحكم الدقيق في مستند OpenAPI الذي تم إنشاؤه:
app.MapGet("/todos/{id:int}", (int id) => Results.Ok(new Todo(id, "Sample", false)))
.WithOpenApi(operation =>
{
operation.Summary = "Get a specific todo";
operation.Description = "Retrieves a single todo item by its unique identifier.";
operation.Parameters[0].Description = "The unique identifier of the todo item";
return operation;
});
يمنحك هذا نفس مستوى التحكم في التوثيق الذي تحصل عليه مع تعليقات Swashbuckle XML على وحدات التحكم، ولكن بطريقة أكثر وضوحًا، تعتمد على الكود أولاً.
المصادقة والترخيص
يتبع تأمين نقاط نهاية Minimal API نفس الأنماط مثل بقية ASP.NET Core - ما عليك سوى تطبيقها بشكل مختلف.
الإعداد الأساسي```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = “https://your-identity-provider.com”; options.Audience = “your-api”; });
builder.Services.AddAuthorizationBuilder() .AddPolicy(“AdminOnly”, policy => policy.RequireRole(“Admin”)) .AddPolicy(“PremiumUser”, policy => policy.RequireClaim(“subscription”, “premium”));
var app = builder.Build();
app.UseAuthentication(); app.UseAuthorization();
### تطبيق التفويض على نقاط النهاية
```csharp
// Require any authenticated user
app.MapGet("/profile", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Name = user.FindFirstValue(ClaimTypes.Name),
Email = user.FindFirstValue(ClaimTypes.Email)
});
}).RequireAuthorization();
// Require a specific policy
app.MapDelete("/admin/users/{id}", (int id) =>
{
// Delete user logic
return Results.NoContent();
}).RequireAuthorization("AdminOnly");
// Allow anonymous access
app.MapGet("/public/health", () => Results.Ok(new { Status = "Healthy" }))
.AllowAnonymous();
لاحظ كيف يمكنك إدخال ClaimsPrincipal مباشرة في معلمات المعالج الخاص بك - يعتني إطار العمل بالباقي. هذا هو أحد الأشياء الصغيرة التي تجعل Minimal APIs تبدو أنيقة حقًا.
تنظيم واجهات برمجة التطبيقات الكبيرة
إن أهم ما يميز الغرفة مع الحد الأدنى من واجهات برمجة التطبيقات (APIs) هو التنظيم. عندما يحتوي Program.cs على 50 نقطة نهاية، يصبح الأمر في حالة من الفوضى. فيما يلي الأنماط التي أستخدمها لإبقاء الأمور قابلة للإدارة.
مجموعات الطريق
تتيح لك مجموعات المسار (المقدمة في .NET 7) مشاركة التكوين عبر نقاط النهاية ذات الصلة:
var todos = app.MapGroup("/todos")
.WithTags("Todos")
.RequireAuthorization();
todos.MapGet("/", (AppDbContext db) => db.Todos.ToListAsync());
todos.MapGet("/{id:int}", (int id, AppDbContext db) => db.Todos.FindAsync(id));
todos.MapPost("/", (Todo todo, AppDbContext db) =>
{
db.Todos.Add(todo);
db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
});
تشترك جميع نقاط النهاية في المجموعة في البادئة /todos والعلامة Todos ومتطلبات الترخيص. ينظف.
طرق التمديد
هذا هو النمط الذي يقاس حقًا. انقل كل مجموعة من نقاط النهاية إلى فئتها الثابتة الخاصة بها:
// Endpoints/TodoEndpoints.cs
public static class TodoEndpoints
{
public static RouteGroupBuilder MapTodoEndpoints(this WebApplication app)
{
var group = app.MapGroup("/todos").WithTags("Todos");
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id:int}", Update);
group.MapDelete("/{id:int}", Delete);
return group;
}
private static async Task<IResult> GetAll(AppDbContext db)
=> Results.Ok(await db.Todos.ToListAsync());
private static async Task<IResult> GetById(int id, AppDbContext db)
=> await db.Todos.FindAsync(id) is { } todo
? Results.Ok(todo)
: Results.NotFound();
private static async Task<IResult> Create(Todo todo, AppDbContext db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
}
private static async Task<IResult> Update(int id, Todo updated, AppDbContext db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Title = updated.Title;
todo.IsComplete = updated.IsComplete;
await db.SaveChangesAsync();
return Results.Ok(todo);
}
private static async Task<IResult> Delete(int id, AppDbContext db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
}
// Program.cs — stays clean
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapTodoEndpoints();
app.MapUserEndpoints();
app.MapOrderEndpoints();
app.Run();
يصبح Program.cs الخاص بك جدول محتويات لواجهة برمجة التطبيقات (API) الخاصة بك. تعيش كل مجموعة نقاط نهاية في ملف خاص بها. هذا هو النهج الذي أوصي به لتطبيقات الإنتاج.
مكتبة كارتر
إذا كنت تريد المزيد من البنية، فإن مكتبة كارتر توفر منهجًا يعتمد على الوحدة:
public class TodoModule : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/todos", async (AppDbContext db) =>
await db.Todos.ToListAsync());
app.MapPost("/todos", async (Todo todo, AppDbContext db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
});
}
}
يكتشف كارتر جميع الوحدات ويسجلها تلقائيًا. إنها حل وسط لطيف بين نهج Minimal API الخام ووحدات التحكم الكاملة.
النتائج المكتوبة وأنواع الاستجابات
بدءًا من .NET 7، يمكنك استخدام TypedResults بدلاً من Results للاستجابات الآمنة. قد يبدو هذا تغييرًا بسيطًا، لكن له فوائد حقيقية لتوثيق OpenAPI وقابلية الاختبار.
app.MapGet("/todos/{id:int}", async Task<Results<Ok<Todo>, NotFound>> (int id, AppDbContext db) =>
{
var todo = await db.Todos.FindAsync(id);
return todo is not null
? TypedResults.Ok(todo)
: TypedResults.NotFound();
});
يخبر نوع الإرجاع Results<Ok<Todo>, NotFound> بوضوح إطار العمل (ومستندات OpenAPI الخاصة بك) بأنواع الاستجابة التي يمكن أن تنتجها نقطة النهاية هذه بالضبط. لا مزيد من التخمين، ولا مزيد من الاستدعاءات اليدوية Produces<>() للحالات الأساسية.
للحصول على نتائج محتملة متعددة:
app.MapPost("/todos",
async Task<Results<Created<Todo>, ValidationProblem, Conflict>>
(Todo todo, AppDbContext db) =>
{
if (string.IsNullOrEmpty(todo.Title))
return TypedResults.ValidationProblem(
new Dictionary<string, string[]>
{
{ "Title", new[] { "Title is required" } }
});
if (await db.Todos.AnyAsync(t => t.Title == todo.Title))
return TypedResults.Conflict();
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todos/{todo.Id}", todo);
});
لقد بدأت باستخدام TypedResults في جميع المشاريع الجديدة. يلتقط المترجم عدم التطابق بين أنواع الإرجاع المعلنة وما تقوم بإرجاعه بالفعل، مما يلغي فئة كاملة من مفاجآت وقت التشغيل.
اعتبارات الأداء
يعد الأداء إحدى نقاط البيع لواجهات برمجة التطبيقات البسيطة (Minimal APIs)، ومن الجدير أن نفهم لماذا تكون أسرع.
خفض تكاليف بدء التشغيل. تعتمد وحدات التحكم بشكل كبير على الانعكاس لاكتشاف نقاط النهاية وربط النماذج وتطبيق عوامل التصفية. يستخدم الحد الأدنى من واجهات برمجة التطبيقات مولدات المصدر (تبدأ في .NET 7) لإنشاء تعليمات برمجية ملزمة في وقت الترجمة. وهذا يعني عملاً أقل عند بدء التشغيل وتخصيصًا أقل للذاكرة لكل طلب.
لا يوجد خط أنابيب MVC. تمر واجهات برمجة التطبيقات المستندة إلى وحدة التحكم عبر خط أنابيب MVC الكامل: اختيار الإجراء، وربط النموذج، ومرشحات الإجراء، وتنفيذ النتائج. تتخطى واجهات برمجة التطبيقات البسيطة كل ذلك وتنتقل مباشرة من التوجيه إلى المعالج الخاص بك.
تجميع RequestDelegate. يقوم إطار العمل بتجميع تعبيرات lambda الخاصة بك في مثيلات RequestDelegate المحسنة. الكود الناتج قريب جدًا مما كنت ستكتبه يدويًا إذا كنت تعمل مباشرة مع HttpContext.
فيما يلي بعض النصائح العملية لتحقيق أقصى قدر من الأداء:
// Use AsNoTracking for read-only queries
app.MapGet("/products", async (AppDbContext db) =>
await db.Products.AsNoTracking().ToListAsync());
// Return results directly — avoid unnecessary allocations
app.MapGet("/count", async (AppDbContext db) =>
await db.Products.CountAsync());
// Use cancellation tokens for long-running operations
app.MapGet("/report", async (AppDbContext db, CancellationToken ct) =>
await db.Orders
.AsNoTracking()
.GroupBy(o => o.Status)
.Select(g => new { Status = g.Key, Count = g.Count() })
.ToListAsync(ct));
```ومن الجدير بالذكر أيضًا أن فجوة الأداء بين وحدات التحكم وواجهات برمجة التطبيقات البسيطة تستمر في التضييق مع كل إصدار .NET. بالنسبة لمعظم التطبيقات، لن يكون الاختلاف هو عنق الزجاجة لديك، بل سيكون ذلك من خلال استعلامات قاعدة البيانات واستدعاءات الخدمة الخارجية. اختر بناءً على خبرة المطور واحتياجات المشروع، وليس المعايير.
## الخلاصة
لقد قطع الحد الأدنى من واجهات برمجة التطبيقات (APIs) شوطًا طويلًا منذ تقديمها في .NET 6. وما بدأ كميزة تجريبية "hello World" قد تطور ليصبح خيارًا مشروعًا لواجهات برمجة تطبيقات الإنتاج. باستخدام عوامل تصفية نقطة النهاية ومجموعات المسارات والنتائج المكتوبة ودعم OpenAPI القوي، لديك كل ما تحتاجه لإنشاء خدمات جيدة التنظيم وقابلة للصيانة.
توصيتي؟ إذا كنت تبدأ مشروعًا جديدًا لواجهة برمجة التطبيقات (API) - خاصةً خدمة صغيرة أو واجهة برمجة تطبيقات داخلية مركزة - فجرّب Minimal APIs بجدية. استخدم نمط أسلوب الامتداد للتنظيم، واعتمد على مرشحات نقطة النهاية للاهتمامات الشاملة، واستفد من `TypedResults` لسلامة النوع.
بالنسبة للمشاريع الحالية القائمة على وحدات التحكم، ليس هناك عجلة من أمرها للترحيل. كلا الطريقتين تعملان بشكل جيد، ويمكنك حتى استخدامهما جنبًا إلى جنب. ولكن في المرة القادمة التي تحتاج فيها إلى إضافة خدمة صغيرة أو واجهة برمجة تطبيقات داخلية سريعة، تخطي وحدات التحكم واتجه إلى الحد الأدنى. قد لا تعود.
ترميز سعيد!