Repository pattern abstractions that define contracts for data access operations, enabling clean separation between domain logic and data persistence concerns while supporting Domain-Driven Design (DDD) and Onion Architecture principles.
DKNet.EfCore.Repos.Abstractions provides the fundamental contracts and interfaces for implementing the Repository pattern in Entity Framework Core applications. It defines clear separation between read and write operations, supports projection patterns, and enables testable data access layers.
The Repository Abstractions implement the Repository pattern as defined in DDD, providing a clean separation between the domain and infrastructure layers:
┌─────────────────────────────────────────────────────────────────┐
│ 🌐 Presentation Layer │
│ (Controllers, API Endpoints) │
│ │
│ No direct dependencies on repository abstractions │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────┴───────────────────────────────────────┐
│ 🎯 Application Layer │
│ (Use Cases, Application Services) │
│ │
│ Uses: IRepository<T> for orchestrating domain operations │
│ Benefits: Clear contracts, easy testing, dependency injection │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────┴───────────────────────────────────────┐
│ 💼 Domain Layer │
│ (Entities, Aggregates, Domain Services) │
│ │
│ 📋 Defines: ICustomerRepository, IOrderRepository │
│ 📝 Extends: IRepository<T> with domain-specific operations │
│ 🏷️ Benefits: Technology-agnostic, testable, focused on domain │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────┴───────────────────────────────────────┐
│ 🗄️ Infrastructure Layer │
│ (Data Access, Persistence) │
│ │
│ 🗃️ Implements: CustomerRepository : ICustomerRepository │
│ 📊 Implements: OrderRepository : IOrderRepository │
│ ⚙️ Uses: EF Core, DbContext, specific database technologies │
└─────────────────────────────────────────────────────────────────┘
dotnet add package DKNet.EfCore.Repos.Abstractions
using DKNet.EfCore.Repos.Abstractions;
using DKNet.EfCore.Abstractions.Entities;
// Domain entity
public class Customer : Entity
{
public string FirstName { get; private set; } = null!;
public string LastName { get; private set; } = null!;
public string Email { get; private set; } = null!;
public CustomerStatus Status { get; private set; }
// Domain methods...
}
// Domain-specific repository interface (defined in Domain layer)
public interface ICustomerRepository : IRepository<Customer>
{
// Domain-specific query methods
Task<Customer?> GetByEmailAsync(string email);
Task<IEnumerable<Customer>> GetActiveCustomersAsync();
Task<IEnumerable<Customer>> GetCustomersByStatusAsync(CustomerStatus status);
Task<bool> EmailExistsAsync(string email, Guid? excludeCustomerId = null);
// Projection methods for read models
Task<CustomerSummaryDto?> GetCustomerSummaryAsync(Guid customerId);
Task<IEnumerable<CustomerListDto>> GetCustomerListAsync(int page, int pageSize);
}
// DTOs for projections
public record CustomerSummaryDto(Guid Id, string FullName, string Email, int OrderCount);
public record CustomerListDto(Guid Id, string FullName, string Email, CustomerStatus Status);
using DKNet.EfCore.Repos.Abstractions;
public class CustomerQueryService
{
private readonly IReadRepository<Customer> _customerReadRepository;
public CustomerQueryService(IReadRepository<Customer> customerReadRepository)
{
_customerReadRepository = customerReadRepository;
}
// Basic querying with IQueryable
public async Task<List<Customer>> GetActiveCustomersAsync()
{
return await _customerReadRepository
.Gets()
.Where(c => c.Status == CustomerStatus.Active)
.OrderBy(c => c.LastName)
.ToListAsync();
}
// Efficient projection queries
public async Task<List<CustomerListDto>> GetCustomerListAsync()
{
return await _customerReadRepository
.GetProjection<CustomerListDto>()
.Where(c => c.Status == CustomerStatus.Active)
.OrderBy(c => c.FullName)
.ToListAsync();
}
// Find by ID
public async Task<Customer?> GetCustomerAsync(Guid id)
{
return await _customerReadRepository.FindAsync(id);
}
// Find by filter
public async Task<Customer?> GetCustomerByEmailAsync(string email)
{
return await _customerReadRepository.FindAsync(c => c.Email == email);
}
}
using DKNet.EfCore.Repos.Abstractions;
public class CustomerManagementService
{
private readonly IWriteRepository<Customer> _customerWriteRepository;
public CustomerManagementService(IWriteRepository<Customer> customerWriteRepository)
{
_customerWriteRepository = customerWriteRepository;
}
// Single entity operations
public async Task<Result> CreateCustomerAsync(string firstName, string lastName, string email)
{
try
{
var customer = Customer.Create(firstName, lastName, email);
_customerWriteRepository.Add(customer);
await _customerWriteRepository.SaveChangesAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to create customer: {ex.Message}");
}
}
// Batch operations
public async Task<Result> CreateMultipleCustomersAsync(IEnumerable<CreateCustomerRequest> requests)
{
try
{
var customers = requests.Select(r => Customer.Create(r.FirstName, r.LastName, r.Email));
_customerWriteRepository.AddRange(customers);
await _customerWriteRepository.SaveChangesAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure($"Failed to create customers: {ex.Message}");
}
}
// Transaction management
public async Task<Result> TransferCustomerDataAsync(Guid fromCustomerId, Guid toCustomerId)
{
using var transaction = await _customerWriteRepository.BeginTransactionAsync();
try
{
// Complex business operation requiring transaction
var fromCustomer = await _customerWriteRepository.FindAsync(fromCustomerId);
var toCustomer = await _customerWriteRepository.FindAsync(toCustomerId);
if (fromCustomer == null || toCustomer == null)
return Result.Failure("Customer not found");
// Perform business logic
fromCustomer.DeactivateAccount();
toCustomer.MergeDataFrom(fromCustomer);
_customerWriteRepository.Update(fromCustomer);
_customerWriteRepository.Update(toCustomer);
await _customerWriteRepository.SaveChangesAsync();
await transaction.CommitAsync();
return Result.Success();
}
catch (Exception ex)
{
await transaction.RollbackAsync();
return Result.Failure($"Failed to transfer customer data: {ex.Message}");
}
}
}
using DKNet.EfCore.Repos.Abstractions;
public class CustomerService
{
private readonly IRepository<Customer> _customerRepository;
public CustomerService(IRepository<Customer> customerRepository)
{
_customerRepository = customerRepository;
}
// Create
public async Task<Result<Guid>> CreateCustomerAsync(CreateCustomerRequest request)
{
// Check if email already exists
var existingCustomer = await _customerRepository.FindAsync(c => c.Email == request.Email);
if (existingCustomer != null)
return Result<Guid>.Failure("Email already exists");
var customer = Customer.Create(request.FirstName, request.LastName, request.Email);
_customerRepository.Add(customer);
await _customerRepository.SaveChangesAsync();
return Result<Guid>.Success(customer.Id);
}
// Read
public async Task<Result<CustomerDto>> GetCustomerAsync(Guid id)
{
var customer = await _customerRepository.FindAsync(id);
if (customer == null)
return Result<CustomerDto>.Failure("Customer not found");
return Result<CustomerDto>.Success(MapToDto(customer));
}
// Update
public async Task<Result> UpdateCustomerAsync(Guid id, UpdateCustomerRequest request)
{
var customer = await _customerRepository.FindAsync(id);
if (customer == null)
return Result.Failure("Customer not found");
customer.UpdateDetails(request.FirstName, request.LastName);
_customerRepository.Update(customer);
await _customerRepository.SaveChangesAsync();
return Result.Success();
}
// Delete
public async Task<Result> DeleteCustomerAsync(Guid id)
{
var customer = await _customerRepository.FindAsync(id);
if (customer == null)
return Result.Failure("Customer not found");
_customerRepository.Delete(customer);
await _customerRepository.SaveChangesAsync();
return Result.Success();
}
// List with projections
public async Task<PagedResult<CustomerListDto>> GetCustomersAsync(int page, int pageSize)
{
var query = _customerRepository.GetProjection<CustomerListDto>();
var totalCount = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<CustomerListDto>(items, totalCount, page, pageSize);
}
}
using DKNet.EfCore.Repos.Abstractions;
// Implementation in Infrastructure layer
public class CustomerRepository : ICustomerRepository
{
private readonly IRepository<Customer> _baseRepository;
public CustomerRepository(IRepository<Customer> baseRepository)
{
_baseRepository = baseRepository;
}
// Delegate base operations
public IQueryable<Customer> Gets() => _baseRepository.Gets();
public IQueryable<TModel> GetProjection<TModel>() where TModel : class => _baseRepository.GetProjection<TModel>();
public ValueTask<Customer?> FindAsync(params object[] id) => _baseRepository.FindAsync(id);
public Task<Customer?> FindAsync(Expression<Func<Customer, bool>> filter, CancellationToken cancellationToken = default) => _baseRepository.FindAsync(filter, cancellationToken);
public void Add(Customer entity) => _baseRepository.Add(entity);
public void AddRange(IEnumerable<Customer> entities) => _baseRepository.AddRange(entities);
public void Update(Customer entity) => _baseRepository.Update(entity);
public void UpdateRange(IEnumerable<Customer> entities) => _baseRepository.UpdateRange(entities);
public void Delete(Customer entity) => _baseRepository.Delete(entity);
public void DeleteRange(IEnumerable<Customer> entities) => _baseRepository.DeleteRange(entities);
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) => _baseRepository.SaveChangesAsync(cancellationToken);
public Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) => _baseRepository.BeginTransactionAsync(cancellationToken);
// Domain-specific implementations
public async Task<Customer?> GetByEmailAsync(string email)
{
return await _baseRepository.FindAsync(c => c.Email.ToLower() == email.ToLower());
}
public async Task<IEnumerable<Customer>> GetActiveCustomersAsync()
{
return await _baseRepository
.Gets()
.Where(c => c.Status == CustomerStatus.Active)
.OrderBy(c => c.LastName)
.ToListAsync();
}
public async Task<IEnumerable<Customer>> GetCustomersByStatusAsync(CustomerStatus status)
{
return await _baseRepository
.Gets()
.Where(c => c.Status == status)
.ToListAsync();
}
public async Task<bool> EmailExistsAsync(string email, Guid? excludeCustomerId = null)
{
var query = _baseRepository.Gets().Where(c => c.Email.ToLower() == email.ToLower());
if (excludeCustomerId.HasValue)
query = query.Where(c => c.Id != excludeCustomerId.Value);
return await query.AnyAsync();
}
public async Task<CustomerSummaryDto?> GetCustomerSummaryAsync(Guid customerId)
{
return await _baseRepository
.GetProjection<CustomerSummaryDto>()
.FirstOrDefaultAsync(c => c.Id == customerId);
}
public async Task<IEnumerable<CustomerListDto>> GetCustomerListAsync(int page, int pageSize)
{
return await _baseRepository
.GetProjection<CustomerListDto>()
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
}
using Moq;
using DKNet.EfCore.Repos.Abstractions;
public class CustomerServiceTests
{
private readonly Mock<ICustomerRepository> _mockRepository;
private readonly CustomerService _service;
public CustomerServiceTests()
{
_mockRepository = new Mock<ICustomerRepository>();
_service = new CustomerService(_mockRepository.Object);
}
[Fact]
public async Task CreateCustomerAsync_WithValidData_ShouldReturnSuccess()
{
// Arrange
var request = new CreateCustomerRequest("John", "Doe", "john@example.com");
_mockRepository.Setup(r => r.FindAsync(It.IsAny<Expression<Func<Customer, bool>>>(), default))
.ReturnsAsync((Customer?)null);
_mockRepository.Setup(r => r.SaveChangesAsync(default))
.ReturnsAsync(1);
// Act
var result = await _service.CreateCustomerAsync(request);
// Assert
Assert.True(result.IsSuccess);
_mockRepository.Verify(r => r.Add(It.IsAny<Customer>()), Times.Once);
_mockRepository.Verify(r => r.SaveChangesAsync(default), Times.Once);
}
[Fact]
public async Task CreateCustomerAsync_WithExistingEmail_ShouldReturnFailure()
{
// Arrange
var request = new CreateCustomerRequest("John", "Doe", "john@example.com");
var existingCustomer = Customer.Create("Jane", "Doe", "john@example.com");
_mockRepository.Setup(r => r.FindAsync(It.IsAny<Expression<Func<Customer, bool>>>(), default))
.ReturnsAsync(existingCustomer);
// Act
var result = await _service.CreateCustomerAsync(request);
// Assert
Assert.False(result.IsSuccess);
Assert.Equal("Email already exists", result.Error);
_mockRepository.Verify(r => r.Add(It.IsAny<Customer>()), Times.Never);
}
}
using Microsoft.Extensions.DependencyInjection;
using DKNet.EfCore.Repos.Abstractions;
public void ConfigureServices(IServiceCollection services)
{
// Register DbContext
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
// Register generic repository
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped(typeof(IReadRepository<>), typeof(ReadRepository<>));
services.AddScoped(typeof(IWriteRepository<>), typeof(WriteRepository<>));
// Register domain-specific repositories
services.AddScoped<ICustomerRepository, CustomerRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
// Register application services
services.AddScoped<CustomerService>();
services.AddScoped<OrderService>();
}
GetProjection<T>()
for read-only scenariosIQueryable
for complex queriesDKNet.EfCore.Repos.Abstractions integrates seamlessly with other DKNet components:
💡 Architecture Tip: Use repository abstractions to define clear contracts in your domain layer. This enables dependency inversion and makes your domain logic completely independent of data access technology.