Repository Pattern در .NET؛ چرا همیشه نباید از Repository استفاده کنیم؟

Repository Pattern در .NET؛ چرا همیشه نباید از Repository استفاده کنیم؟

اگر با ASP.NET Core و Entity Framework Core کار کرده باشید، احتمالاً با این ساختار مواجه شده‌اید:

Controllers
    ↓
Services
    ↓
Repositories
    ↓
DbContext
    ↓
Database

در نگاه اول همه‌چیز مرتب و تمیز به نظر می‌رسد. برای هر Entity یک Repository می‌سازیم:

UserRepository
ProductRepository
OrderRepository
CustomerRepository
...

اما یک سؤال مهم وجود دارد:

اگر EF Core خودش یک abstraction برای دسترسی به داده فراهم کرده، چرا باید دوباره یک Repository روی آن بسازیم؟

جواب کوتاه این است:

Repository Pattern همیشه اشتباه نیست، اما استفاده از آن در همه پروژه‌ها و برای همه Entityها معمولاً ارزش واقعی ایجاد نمی‌کند.

در این مقاله بررسی می‌کنیم Repository Pattern چیست، چرا در پروژه‌های .NET زیاد استفاده می‌شود، چه مشکلاتی ایجاد می‌کند و چه زمانی واقعاً استفاده از آن منطقی است.


Repository Pattern چیست؟

Repository Pattern یک abstraction بین منطق برنامه و منبع داده ایجاد می‌کند.

به جای اینکه Application مستقیماً با دیتابیس کار کند، Repository مسئول عملیات مربوط به داده خواهد بود.

برای مثال:

public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id);
    Task<List<User>> GetAllAsync();
    Task AddAsync(User user);
    Task DeleteAsync(User user);
}

و پیاده‌سازی:

public class UserRepository : IUserRepository
{
    private readonly AppDbContext _context;

    public UserRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<User?> GetByIdAsync(int id)
    {
        return await _context.Users
            .FirstOrDefaultAsync(x => x.Id == id);
    }

    public async Task<List<User>> GetAllAsync()
    {
        return await _context.Users.ToListAsync();
    }

    public async Task AddAsync(User user)
    {
        await _context.Users.AddAsync(user);
    }

    public async Task DeleteAsync(User user)
    {
        _context.Users.Remove(user);
    }
}

حالا Service به جای DbContext با IUserRepository کار می‌کند.


چرا Repository Pattern محبوب شد؟

یکی از دلایل اصلی محبوبیت Repository Pattern این است که در معماری‌های قدیمی‌تر، دسترسی به دیتابیس معمولاً مستقیماً با ORM یا SQL انجام می‌شد.

Repository کمک می‌کرد این جزئیات از Business Logic جدا شوند.

مثلاً:

OrderService
      ↓
IOrderRepository
      ↓
OrderRepository
      ↓
Database

این ایده کاملاً منطقی است.

مشکل زمانی شروع می‌شود که Repository تبدیل شود به یک لایه اجباری که فقط متدهای EF Core را دوباره با نام دیگری ارائه می‌کند.


مشکل اول: شما ممکن است API مربوط به EF Core را دوباره بسازید

فرض کنید این Repository را داریم:

public interface IProductRepository
{
    Task<Product?> GetByIdAsync(int id);
    Task<List<Product>> GetAllAsync();
    Task AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(Product product);
}

در پیاده‌سازی احتمالاً به این می‌رسیم:

public async Task<Product?> GetByIdAsync(int id)
{
    return await _context.Products
        .FirstOrDefaultAsync(x => x.Id == id);
}

و:

public async Task<List<Product>> GetAllAsync()
{
    return await _context.Products.ToListAsync();
}

در واقع چه اتفاقی افتاده؟

ما API مربوط به EF Core را گرفته‌ایم و یک API جدید روی آن ساخته‌ایم.

EF Core:

_context.Products.ToListAsync();

Repository:

_productRepository.GetAllAsync();

اگر Repository هیچ رفتار خاصی اضافه نکند، ممکن است فقط یک wrapper دور EF Core باشد.


مشکل دوم: Queryهای پیچیده Repository را شلوغ می‌کنند

فرض کنید یک صفحه داریم که باید سفارش‌های یک مشتری را با چند شرط مختلف برگرداند:

CustomerId
OrderStatus
Date Range
Payment Status
Search
Pagination
Sorting

ممکن است Repository به این شکل رشد کند:

GetOrdersByCustomerAsync(...)
GetOrdersByCustomerAndStatusAsync(...)
GetRecentOrdersByCustomerAsync(...)
GetPaidOrdersByCustomerAsync(...)
GetOrdersForAdminAsync(...)
GetOrdersForReportAsync(...)

و بعد از مدتی:

IOrderRepository
    ├── GetByIdAsync()
    ├── GetAllAsync()
    ├── GetByCustomerAsync()
    ├── GetByCustomerAndStatusAsync()
    ├── GetRecentAsync()
    ├── GetPaidAsync()
    ├── GetUnpaidAsync()
    ├── GetForReportAsync()
    └── ...

Repository به جای اینکه abstraction ساده‌ای باشد، تبدیل شده به یک محل تجمع Queryها.

این یکی از مشکلاتی است که در پروژه‌های واقعی زیاد دیده می‌شود.


Generic Repository؛ راه‌حل یا مشکل جدید؟

احتمالاً بعد از دیدن این مشکل کسی پیشنهاد می‌دهد:

بیا یک Generic Repository بسازیم!

مثلاً:

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
}

و:

public class Repository<T> : IRepository<T>
    where T : class
{
    private readonly AppDbContext _context;

    public Repository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<T?> GetByIdAsync(int id)
    {
        return await _context.Set<T>()
            .FindAsync(id);
    }

    public async Task<List<T>> GetAllAsync()
    {
        return await _context.Set<T>()
            .ToListAsync();
    }

    public async Task AddAsync(T entity)
    {
        await _context.Set<T>().AddAsync(entity);
    }

    public void Update(T entity)
    {
        _context.Set<T>().Update(entity);
    }

    public void Delete(T entity)
    {
        _context.Set<T>().Remove(entity);
    }
}

در نگاه اول خیلی تمیز است.

اما سؤال مهم‌تر:

چه چیزی به EF Core اضافه کرده‌ایم؟

EF Core همین قابلیت‌ها را از قبل دارد.

_context.Set<Product>().FindAsync(id);

_context.Products.ToListAsync();

_context.Products.Add(product);

_context.Products.Remove(product);

پس Generic Repository ممکن است فقط این API را:

EF Core
   ↓
Generic Repository

دوباره بسته‌بندی کند.


آیا Repository Pattern در EF Core کاملاً بی‌فایده است؟

نه.

این قسمت مهم است.

مشکل، خود Repository Pattern نیست.

مشکل این است که آن را بدون دلیل معماری وارد پروژه کنیم.

در بعضی پروژه‌ها Repository کاملاً منطقی است.

مثلاً فرض کنید Domain شما یک مفهوم مشخص دارد:

public interface IOrderRepository
{
    Task<Order?> GetPendingOrderAsync(Guid customerId);

    Task AddAsync(Order order);

    Task SaveAsync();
}

اینجا Repository فقط CRUD عمومی ارائه نمی‌دهد.

بلکه یک مفهوم دامنه‌ای را بیان می‌کند:

GetPendingOrderAsync

این خیلی متفاوت است با:

GetByIdAsync
GetAllAsync
AddAsync
UpdateAsync
DeleteAsync

اولی درباره Domain صحبت می‌کند.

دومی بیشتر شبیه CRUD است.


چه زمانی Repository Pattern انتخاب خوبی است؟

1. وقتی Domain Logic پیچیده دارید

اگر پروژه شما فقط CRUD نیست و Domain Logic قابل توجهی دارد، Repository می‌تواند abstraction مناسبی ایجاد کند.

مثلاً:

Task<Order?> GetActiveOrderForCustomerAsync(Guid customerId);

این متد یک مفهوم واقعی از سیستم را بیان می‌کند.


2. وقتی منبع داده فقط یک Database ساده نیست

اگر Application شما ممکن است داده را از چند منبع دریافت کند:

SQL Server
Redis
External API
Message Queue
File Storage

Repository می‌تواند مرز مناسبی ایجاد کند.

البته این به معنی این نیست که باید صرفاً برای «قابل تعویض بودن دیتابیس» Repository بسازید.


3. وقتی می‌خواهید Persistence را از Application جدا کنید

در معماری‌هایی مثل Clean Architecture ممکن است بخواهید Application Layer از جزئیات Infrastructure خبر نداشته باشد.

مثلاً:

Application
    ↓
IOrderRepository
    ↓
Infrastructure
    ↓
EF Core

در این حالت Interface می‌تواند در Application یا Domain قرار بگیرد و Implementation در Infrastructure باشد.


چه زمانی Repository Pattern احتمالاً انتخاب خوبی نیست؟

اگر پروژه شما چیزی شبیه این است:

Products
Customers
Categories
Orders

و تقریباً تمام عملیات شما CRUD است، احتمالاً ایجاد Repository برای تک‌تک Entityها ارزش زیادی ندارد.

مثلاً این:

IProductRepository
ProductRepository

ICategoryRepository
CategoryRepository

ICustomerRepository
CustomerRepository

IOrderRepository
OrderRepository

ممکن است فقط تعداد فایل‌های پروژه را زیاد کند، بدون اینکه abstraction معناداری ایجاد کند.

در چنین پروژه‌ای استفاده مستقیم و کنترل‌شده از EF Core می‌تواند ساده‌تر و خواناتر باشد.


پس آیا Service هم لازم نیست؟

نه، این دو موضوع را نباید با هم اشتباه گرفت.

Repository مسئول Persistence است.

Service معمولاً مسئول Application/Business Logic است.

مثلاً:

Controller
    ↓
OrderService
    ↓
DbContext

ممکن است کاملاً قابل قبول باشد.

Service:

public class OrderService
{
    private readonly AppDbContext _context;

    public OrderService(AppDbContext context)
    {
        _context = context;
    }

    public async Task CancelOrderAsync(Guid orderId)
    {
        var order = await _context.Orders
            .FirstOrDefaultAsync(x => x.Id == orderId);

        if (order == null)
            throw new Exception("Order not found");

        if (!order.CanBeCancelled())
            throw new InvalidOperationException(
                "Order cannot be cancelled.");

        order.Cancel();
        
        await _context.SaveChangesAsync();
    }
}

اینجا Service منطق واقعی دارد.

اما اگر Service فقط این کار را انجام دهد:

public Task<Product?> GetProductAsync(int id)
{
    return _repository.GetByIdAsync(id);
}

و Repository هم فقط این کار را انجام دهد:

return await _context.Products
    .FirstOrDefaultAsync(x => x.Id == id);

شما عملاً سه لایه برای یک Query ساده ساخته‌اید:

Controller
    ↓
Service
    ↓
Repository
    ↓
EF Core

در حالی که هیچ رفتار معناداری در این زنجیره وجود ندارد.


Repository Pattern در برابر DbContext

برای پروژه‌های معمول ASP.NET Core + EF Core می‌توان این‌طور نگاه کرد:

روشمزیتمشکل احتمالی
مستقیم استفاده کردن از DbContextساده و کم‌کدممکن است Application به EF Core وابسته شود
Generic Repositoryabstraction ظاهری و CRUD مشترکاغلب فقط wrapper برای EF Core است
Specific Repositoryامکان تعریف Queryهای دامنه‌ایتعداد فایل و کد بیشتر
Repository + Specificationمناسب Queryهای پیچیدهپیچیدگی بیشتر و نیاز به طراحی درست

هیچ‌کدام به‌صورت مطلق بهترین انتخاب نیستند.

اما برای پروژه‌ای که EF Core خودش بخش بزرگی از Repository/Data Access Pattern را فراهم می‌کند، من معمولاً Generic Repository را به صورت پیش‌فرض پیشنهاد نمی‌کنم.


Specification Pattern چه زمانی وارد بازی می‌شود؟

اگر Queryهای پیچیده زیادی دارید، به جای اینکه Repository شما پر از متدهای مختلف شود، می‌توانید از Specification Pattern استفاده کنید.

مثلاً:

public class ActiveOrdersSpecification
{
    public IQueryable<Order> Apply(IQueryable<Order> query)
    {
        return query
            .Where(x => x.IsActive)
            .Include(x => x.Customer)
            .OrderByDescending(x => x.CreatedAt);
    }
}

حالا Query می‌تواند قابل ترکیب‌تر باشد.

البته Specification Pattern هم یک چکش جادویی نیست.

اگر پروژه ساده است، اضافه کردن Specification فقط برای اینکه معماری «تمیزتر به نظر برسد» می‌تواند نتیجه معکوس داشته باشد.


یک اشتباه رایج در Clean Architecture

یکی از اشتباه‌هایی که زیاد دیده می‌شود این است:

Clean Architecture
        ↓
Repository Pattern
        ↓
Generic Repository
        ↓
Unit Of Work
        ↓
Specification
        ↓
CQRS

همه این‌ها به پروژه اضافه می‌شوند، چون در یک معماری نمونه دیده شده‌اند.

اما سؤال اصلی باید این باشد:

این abstraction دقیقاً چه مشکلی را حل می‌کند؟

اگر پاسخ مشخصی ندارید، احتمالاً هنوز به آن abstraction نیاز ندارید.

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


آیا DbContext خودش Repository نیست؟

تا حد زیادی، بله.

در EF Core:

DbSet<T>

رفتاری شبیه Repository دارد و:

DbContext

بخش‌هایی از نقش Unit of Work را نیز بر عهده دارد.

مثلاً:

_context.Products.Add(product);

_context.Products.Remove(product);

await _context.SaveChangesAsync();

بنابراین وقتی یک Generic Repository روی DbSet<T> می‌سازید، باید دلیل مشخصی برای این abstraction داشته باشید.

این دقیقاً جایی است که بسیاری از پروژه‌ها دچار Abstraction Overload می‌شوند.


پیشنهاد عملی برای پروژه‌های ASP.NET Core

اگر پروژه شما یک CRUD API معمولی است، من از این ساختار شروع می‌کنم:

Controller
    ↓
Application Service / Use Case
    ↓
DbContext
    ↓
Database

مثلاً:

public class ProductService
{
    private readonly AppDbContext _context;

    public ProductService(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Product?> GetByIdAsync(int id)
    {
        return await _context.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == id);
    }
}

اگر بعداً یک abstraction واقعاً لازم شد، آن را اضافه کنید.

مثلاً:

OrderService
      ↓
IOrderRepository
      ↓
OrderRepository
      ↓
DbContext

این رویکرد یک مزیت مهم دارد:

شما معماری را بر اساس نیاز واقعی پروژه رشد می‌دهید، نه بر اساس یک Template.


یک قانون ساده برای تصمیم‌گیری

قبل از ساخت Repository برای یک Entity، این چند سؤال را از خودتان بپرسید:

آیا Repository من فقط CRUD را تکرار می‌کند؟

اگر جواب بله است، احتمالاً به آن نیاز ندارید.

آیا Repository Queryهای معنادار دامنه را کپسوله می‌کند؟

اگر جواب بله است، Repository می‌تواند انتخاب خوبی باشد.

آیا Repository قرار است فقط EF Core را از Application مخفی کند؟

این به‌تنهایی دلیل کافی برای ساخت یک Generic Repository نیست.

آیا پروژه آن‌قدر پیچیده است که این abstraction ارزش خودش را داشته باشد؟

اگر پروژه کوچک است، سادگی معمولاً برنده است.


جمع‌بندی

Repository Pattern یک Pattern بد نیست.

مشکل زمانی ایجاد می‌شود که آن را به یک قانون تبدیل کنیم:

«در هر پروژه .NET باید برای هر Entity یک Repository داشته باشیم.»

این قانون در پروژه‌های واقعی همیشه جواب نمی‌دهد.

EF Core خودش abstractionهای قدرتمندی برای Data Access در اختیار شما قرار می‌دهد. بنابراین اگر Repository شما فقط متدهای EF Core را با اسم‌های دیگری expose می‌کند، احتمالاً یک لایه اضافی ساخته‌اید.

از طرف دیگر، اگر Repository بتواند مفاهیم دامنه‌ای و Queryهای پیچیده را کپسوله کند، استفاده از آن می‌تواند کاملاً منطقی باشد.

پس به جای اینکه بپرسیم:

«آیا باید Repository Pattern استفاده کنم؟»

سؤال بهتر این است:

«این Repository دقیقاً چه مشکلی را در پروژه من حل می‌کند؟»

اگر جواب مشخصی دارید، بسازیدش.

اگر فقط به خاطر اینکه «Clean Architecture این‌طوریه» می‌خواهید آن را اضافه کنید، احتمالاً وقت آن رسیده که کمی ساده‌ترش کنید. 🙂

سوال برای شما

در پروژه‌های .NET خودتان از Repository Pattern استفاده می‌کنید؟

به نظرتان Repository واقعاً abstraction مفیدی است یا در بسیاری از پروژه‌ها فقط یک wrapper دور EF Core محسوب می‌شود؟

1 دیدگاه دربارهٔ «Repository Pattern در .NET؛ چرا همیشه نباید از Repository استفاده کنیم؟»

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

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

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