Persistence and Data Modeling in ConnectSoft Microservice Template¶
Purpose & Overview¶
Persistence is a fundamental infrastructure concern in the ConnectSoft Microservice Template, providing reliable, scalable, and maintainable data storage capabilities. The template treats persistence as an infrastructure service that serves the domain layer while maintaining strict separation of concerns.
Key Principles¶
The persistence layer follows these core principles:
- Persistence Ignorance: Domain models remain completely unaware of underlying storage technologies
- Framework Agnostic: Application and domain layers depend only on abstractions, not specific ORMs or databases
- Flexibility: Support for multiple persistence technologies (NHibernate for SQL, MongoDB for NoSQL)
- Testability: Repository interfaces enable easy unit and integration testing
- Scalability: Support for CQRS, Event Sourcing, and polyglot persistence patterns
- Transaction Safety: Unit of Work pattern ensures atomic operations and consistency
Persistence Philosophy
Persistence is treated as an infrastructure concern—it serves the domain but never controls or pollutes it. The goal is Persistence Ignorance: business logic should never care about how or where data is stored. This enables flexible, maintainable, and future-proof microservices.
Architecture Overview¶
Persistence Layer Structure¶
Application Layer (DomainModel)
├── Processors (Commands/Writes)
└── Retrievers (Queries/Reads)
↓ (Uses Repository Interfaces)
Infrastructure Layer (PersistenceModel)
├── PersistenceModel (Interfaces)
│ ├── IMicroserviceAggregateRootsRepository
│ ├── IMicroserviceAggregateRootsSpecification
│ └── IUnitOfWork
├── PersistenceModel.NHibernate (SQL Implementation)
│ ├── Repositories (NHibernate-based)
│ ├── Mappings (Fluent NHibernate)
│ └── Specifications (LINQ-based)
└── PersistenceModel.MongoDb (NoSQL Implementation)
├── Repositories (MongoDB Driver-based)
└── Specifications (MongoDB Builder-based)
↓
Database Layer
├── SQL Server / PostgreSQL / MySQL (via NHibernate)
└── MongoDB (via MongoDB Driver)
Project Organization¶
| Project | Responsibility |
|---|---|
| PersistenceModel | Repository and specification interfaces (abstraction layer) |
| PersistenceModel.NHibernate | NHibernate-based repository and specification implementations |
| PersistenceModel.MongoDb | MongoDB-based repository and specification implementations |
| DatabaseModel.Migrations | FluentMigrator SQL migrations |
| DatabaseModel.MongoDb.Migrations | MongoDB migrations |
| Extensions.PersistenceModel | Base repository and specification abstractions from ConnectSoft framework |
Key Integration Points¶
| Component | Layer | Responsibility |
|---|---|---|
| Repository Interfaces | PersistenceModel | Define contracts for data access |
| Repository Implementations | PersistenceModel.NHibernate/MongoDb | Concrete persistence implementations |
| Specification Interfaces | PersistenceModel | Query abstraction contracts |
| Queryable Specifications | PersistenceModel.NHibernate/MongoDb | Query execution implementations |
| Unit of Work | Infrastructure | Transaction management abstraction |
| Migrations | DatabaseModel | Schema evolution management |
Repository Pattern¶
Purpose¶
The Repository Pattern provides a collection-like abstraction over data persistence, serving as the sole access point for persisting and retrieving aggregates and entities. It enforces Clean Architecture boundaries by keeping persistence details out of the domain and application layers.
See Repository Pattern for detailed information.
Repository Interface¶
Each aggregate root has its own repository interface:
// IMicroserviceAggregateRootsRepository.cs
namespace ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories
{
using System;
using ConnectSoft.Extensions.PersistenceModel.Repositories;
using ConnectSoft.MicroserviceTemplate.EntityModel;
/// <summary>
/// Mediates between the domain and data mapping layers
/// using a collection-like interface for accessing
/// MicroserviceAggregateRoots domain objects.
/// </summary>
public interface IMicroserviceAggregateRootsRepository
: IGenericRepository<IMicroserviceAggregateRoot, Guid>
{
}
}
The interface inherits from IGenericRepository<TEntity, TIdentity> which provides:
- CRUD Operations: Insert, Update, Delete, GetById, GetAll
- Async Support: All operations have async variants
- Specification Pattern: Specify<TSpecification>() method for querying
- Expression Queries: Query and QueryAsync methods
Repository Implementations¶
NHibernate Implementation¶
// MicroserviceAggregateRootsRepository.cs
namespace ConnectSoft.MicroserviceTemplate.PersistenceModel.NHibernate.Repositories
{
using System;
using ConnectSoft.Extensions.PersistenceModel;
using ConnectSoft.Extensions.PersistenceModel.Repositories;
using ConnectSoft.Extensions.PersistenceModel.Specifications;
using ConnectSoft.MicroserviceTemplate.EntityModel;
using ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories;
/// <summary>
/// Generic repository implementation that provides unified access
/// to the MicroserviceAggregateRoots entities stored in underlying data storage.
/// </summary>
public class MicroserviceAggregateRootsRepository(
IUnitOfWork unitOfWork,
ISpecificationLocator specificationLocator)
: GenericRepository<IMicroserviceAggregateRoot, Guid>(
unitOfWork,
specificationLocator),
IMicroserviceAggregateRootsRepository
{
// Inherits all CRUD operations from GenericRepository:
// - Insert, InsertAsync
// - Update, UpdateAsync
// - Delete, DeleteAsync
// - GetById, GetByIdAsync
// - GetAll, GetAllAsync
// - Query, QueryAsync
// - Specify<T> (Specification Pattern)
}
}
MongoDB Implementation¶
// MicroserviceAggregateRootsMongoDbRepository.cs
namespace ConnectSoft.MicroserviceTemplate.PersistenceModel.MongoDb.Repositories
{
using System;
using ConnectSoft.Extensions.PersistenceModel;
using ConnectSoft.Extensions.PersistenceModel.MongoDb.Repositories;
using ConnectSoft.Extensions.PersistenceModel.Specifications;
using ConnectSoft.MicroserviceTemplate.EntityModel;
using ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
/// <summary>
/// MongoDb repository implementation that provides unified access
/// to the MicroserviceAggregateRoots entities stored in underlying data storage.
/// </summary>
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
}
}
Repository Usage in Application Layer¶
Repositories are used in processors and retrievers:
// DefaultMicroserviceAggregateRootsProcessor.cs
public class DefaultMicroserviceAggregateRootsProcessor : IMicroserviceAggregateRootsProcessor
{
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 this.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
this.unitOfWork.ExecuteTransactional(() =>
{
this.repository.Insert(newEntity);
});
// Publish domain events after successful commit
#if (UseMassTransit || UseNServiceBus)
await this.eventBus.PublishEvent<MicroserviceAggregateRootCreatedEvent>(
new MicroserviceAggregateRootCreatedEvent { ObjectId = newEntity.ObjectId },
token).ConfigureAwait(false);
#endif
return newEntity;
}
}
Specification Pattern¶
Purpose¶
The Specification Pattern encapsulates query logic and separates business rules from repository implementations. It provides a domain-oriented way to express complex queries while maintaining persistence ignorance.
See Specification Pattern for detailed information.
Specification Interface¶
Each aggregate has its own specification interface:
// IMicroserviceAggregateRootsSpecification.cs
namespace ConnectSoft.MicroserviceTemplate.PersistenceModel.Specifications
{
using System;
using ConnectSoft.Extensions.PersistenceModel.Specifications;
using ConnectSoft.MicroserviceTemplate.EntityModel;
/// <summary>
/// Base interface for the MicroserviceAggregateRoots (domain oriented queries).
/// </summary>
public interface IMicroserviceAggregateRootsSpecification
: ISpecification<IMicroserviceAggregateRoot, Guid>
{
// Inherits fluent query methods from ISpecification:
// - Where, And, Or, Not
// - OrderBy, OrderByDescending
// - Skip, Take
// - ToListAsync, FirstOrDefaultAsync, AnyAsync, CountAsync
}
}
Queryable Specification Implementations¶
NHibernate Queryable Specification¶
// MicroserviceAggregateRootsQueryableSpecification.cs
public class MicroserviceAggregateRootsQueryableSpecification(
IUnitOfWorkConvertor unitOfWorkConvertor)
: QueryableSpecification<IMicroserviceAggregateRoot, Guid>(unitOfWorkConvertor),
IMicroserviceAggregateRootsSpecification
{
// Translates specifications to LINQ queries over NHibernate IQueryable
}
MongoDB Queryable Specification¶
// MicroserviceAggregateRootsMongoDbQueryableSpecification.cs
public class MicroserviceAggregateRootsMongoDbQueryableSpecification(
IMongoDatabase mongoDatabase,
ILogger<MongoDbQueryableSpecification<IMicroserviceAggregateRoot, Guid>> logger)
: MongoDbQueryableSpecification<IMicroserviceAggregateRoot, Guid>(mongoDatabase, logger),
IMicroserviceAggregateRootsSpecification
{
// Translates specifications to MongoDB filter builders
}
Specification Usage¶
// Using specifications in retrievers
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetActiveAggregatesAsync(
CancellationToken ct)
{
var specification = this.repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active)
.OrderByDescending(x => x.CreatedOn);
return await specification.ToListAsync(ct);
}
Unit of Work Pattern¶
Purpose¶
The Unit of Work pattern manages transactions and ensures atomicity of operations. It groups multiple repository operations into a single transaction, ensuring all-or-nothing semantics.
IUnitOfWork Interface¶
The IUnitOfWork interface provides transaction management:
public interface IUnitOfWork
{
// Execute operations within a transaction
void ExecuteTransactional(Action action);
Task ExecuteTransactionalAsync(Func<Task> action, CancellationToken ct = default);
// Begin explicit transaction
Task<ITransaction> BeginTransactionAsync(CancellationToken ct = default);
}
Usage in Application Layer¶
// Using ExecuteTransactional helper
public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
CreateMicroserviceAggregateRootInput input,
CancellationToken token = default)
{
MicroserviceAggregateRootEntity newEntity = new MicroserviceAggregateRootEntity();
this.unitOfWork.ExecuteTransactional(() =>
{
newEntity.ObjectId = input.ObjectId;
this.repository.Insert(newEntity);
});
return newEntity;
}
Explicit Transaction Management¶
// Using explicit transaction
using var transaction = await this.unitOfWork.BeginTransactionAsync(token);
try
{
await this.repository.InsertAsync(entity1, token);
await this.repository.UpdateAsync(entity2, token);
await transaction.CommitAsync(token);
}
catch
{
// Transaction rolls back automatically on disposal if not committed
throw;
}
NHibernate Implementation¶
Overview¶
NHibernate provides SQL-based persistence with support for SQL Server, PostgreSQL, MySQL, and other relational databases. It uses Fluent NHibernate for code-based mapping configuration.
See NHibernate for detailed information.
Configuration¶
NHibernate is registered via extension methods:
// MicroserviceRegistrationExtensions.cs
#if UseNHibernate
services.AddNHibernatePersistenceModel(configuration);
#endif
#if (UseNHibernate || UseMongoDb)
services.AddPersistenceModel();
#endif
Entity Mappings¶
Entities are mapped using Fluent NHibernate:
// MicroserviceAggregateRootEntityMap.cs
public class MicroserviceAggregateRootEntityMap : ClassMapping<MicroserviceAggregateRootEntity>
{
public MicroserviceAggregateRootEntityMap()
{
this.Id(x => x.ObjectId, mapper => mapper.Generator(Generators.GuidComb));
this.Property(x => x.SomeValue, mapper => mapper.Length(500));
// Additional property mappings...
}
}
Session Management¶
- ISessionFactory: Singleton (created once, reused)
- ISession: Scoped (one per HTTP request)
- ITransaction: Managed by Unit of Work pattern
MongoDB Implementation¶
Overview¶
MongoDB provides NoSQL document-based persistence with schema flexibility and horizontal scalability. It uses the MongoDB .NET Driver for database operations.
Configuration¶
MongoDB is registered via extension methods:
// MicroserviceRegistrationExtensions.cs
#if UseMongoDb
services.AddMongoDbPersistence(configuration);
#endif
#if (UseNHibernate || UseMongoDb)
services.AddPersistenceModel();
#endif
Connection Setup¶
MongoDB connection is configured via options:
{
"PersistenceModel": {
"MongoDb": {
"MongoDbConnectionStringKey": "MongoDb",
"DatabaseName": "MicroserviceTemplateDb"
}
},
"ConnectionStrings": {
"MongoDb": "mongodb://localhost:27017"
}
}
Collection Management¶
Collections are created automatically on first use or via migrations:
// Repository uses collection name
var collection = database.GetCollection<MicroserviceAggregateRoot>("MicroserviceAggregateRoots");
Database Migrations¶
Purpose¶
Migrations provide versioned, repeatable, and safe database schema evolution. They ensure consistent schema across environments and enable safe production deployments.
FluentMigrator (SQL Migrations)¶
FluentMigrator manages SQL database schema evolution using code-first migrations.
Migration Example¶
// MicroserviceMigration.cs
namespace ConnectSoft.MicroserviceTemplate.DatabaseModel.Migrations
{
using FluentMigrator;
[Migration(1)]
public class MicroserviceMigration : Migration
{
internal const string SchemaName = "ConnectSoft.MicroserviceTemplate";
public override void Up()
{
this.Create.Schema(SchemaName);
this.Create
.Table("MicroserviceAggregateRoots")
.InSchema(SchemaName)
.WithColumn("ObjectId")
.AsGuid()
.NotNullable()
.PrimaryKey();
}
public override void Down()
{
this.Delete
.Table("MicroserviceAggregateRoots")
.InSchema(SchemaName);
this.Delete.Schema(SchemaName);
}
}
}
Migration Registration¶
// MicroserviceRegistrationExtensions.cs
#if Migrations
services.AddMicroserviceFluentMigrator(
configuration,
typeof(MicroserviceMigration).Assembly);
#endif
Migration Execution¶
Migrations run automatically at application startup:
// MicroserviceRegistrationExtensions.cs - UseMicroserviceServices
#if UseNHibernate
application.RunMicroserviceFluentMigrations();
#endif
MongoDB Migrations¶
MongoDB migrations manage collection creation, indexes, and schema evolution for document databases.
Migration Example¶
// MicroserviceMongoDbMigration.cs
namespace ConnectSoft.MicroserviceTemplate.DatabaseModel.MongoDb.Migrations
{
using CSharpMongoMigrations;
[Migration(0, "First ConnectSoft.MicroserviceTemplate's MongoDb migration")]
public class MicroserviceMongoDbMigration : Migration
{
public override void Up()
{
this.Database.CreateCollection("MicroserviceAggregateRoots");
}
public override void Down()
{
this.Database.DropCollection("MicroserviceAggregateRoots");
}
}
}
Migration Registration¶
// MicroserviceRegistrationExtensions.cs
#if UseMongoDb
#if Migrations
services.AddMongoDbMigrator(
configuration,
typeof(MicroserviceMongoDbMigration));
#endif
#endif
Migration Best Practices¶
Do's¶
-
Version Migrations Sequentially
-
Always Provide Down Methods
-
Keep Migrations Atomic
-
Test Migrations
Don'ts¶
-
Don't Modify Existing Migrations
-
Don't Skip Migration Versions
-
Don't Include Data Changes in Schema Migrations
Data Modeling¶
Domain Entities¶
Domain entities are pure POCOs with no persistence dependencies:
// MicroserviceAggregateRootEntity.cs
namespace ConnectSoft.MicroserviceTemplate.EntityModel
{
using System;
using ConnectSoft.Extensions.PersistenceModel;
public class MicroserviceAggregateRootEntity : MicroserviceAggregateRoot, IMicroserviceAggregateRoot
{
public Guid ObjectId { get; set; }
public string? SomeValue { get; set; }
public DateTimeOffset CreatedOn { get; set; }
}
}
Characteristics: - No ORM attributes (NHibernate, MongoDB, etc.) - No infrastructure dependencies - Pure domain logic - Implements domain interfaces
Entity Mapping¶
Mappings are defined separately in the infrastructure layer:
NHibernate Mapping¶
// MicroserviceAggregateRootEntityMap.cs
public class MicroserviceAggregateRootEntityMap : ClassMapping<MicroserviceAggregateRootEntity>
{
public MicroserviceAggregateRootEntityMap()
{
this.Id(x => x.ObjectId, mapper => mapper.Generator(Generators.GuidComb));
this.Property(x => x.SomeValue, mapper => mapper.Length(500).Nullable());
this.Property(x => x.CreatedOn, mapper => mapper.NotNullable());
}
}
MongoDB Mapping¶
MongoDB uses convention-based mapping by default, with optional explicit configuration:
// MongoDB automatically maps:
// - ObjectId → _id field
// - Properties → Document fields
// - Collection name from class name (or attribute)
Polyglot Persistence¶
The template supports using multiple persistence technologies simultaneously:
// Register both NHibernate and MongoDB
#if UseNHibernate
services.AddNHibernatePersistenceModel(configuration);
#endif
#if UseMongoDb
services.AddMongoDbPersistence(configuration);
#endif
#if (UseNHibernate || UseMongoDb)
services.AddPersistenceModel();
#endif
Use Cases: - SQL for transactional aggregates (orders, payments) - MongoDB for document-heavy aggregates (product catalogs, user profiles) - Different databases for different bounded contexts
Service Registration¶
Persistence Model Registration¶
All persistence services are registered via AddPersistenceModel():
// PersistenceModelExtensions.cs
internal static IServiceCollection AddPersistenceModel(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
// Register repositories based on selected persistence technology
#if UseNHibernate
services.AddScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsRepository>();
services.AddScoped<IMicroserviceAggregateRootsQueryableSpecification, MicroserviceAggregateRootsQueryableSpecification>();
#elif UseMongoDb
services.AddScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsMongoDbRepository>();
services.AddScoped<IMicroserviceAggregateRootsQueryableSpecification, MicroserviceAggregateRootsMongoDbQueryableSpecification>();
#endif
return services;
}
Keyed Services (Multi-Database)¶
For scenarios with multiple databases, repositories can use keyed services:
// Register NHibernate repository with key
services.AddKeyedScoped<IUnitOfWork, NHibernateUnitOfWork>(MicroserviceConstants.NHibernateDIKey);
services.AddKeyedScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsKeyedRepository>(
MicroserviceConstants.NHibernateDIKey);
// Register MongoDB repository with key
services.AddKeyedScoped<IUnitOfWork, MongoDbUnitOfWork>(MicroserviceConstants.MongoDbDIKey);
services.AddKeyedScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsMongoDbKeyedRepository>(
MicroserviceConstants.MongoDbDIKey);
Usage with keyed services:
public class MyService
{
private readonly IMicroserviceAggregateRootsRepository sqlRepository;
private readonly IMicroserviceAggregateRootsRepository mongoRepository;
public MyService(
[FromKeyedServices(MicroserviceConstants.NHibernateDIKey)]
IMicroserviceAggregateRootsRepository sqlRepository,
[FromKeyedServices(MicroserviceConstants.MongoDbDIKey)]
IMicroserviceAggregateRootsRepository mongoRepository)
{
this.sqlRepository = sqlRepository;
this.mongoRepository = mongoRepository;
}
}
Complete Persistence Flow¶
Creating an Aggregate¶
flowchart TD
API[API Request] --> Processor[Processor]
Processor --> Validate[Validate Input]
Validate --> CheckExists[Check if Exists]
CheckExists --> CreateEntity[Create Entity]
CreateEntity --> BeginTransaction[Begin Transaction]
BeginTransaction --> Repository[Repository.Insert]
Repository --> Database[Database Write]
Database --> Commit[Commit Transaction]
Commit --> PublishEvent[Publish Domain Event]
PublishEvent --> Return[Return Entity]
Querying with Specifications¶
flowchart TD
Retriever[Retriever] --> CreateSpec[Create Specification]
CreateSpec --> Repository[Repository.Specify]
Repository --> QueryableSpec[Queryable Specification]
QueryableSpec --> Translate[Translate to DB Query]
Translate --> Execute[Execute Query]
Execute --> Database[Database]
Database --> Results[Return Results]
Full Operational Flow¶
- API Request arrives at controller
- Controller maps DTO to Application Input
- Processor/Retriever receives input
- Validation occurs (FluentValidation)
- Business Logic executes
- Unit of Work begins transaction
- Repository performs persistence operations
- Transaction commits
- Domain Events published (if any)
- Metrics recorded
- Response returned to client
Best Practices¶
Do's¶
-
Always Operate at Aggregate Root Level
-
Use Unit of Work for Transactions
-
Use Specifications for Queries
-
Handle Null Returns
-
Use Async Methods
-
Validate Before Persistence
Don'ts¶
-
Don't Expose IQueryable or DbSet
-
Don't Access Child Entities Directly
-
Don't Perform Business Logic in Repositories
-
Don't Forget Transaction Boundaries
-
Don't Mix Sync and Async
-
Don't Store Secrets in Entity Properties
Testing¶
Unit Testing Repositories¶
Mock repository interfaces in unit tests:
[TestMethod]
public async Task CreateAggregate_Should_Insert_Into_Repository()
{
// Arrange
var mockRepository = new Mock<IMicroserviceAggregateRootsRepository>();
var mockUnitOfWork = new Mock<IUnitOfWork>();
var processor = new DefaultMicroserviceAggregateRootsProcessor(
Mock.Of<ILogger<DefaultMicroserviceAggregateRootsProcessor>>(),
TimeProvider.System,
mockRepository.Object,
mockUnitOfWork.Object,
// ... other dependencies
);
var input = new CreateMicroserviceAggregateRootInput
{
ObjectId = Guid.NewGuid()
};
// Act
var result = await processor.CreateMicroserviceAggregateRoot(input);
// Assert
mockRepository.Verify(r => r.Insert(It.IsAny<IMicroserviceAggregateRoot>()), Times.Once);
mockUnitOfWork.Verify(u => u.ExecuteTransactional(It.IsAny<Action>()), Times.Once);
}
Unit Testing Specifications¶
Test specifications independently:
[TestMethod]
public void Specification_Should_Filter_Correctly()
{
// Arrange
var entities = new List<MicroserviceAggregateRootEntity>
{
new() { SomeValue = "Value1" },
new() { SomeValue = "Target" },
new() { SomeValue = "Value2" }
};
var spec = new MicroserviceAggregateRootsByValueSpecification("Target");
// Act
var result = entities.AsQueryable()
.Where(spec.ToExpression())
.ToList();
// Assert
Assert.AreEqual(1, result.Count);
Assert.AreEqual("Target", result[0].SomeValue);
}
Integration Testing¶
Test against real databases (or in-memory/test containers):
[TestMethod]
public async Task Repository_Should_Save_And_Retrieve_Entity()
{
// Arrange
var services = new ServiceCollection();
services.AddNHibernatePersistenceModel(/* test configuration */);
var provider = services.BuildServiceProvider();
var repository = provider.GetRequiredService<IMicroserviceAggregateRootsRepository>();
var unitOfWork = provider.GetRequiredService<IUnitOfWork>();
var entity = new MicroserviceAggregateRootEntity
{
ObjectId = Guid.NewGuid(),
SomeValue = "TestValue"
};
// Act
unitOfWork.ExecuteTransactional(() =>
{
repository.Insert(entity);
});
var retrieved = await repository.GetByIdAsync(entity.ObjectId);
// Assert
Assert.IsNotNull(retrieved);
Assert.AreEqual(entity.SomeValue, retrieved.SomeValue);
}
Migration Testing¶
Test migrations independently:
[TestMethod]
public async Task Should_Apply_All_Migrations_Successfully()
{
// Arrange
var runner = GetMigrationRunner();
// Act
await runner.MigrateUpAsync();
// Assert
// Verify schema exists, tables created, etc.
}
Common Scenarios¶
Scenario 1: Creating a New Aggregate¶
public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
CreateMicroserviceAggregateRootInput input,
CancellationToken token = default)
{
// 1. Validate input
await this.validator.ValidateAndThrowAsync(input, token);
// 2. Check if already exists
var existing = await this.repository.GetByIdAsync(input.ObjectId, token);
if (existing != null)
{
throw new MicroserviceAggregateRootAlreadyExistsException(input.ObjectId);
}
// 3. Create entity
var newEntity = new MicroserviceAggregateRootEntity
{
ObjectId = input.ObjectId
};
// 4. Persist within transaction
this.unitOfWork.ExecuteTransactional(() =>
{
this.repository.Insert(newEntity);
});
// 5. Publish events (after commit)
await this.eventBus.PublishEvent(new MicroserviceAggregateRootCreatedEvent
{
ObjectId = newEntity.ObjectId
}, token);
return newEntity;
}
Scenario 2: Querying with Multiple Conditions¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> SearchAggregatesAsync(
string? searchTerm,
Status? status,
DateTimeOffset? fromDate,
CancellationToken ct)
{
var specification = this.repository.Specify<IMicroserviceAggregateRootsSpecification>();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
specification = specification.Where(x => x.SomeValue.Contains(searchTerm));
}
if (status.HasValue)
{
specification = specification.And(x => x.Status == status.Value);
}
if (fromDate.HasValue)
{
specification = specification.And(x => x.CreatedOn >= fromDate.Value);
}
return await specification
.OrderByDescending(x => x.CreatedOn)
.ToListAsync(ct);
}
Scenario 3: Batch Operations¶
public async Task CreateMultipleAggregatesAsync(
IEnumerable<CreateMicroserviceAggregateRootInput> inputs,
CancellationToken token = default)
{
this.unitOfWork.ExecuteTransactional(() =>
{
foreach (var input in inputs)
{
var entity = new MicroserviceAggregateRootEntity
{
ObjectId = input.ObjectId
};
this.repository.Insert(entity);
}
});
}
Scenario 4: Pagination¶
public async Task<PagedResult<IMicroserviceAggregateRoot>> GetAggregatesPageAsync(
int pageNumber,
int pageSize,
CancellationToken ct)
{
var skip = (pageNumber - 1) * pageSize;
var specification = this.repository.Specify<IMicroserviceAggregateRootsSpecification>()
.OrderBy(x => x.CreatedOn)
.Skip(skip)
.Take(pageSize);
var items = await specification.ToListAsync(ct);
var totalCount = await this.repository.Specify<IMicroserviceAggregateRootsSpecification>()
.CountAsync(ct);
return new PagedResult<IMicroserviceAggregateRoot>
{
Items = items,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize
};
}
Troubleshooting¶
Issue: Repository Not Found¶
Symptom: InvalidOperationException: No service for type 'IMicroserviceAggregateRootsRepository'
Solutions:
1. Verify AddPersistenceModel() is called in startup
2. Check conditional compilation directives (#if UseNHibernate or #if UseMongoDb)
3. Ensure repository implementation is registered in DI container
Issue: Transaction Not Committing¶
Symptom: Changes not persisted to database
Solutions:
1. Verify unitOfWork.ExecuteTransactional() is called
2. Check for exceptions before commit
3. Ensure transaction is not being rolled back
4. Verify database connection is valid
Issue: Specification Not Working¶
Symptom: Specification returns wrong results or errors
Solutions: 1. Verify specification implementation is registered 2. Check specification expression is valid 3. Test specification independently with unit tests 4. Verify queryable specification is correctly implemented
Issue: Migration Fails¶
Symptom: Application fails to start with migration errors
Solutions:
1. Check migration version numbers are sequential
2. Verify Up() and Down() methods are correct
3. Test migrations in development environment first
4. Check database connection string is correct
5. Verify migration assembly is registered
Issue: Performance Issues¶
Symptom: Slow queries, high database load
Solutions: 1. Add Indexes: Create indexes for frequently queried columns 2. Use Projections: Select only needed fields 3. Implement Pagination: Avoid loading large result sets 4. Enable Caching: Use second-level cache for read-heavy scenarios 5. Optimize Queries: Review generated SQL/MongoDB queries
Summary¶
Persistence and data modeling in the ConnectSoft Microservice Template provides:
- ✅ Persistence Ignorance: Domain models remain pure and framework-agnostic
- ✅ Repository Pattern: Clean abstraction over data access
- ✅ Specification Pattern: Domain-oriented query abstraction
- ✅ Unit of Work: Transaction management and atomicity
- ✅ Multi-Persistence Support: NHibernate (SQL) and MongoDB (NoSQL)
- ✅ Migrations: Versioned, automated schema evolution
- ✅ Testability: Interfaces enable easy unit and integration testing
- ✅ Flexibility: Easy to switch or add persistence technologies
By following these patterns, teams can:
- Build Maintainable Services: Clear separation of concerns
- Scale Effectively: Support for multiple databases and polyglot persistence
- Test Confidently: Repository and specification interfaces enable comprehensive testing
- Evolve Safely: Migrations ensure safe schema evolution
- Adapt Quickly: Easy to swap or add persistence technologies
The persistence layer is architected as a strategic enabler—not an afterthought—providing the foundation for reliable, scalable, and maintainable microservices.