Implement repository pattern for data access #61

Closed
opened 2026-01-18 22:29:56 +00:00 by joezombie · 1 comment
joezombie commented 2026-01-18 22:29:56 +00:00 (Migrated from github.com)

Parent Epic

Part of #58 - Migrate to Entity Framework Core

Description

Implement the repository pattern to abstract data access and make the codebase easier to test and maintain.

Repository Interfaces

// Data/Repositories/IFeedRepository.cs
public interface IFeedRepository
{
    Task<List<Feed>> GetAllAsync(CancellationToken ct = default);
    Task<Feed?> GetByIdAsync(int id, CancellationToken ct = default);
    Task<Feed?> GetByNameAsync(string name, CancellationToken ct = default);
    Task<Feed> AddAsync(Feed feed, CancellationToken ct = default);
    Task UpdateAsync(Feed feed, CancellationToken ct = default);
    Task DeleteAsync(int id, CancellationToken ct = default);
}

// Data/Repositories/IEpisodeRepository.cs
public interface IEpisodeRepository
{
    Task<List<Episode>> GetByFeedIdAsync(int feedId, CancellationToken ct = default);
    Task<Episode?> GetByIdAsync(int id, CancellationToken ct = default);
    Task<Episode?> GetByFilenameAsync(int feedId, string filename, CancellationToken ct = default);
    Task AddAsync(Episode episode, CancellationToken ct = default);
    Task AddRangeAsync(IEnumerable<Episode> episodes, CancellationToken ct = default);
    Task UpdateAsync(Episode episode, CancellationToken ct = default);
    Task<int> GetCountByFeedIdAsync(int feedId, CancellationToken ct = default);
}

// Data/Repositories/IDownloadedVideoRepository.cs
public interface IDownloadedVideoRepository
{
    Task<bool> ExistsAsync(int feedId, string videoId, CancellationToken ct = default);
    Task<HashSet<string>> GetVideoIdsByFeedAsync(int feedId, CancellationToken ct = default);
    Task AddAsync(DownloadedVideo video, CancellationToken ct = default);
    Task RemoveAsync(int feedId, string videoId, CancellationToken ct = default);
}

// Data/Repositories/IActivityLogRepository.cs
public interface IActivityLogRepository
{
    Task LogAsync(int? feedId, string activityType, string message, string? details = null, CancellationToken ct = default);
    Task<List<ActivityLog>> GetRecentAsync(int? feedId = null, int count = 100, CancellationToken ct = default);
}

// Data/Repositories/IDownloadQueueRepository.cs
public interface IDownloadQueueRepository
{
    Task<DownloadQueueItem> AddAsync(int feedId, string videoId, string? title, CancellationToken ct = default);
    Task UpdateProgressAsync(int id, string status, int percent, string? error = null, CancellationToken ct = default);
    Task<List<DownloadQueueItem>> GetAllAsync(int? feedId = null, CancellationToken ct = default);
    Task<DownloadQueueItem?> GetAsync(int feedId, string videoId, CancellationToken ct = default);
    Task RemoveAsync(int id, CancellationToken ct = default);
}

Repository Implementations

// Data/Repositories/FeedRepository.cs
public class FeedRepository : IFeedRepository
{
    private readonly CastrDbContext _context;
    
    public FeedRepository(CastrDbContext context)
    {
        _context = context;
    }
    
    public async Task<List<Feed>> GetAllAsync(CancellationToken ct = default)
    {
        return await _context.Feeds
            .OrderBy(f => f.Name)
            .ToListAsync(ct);
    }
    
    public async Task<Feed?> GetByNameAsync(string name, CancellationToken ct = default)
    {
        return await _context.Feeds
            .FirstOrDefaultAsync(f => f.Name == name, ct);
    }
    
    // ... other methods
}

Unit of Work (Optional)

// Data/IUnitOfWork.cs
public interface IUnitOfWork : IDisposable
{
    IFeedRepository Feeds { get; }
    IEpisodeRepository Episodes { get; }
    IDownloadedVideoRepository DownloadedVideos { get; }
    IActivityLogRepository ActivityLogs { get; }
    IDownloadQueueRepository DownloadQueue { get; }
    
    Task<int> SaveChangesAsync(CancellationToken ct = default);
    Task BeginTransactionAsync(CancellationToken ct = default);
    Task CommitTransactionAsync(CancellationToken ct = default);
    Task RollbackTransactionAsync(CancellationToken ct = default);
}

File Structure

Castr/Data/
├── Repositories/
│   ├── IFeedRepository.cs
│   ├── FeedRepository.cs
│   ├── IEpisodeRepository.cs
│   ├── EpisodeRepository.cs
│   ├── IDownloadedVideoRepository.cs
│   ├── DownloadedVideoRepository.cs
│   ├── IActivityLogRepository.cs
│   ├── ActivityLogRepository.cs
│   ├── IDownloadQueueRepository.cs
│   └── DownloadQueueRepository.cs
├── IUnitOfWork.cs
└── UnitOfWork.cs

Tasks

  • Create repository interfaces
  • Implement FeedRepository
  • Implement EpisodeRepository
  • Implement DownloadedVideoRepository
  • Implement ActivityLogRepository
  • Implement DownloadQueueRepository
  • Implement UnitOfWork pattern
  • Register repositories in DI container
  • Add cancellation token support throughout

Acceptance Criteria

  • All repositories implement their interfaces
  • Repositories use async/await with EF Core
  • Cancellation tokens supported
  • Transaction support via UnitOfWork
  • Proper DI registration
## Parent Epic Part of #58 - Migrate to Entity Framework Core ## Description Implement the repository pattern to abstract data access and make the codebase easier to test and maintain. ## Repository Interfaces ```csharp // Data/Repositories/IFeedRepository.cs public interface IFeedRepository { Task<List<Feed>> GetAllAsync(CancellationToken ct = default); Task<Feed?> GetByIdAsync(int id, CancellationToken ct = default); Task<Feed?> GetByNameAsync(string name, CancellationToken ct = default); Task<Feed> AddAsync(Feed feed, CancellationToken ct = default); Task UpdateAsync(Feed feed, CancellationToken ct = default); Task DeleteAsync(int id, CancellationToken ct = default); } // Data/Repositories/IEpisodeRepository.cs public interface IEpisodeRepository { Task<List<Episode>> GetByFeedIdAsync(int feedId, CancellationToken ct = default); Task<Episode?> GetByIdAsync(int id, CancellationToken ct = default); Task<Episode?> GetByFilenameAsync(int feedId, string filename, CancellationToken ct = default); Task AddAsync(Episode episode, CancellationToken ct = default); Task AddRangeAsync(IEnumerable<Episode> episodes, CancellationToken ct = default); Task UpdateAsync(Episode episode, CancellationToken ct = default); Task<int> GetCountByFeedIdAsync(int feedId, CancellationToken ct = default); } // Data/Repositories/IDownloadedVideoRepository.cs public interface IDownloadedVideoRepository { Task<bool> ExistsAsync(int feedId, string videoId, CancellationToken ct = default); Task<HashSet<string>> GetVideoIdsByFeedAsync(int feedId, CancellationToken ct = default); Task AddAsync(DownloadedVideo video, CancellationToken ct = default); Task RemoveAsync(int feedId, string videoId, CancellationToken ct = default); } // Data/Repositories/IActivityLogRepository.cs public interface IActivityLogRepository { Task LogAsync(int? feedId, string activityType, string message, string? details = null, CancellationToken ct = default); Task<List<ActivityLog>> GetRecentAsync(int? feedId = null, int count = 100, CancellationToken ct = default); } // Data/Repositories/IDownloadQueueRepository.cs public interface IDownloadQueueRepository { Task<DownloadQueueItem> AddAsync(int feedId, string videoId, string? title, CancellationToken ct = default); Task UpdateProgressAsync(int id, string status, int percent, string? error = null, CancellationToken ct = default); Task<List<DownloadQueueItem>> GetAllAsync(int? feedId = null, CancellationToken ct = default); Task<DownloadQueueItem?> GetAsync(int feedId, string videoId, CancellationToken ct = default); Task RemoveAsync(int id, CancellationToken ct = default); } ``` ## Repository Implementations ```csharp // Data/Repositories/FeedRepository.cs public class FeedRepository : IFeedRepository { private readonly CastrDbContext _context; public FeedRepository(CastrDbContext context) { _context = context; } public async Task<List<Feed>> GetAllAsync(CancellationToken ct = default) { return await _context.Feeds .OrderBy(f => f.Name) .ToListAsync(ct); } public async Task<Feed?> GetByNameAsync(string name, CancellationToken ct = default) { return await _context.Feeds .FirstOrDefaultAsync(f => f.Name == name, ct); } // ... other methods } ``` ## Unit of Work (Optional) ```csharp // Data/IUnitOfWork.cs public interface IUnitOfWork : IDisposable { IFeedRepository Feeds { get; } IEpisodeRepository Episodes { get; } IDownloadedVideoRepository DownloadedVideos { get; } IActivityLogRepository ActivityLogs { get; } IDownloadQueueRepository DownloadQueue { get; } Task<int> SaveChangesAsync(CancellationToken ct = default); Task BeginTransactionAsync(CancellationToken ct = default); Task CommitTransactionAsync(CancellationToken ct = default); Task RollbackTransactionAsync(CancellationToken ct = default); } ``` ## File Structure ``` Castr/Data/ ├── Repositories/ │ ├── IFeedRepository.cs │ ├── FeedRepository.cs │ ├── IEpisodeRepository.cs │ ├── EpisodeRepository.cs │ ├── IDownloadedVideoRepository.cs │ ├── DownloadedVideoRepository.cs │ ├── IActivityLogRepository.cs │ ├── ActivityLogRepository.cs │ ├── IDownloadQueueRepository.cs │ └── DownloadQueueRepository.cs ├── IUnitOfWork.cs └── UnitOfWork.cs ``` ## Tasks - [ ] Create repository interfaces - [ ] Implement FeedRepository - [ ] Implement EpisodeRepository - [ ] Implement DownloadedVideoRepository - [ ] Implement ActivityLogRepository - [ ] Implement DownloadQueueRepository - [ ] Implement UnitOfWork pattern - [ ] Register repositories in DI container - [ ] Add cancellation token support throughout ## Acceptance Criteria - [ ] All repositories implement their interfaces - [ ] Repositories use async/await with EF Core - [ ] Cancellation tokens supported - [ ] Transaction support via UnitOfWork - [ ] Proper DI registration
joezombie commented 2026-01-26 22:25:35 +00:00 (Migrated from github.com)

Completed: Implemented repository pattern with 4 repositories (FeedRepository, EpisodeRepository, DownloadRepository, ActivityRepository) and their interfaces for clean data access abstraction.

Completed: Implemented repository pattern with 4 repositories (FeedRepository, EpisodeRepository, DownloadRepository, ActivityRepository) and their interfaces for clean data access abstraction.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joezombie/castr#61
No description provided.