Skip to content

Repository Pattern in ConnectSoft Microservice Template

Purpose & Overview

The Repository Pattern is a fundamental design pattern in the ConnectSoft Microservice Template that provides a collection-like abstraction over data persistence. It serves as the sole access point for persisting and retrieving aggregates and entities, enforcing strict separation between domain logic and data access concerns.

Why Repository Pattern?

The Repository Pattern provides several key benefits:

  • Persistence Ignorance: Domain and application layers remain completely unaware of underlying database technologies (SQL Server, MongoDB, etc.)
  • Testability: Repository interfaces enable easy unit testing with mock implementations
  • Flexibility: Swap between different persistence technologies without changing business logic
  • Consistency: Standardized data access patterns across all aggregates
  • Clean Architecture: Maintains proper dependency flow (infrastructure depends on domain, never the reverse)

How It's Implemented in Template

Architecture Overview

The repository pattern is implemented across multiple projects following Clean Architecture principles:

Domain Layer (EntityModel)
Application Layer (DomainModel)
Infrastructure Layer (PersistenceModel.*)
    ├── PersistenceModel (Interfaces)
    ├── PersistenceModel.NHibernate (NHibernate Implementation)
    └── PersistenceModel.MongoDb (MongoDB Implementation)

Core Components

1. Generic Repository Interface

The template uses IGenericRepository<TEntity, TIdentity> from ConnectSoft.Extensions.PersistenceModel as the base interface:

public interface IGenericRepository<TEntity, TIdentity>
    where TEntity : class, IGenericEntity<TIdentity>
{
    // CRUD Operations
    void Insert(TEntity entity);
    Task InsertAsync(TEntity entity, CancellationToken ct);

    void Update(TEntity entity);
    Task UpdateAsync(TEntity entity, CancellationToken ct);

    void Delete(TEntity entity);
    void Delete(TIdentity id);
    Task DeleteAsync(TEntity entity, CancellationToken ct);

    // Query Operations
    TEntity GetById(TIdentity id);
    Task<TEntity> GetByIdAsync(TIdentity id, CancellationToken ct);

    IList<TEntity> GetAll(bool cacheble = false);
    Task<IList<TEntity>> GetAllAsync(bool cacheble = false, CancellationToken ct = default);

    // Specification Pattern Support
    TSpecification Specify<TSpecification>()
        where TSpecification : class, ISpecification<TEntity, TIdentity>;

    // Expression-based Queries
    IList<TEntity> Query(Expression<Func<TEntity, bool>> predicate);
    Task<IList<TEntity>> QueryAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct);
}

2. Aggregate-Specific Repository Interface

Each aggregate root has its own repository interface that extends IGenericRepository:

// Example from IMicroserviceAggregateRootsRepository.cs
public interface IMicroserviceAggregateRootsRepository 
    : IGenericRepository<IMicroserviceAggregateRoot, Guid>
{
    // Additional aggregate-specific methods can be added here
}

3. Repository Implementations

NHibernate Implementation:

// MicroserviceAggregateRootsRepository.cs
public class MicroserviceAggregateRootsRepository(
    IUnitOfWork unitOfWork, 
    ISpecificationLocator specificationLocator)
    : GenericRepository<IMicroserviceAggregateRoot, Guid>(
        unitOfWork, 
        specificationLocator), 
      IMicroserviceAggregateRootsRepository
{
    // Inherits all CRUD and query operations from GenericRepository
}

MongoDB Implementation:

// MicroserviceAggregateRootsMongoDbRepository.cs
public class MicroserviceAggregateRootsMongoDbRepository(
    IUnitOfWork unitOfWork,
    IMongoDatabase mongoDatabase,
    ISpecificationLocator specificationLocator,
    ILogger<MongoDbRepository<IMicroserviceAggregateRoot, Guid>> logger)
    : MongoDbRepository<IMicroserviceAggregateRoot, Guid>(
        unitOfWork, 
        mongoDatabase, 
        specificationLocator, 
        logger), 
      IMicroserviceAggregateRootsRepository
{
    // Inherits MongoDB-specific implementation
}

Project Structure

Project Responsibility
PersistenceModel Defines repository interfaces (IMicroserviceAggregateRootsRepository)
PersistenceModel.NHibernate NHibernate-based repository implementations
PersistenceModel.MongoDb MongoDB-based repository implementations
Extensions.PersistenceModel Base IGenericRepository and GenericRepository abstractions

Code Examples

Basic CRUD Operations

Creating an Entity

public class DefaultMicroserviceAggregateRootsProcessor
{
    private readonly IMicroserviceAggregateRootsRepository repository;
    private readonly IUnitOfWork unitOfWork;

    public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
        CreateMicroserviceAggregateRootInput input, 
        CancellationToken token = default)
    {
        // Check if entity already exists
        var existing = await repository.GetByIdAsync(input.ObjectId, token)
            .ConfigureAwait(false);

        if (existing != null)
        {
            throw new MicroserviceAggregateRootAlreadyExistsException(input.ObjectId);
        }

        // Create new entity
        var newEntity = new MicroserviceAggregateRootEntity
        {
            ObjectId = input.ObjectId
        };

        // Insert within transaction
        unitOfWork.ExecuteTransactional(() =>
        {
            repository.Insert(newEntity);
        });

        return newEntity;
    }
}

Reading an Entity

public async Task<IMicroserviceAggregateRoot> GetByIdAsync(
    Guid id, 
    CancellationToken token = default)
{
    var entity = await repository.GetByIdAsync(id, token)
        .ConfigureAwait(false);

    if (entity == null)
    {
        throw new MicroserviceAggregateRootNotFoundException(id);
    }

    return entity;
}

Updating an Entity

public async Task UpdateMicroserviceAggregateRoot(
    UpdateMicroserviceAggregateRootInput input,
    CancellationToken token = default)
{
    var entity = await repository.GetByIdAsync(input.ObjectId, token)
        .ConfigureAwait(false);

    if (entity == null)
    {
        throw new MicroserviceAggregateRootNotFoundException(input.ObjectId);
    }

    // Modify entity properties
    entity.UpdateProperties(input);

    // Update within transaction
    unitOfWork.ExecuteTransactional(() =>
    {
        repository.Update(entity);
    });
}

Deleting an Entity

public async Task DeleteMicroserviceAggregateRoot(
    DeleteMicroserviceAggregateRootInput input,
    CancellationToken token = default)
{
    var entity = await repository.GetByIdAsync(input.ObjectId, token)
        .ConfigureAwait(false);

    if (entity == null)
    {
        throw new MicroserviceAggregateRootNotFoundException(input.ObjectId);
    }

    // Delete within transaction
    unitOfWork.ExecuteTransactional(() =>
    {
        repository.Delete(entity);
    });
}

Querying with Specifications

The repository pattern integrates seamlessly with the Specification Pattern:

// Using specification for complex queries
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
    .Where(x => x.CreatedOn >= startDate)
    .And(x => x.Status == ActiveStatus);

var results = await specification.ToListAsync(token)
    .ConfigureAwait(false);

Querying with Expressions

For simpler queries, you can use expression predicates:

var results = await repository.QueryAsync(
    x => x.CreatedOn >= DateTimeOffset.UtcNow.AddDays(-30) && 
         x.Status == "Active",
    token)
    .ConfigureAwait(false);

Configuration

Dependency Injection Setup

Repositories are registered in the ApplicationModel project:

// PersistenceModelExtensions.cs
public static IServiceCollection AddMicroservicePersistenceModel(
    this IServiceCollection services)
{
#if UseNHibernate
    // Register NHibernate repositories
    services.AddScoped<IMicroserviceAggregateRootsRepository>(
        provider => provider.GetRequiredService<MicroserviceAggregateRootsRepository>());

    services.AddScoped<MicroserviceAggregateRootsRepository>();
#endif

#if UseMongoDb
    // Register MongoDB repositories
    services.AddScoped<IMicroserviceAggregateRootsRepository>(
        provider => provider.GetRequiredService<MicroserviceAggregateRootsMongoDbRepository>());

    services.AddScoped<MicroserviceAggregateRootsMongoDbRepository>();
#endif

    return services;
}

Keyed Services Support

For microservices using multiple persistence technologies, repositories can be registered as keyed services:

// NHibernate Repository with key
services.AddKeyedScoped<IUnitOfWork, NHibernateUnitOfWork>(
    MicroserviceConstants.NHibernateDIKey);

services.AddKeyedScoped<IMicroserviceAggregateRootsRepository, 
    MicroserviceAggregateRootsKeyedRepository>(
    MicroserviceConstants.NHibernateDIKey);

// MongoDB Repository with key
services.AddKeyedScoped<IUnitOfWork, MongoDbUnitOfWork>(
    MicroserviceConstants.MongoDbDIKey);

services.AddKeyedScoped<IMicroserviceAggregateRootsRepository,
    MicroserviceAggregateRootsMongoDbKeyedRepository>(
    MicroserviceConstants.MongoDbDIKey);

Best Practices

Do's

  1. Always operate at Aggregate Root level
  2. Only persist and retrieve aggregate roots directly
  3. Child entities are managed through their aggregate root

  4. Use async methods

  5. Prefer *Async methods for all repository operations
  6. Always use ConfigureAwait(false) in library code

  7. Wrap operations in Unit of Work

  8. All mutations (Insert, Update, Delete) should be within unitOfWork.ExecuteTransactional()
  9. This ensures atomicity and consistency

  10. Use Specification Pattern for complex queries

  11. Encapsulate query logic in specifications
  12. Enables reuse and testability

  13. Handle null returns

  14. Always check for null when using GetByIdAsync
  15. Throw domain exceptions for not found scenarios

Don'ts

  1. Don't expose IQueryable or DbSet
  2. This leaks persistence details to application layer
  3. Breaks Clean Architecture boundaries

  4. Don't access child entities directly

  5. Always work through aggregate roots
  6. Maintains consistency boundaries

  7. Don't perform business logic in repositories

  8. Repositories are pure data access
  9. Business logic belongs in domain services or processors

  10. Don't forget transaction boundaries

  11. Always use Unit of Work for multi-step operations
  12. Prevents partial updates

  13. Don't mix sync and async

  14. Choose async pattern and stick with it
  15. Avoid .Result or .Wait() in async contexts

Integration Points

Unit of Work

Repositories work closely with Unit of Work pattern:

// Transactional operations require Unit of Work
unitOfWork.ExecuteTransactional(() =>
{
    repository.Insert(newEntity);
    // Multiple repository operations
    // All succeed or all fail
});

Domain Events

Repositories support domain event publishing after persistence:

unitOfWork.ExecuteTransactional(() =>
{
    repository.Insert(aggregate);
});

// Publish domain events after successful persistence
await eventBus.PublishEvent(new AggregateCreatedEvent
{
    AggregateId = aggregate.ObjectId
}, token);

Specifications

Repositories provide fluent specification API:

var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
    .Where(x => x.Status == Active)
    .OrderBy(x => x.CreatedOn)
    .Take(10);

var results = await specification.ToListAsync(token);

Common Scenarios

Scenario 1: Creating a New Aggregate

public async Task<Guid> CreateAggregateAsync(CreateInput input, CancellationToken ct)
{
    // Validate input
    await validator.ValidateAndThrowAsync(input, ct);

    // Check for duplicates
    var existing = await repository.GetByIdAsync(input.ObjectId, ct);
    if (existing != null)
        throw new AggregateAlreadyExistsException(input.ObjectId);

    // Create and persist
    var aggregate = new AggregateRoot(input.ObjectId);
    unitOfWork.ExecuteTransactional(() =>
    {
        repository.Insert(aggregate);
    });

    // Publish event
    await eventBus.PublishEvent(new AggregateCreatedEvent(aggregate.ObjectId), ct);

    return aggregate.ObjectId;
}

Scenario 2: Batch Operations

public async Task CreateMultipleAsync(IEnumerable<CreateInput> inputs, CancellationToken ct)
{
    var aggregates = inputs.Select(i => new AggregateRoot(i.ObjectId)).ToList();

    unitOfWork.ExecuteTransactional(() =>
    {
        foreach (var aggregate in aggregates)
        {
            repository.Insert(aggregate);
        }
    });
}

Scenario 3: Querying with Filters

public async Task<IReadOnlyList<AggregateRoot>> GetActiveAggregatesAsync(
    DateTimeOffset since,
    CancellationToken ct)
{
    return await repository.QueryAsync(
        x => x.Status == Status.Active && x.CreatedOn >= since,
        ct);
}

Scenario 4: Existence Checks

public async Task<bool> ExistsAsync(Guid id, CancellationToken ct)
{
    var entity = await repository.GetByIdAsync(id, ct);
    return entity != null;
}

Troubleshooting

Issue: Repository methods not persisting changes

Cause: Operations not wrapped in Unit of Work transaction.

Solution: Always use unitOfWork.ExecuteTransactional():

// ❌ Wrong - changes may not persist
repository.Insert(entity);

// ✅ Correct - changes will persist
unitOfWork.ExecuteTransactional(() =>
{
    repository.Insert(entity);
});

Issue: Null reference when calling GetByIdAsync

Cause: Entity doesn't exist in database.

Solution: Always check for null and throw domain exception:

var entity = await repository.GetByIdAsync(id, ct);
if (entity == null)
{
    throw new AggregateNotFoundException(id);
}

Issue: Specification not working as expected

Cause: Specification implementation may not be registered in DI container.

Solution: Ensure specification implementations are registered:

services.AddScoped<IMicroserviceAggregateRootsSpecification, 
    MicroserviceAggregateRootsQueryableSpecification>();

Issue: Multiple repository instances in same request

Cause: Repository registered as transient instead of scoped.

Solution: Ensure repositories are registered as scoped services:

// ✅ Correct
services.AddScoped<IMicroserviceAggregateRootsRepository, 
    MicroserviceAggregateRootsRepository>();

// ❌ Wrong for repositories
services.AddTransient<IMicroserviceAggregateRootsRepository, 
    MicroserviceAggregateRootsRepository>();

References

Summary

The Repository Pattern in ConnectSoft Microservice Template:

  • Provides clean abstraction over persistence
  • Enforces Persistence Ignorance principle
  • Supports multiple persistence technologies (NHibernate, MongoDB)
  • Integrates with Unit of Work for transactions
  • Works seamlessly with Specification Pattern for queries
  • Enables testable and maintainable code
  • Maintains Clean Architecture boundaries

This pattern is fundamental to building robust, testable, and maintainable microservices that can evolve and adapt to changing persistence requirements.