عنوان:

‫آیا استفاده از مقادیر اختیاری Optional در Interface صحیح است؟


نویسنده: هادی مزارعی
تاریخ: ۱۴۰۴/۰۸/۳۰ ۱۹:۲۱
آدرس: www.dntips.ir
آیا تعریف پارامتر با مقادیر اختیاری در متدهای یک Interface کار درستی است یا آن را به کلاس‌های پیاده‌سازی کننده واگذار کنیم؟ در این نمونه مشخصا پارامتر CancellationToken برای متدهای Async مد نظر می‌باشد. فرض کنیم از دریافت درخواست در API تا لایه‌هایی که متدهای Interface پیاده سازی شده‌اند بطور فرضی در چند کلاس متدهای Async اجرا می‌شوند:
  1. Api: ابتدا ActionResult یک پارامتر از نوع CancellationToken دریافت خواهد کرد.
  2. Handler: که با استفاده از MediatR پیاده سازی شده است و قرار است متدهای Repository را صدا بزند. ممکن است بدلیل پیچیدگی منطق برنامه، عملکردهای مختلف در یک Handler به کلاس‌های کوچکتری تقسیم شده باشند که در هر یک از آن‌ها ممکن است یک متد Async دیگری اجرا شود.
  3. Repository: متدهای Interface را پیاده سازی کرده است.

مبنای این سوال بر این فرض بوده که به کلاینت (استفاده کننده نهایی) اجازه داده شود که CancellationToken را بصورت اختیاری مقدار دهی کند. حال در صورت استفاده از مقدار default آیا ضرورتا باید برای تمام متدهایی که لایه به لایه فراخوانی می‌شوند پارامتر CancellationToken را با مقدار پیش‌فرض default تعریف کنیم یا فقط در ActionMethod در لایه Api کافیست؟

نظرات

  • هادی مزارعی در ۱۴۰۴/۰۹/۰۱ ۰۷:۴۸
    در جستجویی که انجام دادم و با توجه به اینکه به دنبال راهی برای کاهش عملیات پاس دادن token به متدها بودم با دو آیتم AsyncLocal<CancellationToken> و IHttpContextAccessor.HttpContext.RequestAborted برخوردم ولی کارکرد این دو را متوجه نشدم. البته در خصوص HttpContext.RequestAborted مادامی که در لایه Service دسترسی به سرویس IHttpContextAccessor میسر باشد امکان بررسی درخواست لغو شده وجود دارد اما کاربرد AsyncLocal را متوجه نشدم. ممنون میشم چگونگی استفاده از AsyncLocal را توضیح بدید.
    • وحید نصیری در ۱۴۰۴/۰۹/۰۱ ۱۰:۴۶
      ۱. آیا تعریف پارامتر اختیاری در Interface درست است؟

      پاسخ کوتاه: بله، کاملاً استاندارد است.
      در اکوسیستم .NET، استاندارد پذیرفته شده (Best Practice) این است که پارامتر CancellationToken را به عنوان آخرین پارامتر و با مقدار پیش‌فرض در نظر بگیرید.
      public interface IUserRepository
      {
          Task<User> GetByIdAsync(int id, CancellationToken cancellationToken = default);
      }
      چرا این کار درست است؟
      • Caller Convenience: به استفاده‌کننده (مثلاً یک سرویس دیگر یا Unit Test) اجازه می‌دهد اگر نیازی به لغو عملیات ندارد، پارامتر را پاس ندهد.
      • Flexibility: پیاده‌سازی کننده (Implementation) همیشه توکن را دریافت می‌کند (حتی اگر None باشد) و می‌تواند تصمیم بگیرد که آیا از آن در EF Core یا سایر عملیات I/O استفاده کند یا خیر.

      ۲. آیا باید توکن را لایه به لایه پاس بدهیم؟ (Explicit Passing)
      در روش کلاسیک و صریح، پاسخ بله است.
      شما توکن را در Controller دریافت می‌کنید، به MediatR Handler می‌دهید، او به Service و در نهایت به Repository می‌دهد.
      • مزیت: کد بسیار شفاف است. با نگاه به متد می‌دانید که این متد قابلیت لغو شدن دارد.
      • عیب: کد تکراری (Boilerplate) زیاد می‌شود و امضای متدها (Method Signatures) شلوغ می‌شود.

      ۳. استفاده ازAsyncLocalبرای حذف پارامتر (Implicit Passing)

      شما به درستی به AsyncLocal اشاره کردید. این روشی است برای ایجاد یک "محیط" یا Context که در طول جریان اجرای کدهای Async (از بالا به پایین) در دسترس است، بدون اینکه نیاز باشد آن را به عنوان پارامتر پاس دهید.

      AsyncLocalچیست و چگونه کار می‌کند؟

      تصور کنید AsyncLocal مانند یک متغیر Static است، اما با این تفاوت که مقدار آن برای هر "Request" یا هر "Thread منطقی" جداگانه است. وقتی شما یک متد async را صدا می‌زنید (await می‌کنید)، مقدار AsyncLocal به آن متد و متدهای زیرمجموعه‌ی آن "جریان" (Flow) پیدا می‌کند.

      راهکار عملی: پیاده‌سازی Ambient CancellationToken
      برای اینکه توکن را لایه به لایه پاس ندهید، می‌توانید از یک الگو به نام Ambient Context استفاده کنید.

      مرحله ۱: تعریف یک اینترفیس برای دسترسی به توکن
      این کار باعث می‌شود وابستگی مستقیم به وب نداشته باشید (مناسب برای لایه Domain/Application).
      // Core/Application Layer
      public interface ICancellationContext
      {
          CancellationToken CurrentToken { get; }
      }

      مرحله ۲: پیاده‌سازی با استفاده از AsyncLocal
      این کلاس در لایه Infrastructure قرار می‌گیرد.
      // Infrastructure Layer
      public class AsyncLocalCancellationContext : ICancellationContext
      {
          // این متغیر استاتیک است اما مقدارش برای هر درخواست ایزوله است
          private static readonly AsyncLocal<CancellationToken> _token = new AsyncLocal<CancellationToken>();
      
          public CancellationToken CurrentToken => _token.Value;
      
          // متدی برای ست کردن توکن در ابتدای درخواست
          public void SetToken(CancellationToken token)
          {
              _token.Value = token;
          }
      }

      مرحله ۳: ایجاد Middleware برای مقداردهی اولیه
      در لایه API، یک میدل‌ور می‌سازیم که توکنِ درخواست جاری (HttpContext.RequestAborted) را درون AsyncLocal قرار دهد.
      // API Layer
      public class CancellationMiddleware
      {
          private readonly RequestDelegate _next;
      
          public CancellationMiddleware(RequestDelegate next)
          {
              _next = next;
          }
      
          public async Task Invoke(HttpContext context, AsyncLocalCancellationContext cancellationContext)
          {
              // توکن مربوط به این درخواست را درون کانتینر سراسری می‌ریزیم
              cancellationContext.SetToken(context.RequestAborted);
      
              await _next(context);
          }
      }

      مرحله ۴: استفاده در Repository (بدون پاس دادن پارامتر)
      // Infrastructure / Repository Layer
      public class UserRepository : IUserRepository
      {
          private readonly DbContext _db;
          private readonly ICancellationContext _canContext; // تزریق وابستگی
      
          public UserRepository(DbContext db, ICancellationContext canContext)
          {
              _db = db;
              _canContext = canContext;
          }
      
          public async Task<User> GetByIdAsync(int id) // دیگر پارامتر توکن ندارد
          {
              // توکن را از کانتکست می‌خوانیم
              return await _db.Users.FindAsync(new object[] { id }, _canContext.CurrentToken);
          }
      }

      ۴. تفاوت باIHttpContextAccessor

      شما پرسیدید چرا از IHttpContextAccessor استفاده نکنیم؟
      • IHttpContextAccessor: مستقیماً به ASP.NET Core وابسته است. اگر فردا بخواهید از کدهای لایه Business یا Repository خود در یک Console App یا Background Service استفاده کنید، کد شما خطا می‌دهد (چون HttpContext وجود ندارد).
      • AsyncLocal: وابسته به وب نیست. بخشی از Base Class Library دات‌نت است. اگر فردا کد را در یک جاب پس‌زمینه اجرا کنید، کافیست در ابتدای جاب، SetToken را صدا بزنید و همه چیز کار می‌کند.

      جمع‌بندی و توصیه

      روششفافیت کدراحتی استفادهوابستگیتوصیه
      Explicit (لایه به لایه)عالیکم (کد زیاد)نداردتوصیه مایکروسافت
      AsyncLocalمتوسط عالینداردگزینه دوم
      IHttpContextAccessorبدخوبشدید به Webاصلاً استفاده نکنید
      نظر نهایی:
      اگر پروژه خیلی بزرگ نیست، روش صریح (Explicit) که در بخش اول توضیح دادم (استفاده از default) بهترین و کم‌ریسک‌ترین گزینه است. برنامه‌نویسان دیگر با دیدن کد دقیقاً می‌فهمند چه خبر است.
      اما اگر تعداد لایه‌ها بسیار زیاد است و پاس دادن توکن باعث آلودگی شدید کد شده است، استفاده از راهکار AsyncLocal (با همان ساختار ICancellationContext که نوشتم) یک الگوی بسیار حرفه‌ای و تمیز برای حل این مشکل است.