پیکربندی و استفاده از Dependency Injection در ASP.NET Core

مقدمه

یکی از ویژگی‌های برجسته‌ی ASP.NET Core، پشتیبانی داخلی از Dependency Injection (DI) است. این قابلیت یکی از اصول اساسی در معماری نرم‌افزار مدرن محسوب می‌شود و به توسعه‌دهندگان اجازه می‌دهد تا برنامه‌هایی منعطف‌تر، قابل‌تست‌تر و قابل‌نگهداری‌تر طراحی کنند.

در گذشته، برنامه‌نویسان اغلب وابستگی‌ها را به‌صورت مستقیم درون کلاس‌ها ایجاد می‌کردند. اما این روش باعث می‌شد تغییر و تست کد دشوار شود. با معرفی DI در .NET Core، مایکروسافت یک چارچوب قدرتمند و ساده برای مدیریت وابستگی‌ها ارائه داد که به شکل پیش‌فرض در هسته‌ی ASP.NET Core قرار دارد.

مفهوم وابستگی (Dependency) در نرم‌افزار

هر زمان که یک کلاس برای انجام وظایف خود به شیء یا کلاسی دیگر نیاز داشته باشد، می‌گوییم آن کلاس به کلاس دیگر وابسته (Dependent) است.

نمونه‌ای از وابستگی مستقیم:

public class OrderService
{
    private readonly OrderRepository _orderRepository;

    public OrderService()
    {
        _orderRepository = new OrderRepository();
    }

    public void CreateOrder(Order order)
    {
        _orderRepository.Save(order);
    }
}

در این مثال، کلاس OrderService به طور مستقیم نمونه‌ای از OrderRepository می‌سازد. این روش باعث می‌شود که تغییر پیاده‌سازی OrderRepository نیازمند تغییر مستقیم در OrderService باشد و تست‌پذیری کاهش یابد.

مفهوم Dependency Injection

Dependency Injection روشی است برای جداسازی وابستگی‌ها از کلاس اصلی. به جای اینکه کلاس خودش وابستگی‌هایش را بسازد، آن‌ها از بیرون به آن تزریق می‌شوند.

نسخه‌ی بهبود‌یافته‌ی مثال بالا:

public class OrderService
{
    private readonly IOrderRepository _orderRepository;

    public OrderService(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public void CreateOrder(Order order)
    {
        _orderRepository.Save(order);
    }
}

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

آشنایی با IoC (Inversion of Control)

Inversion of Control (IoC) مفهومی است که در آن کنترل ساخت و مدیریت اشیاء از دست کلاس‌ها گرفته می‌شود و به یک بخش دیگر (Container یا Framework) واگذار می‌شود. در ASP.NET Core، این کار توسط IoC Container داخلی انجام می‌شود.

IoC Container وظایف زیر را برعهده دارد:

  • شناسایی وابستگی‌ها
  • ایجاد نمونه‌های مناسب از سرویس‌ها
  • تزریق نمونه‌ها در زمان مناسب

پیکربندی DI در ASP.NET Core

در ASP.NET Core، همه‌چیز از متد ConfigureServices در فایل Program.cs یا Startup.cs شروع می‌شود. در این متد، سرویس‌ها و وابستگی‌های مورد نیاز برنامه ثبت (Register) می‌شوند.

var builder = WebApplication.CreateBuilder(args);

// ثبت سرویس‌ها
builder.Services.AddScoped();
builder.Services.AddScoped();

var app = builder.Build();

app.MapGet("/", (OrderService orderService) =>
{
    orderService.CreateOrder(new Order());
    return "Order created successfully!";
});

app.Run();

در این مثال:
- IOrderRepository با پیاده‌سازی OrderRepository ثبت شده است.
- OrderService نیز در Container ثبت شده است.
- در زمان نیاز به OrderService، ASP.NET Core نمونه‌های مورد نیاز را به صورت خودکار تزریق می‌کند.

انواع Service Lifetime در ASP.NET Core

در زمان ثبت سرویس‌ها باید مشخص کنید هر سرویس چه مدت در حافظه باقی بماند. سه نوع اصلی وجود دارد:

Transient

هر بار که از سرویس استفاده شود، یک نمونه‌ی جدید ساخته می‌شود.

builder.Services.AddTransient<IEmailSender, EmailSender>();

مناسب برای: سرویس‌های سبک و بدون وضعیت (stateless).

Scoped

برای هر درخواست HTTP یک نمونه جدید ساخته می‌شود و در طول آن درخواست از همان نمونه استفاده می‌شود.

builder.Services.AddScoped<IOrderRepository, OrderRepository>();

مناسب برای: سرویس‌هایی که داده‌های مرتبط با هر درخواست را نگهداری می‌کنند (مانند DbContext).

Singleton

تنها یک نمونه از سرویس در کل طول عمر برنامه ساخته می‌شود و همه‌ی درخواست‌ها از همان نمونه استفاده می‌کنند.

builder.Services.AddSingleton<ILogger, Logger>();

مناسب برای: سرویس‌هایی که وضعیت ثابتی دارند و thread-safe هستند.

مثال عملی از Dependency Injection

در این مثال ساده، یک سرویس ارسال ایمیل را با DI پیاده‌سازی می‌کنیم.

۱. تعریف اینترفیس

public interface IEmailService
{
    void Send(string to, string subject, string body);
}

۲. پیاده‌سازی سرویس

public class SmtpEmailService : IEmailService
{
    public void Send(string to, string subject, string body)
    {
        Console.WriteLine($"Sending Email to {to} - {subject}");
        // ارسال واقعی ایمیل در اینجا انجام می‌شود
    }
}

۳. ثبت سرویس در Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IEmailService, SmtpEmailService>();
var app = builder.Build();

۴. تزریق در کنترلر یا Endpoint

app.MapGet("/send", (IEmailService emailService) =>
{
    emailService.Send("user@example.com", "Welcome!", "Hello from ASP.NET Core!");
    return "Email sent successfully.";
});

ASP.NET Core به طور خودکار نمونه‌ی SmtpEmailService را تزریق می‌کند و نیاز به new کردن دستی وجود ندارد.

مدیریت وابستگی‌های پیچیده

در پروژه‌های بزرگ ممکن است یک سرویس خود وابسته به چند سرویس دیگر باشد. DI Container به صورت بازگشتی این وابستگی‌ها را حل می‌کند.

public class NotificationService
{
    private readonly IEmailService _emailService;
    private readonly ISmsService _smsService;

    public NotificationService(IEmailService emailService, ISmsService smsService)
    {
        _emailService = emailService;
        _sms_service = smsService; // توجه: نام متغیرها باید سازگار باشد
    }

    public void NotifyUser(string message)
    {
        _emailService.Send("user@example.com", "Notification", message);
        _smsService.Send("09120000000", message);
    }
}

کافی است در Program.cs همه‌ی سرویس‌ها را ثبت کنید؛ بقیه کار توسط DI Container انجام می‌شود.

نکات حرفه‌ای در استفاده از Dependency Injection

  1. همیشه از Interface استفاده کنید. وابستگی به اینترفیس باعث جداسازی بهتر لایه‌ها می‌شود.
  2. از Service Locator اجتناب کنید. به جای درخواست مستقیم سرویس‌ها از Container، از تزریق Constructor یا تزریق متد استفاده کنید.
  3. در Singletonها از سرویس Scoped استفاده نکنید. این کار موجب خطاهای عمر سرویس (lifetime mismatch) می‌شود.
  4. تست واحد را جدی بگیرید. با DI می‌توانید به‌راحتی Mock یا Fake برای تست فراهم کنید.

خطاهای رایج در پیاده‌سازی DI

  • عدم ثبت سرویس: اگر سرویسی را ثبت نکنید، در زمان اجرا خطای "Unable to resolve service for type" را خواهید دید.
  • تزریق اشتباه Lifetime: استفاده نادرست از Scoped و Singleton می‌تواند باعث اشتراک ناخواسته‌ی وضعیت بین درخواست‌ها شود.
  • وابستگی‌های چرخشی (Circular Dependencies): هنگامی که دو سرویس به صورت متقابل به هم نیاز داشته باشند، باید معماری را بازنگری کنید (معمولاً با استخراج رابط یا استفاده از الگوهای دیگری مانند Events حل می‌شود).

مزایای استفاده از Dependency Injection

خلاصه‌ای از مزایا:

  • کاهش coupling بین کلاس‌ها
  • افزایش قابلیت تست و امکان Mock کردن سرویس‌ها
  • تعویض آسان پیاده‌سازی‌ها بدون تغییر در منطق کسب‌وکار
  • کدنویسی خواناتر و قابل نگهداری‌تر
  • پشتیبانی داخلی ASP.NET Core بدون نیاز به کتابخانه خارجی

جمع‌بندی

در این مقاله آموختیم که Dependency Injection یکی از اصول کلیدی در معماری مدرن .NET Core است که باعث می‌شود پروژه‌ها ساختاری منظم، قابل تست و منعطف داشته باشند. ASP.NET Core با IoC Container داخلی خود، این فرآیند را بسیار ساده کرده و تنها با چند خط کد می‌توان وابستگی‌ها را مدیریت کرد.