dependency injection

Dependency Injection در ASP.NET Core چیست؟ آموزش کامل DI با مثال عملی C#

Dependency Injection در ASP.NET Core چیست؟ آموزش کامل DI با مثال عملی C#

اگر با ASP.NET Core کار کرده باشید، احتمالاً بارها با کدهایی مثل AddScoped، AddTransient و AddSingleton مواجه شده‌اید.

اما Dependency Injection دقیقاً چیست و چرا تقریباً در هر پروژه واقعی ASP.NET Core از آن استفاده می‌شود؟

در این مقاله می‌خواهیم مفهوم Dependency Injection یا DI را از پایه بررسی کنیم و بعد با یک مثال واقعی در ASP.NET Core ببینیم چطور یک Service را ایجاد، ثبت و داخل Controller استفاده کنیم.


Dependency Injection چیست؟

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

برای مثال فرض کنید یک Controller برای دریافت اطلاعات کاربران به UserService نیاز دارد.

روش ساده و نامناسب این است:

public class UsersController
{
    private readonly UserService _userService;

    public UsersController()
    {
        _userService = new UserService();
    }
}

در این حالت Controller خودش مسئول ساختن UserService است.

این موضوع باعث می‌شود وابستگی بین کلاس‌ها زیاد شود.

روش بهتر این است که Controller فقط بگوید:

من به یک IUserService نیاز دارم.

و ASP.NET Core نمونه مناسب را در اختیار Controller قرار دهد.

public class UsersController
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }
}

اینجا Dependency از بیرون وارد کلاس شده است.

به همین دلیل به آن Dependency Injection می‌گوییم.


چرا Dependency Injection مهم است؟

DI فقط برای کوتاه‌تر شدن کد نیست.

در پروژه‌های واقعی، Dependency Injection چند مشکل مهم را حل می‌کند.

1. کاهش وابستگی بین کلاس‌ها

Controller دیگر به یک implementation مشخص وابسته نیست.

مثلاً:

private readonly IUserService _userService;

به‌جای:

private readonly UserService _userService;

این موضوع باعث می‌شود بتوانیم implementation را راحت‌تر تغییر دهیم.


2. تست‌پذیری بهتر

فرض کنید Controller به دیتابیس دسترسی دارد.

اگر Controller خودش Repository را بسازد، Unit Test کردن آن سخت‌تر می‌شود.

اما با DI می‌توانیم یک implementation تستی یا Mock به آن بدهیم.


3. مدیریت طول عمر Objectها

ASP.NET Core می‌تواند مشخص کند یک Service چه مدت در حافظه وجود داشته باشد.

برای همین مفاهیمی مثل:

  • Singleton
  • Scoped
  • Transient

وجود دارند.


یک مثال ساده از Dependency Injection

فرض کنیم یک API برای مدیریت محصولات داریم.

ابتدا Interface را ایجاد می‌کنیم:

public interface IProductService
{
    IEnumerable<string> GetProducts();
}

حالا implementation را ایجاد می‌کنیم:

public class ProductService : IProductService
{
    public IEnumerable<string> GetProducts()
    {
        return new[]
        {
            "Laptop",
            "Mouse",
            "Keyboard"
        };
    }
}

حالا باید به ASP.NET Core بگوییم:

هر وقت کسی IProductService خواست، یک ProductService در اختیارش قرار بده.

در Program.cs:

builder.Services.AddScoped<IProductService, ProductService>();

حالا می‌توانیم Service را داخل Controller دریافت کنیم:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public IActionResult Get()
    {
        var products = _productService.GetProducts();

        return Ok(products);
    }
}

ASP.NET Core خودش ProductService را ایجاد می‌کند و به Controller می‌دهد.


Service Lifetime در ASP.NET Core چیست؟

یکی از مهم‌ترین بخش‌های Dependency Injection در ASP.NET Core، Service Lifetime است.

سه Lifetime اصلی داریم:

  1. Transient
  2. Scoped
  3. Singleton

انتخاب اشتباه Lifetime می‌تواند در پروژه واقعی باعث مصرف حافظه، رفتارهای غیرمنتظره یا حتی خطاهای مربوط به Thread Safety شود.


AddTransient چیست؟

در حالت Transient، هر بار که Service درخواست شود، یک Instance جدید ایجاد می‌شود.

builder.Services.AddTransient<IProductService, ProductService>();

مثلاً اگر Service چند بار Resolve شود، ممکن است چند Instance متفاوت ایجاد شود.

Transient معمولاً برای Serviceهای سبک و Stateless مناسب است.

مثلاً:

builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();

اگر Service وضعیت خاصی را بین درخواست‌ها نگه نمی‌دارد، Transient می‌تواند انتخاب مناسبی باشد.


AddScoped چیست؟

در Scoped، یک Instance برای هر Request ایجاد می‌شود.

builder.Services.AddScoped<IProductService, ProductService>();

فرض کنید کاربر یک درخواست HTTP به API ارسال می‌کند.

در طول همان Request، هر جا IProductService درخواست شود، همان Scope مورد استفاده قرار می‌گیرد.

برای بسیاری از Serviceهای Web Application و API، Scoped انتخاب رایجی است.

به‌خصوص زمانی که Service با DbContext کار می‌کند.

مثلاً:

builder.Services.AddScoped<IProductService, ProductService>();

و:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

DbContext نیز معمولاً Scoped است.


AddSingleton چیست؟

در Singleton فقط یک Instance از Service در طول عمر Application ایجاد می‌شود.

builder.Services.AddSingleton<ICacheService, CacheService>();

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

بنابراین باید در استفاده از Singleton دقت بیشتری داشته باشید.

اگر یک Service دارای State مشترک باشد، باید Thread Safety آن را جدی بگیرید.


تفاوت Scoped، Transient و Singleton

Lifetimeزمان ایجادطول عمر
Transientهر بار درخواست Serviceکوتاه
Scopedیک بار در هر Requestتا پایان Request
Singletonیک بار برای Applicationکل عمر Application

به شکل ساده:

Transient
Request → Service A
Request → Service B
Request → Service C

در Scoped:

Request 1 → Service A
Request 1 → همان Service A

Request 2 → Service B
Request 2 → همان Service B

و در Singleton:

Application
     ↓
Service A
     ↓
همه Requestها

یک اشتباه رایج در Service Lifetime

یکی از خطاهای مهم این است که یک Service با Lifetime طولانی‌تر، مستقیماً به Service با Lifetime کوتاه‌تر وابسته شود.

مثلاً:

Singleton
   ↓
Scoped

این طراحی می‌تواند مشکل‌ساز باشد، چون Singleton برای مدت طولانی زنده می‌ماند ولی Scoped فقط در محدوده یک Request معتبر است.

به همین دلیل باید Dependencyهای Serviceها را هنگام انتخاب Lifetime در نظر بگیرید.


Dependency Injection و Interface

یکی از الگوهای رایج در پروژه‌های حرفه‌ای این است:

Controller
    ↓
Interface
    ↓
Service
    ↓
Repository
    ↓
Database

مثلاً:

public interface IProductService
{
    Task<ProductDto?> GetByIdAsync(int id);
}

Implementation:

public class ProductService : IProductService
{
    private readonly IProductRepository _repository;

    public ProductService(IProductRepository repository)
    {
        _repository = repository;
    }

    public async Task<ProductDto?> GetByIdAsync(int id)
    {
        var product = await _repository.GetByIdAsync(id);

        if (product == null)
            return null;

        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name
        };
    }
}

در این حالت Service خودش Repository را ایجاد نمی‌کند.

Repository نیز از طریق DI دریافت می‌شود.

این ساختار باعث می‌شود اجزای مختلف Application به شکل loosely coupled کار کنند.


Dependency Injection در Program.cs

در پروژه‌های جدید ASP.NET Core معمولاً Registrationها در Program.cs قرار می‌گیرند.

مثلاً:

builder.Services.AddScoped<IProductRepository, ProductRepository>();

builder.Services.AddScoped<IProductService, ProductService>();

حالا ASP.NET Core می‌تواند Dependencyها را به شکل زنجیره‌ای ایجاد کند.

مثلاً:

ProductsController
       ↓
IProductService
       ↓
ProductService
       ↓
IProductRepository
       ↓
ProductRepository

Dependency Injection و Unit Testing

یکی از مزیت‌های مهم DI زمانی مشخص می‌شود که بخواهیم تست بنویسیم.

فرض کنید Controller ما این Dependency را دارد:

private readonly IProductService _productService;

در تست می‌توانیم به‌جای implementation واقعی، یک Mock در اختیار Controller قرار دهیم.

مثلاً با Moq:

var mockService = new Mock<IProductService>();

mockService
    .Setup(x => x.GetProducts())
    .Returns(new[]
    {
        "Laptop",
        "Mouse"
    });

حالا Controller را با همین Mock تست می‌کنیم.

این یکی از دلایل اصلی استفاده از Interface و Dependency Injection در پروژه‌های قابل تست است.


آیا همیشه باید از Interface استفاده کنیم؟

نه.

این تصور که:

هر Class حتماً باید Interface داشته باشد

لزوم فنی ندارد.

اگر یک کلاس ساده و بدون چند implementation است و نیازی به abstraction ندارد، ایجاد Interface صرفاً برای اینکه «معماری تمیزتر به نظر برسد» می‌تواند کد را شلوغ کند.

Interface زمانی ارزش بیشتری دارد که واقعاً abstraction، جایگزینی implementation یا testability برایمان مهم باشد.


جمع‌بندی

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

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

در ASP.NET Core معمولاً با این سه Lifetime کار می‌کنیم:

  • Transient
  • Scoped
  • Singleton

و برای ثبت Dependencyها معمولاً از Program.cs استفاده می‌کنیم.

یک ساختار رایج در پروژه‌های واقعی می‌تواند به شکل زیر باشد:

Controller
     ↓
Service
     ↓
Repository
     ↓
Database

Dependency Injection باعث می‌شود این اجزا وابستگی کمتری به implementationهای یکدیگر داشته باشند و تست و نگهداری Application ساده‌تر شود.

اگر در حال یادگیری ASP.NET Core هستید، درک DI یکی از پایه‌هایی است که بعداً در مباحثی مثل Clean Architecture، Repository Pattern، Unit Testing و معماری پروژه‌های بزرگ مرتب با آن مواجه خواهید شد.

دیدگاه‌ خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

پیمایش به بالا