عنوان:

‫مدیریت جامع مهلت زمانی و لغو درخواست‌ها در ASP.NET Core و Blazor


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۶/۱۰ ۰۹:۵۰
آدرس: www.dntips.ir
۱. چکیده: در برنامه‌های تحت وب مدرن، پردازش درخواست‌هایی که کلاینت پیش از دریافت پاسخ اتصال آن‌ها را قطع کرده است، یا کوئری‌های سنگینی که بیش از حد مجاز به طول می‌انجامند، هدررفت جدی منابع سرور (CPU، حافظه، اتصالات پایگاه‌داده و پهنای باند) را به همراه دارد. به صورت پیش‌فرض، فریم‌ورک ASP.NET Core هیچ مهلت زمانی (Timeout) خودکاری روی درخواست‌های ورودی اعمال نمی‌کند. این مقاله به بررسی جامع مکانیزم لغو همکارانه (Cooperative Cancellation) از طریق توکن‌های لغو (CancellationToken)، قابلیت نوین میدل‌ور مهلت زمانی درخواست (Request Timeout Middleware) معرفی‌شده در دات‌نت ۸، و نحوه انتشار بهینه این توکن‌ها در تمام لایه‌های معماری (شامل کنترلرها، Minimal APIs، لایه سرویس، EF Core و مؤلفه‌های Blazor) می‌پردازد.

۲. مقدمه
هنگامی که یک کلاینت (کاربر یا سرویس خارجی) درخواستی را به سرور ASP.NET Core ارسال می‌کند، سناریوهای متعددی ممکن است فرآیند پاسخ‌دهی را بیهوده سازند:
  • قطع اتصال کلاینت (Client Disconnection) به دلیل بستن تب، خروج از صفحه یا از دست رفتن اتصال شبکه.
  • طولانی شدن یک کوئری در پایگاه‌داده یا قفل شدن یک فراخوانی HTTP خارجی (External API Dependency).

اگرچه ابزارهای پراکسی معکوس (Reverse Proxy) نظیر Nginx یا YARP می‌توانند Timeout مستقل خود را داشته باشند، اما این نوع لغو در لایه بالادست رخ می‌دهد؛ یعنی پراکسی به کلاینت پاسخ خطا می‌دهد در حالی که سرور میزبان همچنان به پردازش بیهوده ادامه می‌دهد.
برای حل این چالش، اکوسیستم دات‌نت از الگوی لغو همکارانه استفاده می‌کند. این رویکرد به دو ابزار مکمل متکی است:
  • HttpContext.RequestAborted: جهت آگاهی از قطعی ارتباط کلاینت.
  • Request Timeout Middleware (در دات‌نت ۸ و نسخه‌های جدیدتر): جهت اعمال ضرب‌الاجل‌های مشخص برای پردازش درخواست‌ها.

۳. الگوی لغو همکارانه (Cooperative Cancellation Model)
مدل لغو در پلتفرم دات‌نت ماهیتی همکارانه دارد؛ بدین معنا که فریم‌ورک یا وب‌سرور نخ‌های اجرایی (Threads) را به صورت اجباری متوقف (Thread.Abort) نمی‌کند، بلکه صرفاً وضعیت لغو شدن را از طریق آبجکت CancellationToken اعلام می‌نماید. وظیفه توسعه‌دهنده این است که این توکن را به تمامی لایه‌ها و عملگرهای ناهمگام (Asynchronous Operations) منتقل کند تا در صورت لغو، فرآیند متوقف شده و استثنای OperationCanceledException پرتاب شود.
[کلاینت / میدل‌ور Timeout]
           │
           ▼ (سیگنال لغو)
  HttpContext.RequestAborted
           │
           ▼
 [کنترلر یا Minimal API]
           │
           ▼
    [لایه سرویس/بیزینس]
           │
           ▼
[EF Core / HttpClient Provider] ──► لغو اتصال و بازپس‌گیری منابع

۴. ابزار نوین دات‌نت ۸: میدل‌ور مهلت زمانی درخواست (Request Timeouts)
دات‌نت ۸ قابلیت اعمال مهلت زمانی را به عنوان یک میدل‌ور رسمی ارائه داده است. این میدل‌ور به محض اتمام زمان تعریف‌شده، HttpContext.RequestAborted را لغو می‌کند و در صورتی که پاسخی پیش از این به کلاینت ارسال نشده باشد، کد خطای استاندارد 504 Gateway Timeout را بازمی‌گرداند.

۴.۱. پیکربندی پایه در Minimal APIs
برای فعال‌سازی، ابتدا سرویس‌ها را ثبت و میدل‌ور را در خط لوله (Pipeline) فعال می‌کنیم:
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) اجرا کنید.

۴.۲. تعریف سیاست‌های نام‌گذاری‌شده (Named Timeout Policies)
تمام اندپوینت‌ها بودجه تأخیر (Latency Budget) یکسانی ندارند. برای مثال یک فراخوانی ساده خواندن به ۳ ثانیه زمان نیاز دارد، اما تولید خروجی اکسل یا گزارش‌های تحلیلی ممکن است تا ۳۰ ثانیه زمان ببرد. همچنین اندپوینت‌های مبتنی بر استریم، نیازمند غیرفعال‌سازی مهلت زمانی هستند.
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();

۵. نحوه تزریق و دریافت CancellationToken در لایه‌های مختلف
برای اینکه مکانیزم لغو اثرگذار باشد، توکن باید از اندپوینت تا آخرین نقطه اجرای کار (دیتابیس یا سرویس خارجی) دست‌به‌دست شود.

۵.۱. کنترلرهای MVC و Web API
در کنترلرها می‌توان توکن را مستقیماً از طریق پارامتر متد (با بهره‌گیری از Model Binding خودکار) یا از طریق نمونه 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>();
    }
}

۵.۲. صفحات Razor Pages
در صفحات Razor، دسترسی از طریق نمونه 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 "تکمیل شد";
    }
}

۵.۳. لایه دسترسی به داده (EF Core) و کلاینت‌های HTTP
انتشار توکن در لایه‌های زیرین، اتصال پایگاه‌داده را آزاد کرده و ارسال بسته‌ها در شبکه را متوقف می‌کند:
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;
    }
}

۶. پیاده‌سازی متمرکز CancellationToken در معماری Blazor
در معماری کامپوننت‌محور Blazor، لغو فرآیندهای ناهمگام هنگام خروج کاربر از کامپوننت یا از بین رفتن آن (Dispose)، از نشت حافظه و عملیات پس‌زمینه ناخواسته جلوگیری می‌کند.

۶.۱. ایجاد یک Base Component با قابلیت لغو خودکار
می‌توان یک کامپوننت پایه انتزاعی ایجاد کرد که چرخه عمر 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.razor
برای جلوگیری از نوشتن تکراری inherits@ در تمام صفحات، می‌توان ارث‌بری پیش‌فرض را در فایل _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() به دلیل عدم امکان بازگرداندن ۵۰۴ پس از شروع استریم
عملیات بحرانی غیرقابل توقفجداسازی کوئری‌های خواندن (قابل لغو) از تراکنش‌های حساس نوشتن دیتابیس (غیرقابل لغو از طریق توکن‌های مجزا)

۸. نتیجه‌گیری
پیاده‌سازی مکانیزم مدیریت مهلت زمانی درخواست‌ها در ASP.NET Core نیازمند رویکردی هماهنگ در معماری نرم‌افزار است:
  • فعال‌سازی میدل‌ور Request Timeouts: اعمال سقف‌های زمانی منطقی و متناسب با ماهیت هر اندپوینت.
  • انتقال پیوسته توکن لغو: هدایت بدون وقفه HttpContext.RequestAborted به تمام متدهای ناهمگام، پایگاه‌های داده و کلاینت‌های HTTP.
  • لغو چرخه‌محور در Blazor: همگام‌سازی عملیات ناهمگام با انهدام کامپوننت‌ها به کمک کامپوننت‌های پایه.

رعایت این اصول، پایداری سرویس را در زیر بار شدید تضمین کرده و مانع از هدررفت منابع ارزشمند زیرساخت در عملیات بی‌فایده می‌گردد.