# Repository Patterns

> **Scope:** blazor-azure,nextjs-supabase
> **Layer:** 1 (on domain)
> **Keywords:** repository, pattern, data access, dal, generic repository
> **Load When:** database work

Repository implementation patterns for data access layer

---

## Basic Repository Pattern

### Interface Definition

```csharp
// Domain layer
public interface IUserRepository
{
    Task<User?> GetByIdAsync(int id, CancellationToken ct = default);
    Task<List<User>> GetAllAsync(CancellationToken ct = default);
    Task<User> CreateAsync(User user, CancellationToken ct = default);
    Task UpdateAsync(User user, CancellationToken ct = default);
    Task DeleteAsync(int id, CancellationToken ct = default);
}
```

### Implementation

```csharp
// Infrastructure layer
public class UserRepository : IUserRepository
{
    private readonly AppDbContext _context;

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

    public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
    {
        return await _context.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(u => u.Id == id, ct);
    }

    public async Task<List<User>> GetAllAsync(CancellationToken ct = default)
    {
        return await _context.Users
            .AsNoTracking()
            .ToListAsync(ct);
    }

    public async Task<User> CreateAsync(User user, CancellationToken ct = default)
    {
        _context.Users.Add(user);
        await _context.SaveChangesAsync(ct);
        return user;
    }

    public async Task UpdateAsync(User user, CancellationToken ct = default)
    {
        _context.Users.Update(user);
        await _context.SaveChangesAsync(ct);
    }

    public async Task DeleteAsync(int id, CancellationToken ct = default)
    {
        var user = await GetByIdAsync(id, ct);
        if (user != null)
        {
            _context.Users.Remove(user);
            await _context.SaveChangesAsync(ct);
        }
    }
}
```

---

## Factory Pattern for Background Operations

**Problem:** DbContext injected in scoped services may be disposed before background tasks complete.

**Solution:** Use Repository Factory pattern to create isolated DbContext instances.

### Interface Base for Segregation

```csharp
// Domain layer - Base interface with common methods
public interface IOrderRepositoryBase
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task UpdateAsync(Order order, CancellationToken ct = default);
}

// Full repository for normal operations
public interface IOrderRepository : IOrderRepositoryBase
{
    Task<List<Order>> GetAllAsync(CancellationToken ct = default);
    Task<Order> CreateAsync(Order order, CancellationToken ct = default);
    Task DeleteAsync(Guid id, CancellationToken ct = default);
    // ... more methods
}

// Scoped repository for background operations (limited methods)
public interface IScopedOrderRepository : IOrderRepositoryBase, IAsyncDisposable
{
    // Inherits only GetByIdAsync and UpdateAsync from IOrderRepositoryBase
}
```

**Why this works:** Background operations only need limited methods, violating ISP to inherit all methods.

### Factory Interface

```csharp
// Domain layer
public interface IOrderRepositoryFactory
{
    IScopedOrderRepository CreateScoped();
}
```

### Factory Implementation

```csharp
// Infrastructure layer
public class OrderRepositoryFactory : IOrderRepositoryFactory
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public OrderRepositoryFactory(IDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public IScopedOrderRepository CreateScoped()
    {
        var context = _contextFactory.CreateDbContext();
        return new ScopedOrderRepository(context);
    }
}

// Scoped repository implementation
public class ScopedOrderRepository : IScopedOrderRepository
{
    private readonly AppDbContext _context;

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

    public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
        return await _context.Orders.FindAsync(new object[] { id }, ct);
    }

    public async Task UpdateAsync(Order order, CancellationToken ct = default)
    {
        _context.Orders.Update(order);
        await _context.SaveChangesAsync(ct);
    }

    public async ValueTask DisposeAsync()
    {
        await _context.DisposeAsync();
    }
}
```

### Registration (Program.cs)

```csharp
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

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

### Usage in Background Jobs

```csharp
public class ProcessOrderJob
{
    private readonly IOrderRepositoryFactory _repositoryFactory;
    private readonly ILogger<ProcessOrderJob> _logger;

    public ProcessOrderJob(
        IOrderRepositoryFactory repositoryFactory,
        ILogger<ProcessOrderJob> logger)
    {
        _repositoryFactory = repositoryFactory;
        _logger = logger;
    }

    public async Task ExecuteAsync(Guid orderId)
    {
        await using var repository = _repositoryFactory.CreateScoped();

        var order = await repository.GetByIdAsync(orderId);
        if (order == null)
        {
            _logger.LogWarning("Order {OrderId} not found", orderId);
            return;
        }

        // Process order
        order.Status = OrderStatus.Completed;
        await repository.UpdateAsync(order);

        // repository.DisposeAsync() called automatically (await using)
    }
}
```

**Benefits:**
- Encapsulates DbContext creation for background operations
- Easy to mock in tests
- No DbContext disposal issues
- Follows Interface Segregation Principle (ISP)

---

## Generic Repository Pattern

### Generic Interface

```csharp
public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id, CancellationToken ct = default);
    Task<List<T>> GetAllAsync(CancellationToken ct = default);
    Task<T> CreateAsync(T entity, CancellationToken ct = default);
    Task UpdateAsync(T entity, CancellationToken ct = default);
    Task DeleteAsync(int id, CancellationToken ct = default);
}
```

### Generic Implementation

```csharp
public class Repository<T> : IRepository<T> where T : class
{
    private readonly AppDbContext _context;
    private readonly DbSet<T> _dbSet;

    public Repository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task<T?> GetByIdAsync(int id, CancellationToken ct = default)
    {
        return await _dbSet.FindAsync(new object[] { id }, ct);
    }

    public async Task<List<T>> GetAllAsync(CancellationToken ct = default)
    {
        return await _dbSet.AsNoTracking().ToListAsync(ct);
    }

    public async Task<T> CreateAsync(T entity, CancellationToken ct = default)
    {
        await _dbSet.AddAsync(entity, ct);
        await _context.SaveChangesAsync(ct);
        return entity;
    }

    public async Task UpdateAsync(T entity, CancellationToken ct = default)
    {
        _dbSet.Update(entity);
        await _context.SaveChangesAsync(ct);
    }

    public async Task DeleteAsync(int id, CancellationToken ct = default)
    {
        var entity = await GetByIdAsync(id, ct);
        if (entity != null)
        {
            _dbSet.Remove(entity);
            await _context.SaveChangesAsync(ct);
        }
    }
}
```

### Specific Repository Extending Generic

```csharp
public interface IUserRepository : IRepository<User>
{
    Task<User?> GetByEmailAsync(string email, CancellationToken ct = default);
    Task<List<User>> GetActiveUsersAsync(CancellationToken ct = default);
}

public class UserRepository : Repository<User>, IUserRepository
{
    private readonly AppDbContext _context;

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

    public async Task<User?> GetByEmailAsync(string email, CancellationToken ct = default)
    {
        return await _context.Users
            .AsNoTracking()
            .FirstOrDefaultAsync(u => u.Email == email, ct);
    }

    public async Task<List<User>> GetActiveUsersAsync(CancellationToken ct = default)
    {
        return await _context.Users
            .AsNoTracking()
            .Where(u => u.IsActive)
            .ToListAsync(ct);
    }
}
```

---

## Unit of Work Pattern

### Interface

```csharp
public interface IUnitOfWork : IDisposable
{
    IUserRepository Users { get; }
    IOrderRepository Orders { get; }
    Task<int> SaveChangesAsync(CancellationToken ct = default);
    Task BeginTransactionAsync(CancellationToken ct = default);
    Task CommitAsync(CancellationToken ct = default);
    Task RollbackAsync(CancellationToken ct = default);
}
```

### Implementation

```csharp
public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;
    private IDbContextTransaction? _transaction;

    public IUserRepository Users { get; }
    public IOrderRepository Orders { get; }

    public UnitOfWork(
        AppDbContext context,
        IUserRepository userRepository,
        IOrderRepository orderRepository)
    {
        _context = context;
        Users = userRepository;
        Orders = orderRepository;
    }

    public async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        return await _context.SaveChangesAsync(ct);
    }

    public async Task BeginTransactionAsync(CancellationToken ct = default)
    {
        _transaction = await _context.Database.BeginTransactionAsync(ct);
    }

    public async Task CommitAsync(CancellationToken ct = default)
    {
        if (_transaction != null)
        {
            await _transaction.CommitAsync(ct);
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public async Task RollbackAsync(CancellationToken ct = default)
    {
        if (_transaction != null)
        {
            await _transaction.RollbackAsync(ct);
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public void Dispose()
    {
        _transaction?.Dispose();
        _context.Dispose();
    }
}
```

### Usage

```csharp
public class OrderService
{
    private readonly IUnitOfWork _uow;

    public OrderService(IUnitOfWork uow)
    {
        _uow = uow;
    }

    public async Task CreateOrderWithUserAsync(Order order, User user)
    {
        await _uow.BeginTransactionAsync();

        try
        {
            await _uow.Users.CreateAsync(user);
            await _uow.Orders.CreateAsync(order);
            await _uow.SaveChangesAsync();
            await _uow.CommitAsync();
        }
        catch
        {
            await _uow.RollbackAsync();
            throw;
        }
    }
}
```

---

## Best Practices

### DO

✅ **Use AsNoTracking for read-only queries**

```csharp
public async Task<List<User>> GetAllAsync()
{
    return await _context.Users
        .AsNoTracking() // ✅ Faster, no tracking overhead
        .ToListAsync();
}
```

✅ **Include related entities explicitly**

```csharp
public async Task<Order?> GetOrderWithItemsAsync(Guid id)
{
    return await _context.Orders
        .Include(o => o.Items) // ✅ Explicit include
        .FirstOrDefaultAsync(o => o.Id == id);
}
```

✅ **Use CancellationToken**

```csharp
public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
{
    return await _context.Users.FindAsync(new object[] { id }, ct);
}
```

### DON'T

❌ **Don't return IQueryable from repository**

```csharp
// ❌ WRONG (exposes EF Core details)
public IQueryable<User> GetUsers()
{
    return _context.Users;
}

// ✅ CORRECT (return materialized data)
public async Task<List<User>> GetUsersAsync()
{
    return await _context.Users.ToListAsync();
}
```

❌ **Don't use repository in background without factory**

```csharp
// ❌ WRONG
public class BackgroundJob
{
    private readonly IOrderRepository _repository; // Scoped, may be disposed!

    public async Task ExecuteAsync(Guid orderId)
    {
        var order = await _repository.GetByIdAsync(orderId); // May fail!
    }
}

// ✅ CORRECT
public class BackgroundJob
{
    private readonly IOrderRepositoryFactory _factory;

    public async Task ExecuteAsync(Guid orderId)
    {
        await using var repository = _factory.CreateScoped();
        var order = await repository.GetByIdAsync(orderId);
    }
}
```

---

## Related Standards

- [EF Core Standards](./ef-core.md)
- [DI Standards](../dotnet/di.md)
- [Async Standards](../dotnet/async.md)

---

*MORPH-SPEC by Polymorphism Tech*
