Skip to main content
Repositories provide an abstraction over data access, allowing the Application layer to work with domain entities without coupling to specific persistence technologies.

Core Interface

IRepository<TEntity>

Base repository interface providing standard CRUD operations.
using System.Linq.Expressions;

namespace Core.Application.Repositories
{
    public interface IRepository<TEntity>
    {
        object Add(TEntity entity);
        Task<object> AddAsync(TEntity entity);
        long Count(Expression<Func<TEntity, bool>> filter);
        Task<long> CountAsync(Expression<Func<TEntity, bool>> filter);
        List<TEntity> FindAll();
        Task<List<TEntity>> FindAllAsync();
        TEntity FindOne(params object[] keyValues);
        Task<TEntity> FindOneAsync(params object[] keyValues);
        void Remove(params object[] keyValues);
        void Update(object id, TEntity entity);
    }
}
Source: Core.Application.Repositories/IRepository.cs:5 Type Parameters:
  • TEntity - The domain entity type this repository manages

Repository Methods

Add Operations

Add

Adds a new entity synchronously.
object Add(TEntity entity);
Parameters:
  • entity - The entity to add
Returns: The ID of the created entity (as object) Example:
var entity = new DummyEntity("value1", DummyValues.Value1);
object id = _repository.Add(entity);

AddAsync

Adds a new entity asynchronously.
Task<object> AddAsync(TEntity entity);
Parameters:
  • entity - The entity to add
Returns: Task containing the ID of the created entity Example:
var entity = new DummyEntity("value1", DummyValues.Value1);
object createdId = await _context.AddAsync(entity);
return createdId.ToString();
Source: Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityHandler.cs:32

Query Operations

FindAll

Retrieves all entities synchronously.
List<TEntity> FindAll();
Returns: List of all entities

FindAllAsync

Retrieves all entities asynchronously.
Task<List<TEntity>> FindAllAsync();
Returns: Task containing list of all entities Example:
IList<Domain.Entities.DummyEntity> entities = await _context.FindAllAsync();

return new QueryResult<DummyEntityDto>(
    entities.To<DummyEntityDto>(), 
    entities.Count, 
    request.PageIndex, 
    request.PageSize
);
Source: Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesHandler.cs:13

FindOne

Retrieves a single entity by its key(s) synchronously.
TEntity FindOne(params object[] keyValues);
Parameters:
  • keyValues - The primary key value(s)
Returns: The entity or null if not found

FindOneAsync

Retrieves a single entity by its key(s) asynchronously.
Task<TEntity> FindOneAsync(params object[] keyValues);
Parameters:
  • keyValues - The primary key value(s)
Returns: Task containing the entity or null if not found Example:
Domain.Entities.DummyEntity entity = await _context.FindOneAsync(request.DummyIdProperty) 
    ?? throw new EntityDoesNotExistException();
Source: Application/UseCases/DummyEntity/Commands/UpdateDummyEntity/UpdateDummyEntityHandler.cs:17

Count

Counts entities matching a filter synchronously.
long Count(Expression<Func<TEntity, bool>> filter);
Parameters:
  • filter - Lambda expression to filter entities
Returns: Number of matching entities Example:
long activeCount = _repository.Count(e => e.IsActive);

CountAsync

Counts entities matching a filter asynchronously.
Task<long> CountAsync(Expression<Func<TEntity, bool>> filter);
Parameters:
  • filter - Lambda expression to filter entities
Returns: Task containing number of matching entities Example:
long count = await _repository.CountAsync(e => e.CreatedDate > startDate);

Update Operations

Update

Updates an existing entity.
void Update(object id, TEntity entity);
Parameters:
  • id - The entity ID
  • entity - The updated entity
Returns: Void Example:
Domain.Entities.DummyEntity entity = await _context.FindOneAsync(request.DummyIdProperty) 
    ?? throw new EntityDoesNotExistException();

entity.SetdummyPropertyOne(request.dummyPropertyOne);
entity.SetdummyPropertyTwo(request.dummyPropertyTwo);

_context.Update(request.DummyIdProperty, entity);
Source: Application/UseCases/DummyEntity/Commands/UpdateDummyEntity/UpdateDummyEntityHandler.cs:23

Delete Operations

Remove

Deletes an entity by its key(s).
void Remove(params object[] keyValues);
Parameters:
  • keyValues - The primary key value(s)
Returns: Void Example:
_context.Remove(request.DummyIdProperty);
Source: Application/UseCases/DummyEntity/Commands/DeleteDummyEntity/DeleteDummyEntityHandler.cs:20

Custom Repository Example

IDummyEntityRepository

Application-specific repository extending the base interface.
using Core.Application.Repositories;
using Domain.Entities;

namespace Application.Repositories
{
    /// <summary>
    /// Ejemplo de interface de un repositorio de entidad Dummy
    /// Todo repositorio debe implementar la interfaz <see cref="IRepository{TEntity}"/>
    /// donde <c TEntity> es la entidad de dominio que queremos persistir
    /// </summary>
    public interface IDummyEntityRepository : IRepository<DummyEntity>
    {
        //Aqui se definen propiedades y metodos Custom.
    }
}
Source: Application/Repositories/IDummyEntityRepository.cs:11

Custom Repository Methods

Repositories can extend IRepository<T> with domain-specific methods:
public interface IOrderRepository : IRepository<Order>
{
    // Custom query methods
    Task<List<Order>> FindByCustomerAsync(string customerId);
    Task<List<Order>> FindPendingOrdersAsync();
    Task<Order> FindByOrderNumberAsync(string orderNumber);
    
    // Custom operations
    Task<bool> ExistsAsync(string orderId);
    Task BulkUpdateStatusAsync(List<string> orderIds, OrderStatus newStatus);
}

Repository Pattern Benefits

  • Application layer doesn’t know about EF Core, Dapper, or other data access technologies
  • Easy to swap implementations (SQL Server → PostgreSQL, EF → Dapper)
  • Enables testing with in-memory or mock implementations
  • Works with domain entities, not database models
  • Encapsulates data access logic
  • Maintains clean separation of concerns
  • Easy to create fake/mock implementations
  • No database required for unit tests
  • Predictable behavior in tests
  • Add custom methods per entity type
  • Implement complex queries in infrastructure
  • Support multiple data sources

Usage Patterns

In Command Handlers

Commands use repositories for write operations:
internal sealed class CreateDummyEntityHandler : IRequestCommandHandler<CreateDummyEntityCommand, string>
{
    private readonly IDummyEntityRepository _context;

    public CreateDummyEntityHandler(IDummyEntityRepository dummyEntityRepository)
    {
        _context = dummyEntityRepository ?? throw new ArgumentNullException(nameof(dummyEntityRepository));
    }

    public async Task<string> Handle(CreateDummyEntityCommand request, CancellationToken cancellationToken)
    {
        var entity = new Domain.Entities.DummyEntity(request.dummyPropertyOne, request.dummyPropertyTwo);
        
        // Use repository to persist
        object createdId = await _context.AddAsync(entity);
        
        return createdId.ToString();
    }
}

In Query Handlers

Queries use repositories for read operations:
internal class GetAllDummyEntitiesHandler : IRequestQueryHandler<GetAllDummyEntitiesQuery, QueryResult<DummyEntityDto>>
{
    private readonly IDummyEntityRepository _context;

    public GetAllDummyEntitiesHandler(IDummyEntityRepository context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task<QueryResult<DummyEntityDto>> Handle(
        GetAllDummyEntitiesQuery request, 
        CancellationToken cancellationToken)
    {
        // Use repository to query
        IList<Domain.Entities.DummyEntity> entities = await _context.FindAllAsync();

        return new QueryResult<DummyEntityDto>(
            entities.To<DummyEntityDto>(),
            entities.Count,
            request.PageIndex,
            request.PageSize
        );
    }
}

IRepository vs IReadRepository

While not explicitly shown in the codebase, many DDD implementations separate read and write repositories:
InterfacePurposeMethodsUse In
IRepository<T>Full CRUD accessAdd, Update, Remove, FindCommand handlers
IReadRepository<T>Read-only accessFind methods onlyQuery handlers

Example IReadRepository

public interface IReadRepository<TEntity>
{
    List<TEntity> FindAll();
    Task<List<TEntity>> FindAllAsync();
    TEntity FindOne(params object[] keyValues);
    Task<TEntity> FindOneAsync(params object[] keyValues);
    long Count(Expression<Func<TEntity, bool>> filter);
    Task<long> CountAsync(Expression<Func<TEntity, bool>> filter);
}
Benefits:
  • Enforces CQRS separation at compile time
  • Prevents accidental modifications in query handlers
  • Clearer intent in code

Best Practices

  • One repository per aggregate root
  • Don’t create repositories for value objects or entities within aggregates
  • Access child entities through the aggregate root
  • Prefer async methods for all I/O operations
  • Use Expression<Func<T, bool>> for flexible filtering
  • Return domain entities, not DTOs
  • Use nullable returns (TEntity?) for “find” operations in C# 11+
  • Add methods that express domain concepts: FindOverdueOrders(), GetActiveUsers()
  • Encapsulate complex queries in repository methods
  • Keep Application layer free of query logic
  • Register repositories in DI container
  • Inject repository interfaces, never concrete implementations
  • Use constructor injection in handlers
  • Let infrastructure layer throw exceptions
  • Return null for “not found” scenarios
  • Don’t catch and swallow exceptions in repositories

Build docs developers (and LLMs) love