Skip to main content
Version: 2.4.1

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 methods
  • IWriteOnlyRepository<TEntity> — async write methods (add, update, delete)
  • ILinqRepository<TEntity> — combines read, write, and exposes IQueryable<TEntity> plus paging helpers
  • IGraphRepository<TEntity> — extends ILinqRepository<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).

NuGet Package
dotnet add package RCommon.Persistence

Configuration​

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

FeatureEF CoreDapperLinq2Db
IGraphRepository✅❌❌
ILinqRepository✅❌✅
IReadOnlyRepository✅✅✅
IWriteOnlyRepository✅✅✅
ISqlMapperRepository❌✅❌
Paging✅❌✅
Eager loading✅❌✅
Soft delete✅✅✅
Change tracking✅❌❌
PackageRCommon.EfCoreRCommon.DapperRCommon.Linq2Db

API Summary​

InterfacePurpose
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
INamedDataSourceBase interface exposing DataStoreName property, inherited by all repository interfaces
RCommonRCommon