Repository Pattern
Overview
The repository pattern in RCommon provides a uniform, provider-agnostic abstraction over data access. Rather than coupling your domain and application code to a specific ORM or database technology, you program against interfaces from RCommon.Persistence.Crud. Swapping from Entity Framework Core to Dapper (or any other supported provider) requires only a configuration change at the composition root.
The abstraction hierarchy works as follows:
IReadOnlyRepository<TEntity>— async query methodsIWriteOnlyRepository<TEntity>— async write methods (add, update, delete)ILinqRepository<TEntity>— combines read, write, and exposesIQueryable<TEntity>plus paging helpersIGraphRepository<TEntity>— extendsILinqRepository<TEntity>with change-tracking control; used with ORM providers such as EF Core
All repository interfaces require the entity to implement IBusinessEntity.
Installation
Install the core persistence package. You will also need at least one provider package (see EF Core, Dapper, or Linq2Db).
dotnet add package RCommon.PersistenceConfiguration
Repositories are registered automatically when you configure a provider. Call WithPersistence<TBuilder> in your application startup:
builder.Services.AddRCommon()
.WithPersistence<EFCorePerisistenceBuilder>(ef =>
{
ef.AddDbContext<LeaveManagementDbContext>(
"LeaveManagement",
options => options.UseSqlServer(connectionString));
ef.SetDefaultDataStore(ds =>
ds.DefaultDataStoreName = "LeaveManagement");
});
When multiple data stores are registered you select which one a repository targets by setting DataStoreName on the repository instance:
public class CreateLeaveTypeCommandHandler
{
private readonly IGraphRepository<LeaveType> _repository;
public CreateLeaveTypeCommandHandler(IGraphRepository<LeaveType> repository)
{
_repository = repository;
_repository.DataStoreName = "LeaveManagement"; // matches the name used in AddDbContext
}
}
Usage
Injecting repositories
Inject the interface that matches your needs. For most domain/application code use IGraphRepository<TEntity> when the provider is EF Core, or ILinqRepository<TEntity> for Linq2Db. Use IReadOnlyRepository<TEntity> or IWriteOnlyRepository<TEntity> when you want to enforce narrower contracts.
public class OrderService
{
private readonly IGraphRepository<Order> _orders;
public OrderService(IGraphRepository<Order> orders)
{
_orders = orders;
}
}
Create
var order = new Order { CustomerId = customerId, Total = 99.99m };
await _orders.AddAsync(order, cancellationToken);
Add multiple entities in one call:
await _orders.AddRangeAsync(newOrders, cancellationToken);
Read
Find by primary key:
var order = await _orders.FindAsync(orderId, cancellationToken);
Find with a lambda expression:
ICollection<Order> pending = await _orders.FindAsync(
o => o.Status == OrderStatus.Pending, cancellationToken);
Find a single entity or default:
Order? draft = await _orders.FindSingleOrDefaultAsync(
o => o.CustomerId == customerId && o.Status == OrderStatus.Draft,
cancellationToken);
Check existence:
bool exists = await _orders.AnyAsync(o => o.Id == orderId, cancellationToken);
Count:
long count = await _orders.GetCountAsync(o => o.Status == OrderStatus.Pending, cancellationToken);
Paging
ILinqRepository<TEntity> and IGraphRepository<TEntity> include paged query methods that return IPaginatedList<TEntity>:
IPaginatedList<Order> page = await _orders.FindAsync(
expression: o => o.CustomerId == customerId,
orderByExpression: o => o.DateCreated,
orderByAscending: false,
pageNumber: 2,
pageSize: 20,
token: cancellationToken);
Or pass a PagedSpecification<TEntity> (see Specifications):
var spec = new PagedSpecification<Order>(
o => o.CustomerId == customerId,
o => o.DateCreated,
orderByAscending: false,
pageNumber: 1,
pageSize: 10);
IPaginatedList<Order> page = await _orders.FindAsync(spec, cancellationToken);
Eager loading
ILinqRepository<TEntity> exposes fluent Include / ThenInclude methods:
var orders = await _orders
.Include(o => o.Lines)
.FindAsync(o => o.CustomerId == customerId, cancellationToken);
Update
order.Status = OrderStatus.Shipped;
await _orders.UpdateAsync(order, cancellationToken);
Delete
Auto-detects soft delete if the entity implements ISoftDelete; otherwise performs a physical delete:
await _orders.DeleteAsync(order, cancellationToken);
Bulk delete by expression:
int affected = await _orders.DeleteManyAsync(
o => o.Status == OrderStatus.Cancelled, cancellationToken);
Override soft-delete behaviour explicitly:
// Force physical delete even when ISoftDelete is implemented
await _orders.DeleteAsync(order, isSoftDelete: false, cancellationToken);
Turning off change tracking
When IGraphRepository<TEntity> is used with EF Core you can disable tracking for read-only scenarios:
_orders.Tracking = false;
var readOnlyOrders = await _orders.FindAsync(o => o.Status == OrderStatus.Active, cancellationToken);
Provider Comparison
Repository Provider Capabilities
| Feature | EF Core | Dapper | Linq2Db |
|---|---|---|---|
| IGraphRepository | ✅ | ❌ | ❌ |
| ILinqRepository | ✅ | ❌ | ✅ |
| IReadOnlyRepository | ✅ | ✅ | ✅ |
| IWriteOnlyRepository | ✅ | ✅ | ✅ |
| ISqlMapperRepository | ❌ | ✅ | ❌ |
| Paging | ✅ | ❌ | ✅ |
| Eager loading | ✅ | ❌ | ✅ |
| Soft delete | ✅ | ✅ | ✅ |
| Change tracking | ✅ | ❌ | ❌ |
| Package | RCommon.EfCore | RCommon.Dapper | RCommon.Linq2Db |
API Summary
| Interface | Purpose |
|---|---|
IReadOnlyRepository<TEntity> | Async query operations: FindAsync, FindSingleOrDefaultAsync, AnyAsync, GetCountAsync |
IWriteOnlyRepository<TEntity> | Async write operations: AddAsync, AddRangeAsync, UpdateAsync, DeleteAsync, DeleteManyAsync |
ILinqRepository<TEntity> | Combines read + write + IQueryable<TEntity> + paging (FindAsync with IPaginatedList) + Include |
IGraphRepository<TEntity> | Extends ILinqRepository with Tracking property for change-tracking control |
ISqlMapperRepository<TEntity> | Raw-SQL repository for Dapper — exposes QueryAsync and ExecuteAsync directly |
INamedDataSource | Base interface exposing DataStoreName property, inherited by all repository interfaces |