Repository Pattern — IRepository<T>, GenericRepository, abstraktsiya

~25 daq · .NET Backend · Entity Framework Core — Ma'lumotlar Bazasi

Muammodan boshlaylik

Controller va servislar to'g'ridan-to'g'ri DbContext bilan ishlaydi:

public class BuyurtmaService(DokonDbContext db)
{
    public async Task<decimal> MijozJamiAsync(int mijozId) =>
        await db.Buyurtmalar
            .Where(b => b.MijozId == mijozId && b.Holat != BuyurtmaHolati.Bekor && !b.Ochirilgan)
            .SumAsync(b => b.Summa);
}

Xuddi shu "mijozning faol buyurtmalari" filtri 6 ta joyda takrorlangan — birida !b.Ochirilgan unutilgan. Servisni testlash uchun haqiqiy baza yoki EF Core ning murakkab mock lari kerak.


Repository nima?

Repository — ma'lumotlarga kirishni bir joyga yig'adigan qatlam. Servis "qanday" saqlanishini (EF Core, SQL, API) bilmaydi — faqat "nima" kerakligini so'raydi:

public interface IBuyurtmaRepository
{
    Task<Buyurtma?> OlAsync(int id, CancellationToken ct = default);
    Task<List<Buyurtma>> MijozFaolBuyurtmalariAsync(int mijozId, CancellationToken ct = default);
    Task<decimal> MijozJamiAsync(int mijozId, CancellationToken ct = default);
    void Qosh(Buyurtma b);
    Task SaqlaAsync(CancellationToken ct = default);
}

public class BuyurtmaRepository(DokonDbContext db) : IBuyurtmaRepository
{
    private IQueryable<Buyurtma> Faollar(int mijozId) =>
        db.Buyurtmalar.Where(b => b.MijozId == mijozId && b.Holat != BuyurtmaHolati.Bekor);

    public Task<Buyurtma?> OlAsync(int id, CancellationToken ct) =>
        db.Buyurtmalar.Include(b => b.Qatorlar).FirstOrDefaultAsync(b => b.Id == id, ct);

    public Task<List<Buyurtma>> MijozFaolBuyurtmalariAsync(int mijozId, CancellationToken ct) =>
        Faollar(mijozId).OrderByDescending(b => b.Sana).ToListAsync(ct);

    public Task<decimal> MijozJamiAsync(int mijozId, CancellationToken ct) =>
        Faollar(mijozId).SumAsync(b => b.Summa, ct);

    public void Qosh(Buyurtma b) => db.Buyurtmalar.Add(b);
    public Task SaqlaAsync(CancellationToken ct) => db.SaveChangesAsync(ct);
}
builder.Services.AddScoped<IBuyurtmaRepository, BuyurtmaRepository>();

public class BuyurtmaService(IBuyurtmaRepository repo)
{
    public Task<decimal> MijozJamiAsync(int mijozId, CancellationToken ct) => repo.MijozJamiAsync(mijozId, ct);
}

Filtr — bitta joyda; servis testda soxta repository bilan ishlaydi (77-dars).


Hayotiy o'xshatma

Kutubxonachi. Siz arxivga o'zingiz tushib, javonlarni titkilamaysiz — kutubxonachiga "Qodiriyning barcha kitoblari" deysiz. U arxiv qanday tuzilganini, qaysi kartotekadan qidirishni biladi. Arxiv boshqa binoga ko'chsa yoki elektron bo'lsa — siz bilan muomala o'zgarmaydi.


Generic repository

Barcha entity lar uchun umumiy CRUD:

public interface IEntity { int Id { get; } }

public interface IRepository<T> where T : class, IEntity
{
    Task<T?> OlAsync(int id, CancellationToken ct = default);
    Task<List<T>> HammasiAsync(CancellationToken ct = default);
    void Qosh(T entity);
    void Ochir(T entity);
    Task SaqlaAsync(CancellationToken ct = default);
}

public class Repository<T>(DokonDbContext db) : IRepository<T> where T : class, IEntity
{
    protected readonly DbSet<T> Set = db.Set<T>();

    public Task<T?> OlAsync(int id, CancellationToken ct) => Set.FirstOrDefaultAsync(e => e.Id == id, ct);
    public Task<List<T>> HammasiAsync(CancellationToken ct) => Set.AsNoTracking().ToListAsync(ct);
    public void Qosh(T entity) => Set.Add(entity);
    public void Ochir(T entity) => Set.Remove(entity);
    public Task SaqlaAsync(CancellationToken ct) => db.SaveChangesAsync(ct);
}
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));   // barcha T lar uchun

public class KategoriyaService(IRepository<Kategoriya> repo) { ... }

Maxsus so'rovlar — generic dan meros olgan aniq repository da:

public class MahsulotRepository(DokonDbContext db) : Repository<Mahsulot>(db), IMahsulotRepository
{
    public Task<List<Mahsulot>> ArzonlarAsync(decimal maks, CancellationToken ct) =>
        Set.Where(m => m.Narx < maks).ToListAsync(ct);
}

Unit of Work

Bir nechta repository o'zgarishini bitta tranzaksiyada saqlash:

public interface IUnitOfWork
{
    IMahsulotRepository Mahsulotlar { get; }
    IBuyurtmaRepository Buyurtmalar { get; }
    Task<int> SaqlaAsync(CancellationToken ct = default);
}
var m = await uow.Mahsulotlar.OlAsync(id, ct);
m!.Qoldiq -= soni;
uow.Buyurtmalar.Qosh(new Buyurtma { ... });
await uow.SaqlaAsync(ct);         // ikkalasi — bitta SaveChanges, bitta tranzaksiya

[!INFO] EF Core ning DbContext i o'zi Unit of Work (change tracking + SaveChanges tranzaksiyasi), DbSet<T> esa — o'zi repository. Shuning uchun EF Core ustiga generic repository + UoW qatlami qo'shishning foydasi haqida jamoalar orasida munozara bor.


Qachon kerak — munozarali savol

Repository foydasi Kamchiligi
So'rovlar bir joyda, takrorlanmaydi Qo'shimcha qatlam va kod
Servislarni bazasiz test qilish Generic IRepository<T> LINQ kuchini yashiradi
Ma'lumot manbasini almashtirish (nazariy) Amalda EF Core ni almashtirish kam bo'ladi
Domen tilidagi metodlar (MijozFaolBuyurtmalari) IQueryable qaytarilsa — abstraktsiya "teshik"

Amaliy tavsiyalar:

  • Kichik loyiha, oddiy CRUD — DbContext ni servislarda to'g'ridan-to'g'ri ishlating
  • Takrorlanadigan murakkab so'rovlar, domen mantiqi — aniq (generic bo'lmagan) repository lar: IBuyurtmaRepository domen tilidagi metodlar bilan
  • Generic IRepository<T> faqat GetById/Add/Remove uchun — ko'pincha ortiqcha

[!WARNING] Repository dan IQueryable<T> qaytarmang (IQueryable<Buyurtma> Hammasi()) — chaqiruvchi istalgan so'rov quradi, va repository abstraktsiyasi ma'nosini yo'qotadi: EF Core ga bog'liqlik baribir servisga "oqib" chiqadi. Natija — List<T>, T? yoki DTO.


Testda

public class SoxtaBuyurtmaRepository : IBuyurtmaRepository
{
    private readonly List<Buyurtma> _r = new();
    public Task<decimal> MijozJamiAsync(int mijozId, CancellationToken ct = default) =>
        Task.FromResult(_r.Where(b => b.MijozId == mijozId).Sum(b => b.Summa));
    // ...
}

Yoki Moq bilan (77-dars). Repository ning o'zini esa — haqiqiy baza bilan integration testda (78-dars).


Tez-tez uchraydigan xatolar

Har bir repository metodi ichida SaveChanges — bir nechta o'zgarish bitta tranzaksiyada saqlanmaydi; saqlash — servis/UoW darajasida.

IQueryable qaytarish — abstraktsiya buziladi.

Repository lar turli DbContext nusxalarida — Scoped ro'yxat va bitta so'rovda bitta DbContext (38-dars).


Xulosa

Repository — ma'lumotlarga kirishni bir joyga yig'adi: servis "nima" kerakligini so'raydi, "qanday" ni bilmaydi; so'rovlar takrorlanmaydi, servislarni bazasiz test qilish oson. Generic IRepository<T> — umumiy CRUD, AddScoped(typeof(IRepository<>), typeof(Repository<>)); maxsus so'rovlar — aniq repository da.

Unit of Work — bir nechta o'zgarish bitta tranzaksiyada; EF Core DbContext o'zi UoW va DbSet o'zi repository. Kichik loyihada DbContext yetarli; murakkab domen — aniq repository lar. IQueryable qaytarmang; SaveChanges — servis darajasida.


Keyingi bo'lim

58-dars: Password Hashing — BCrypt.Net, hash va verify, SALT_ROUNDS