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¶
- Always operate at Aggregate Root level
- Only persist and retrieve aggregate roots directly
-
Child entities are managed through their aggregate root
-
Use async methods
- Prefer
*Asyncmethods for all repository operations -
Always use
ConfigureAwait(false)in library code -
Wrap operations in Unit of Work
- All mutations (Insert, Update, Delete) should be within
unitOfWork.ExecuteTransactional() -
This ensures atomicity and consistency
-
Use Specification Pattern for complex queries
- Encapsulate query logic in specifications
-
Enables reuse and testability
-
Handle null returns
- Always check for null when using
GetByIdAsync - Throw domain exceptions for not found scenarios
Don'ts¶
- Don't expose IQueryable or DbSet
- This leaks persistence details to application layer
-
Breaks Clean Architecture boundaries
-
Don't access child entities directly
- Always work through aggregate roots
-
Maintains consistency boundaries
-
Don't perform business logic in repositories
- Repositories are pure data access
-
Business logic belongs in domain services or processors
-
Don't forget transaction boundaries
- Always use Unit of Work for multi-step operations
-
Prevents partial updates
-
Don't mix sync and async
- Choose async pattern and stick with it
- Avoid
.Resultor.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¶
- Clean Architecture: clean-architecture.md
- Persistence and Data Modeling: persistence-and-data-modeling.md
- Specification Pattern: specification-pattern.md
- Unit of Work Pattern: persistence-and-data-modeling.md
- NHibernate: nhibernate.md
- MongoDB: mongodb.md
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.