Skip to main content
The Repository pattern provides a clean abstraction over data access, allowing you to switch between different persistence technologies without changing your application logic. The framework includes implementations for both SQL Server (Entity Framework Core) and MongoDB.

Core Interface

IRepository<TEntity>

The main repository interface that defines standard CRUD operations.
Add
object
Synchronously adds an entity to the repository.Parameters:
  • entity: The entity to add
Returns: The ID of the created entity
AddAsync
Task<object>
Asynchronously adds an entity to the repository.Parameters:
  • entity: The entity to add
Returns: The ID of the created entity
FindOne
TEntity
Finds a single entity by its key values.Parameters:
  • keyValues: The primary key value(s)
Returns: The entity or null if not found
FindOneAsync
Task<TEntity>
Asynchronously finds a single entity by its key values.Parameters:
  • keyValues: The primary key value(s)
Returns: The entity or null if not found
FindAll
List<TEntity>
Retrieves all entities from the repository.Returns: List of all entities
FindAllAsync
Task<List<TEntity>>
Asynchronously retrieves all entities from the repository.Returns: List of all entities
Update
void
Updates an existing entity.Parameters:
  • id: The entity ID
  • entity: The updated entity
Remove
void
Removes an entity by its key values.Parameters:
  • keyValues: The primary key value(s)
Count
long
Counts entities matching a filter expression.Parameters:
  • filter: Lambda expression for filtering
Returns: Count of matching entities
CountAsync
Task<long>
Asynchronously counts entities matching a filter expression.Parameters:
  • filter: Lambda expression for filtering
Returns: Count of matching entities
Core.Application.Repositories/IRepository.cs
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);
    }
}

SQL Server Implementation

The SQL Server implementation uses Entity Framework Core for data access.

BaseRepository<TEntity> (SQL)

Core.Infraestructure.Repositories.Sql/BaseRepository.cs
using Core.Application.Repositories;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;

namespace Core.Infraestructure.Repositories.Sql
{
    public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        public DbContext Context { get; private set; }
        public DbSet<TEntity> Repository { get; private set; }

        public BaseRepository(DbContext context)
        {
            Context = context;
            Repository = Context.Set<TEntity>();
        }

        public async Task<object> AddAsync(TEntity entity)
        {
            await Repository.AddAsync(entity);
            await Context.SaveChangesAsync();

            return (object)Context.Entry(entity).Property("Id").CurrentValue;
        }

        public async Task<List<TEntity>> FindAllAsync()
        {
            return await Repository.ToListAsync();
        }

        public async Task<TEntity> FindOneAsync(params object[] keyValues)
        {
            return await Repository.FindAsync(keyValues);
        }

        public async Task<long> CountAsync(Expression<Func<TEntity, bool>> filter)
        {
            return Convert.ToInt64(await Repository.CountAsync(filter));
        }

        public void Update(object id, TEntity entity)
        {
            TEntity foundEntity = FindOne(id);

            if(foundEntity != null)
            {
                Repository.Update(entity);
                Context.SaveChanges();
            }
        }

        public void Remove(params object[] keyValues)
        {
            TEntity entity = FindOne(keyValues);

            if (entity != null)
            {
                Repository.Remove(entity);
                Context.SaveChanges();
            }
        }
    }
}

Features

Change Tracking

Automatic tracking of entity changes through EF Core.

LINQ Support

Full LINQ query support for complex queries.

Migrations

Database schema management through EF Core migrations.

Transactions

Built-in transaction support via DbContext.

MongoDB Implementation

The MongoDB implementation provides a document-based data access layer.

BaseRepository<TEntity> (MongoDB)

Core.Infraestructure.Repositories.MongoDb/BaseRepository.cs
using Core.Application.Repositories;
using MongoDB.Driver;
using System.Linq.Expressions;

namespace Core.Infraestructure.Repositories.MongoDb
{
    public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        public DbContext Context { get; private set; }
        public IMongoCollection<TEntity> Collection { get; private set; }

        public BaseRepository(DbContext context)
        {
            Context = context;
            Collection = Context.GetCollection<TEntity>();
        }

        public async Task<object> AddAsync(TEntity entity)
        {
            await Collection.InsertOneAsync(entity);
            var property = typeof(TEntity).GetProperty("Id") ?? typeof(TEntity).GetProperty("_id");
            return (object)property?.GetValue(entity);
        }

        public async Task<List<TEntity>> FindAllAsync()
        {
            FilterDefinition<TEntity> filter = new BsonDocumentFilterDefinition<TEntity>([]);
            return await Collection.Find(filter).ToListAsync();
        }

        public async Task<TEntity> FindOneAsync(params object[] keyValues)
        {
            FilterDefinition<TEntity> filter = Builders<TEntity>.Filter.Eq("_id", keyValues[0]);
            return await Collection.Find(filter).SingleOrDefaultAsync();
        }

        public async Task<long> CountAsync(Expression<Func<TEntity, bool>> filter)
        {
            return await Collection.CountDocumentsAsync(filter);
        }

        public void Update(object id, TEntity entity)
        {
            FilterDefinition<TEntity> filter = Builders<TEntity>.Filter.Eq("_id", id);
            Collection.ReplaceOne(filter, entity);
        }

        public void Remove(params object[] keyValues)
        {
            FilterDefinition<TEntity> filter = Builders<TEntity>.Filter.Eq("_id", keyValues[0]);
            Collection.DeleteOne(filter);
        }
    }
}

Features

Schema-less

Flexible document structure without rigid schemas.

High Performance

Optimized for high-throughput operations.

Scalability

Horizontal scaling through sharding.

Native BSON

Efficient binary JSON storage format.

Creating a Domain Repository

Extend the base repository for domain-specific operations:
Application/Repositories/IDummyEntityRepository.cs
using Core.Application.Repositories;
using Domain.Entities;

namespace Application.Repositories
{
    public interface IDummyEntityRepository : IRepository<DummyEntity>
    {
        // Add domain-specific query methods
        Task<List<DummyEntity>> FindByPropertyAsync(string propertyValue);
        Task<bool> ExistsByIdAsync(string id);
    }
}

Implementation (SQL Server)

Infrastructure/Repositories/Sql/DummyEntityRepository.cs
using Application.Repositories;
using Core.Infraestructure.Repositories.Sql;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace Infrastructure.Repositories.Sql
{
    public class DummyEntityRepository : BaseRepository<DummyEntity>, IDummyEntityRepository
    {
        public DummyEntityRepository(DbContext context) : base(context)
        {
        }

        public async Task<List<DummyEntity>> FindByPropertyAsync(string propertyValue)
        {
            return await Repository
                .Where(e => e.DummyPropertyOne == propertyValue)
                .ToListAsync();
        }

        public async Task<bool> ExistsByIdAsync(string id)
        {
            return await Repository.AnyAsync(e => e.Id == id);
        }
    }
}

Implementation (MongoDB)

Infrastructure/Repositories/MongoDb/DummyEntityRepository.cs
using Application.Repositories;
using Core.Infraestructure.Repositories.MongoDb;
using Domain.Entities;
using MongoDB.Driver;

namespace Infrastructure.Repositories.MongoDb
{
    public class DummyEntityRepository : BaseRepository<DummyEntity>, IDummyEntityRepository
    {
        public DummyEntityRepository(DbContext context) : base(context)
        {
        }

        public async Task<List<DummyEntity>> FindByPropertyAsync(string propertyValue)
        {
            var filter = Builders<DummyEntity>.Filter.Eq(e => e.DummyPropertyOne, propertyValue);
            return await Collection.Find(filter).ToListAsync();
        }

        public async Task<bool> ExistsByIdAsync(string id)
        {
            var filter = Builders<DummyEntity>.Filter.Eq("_id", id);
            var count = await Collection.CountDocumentsAsync(filter);
            return count > 0;
        }
    }
}

Registration

Register repositories based on configuration:
Infrastructure/Registrations/InfraestructureServicesRegistration.cs
public static IServiceCollection AddRepositories(
    this IServiceCollection services, 
    IConfiguration configuration)
{
    string dbType = configuration["Configurations:UseDatabase"];

    services.CreateDataBase(dbType, configuration);

    return services;
}

SQL Server Configuration

appsettings.json
{
  "Configurations": {
    "UseDatabase": "SqlServer"
  },
  "ConnectionStrings": {
    "SqlServer": "Server=localhost;Database=HybridDDD;Trusted_Connection=True;"
  }
}

MongoDB Configuration

appsettings.json
{
  "Configurations": {
    "UseDatabase": "MongoDb"
  },
  "ConnectionStrings": {
    "MongoDb": "mongodb://localhost:27017/HybridDDD"
  }
}

Usage in Application Services

Application/UseCases/DummyEntity/Commands/CreateDummyEntityHandler.cs
public class CreateDummyEntityHandler : IRequestCommandHandler<CreateDummyEntityCommand, string>
{
    private readonly IDummyEntityRepository _repository;

    public CreateDummyEntityHandler(IDummyEntityRepository repository)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
    }

    public async Task<string> Handle(CreateDummyEntityCommand request, CancellationToken cancellationToken)
    {
        var entity = new DummyEntity(request.dummyPropertyOne, request.dummyPropertyTwo);
        
        // Check if entity exists
        bool exists = await _repository.ExistsByIdAsync(entity.Id);
        if (exists) throw new EntityDoesExistException();

        // Add entity
        object createdId = await _repository.AddAsync(entity);

        return createdId.ToString();
    }
}

Best Practices

Always inject repository interfaces (IDummyEntityRepository), never concrete implementations. This enables testing and database switching.
Avoid exposing IQueryable from repositories. Return materialized collections to prevent query logic from leaking into the application layer.
For complex queries, create dedicated repository methods rather than using generic FindAll() and filtering in memory.

Advanced Patterns

Unit of Work

For SQL Server, the DbContext acts as a Unit of Work:
public async Task<string> Handle(CreateMultipleEntitiesCommand request, CancellationToken cancellationToken)
{
    using var transaction = await _context.Database.BeginTransactionAsync();
    
    try
    {
        await _repository1.AddAsync(entity1);
        await _repository2.AddAsync(entity2);
        
        await transaction.CommitAsync();
        return "Success";
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

Specification Pattern

Implement reusable query specifications:
public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
}

public static class SpecificationExtensions
{
    public static IQueryable<T> ApplySpecification<T>(
        this IQueryable<T> query, 
        ISpecification<T> spec) where T : class
    {
        var queryableResultWithIncludes = spec.Includes
            .Aggregate(query, (current, include) => current.Include(include));

        return queryableResultWithIncludes.Where(spec.Criteria);
    }
}

Build docs developers (and LLMs) love