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