مدیریت جامع مهلت زمانی و لغو درخواستها در ASP.NET Core و Blazor
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۶/۱۰ ۰۹:۵۰
آدرس: www.dntips.ir
۱. چکیده: در برنامههای تحت وب مدرن، پردازش درخواستهایی که کلاینت پیش از دریافت پاسخ اتصال آنها را قطع کرده است، یا کوئریهای سنگینی که بیش از حد مجاز به طول میانجامند، هدررفت جدی منابع سرور (CPU، حافظه، اتصالات پایگاهداده و پهنای باند) را به همراه دارد. به صورت پیشفرض، فریمورک ASP.NET Core هیچ مهلت زمانی (Timeout) خودکاری روی درخواستهای ورودی اعمال نمیکند. این مقاله به بررسی جامع مکانیزم لغو همکارانه (Cooperative Cancellation) از طریق توکنهای لغو (CancellationToken)، قابلیت نوین میدلور مهلت زمانی درخواست (Request Timeout Middleware) معرفیشده در داتنت ۸، و نحوه انتشار بهینه این توکنها در تمام لایههای معماری (شامل کنترلرها، Minimal APIs، لایه سرویس، EF Core و مؤلفههای Blazor) میپردازد.HttpContext.RequestAborted: جهت آگاهی از قطعی ارتباط کلاینت.Request Timeout Middleware (در داتنت ۸ و نسخههای جدیدتر): جهت اعمال ضربالاجلهای مشخص برای پردازش درخواستها.Thread.Abort) نمیکند، بلکه صرفاً وضعیت لغو شدن را از طریق آبجکت CancellationToken اعلام مینماید. وظیفه توسعهدهنده این است که این توکن را به تمامی لایهها و عملگرهای ناهمگام (Asynchronous Operations) منتقل کند تا در صورت لغو، فرآیند متوقف شده و استثنای OperationCanceledException پرتاب شود.[کلاینت / میدلور Timeout]
│
▼ (سیگنال لغو)
HttpContext.RequestAborted
│
▼
[کنترلر یا Minimal API]
│
▼
[لایه سرویس/بیزینس]
│
▼
[EF Core / HttpClient Provider] ──► لغو اتصال و بازپسگیری منابعHttpContext.RequestAborted را لغو میکند و در صورتی که پاسخی پیش از این به کلاینت ارسال نشده باشد، کد خطای استاندارد 504 Gateway Timeout را بازمیگرداند.var builder = WebApplication.CreateBuilder(args);
// ثبت سرویسهای مورد نیاز میدلور
builder.Services.AddRequestTimeouts();
var app = builder.Build();
// قرار دادن میدلور در پایپلاین
app.UseRequestTimeouts();
app.MapGet("/reports", async (CancellationToken cancellationToken) =>
{
// شبیهسازی عملیاتی به مدت ۱۰ ثانیه
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
return Results.Ok("گزارش با موفقیت آماده شد.");
})
.WithRequestTimeout(TimeSpan.FromSeconds(3)); // تعیین مهلت ۳ ثانیه
app.Run();نکته مهم در تست و اشکالزدایی (Debugging): هنگام اجرای برنامه در حالت متصل به دیباگر (Attached Debugger)، میدلور جهت تسهیل عیبیابی Timeout را تریگر نمیکند. برای تست صحیح، برنامه را در حالت بدون دیباگر (Release یا Ctrl + F5) اجرا کنید.builder.Services.AddRequestTimeouts(options =>
{
// سیاست پیشفرض برای اندپوینتهای سبک
options.AddPolicy("ApiReadPolicy", TimeSpan.FromSeconds(3));
// سیاست ویژه پردازشهای تحلیلی
options.AddPolicy("ReportExportPolicy", TimeSpan.FromSeconds(30));
});
// تخصیص سیاستها به اندپوینتها
app.MapGet("/orders/{id:guid}", GetOrder)
.WithRequestTimeout("ApiReadPolicy");
app.MapGet("/reports/{id:guid}", ExportReport)
.WithRequestTimeout("ReportExportPolicy");
// غیرفعالسازی برای سناریوهای استریم نظیر SSE یا وبسوکت
app.MapGet("/events/stream", StreamEvents)
.DisableRequestTimeout();HttpContext دریافت کرد:[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
// ۱. تزریق خودکار از طریق ModelBinder
[HttpGet("forecast")]
public async Task<IActionResult> GetForecast(CancellationToken cancellationToken)
{
var data = await GetDataAsync(cancellationToken);
return Ok(data);
}
// ۲. دریافت مستقیم از شیء HttpContext
[HttpGet("direct")]
public async Task<IActionResult> GetDirect()
{
CancellationToken cancellationToken = HttpContext.RequestAborted;
var data = await GetDataAsync(cancellationToken);
return Ok(data);
}
private async Task<IReadOnlyCollection<WeatherForecast>> GetDataAsync(CancellationToken cancellationToken)
{
await Task.Delay(2000, cancellationToken);
return Array.Empty<WeatherForecast>();
}
}HttpContext.RequestAborted یا ارسال به متدهای Handler مقدور است:@page
@model IndexModel
<div>
<p>وضعیت پردازش: @Model.Result</p>
</div>
@functions {
public string Result { get; set; } = string.Empty;
public async Task OnGetAsync(CancellationToken cancellationToken)
{
Result = await FetchExternalDataAsync(cancellationToken);
}
private async Task<string> FetchExternalDataAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
return "تکمیل شد";
}
}public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _dbContext;
private readonly HttpClient _httpClient;
public OrderRepository(ApplicationDbContext dbContext, HttpClient httpClient)
{
_dbContext = dbContext;
_httpClient = httpClient;
}
public async Task<Order?> GetOrderWithDetailsAsync(Guid id, CancellationToken cancellationToken)
{
// لغو اجرای کوئری در پایگاهداده
var order = await _dbContext.Orders
.AsNoTracking()
.SingleOrDefaultAsync(o => o.Id == id, cancellationToken);
if (order is null) return null;
// لغو فراخوانی خارجی
var response = await _httpClient.GetAsync($"https://api.shipping.internal/status/{id}", cancellationToken);
return order;
}
}CancellationTokenSource را همگام با چرخه حیات کامپوننت مدیریت کند:@* فایل: CustomComponentBase.razor *@
@implements IDisposable
@code {
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
protected CancellationToken ComponentCancellationToken => _cts.Token;
public virtual void Dispose()
{
// در زمان خروج یا انهدام کامپوننت، تمامی تسکهای متصل لغو میشوند
_cts.Cancel();
_cts.Dispose();
}
}_Imports.razorinherits@ در تمام صفحات، میتوان ارثبری پیشفرض را در فایل _Imports.razor مشخص کرد:@* فایل: /Components/Pages/_Imports.razor *@ @inherits YourNamespace.Components.CustomComponentBase
@* فایل: Dashboard.razor *@
@page "/dashboard"
<h3>داشبورد وضعیت</h3>
@code {
protected override async Task OnInitializedAsync()
{
// استفاده مستقیم از توکن فراهمشده توسط کلاس پایه
await LoadDashboardDataAsync(ComponentCancellationToken);
}
private async Task LoadDashboardDataAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}
}| چالش / سناریو | رویکرد پیشنهادی |
| عملیات چنددقیقهای و سنگین | استفاده از الگوی Async Request-Reply با بازگرداندن وضعیت 202 Accepted و پردازش به وسیله Worker Service |
| استریم داده (SSE / WebSockets) | بهرهگیری از .DisableRequestTimeout() به دلیل عدم امکان بازگرداندن ۵۰۴ پس از شروع استریم |
| عملیات بحرانی غیرقابل توقف | جداسازی کوئریهای خواندن (قابل لغو) از تراکنشهای حساس نوشتن دیتابیس (غیرقابل لغو از طریق توکنهای مجزا) |
HttpContext.RequestAborted به تمام متدهای ناهمگام، پایگاههای داده و کلاینتهای HTTP.