Generic repository pattern interfaces with async support, specification integration, and pagination. Provides read/write separation, CRUD operations, and extensible repository contracts for clean data access architecture.
$ dotnet add package Myth.Repository
Myth.Repository is a .NET library that provides a clean, standardized set of repository interfaces for data access. It promotes separation of concerns through read/write segregation, integrates seamlessly with the Specification pattern, and supports pagination out of the box. Perfect for building maintainable, testable, and domain-driven applications.
ISpec<T> from Myth.SpecificationIPaginated<T> resultsdotnet add package Myth.RepositoryFor Entity Framework Core implementation:
dotnet add package Myth.Repository.EntityFrameworkpublic class Product {
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
public string Category { get; set; }
}using Myth.Interfaces.Repositories.Base;
public interface IProductRepository : IReadWriteRepositoryAsync<Product> {
// Add custom methods if needed
}using Myth.Repository.EntityFramework.Repositories;
public class ProductRepository : ReadWriteRepositoryAsync<Product>, IProductRepository {
public ProductRepository( DbContext context ) : base( context ) {
}
}public class ProductService {
private readonly IProductRepository _repository;
public ProductService( IProductRepository repository ) {
_repository = repository;
}
public async Task<Product?> GetProductByIdAsync( Guid id, CancellationToken cancellationToken ) {
return await _repository.FirstOrDefaultAsync( p => p.Id == id, cancellationToken );
}
public async Task<IEnumerable<Product>> GetActiveProductsAsync( CancellationToken cancellationToken ) {
return await _repository.SearchAsync( p => p.IsActive, cancellationToken );
}
public async Task CreateProductAsync( Product product, CancellationToken cancellationToken ) {
await _repository.AddAsync( product, cancellationToken );
}
}Base marker interface for all repository types.
public interface IRepository { }Provides read-only operations for querying data.
public interface IReadRepositoryAsync<TEntity> : IRepository, IAsyncDisposable {
// Queryable access
IQueryable<TEntity> Where( ISpec<TEntity> specification );
IQueryable<TEntity> Where( Expression<Func<TEntity, bool>> predicate );
IQueryable<TEntity> AsQueryable( );
IEnumerable<TEntity> AsEnumerable( );
// Search operations
Task<IEnumerable<TEntity>> SearchAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<IEnumerable<TEntity>> SearchAsync( Expression<Func<TEntity, bool>> filterPredicate, Expression<Func<TEntity, bool>>? orderPredicate = null, CancellationToken cancellationToken = default );
// Paginated search
Task<IPaginated<TEntity>> SearchPaginatedAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<IPaginated<TEntity>> SearchPaginatedAsync( Expression<Func<TEntity, bool>> filterPredicate, Pagination pagination, Expression<Func<TEntity, bool>>? orderPredicate = null, CancellationToken cancellationToken = default );
// Single item retrieval
Task<TEntity?> FirstOrDefaultAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity?> FirstOrDefaultAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<TEntity> FirstAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity> FirstAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<TEntity?> LastOrDefaultAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity?> LastOrDefaultAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<TEntity> LastAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity> LastAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
// Aggregation operations
Task<int> CountAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<int> CountAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<bool> AnyAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<bool> AnyAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<bool> AllAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<bool> AllAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
// Get all
Task<IEnumerable<TEntity>> ToListAsync( CancellationToken cancellationToken = default );
}Provides write operations for modifying data.
public interface IWriteRepositoryAsync<TEntity> : IRepository, IAsyncDisposable {
// Single operations
Task AddAsync( TEntity entity, CancellationToken cancellationToken = default );
Task UpdateAsync( TEntity entity, CancellationToken cancellationToken = default );
Task RemoveAsync( TEntity entity, CancellationToken cancellationToken = default );
// Batch operations
Task AddRangeAsync( IEnumerable<TEntity> entity, CancellationToken cancellationToken = default );
Task UpdateRangeAsync( IEnumerable<TEntity> entities, CancellationToken cancellationToken = default );
Task RemoveRangeAsync( IEnumerable<TEntity> entities, CancellationToken cancellationToken = default );
}Combines both read and write operations in a single interface.
public interface IReadWriteRepositoryAsync<TEntity> : IReadRepositoryAsync<TEntity>, IWriteRepositoryAsync<TEntity>, IAsyncDisposable { }Interface representing paginated results:
public interface IPaginated<T> {
int PageNumber { get; } // Current page number (1-based)
int PageSize { get; } // Number of items per page
int TotalPages { get; } // Total number of pages
int TotalItems { get; } // Total number of items across all pages
IEnumerable<T> Items { get; } // Items in current page
}public class Pagination {
public int PageNumber { get; set; } // Default: 1
public int PageSize { get; set; } // Default: 10
// Static helpers
public static readonly Pagination Default = new( 1, 10 );
public static readonly Pagination All = new( -1, -1 );
}using Myth.Extensions;
// Convert any IEnumerable to paginated result
var items = new List<Product> { /* ... */ };
var paginated = items.AsPaginated( pageSize: 20, skip: 0 );
// Or with Pagination object
var pagination = new Pagination( pageNumber: 2, pageSize: 20 );
var paginated = items.AsPaginated( pagination );public class ProductService {
private readonly IProductRepository _repository;
public ProductService( IProductRepository repository ) {
_repository = repository;
}
public async Task CreateAsync( Product product, CancellationToken ct ) {
await _repository.AddAsync( product, ct );
}
public async Task CreateManyAsync( IEnumerable<Product> products, CancellationToken ct ) {
await _repository.AddRangeAsync( products, ct );
}
public async Task UpdateAsync( Product product, CancellationToken ct ) {
await _repository.UpdateAsync( product, ct );
}
public async Task DeleteAsync( Product product, CancellationToken ct ) {
await _repository.RemoveAsync( product, ct );
}
}public class ProductService {
private readonly IProductRepository _repository;
public async Task<Product?> GetByIdAsync( Guid id, CancellationToken ct ) {
return await _repository.FirstOrDefaultAsync( p => p.Id == id, ct );
}
public async Task<IEnumerable<Product>> GetByCategoryAsync( string category, CancellationToken ct ) {
return await _repository.SearchAsync( p => p.Category == category, ct );
}
public async Task<int> CountActiveProductsAsync( CancellationToken ct ) {
return await _repository.CountAsync( p => p.IsActive, ct );
}
public async Task<bool> HasExpensiveProductsAsync( CancellationToken ct ) {
return await _repository.AnyAsync( p => p.Price > 1000, ct );
}
public async Task<bool> AreAllActiveAsync( CancellationToken ct ) {
return await _repository.AllAsync( p => p.IsActive, ct );
}
}using Myth.Specification;
// Define specifications
public static class ProductSpecifications {
public static ISpec<Product> IsActive( this ISpec<Product> spec ) {
return spec.And( p => p.IsActive );
}
public static ISpec<Product> InCategory( this ISpec<Product> spec, string category ) {
return spec.And( p => p.Category == category );
}
public static ISpec<Product> PriceRange( this ISpec<Product> spec, decimal min, decimal max ) {
return spec.And( p => p.Price >= min && p.Price <= max );
}
public static ISpec<Product> OrderByName( this ISpec<Product> spec ) {
return spec.Order( p => p.Name );
}
}
// Use in repository
public class ProductService {
private readonly IProductRepository _repository;
public async Task<IEnumerable<Product>> GetActiveProductsInCategoryAsync(
string category,
CancellationToken ct ) {
var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.InCategory( category )
.OrderByName( );
return await _repository.SearchAsync( spec, ct );
}
public async Task<IEnumerable<Product>> GetAffordableProductsAsync(
decimal maxPrice,
CancellationToken ct ) {
var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.PriceRange( 0, maxPrice )
.OrderByName( );
return await _repository.SearchAsync( spec, ct );
}
}public class ProductService {
private readonly IProductRepository _repository;
// Paginate with specification
public async Task<IPaginated<Product>> GetProductsPageAsync(
int pageNumber,
int pageSize,
CancellationToken ct ) {
var pagination = new Pagination( pageNumber, pageSize );
var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.OrderByName( )
.Skip( (pageNumber - 1) * pageSize )
.Take( pageSize );
return await _repository.SearchPaginatedAsync( spec, ct );
}
// Paginate with expression
public async Task<IPaginated<Product>> GetProductsByCategoryPageAsync(
string category,
Pagination pagination,
CancellationToken ct ) {
return await _repository.SearchPaginatedAsync(
filterPredicate: p => p.Category == category && p.IsActive,
pagination: pagination,
orderPredicate: p => p.Name,
cancellationToken: ct );
}
// Use paginated results
public async Task DisplayProductsAsync( CancellationToken ct ) {
var result = await GetProductsPageAsync( pageNumber: 1, pageSize: 20, ct );
Console.WriteLine( $"Page {result.PageNumber} of {result.TotalPages}" );
Console.WriteLine( $"Total items: {result.TotalItems}" );
foreach ( var product in result.Items ) {
Console.WriteLine( $"- {product.Name}: ${product.Price}" );
}
}
}public class ProductService {
private readonly IProductRepository _repository;
public async Task<IEnumerable<ProductSummary>> GetProductSummariesAsync( CancellationToken ct ) {
var queryable = _repository
.AsQueryable( )
.Where( p => p.IsActive )
.GroupBy( p => p.Category )
.Select( g => new ProductSummary {
Category = g.Key,
Count = g.Count( ),
AveragePrice = g.Average( p => p.Price )
} );
return await queryable.ToListAsync( ct );
}
}// Read-only service
public class ProductQueryService {
private readonly IReadRepositoryAsync<Product> _repository;
public ProductQueryService( IReadRepositoryAsync<Product> repository ) {
_repository = repository;
}
public async Task<IEnumerable<Product>> GetAllAsync( CancellationToken ct ) {
return await _repository.ToListAsync( ct );
}
}
// Write-only service
public class ProductCommandService {
private readonly IWriteRepositoryAsync<Product> _repository;
public ProductCommandService( IWriteRepositoryAsync<Product> repository ) {
_repository = repository;
}
public async Task CreateAsync( Product product, CancellationToken ct ) {
await _repository.AddAsync( product, ct );
}
}var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.InCategory( "Electronics" )
.PriceRange( 100, 500 )
.OrderByName( )
.Take( 50 );
var products = await _repository.SearchAsync( spec, cancellationToken );public class GetProductsQueryHandler : IQueryHandler<GetProductsQuery, IEnumerable<Product>> {
private readonly IReadRepositoryAsync<Product> _repository;
public async Task<QueryResult<IEnumerable<Product>>> HandleAsync(
GetProductsQuery query,
CancellationToken cancellationToken ) {
var products = await _repository.SearchAsync(
p => p.Category == query.Category,
cancellationToken );
return QueryResult<IEnumerable<Product>>.Success( products );
}
}public class ProductCommandService {
private readonly IWriteRepositoryAsync<Product> _repository;
private readonly IValidator _validator;
public async Task CreateAsync( Product product, CancellationToken ct ) {
await _validator.ValidateAsync( product, ValidationContextKey.Create, ct );
await _repository.AddAsync( product, ct );
}
}// Aggregate Root
public class Order : IAggregateRoot {
public Guid Id { get; private set; }
private List<OrderItem> _items = new( );
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly( );
public void AddItem( Product product, int quantity ) {
_items.Add( new OrderItem( product, quantity ) );
}
}
// Repository in domain layer (interface)
public interface IOrderRepository : IReadWriteRepositoryAsync<Order> {
Task<Order?> GetByIdWithItemsAsync( Guid id, CancellationToken ct );
}
// Implementation in infrastructure layer
public class OrderRepository : ReadWriteRepositoryAsync<Order>, IOrderRepository {
public OrderRepository( DbContext context ) : base( context ) { }
public async Task<Order?> GetByIdWithItemsAsync( Guid id, CancellationToken ct ) {
return await AsQueryable( )
.Include( o => o.Items )
.FirstOrDefaultAsync( o => o.Id == id, ct );
}
}See Myth.Repository.EntityFramework for IUnitOfWorkRepository implementation.
public class OrderService {
private readonly IUnitOfWorkRepository _unitOfWork;
public async Task ProcessOrderAsync( Order order, CancellationToken ct ) {
await _unitOfWork.BeginTransactionAsync( ct );
try {
await _unitOfWork.AddAsync( order, ct );
await _unitOfWork.SaveChangesAsync( ct );
await _unitOfWork.CommitTransactionAsync( ct );
}
catch {
await _unitOfWork.RollbackTransactionAsync( ct );
throw;
}
}
}await using or dependency injection for automatic disposalSearchPaginatedAsync to avoid loading too much datapublic class ProductServiceTests {
private readonly Mock<IProductRepository> _mockRepository;
private readonly ProductService _service;
public ProductServiceTests( ) {
_mockRepository = new Mock<IProductRepository>( );
_service = new ProductService( _mockRepository.Object );
}
[Fact]
public async Task GetByIdAsync_ShouldReturnProduct_WhenExists( ) {
var productId = Guid.NewGuid( );
var product = new Product { Id = productId, Name = "Test" };
_mockRepository
.Setup( r => r.FirstOrDefaultAsync( It.IsAny<Expression<Func<Product, bool>>>( ), default ) )
.ReturnsAsync( product );
var result = await _service.GetByIdAsync( productId, default );
result.Should( ).NotBeNull( );
result.Id.Should( ).Be( productId );
}
}This project is licensed under the Apache License 2.0 - see the LICENSE for details.