Specification Pattern in ConnectSoft Microservice Template¶
Purpose & Overview¶
The Specification Pattern is a powerful Domain-Driven Design (DDD) pattern used in the ConnectSoft Microservice Template to encapsulate query logic and separate business rules from repository implementations. It provides a domain-oriented way to express complex queries while maintaining persistence ignorance.
Why Specification Pattern?¶
The Specification Pattern offers several key advantages:
- Encapsulation: Query logic is isolated from repositories and services, making it reusable and testable
- Composability: Simple specifications can be combined (AND, OR, NOT) to create complex queries
- Reusability: Specifications can be shared across multiple services and use cases
- Testability: Specifications can be unit tested independently without database dependencies
- Persistence Ignorance: Specifications express business intent, not SQL or MongoDB syntax
- Readability: Specifications use domain language, making queries self-documenting
How It's Implemented in Template¶
Architecture Overview¶
Specifications work in conjunction with the Repository Pattern:
Domain/Application Layer
↓ (uses)
Repository Interface
↓ (uses)
Specification Interface
↓ (implemented by)
Queryable Specification (NHibernate/MongoDB)
↓ (translates to)
Database Query (LINQ/MongoDB Builder)
Core Components¶
1. Base Specification Interface¶
The template uses ISpecification<TEntity, TIdentity> from ConnectSoft.Extensions.PersistenceModel:
public interface ISpecification<TEntity, TIdentity>
where TEntity : class, IGenericEntity<TIdentity>
{
// Fluent API for building queries
ISpecification<TEntity, TIdentity> Where(Expression<Func<TEntity, bool>> predicate);
ISpecification<TEntity, TIdentity> And(Expression<Func<TEntity, bool>> predicate);
ISpecification<TEntity, TIdentity> Or(Expression<Func<TEntity, bool>> predicate);
ISpecification<TEntity, TIdentity> Not(Expression<Func<TEntity, bool>> predicate);
// Sorting
ISpecification<TEntity, TIdentity> OrderBy<TKey>(Expression<Func<TEntity, TKey>> keySelector);
ISpecification<TEntity, TIdentity> OrderByDescending<TKey>(Expression<Func<TEntity, TKey>> keySelector);
// Paging
ISpecification<TEntity, TIdentity> Skip(int count);
ISpecification<TEntity, TIdentity> Take(int count);
// Execution
Task<IList<TEntity>> ToListAsync(CancellationToken ct = default);
Task<TEntity?> FirstOrDefaultAsync(CancellationToken ct = default);
Task<bool> AnyAsync(CancellationToken ct = default);
Task<int> CountAsync(CancellationToken ct = default);
}
2. Aggregate-Specific Specification Interface¶
Each aggregate has its own specification interface:
// IMicroserviceAggregateRootsSpecification.cs
public interface IMicroserviceAggregateRootsSpecification
: ISpecification<IMicroserviceAggregateRoot, Guid>
{
// Inherits all fluent query methods from ISpecification
}
3. Queryable Specification Implementations¶
NHibernate Implementation:
// MicroserviceAggregateRootsQueryableSpecification.cs
public class MicroserviceAggregateRootsQueryableSpecification(
IUnitOfWorkConvertor unitOfWorkConvertor)
: QueryableSpecification<IMicroserviceAggregateRoot, Guid>(unitOfWorkConvertor),
IMicroserviceAggregateRootsSpecification
{
// Translates specifications to LINQ queries over NHibernate IQueryable
}
MongoDB Implementation:
// MicroserviceAggregateRootsMongoDbQueryableSpecification.cs
public class MicroserviceAggregateRootsMongoDbQueryableSpecification(
IMongoDatabase mongoDatabase,
ILogger<MongoDbQueryableSpecification<IMicroserviceAggregateRoot, Guid>> logger)
: MongoDbQueryableSpecification<IMicroserviceAggregateRoot, Guid>(mongoDatabase, logger),
IMicroserviceAggregateRootsSpecification
{
// Translates specifications to MongoDB filter builders
}
Project Structure¶
| Project | Responsibility |
|---|---|
PersistenceModel |
Defines specification interfaces (IMicroserviceAggregateRootsSpecification) |
PersistenceModel.NHibernate |
LINQ-based queryable specification implementation |
PersistenceModel.MongoDb |
MongoDB builder-based queryable specification |
Extensions.PersistenceModel |
Base ISpecification and queryable abstractions |
Code Examples¶
Basic Specification Usage¶
Simple Query¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetActiveAggregatesAsync(
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active);
return await specification.ToListAsync(ct);
}
Query with Multiple Conditions¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetRecentActiveAggregatesAsync(
DateTimeOffset since,
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active)
.And(x => x.CreatedOn >= since);
return await specification.ToListAsync(ct);
}
Query with Sorting¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetTopRecentAggregatesAsync(
int count,
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.OrderByDescending(x => x.CreatedOn)
.Take(count);
return await specification.ToListAsync(ct);
}
Paginated Query¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetAggregatesPageAsync(
int pageNumber,
int pageSize,
CancellationToken ct)
{
var skip = (pageNumber - 1) * pageSize;
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.OrderBy(x => x.CreatedOn)
.Skip(skip)
.Take(pageSize);
return await specification.ToListAsync(ct);
}
Complex Specifications¶
Combining Multiple Conditions¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> SearchAggregatesAsync(
string? searchTerm,
Status? status,
DateTimeOffset? fromDate,
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
specification = specification.Where(x =>
x.Name.Contains(searchTerm) ||
x.Description.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);
}
Using OR Conditions¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetAggregatesInStatusesAsync(
Status[] statuses,
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>();
// Build OR chain
Expression<Func<IMicroserviceAggregateRoot, bool>>? predicate = null;
foreach (var status in statuses)
{
var statusExpr = (Expression<Func<IMicroserviceAggregateRoot, bool>>)
(x => x.Status == status);
predicate = predicate == null
? statusExpr
: CombineWithOr(predicate, statusExpr);
}
if (predicate != null)
{
specification = specification.Where(predicate);
}
return await specification.ToListAsync(ct);
}
Checking Existence¶
public async Task<bool> HasActiveAggregatesAsync(CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active);
return await specification.AnyAsync(ct);
}
Getting First Matching Entity¶
public async Task<IMicroserviceAggregateRoot?> GetFirstActiveAggregateAsync(
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active)
.OrderBy(x => x.CreatedOn);
return await specification.FirstOrDefaultAsync(ct);
}
Counting Results¶
public async Task<int> CountActiveAggregatesAsync(CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active);
return await specification.CountAsync(ct);
}
Configuration¶
Dependency Injection Setup¶
Specifications are registered in the ApplicationModel project:
// NHibernateExtensions.cs
public static IServiceCollection AddMicroserviceNHibernate(
this IServiceCollection services)
{
// Register queryable specification for NHibernate
services.AddScoped<IMicroserviceAggregateRootsSpecification,
MicroserviceAggregateRootsQueryableSpecification>();
// Register specification locator
services.AddScoped<ISpecificationLocator, SpecificationLocator>();
return services;
}
// MongoDbExtensions.cs
public static IServiceCollection AddMicroserviceMongoDb(
this IServiceCollection services)
{
// Register queryable specification for MongoDB
services.AddScoped<IMicroserviceAggregateRootsSpecification,
MicroserviceAggregateRootsMongoDbQueryableSpecification>();
return services;
}
Keyed Services for Multiple Persistence¶
When using multiple persistence technologies:
// NHibernate keyed specification
services.AddKeyedScoped<IMicroserviceAggregateRootsSpecification,
MicroserviceAggregateRootsQueryableKeyedSpecification>(
MicroserviceConstants.NHibernateDIKey);
// MongoDB keyed specification
services.AddKeyedScoped<IMicroserviceAggregateRootsSpecification,
MicroserviceAggregateRootsMongoDbQueryableKeyedSpecification>(
MicroserviceConstants.MongoDbDIKey);
Best Practices¶
Do's¶
- Use specifications for all dynamic queries
- Encapsulate query logic in specifications
-
Keep repositories focused on CRUD operations
-
Name specifications clearly
- Use domain language that reflects business intent
-
Example:
GetActiveOrdersByCustomerSpecificationinstead ofGetOrders1 -
Compose specifications when needed
- Build complex queries from simple building blocks
-
Use AND, OR, NOT operators for composition
-
Keep specifications pure
- No side effects or business logic
-
No direct database access or external service calls
-
Test specifications independently
- Unit test specification logic without database
- Verify predicates behave correctly
Don'ts¶
- Don't put business logic in specifications
- Specifications are for querying, not processing
-
Business logic belongs in domain services
-
Don't create specifications for simple single-use queries
- Use expression predicates directly for simple cases
-
Specifications are for reusable, complex queries
-
Don't expose database-specific syntax
- Keep specifications persistence-agnostic
-
Let infrastructure layer handle translation
-
Don't nest specifications too deeply
- Keep specifications readable and maintainable
-
Consider extracting complex nested logic into separate specifications
-
Don't forget to handle null cases
- Always check for null when using
FirstOrDefaultAsync - Handle empty result sets appropriately
Integration Points¶
Repository Integration¶
Specifications are accessed through repositories:
// Repository provides Specify method
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active);
Unit of Work¶
Specifications execute queries through the Unit of Work pattern:
// Specifications use IUnitOfWorkConvertor to access queryable sources
// This ensures proper session/database context management
Query Translation¶
NHibernate Path: - Specification → LINQ Expression → NHibernate IQueryable → SQL
MongoDB Path: - Specification → MongoDB Filter Builder → MongoDB Query → Document Query
Common Scenarios¶
Scenario 1: Dynamic Search with Multiple Filters¶
public async Task<SearchResult<IMicroserviceAggregateRoot>> SearchAsync(
SearchRequest request,
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>();
// Apply filters dynamically
if (!string.IsNullOrWhiteSpace(request.Name))
{
specification = specification.Where(x => x.Name.Contains(request.Name));
}
if (request.Status.HasValue)
{
specification = specification.And(x => x.Status == request.Status.Value);
}
if (request.CreatedAfter.HasValue)
{
specification = specification.And(x => x.CreatedOn >= request.CreatedAfter.Value);
}
// Get total count
var totalCount = await specification.CountAsync(ct);
// Apply pagination and sorting
var results = await specification
.OrderByDescending(x => x.CreatedOn)
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync(ct);
return new SearchResult<IMicroserviceAggregateRoot>
{
Items = results,
TotalCount = totalCount,
Page = request.Page,
PageSize = request.PageSize
};
}
Scenario 2: Reusable Specification Classes¶
For frequently used queries, create dedicated specification classes:
public class ActiveAggregatesSpecification
{
public static IMicroserviceAggregateRootsSpecification Create(
IMicroserviceAggregateRootsRepository repository)
{
return repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Active);
}
}
// Usage
var activeAggregates = await ActiveAggregatesSpecification
.Create(repository)
.ToListAsync(ct);
Scenario 3: Complex Business Rule Queries¶
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetEligibleForProcessingAsync(
CancellationToken ct)
{
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.Status == Status.Pending)
.And(x => x.CreatedOn <= DateTimeOffset.UtcNow.AddMinutes(-5)) // At least 5 minutes old
.And(x => x.AttemptCount < 3) // Not exceeded retry limit
.And(x => x.NextProcessingTime <= DateTimeOffset.UtcNow); // Ready for processing
return await specification
.OrderBy(x => x.CreatedOn)
.Take(100) // Process in batches
.ToListAsync(ct);
}
Troubleshooting¶
Issue: Specification not returning expected results¶
Cause: Predicate logic may be incorrect or conditions not properly combined.
Solution: Test specification predicates independently:
// Test the predicate
var predicate = (Expression<Func<IMicroserviceAggregateRoot, bool>>)
(x => x.Status == Status.Active && x.CreatedOn >= DateTimeOffset.UtcNow.AddDays(-7));
// Verify logic with in-memory test data
var testData = new List<IMicroserviceAggregateRoot> { /* ... */ };
var compiled = predicate.Compile();
var results = testData.Where(compiled).ToList();
Issue: Performance issues with large datasets¶
Cause: Missing indexes on queried fields or inefficient predicate ordering.
Solution: - Ensure database indexes exist for frequently queried fields - Optimize predicate order (most selective conditions first) - Use pagination for large result sets
Issue: Specification methods not available¶
Cause: Specification interface may not be properly registered or wrong interface used.
Solution: Verify DI registration:
// Ensure specification implementation is registered
services.AddScoped<IMicroserviceAggregateRootsSpecification,
MicroserviceAggregateRootsQueryableSpecification>();
Issue: OR conditions not working as expected¶
Cause: OR conditions require careful predicate combination.
Solution: Build OR conditions explicitly:
// For multiple OR conditions, combine predicates
var statuses = new[] { Status.Active, Status.Pending };
var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>();
// Use multiple Where calls or combine expressions manually
specification = specification.Where(x => statuses.Contains(x.Status));
References¶
- Repository Pattern: repository-pattern.md
- Persistence and Data Modeling: persistence-and-data-modeling.md
- Domain-Driven Design: domain-driven-design.md
- NHibernate: nhibernate.md
- MongoDB: mongodb.md
Summary¶
The Specification Pattern in ConnectSoft Microservice Template:
- Provides domain-oriented query abstraction
- Encapsulates reusable query logic
- Supports fluent, composable queries
- Maintains persistence ignorance
- Enables testable, maintainable code
- Works seamlessly with Repository Pattern
- Translates to efficient database queries (LINQ/MongoDB)
This pattern is essential for building maintainable, testable microservices where query logic can evolve independently of business logic and persistence implementations.