Repository Pattern Implementation
Conduit implements the Repository Pattern to abstract and encapsulate data access logic. This approach provides a clean separation between the data access layer and business logic, making the code more maintainable and testable.
Overview
The Repository Pattern provides several benefits:
- Abstraction: Hides data access implementation details
- Decoupling: Reduces dependencies between components
- Testability: Simplifies unit testing with mock repositories
- Consistency: Provides a uniform way to access data
- Centralization: Centralizes data access logic and validation
Implementation Structure
Core Interfaces
Generic Repository Interface
The IRepository<T>
interface defines standard CRUD operations:
public interface IRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetByIdAsync(string id);
Task<T> AddAsync(T entity);
Task<bool> UpdateAsync(T entity);
Task<bool> DeleteAsync(string id);
}
Entity-Specific Repositories
Entity-specific repositories extend the generic interface with specialized methods:
public interface IVirtualKeyRepository : IRepository<VirtualKey>
{
Task<VirtualKey> GetByKeyAsync(string key);
Task<IEnumerable<VirtualKey>> GetActiveKeysAsync();
Task<bool> IncrementUsageAsync(string id, decimal cost);
// ...other specialized methods
}
Repository Implementations
Each repository interface has a corresponding implementation that interacts with the database:
public class VirtualKeyRepository : IVirtualKeyRepository
{
private readonly ConfigurationDbContext _dbContext;
public VirtualKeyRepository(ConfigurationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<VirtualKey> GetByKeyAsync(string key)
{
return await _dbContext.VirtualKeys
.Include(vk => vk.AllowedModels)
.FirstOrDefaultAsync(vk => vk.Key == key && vk.IsActive);
}
// ...other method implementations
}
Database Context
The repositories use Entity Framework Core's DbContext
for data access:
public class ConfigurationDbContext : DbContext, IConfigurationDbContext
{
public ConfigurationDbContext(DbContextOptions<ConfigurationDbContext> options)
: base(options)
{
}
public DbSet<VirtualKey> VirtualKeys { get; set; }
public DbSet<ProviderCredential> ProviderCredentials { get; set; }
public DbSet<ModelProviderMapping> ModelProviderMappings { get; set; }
public DbSet<RequestLog> RequestLogs { get; set; }
// ...other entity sets
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure entity relationships and constraints
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ConfigurationDbContext).Assembly);
}
}
Entity Configuration
Entity configurations are defined using the Fluent API:
public class VirtualKeyConfiguration : IEntityTypeConfiguration<VirtualKey>
{
public void Configure(EntityTypeBuilder<VirtualKey> builder)
{
builder.HasKey(vk => vk.Id);
builder.Property(vk => vk.Key)
.IsRequired()
.HasMaxLength(100);
builder.Property(vk => vk.Name)
.IsRequired()
.HasMaxLength(100);
builder.HasIndex(vk => vk.Key)
.IsUnique();
builder.HasMany(vk => vk.AllowedModels)
.WithOne(am => am.VirtualKey)
.HasForeignKey(am => am.VirtualKeyId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Dependency Injection
Repositories are registered in the dependency injection container:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRepositories(this IServiceCollection services,
RepositoryPatternOptions options)
{
// Register DB context
services.AddDbContext<ConfigurationDbContext>(opt =>
opt.UseSqlite(options.ConnectionString));
// Register repositories
services.AddScoped<IVirtualKeyRepository, VirtualKeyRepository>();
services.AddScoped<IProviderCredentialRepository, ProviderCredentialRepository>();
services.AddScoped<IModelProviderMappingRepository, ModelProviderMappingRepository>();
services.AddScoped<IRequestLogRepository, RequestLogRepository>();
// ...other repositories
return services;
}
}
Using Repositories
Services depend on repository interfaces, not implementations:
public class VirtualKeyService : IVirtualKeyService
{
private readonly IVirtualKeyRepository _virtualKeyRepository;
public VirtualKeyService(IVirtualKeyRepository virtualKeyRepository)
{
_virtualKeyRepository = virtualKeyRepository;
}
public async Task<VirtualKeyDto> CreateVirtualKeyAsync(CreateVirtualKeyRequestDto request)
{
// Business logic...
var virtualKey = new VirtualKey
{
Name = request.Name,
Description = request.Description,
Key = GenerateKey(),
// ...other properties
};
var result = await _virtualKeyRepository.AddAsync(virtualKey);
// Map to DTO and return...
}
// ...other methods
}
Unit Testing
The Repository Pattern simplifies unit testing by allowing repository interfaces to be mocked:
public class VirtualKeyServiceTests
{
private readonly Mock<IVirtualKeyRepository> _mockRepository;
private readonly IVirtualKeyService _service;
public VirtualKeyServiceTests()
{
_mockRepository = new Mock<IVirtualKeyRepository>();
_service = new VirtualKeyService(_mockRepository.Object);
}
[Fact]
public async Task CreateVirtualKey_WithValidRequest_ReturnsVirtualKey()
{
// Arrange
var request = new CreateVirtualKeyRequestDto
{
Name = "Test Key",
Description = "For testing"
};
_mockRepository
.Setup(r => r.AddAsync(It.IsAny<VirtualKey>()))
.ReturnsAsync((VirtualKey vk) => vk);
// Act
var result = await _service.CreateVirtualKeyAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(request.Name, result.Name);
_mockRepository.Verify(r => r.AddAsync(It.IsAny<VirtualKey>()), Times.Once);
}
}
Advanced Features
Generic Repository Base
A generic repository base class reduces code duplication:
public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
protected readonly ConfigurationDbContext DbContext;
protected RepositoryBase(ConfigurationDbContext dbContext)
{
DbContext = dbContext;
}
public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await DbContext.Set<T>().ToListAsync();
}
public virtual async Task<T> GetByIdAsync(string id)
{
return await DbContext.Set<T>().FindAsync(id);
}
public virtual async Task<T> AddAsync(T entity)
{
DbContext.Set<T>().Add(entity);
await DbContext.SaveChangesAsync();
return entity;
}
// ...other methods
}
Specification Pattern
For complex queries, Conduit uses the Specification Pattern:
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
List<string> IncludeStrings { get; }
}
public abstract class BaseSpecification<T> : ISpecification<T>
{
public Expression<Func<T, bool>> Criteria { get; private set; }
public List<Expression<Func<T, object>>> Includes { get; } = new List<Expression<Func<T, object>>>();
public List<string> IncludeStrings { get; } = new List<string>();
protected BaseSpecification(Expression<Func<T, bool>> criteria)
{
Criteria = criteria;
}
protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
protected virtual void AddInclude(string includeString)
{
IncludeStrings.Add(includeString);
}
}
Usage example:
public class ActiveVirtualKeysSpecification : BaseSpecification<VirtualKey>
{
public ActiveVirtualKeysSpecification()
: base(vk => vk.IsActive)
{
AddInclude(vk => vk.AllowedModels);
}
}
Next Steps
- API Gateway: Learn about the API Gateway
- Virtual Keys: Learn about virtual key management
- Database Setup: Configure database settings