MongoDB in ConnectSoft Microservice Template¶
Purpose & Overview¶
MongoDB is a NoSQL document database that provides flexible, schema-less persistence for microservices. In the ConnectSoft Microservice Template, MongoDB serves as a document-based persistence option alongside NHibernate (SQL), enabling polyglot persistence strategies where different aggregates can use different storage technologies based on their requirements.
MongoDB integration provides:
- Document-Based Storage: Store JSON-like documents with flexible schema
- Horizontal Scalability: Built-in support for sharding and replication
- High Performance: Optimized for read-heavy workloads and complex queries
- Rich Query Language: Powerful querying with filters, projections, and aggregations
- Schema Flexibility: Evolve document structure without migrations (though migrations are supported)
- Repository Pattern: Consistent abstraction through repository interfaces
- Specification Pattern: Type-safe query building with MongoDB filter builders
- Unit of Work: Transaction management for multi-document operations (MongoDB 4.0+)
MongoDB Philosophy
MongoDB is ideal for document-heavy aggregates, content management, user profiles, product catalogs, and scenarios requiring schema flexibility. The template treats MongoDB as a first-class persistence option, providing the same repository and specification abstractions as SQL-based persistence, ensuring business logic remains persistence-agnostic.
Architecture Overview¶
MongoDB Position in Clean Architecture¶
Application Layer (DomainModel)
├── Processors (Commands/Writes)
└── Retrievers (Queries/Reads)
↓ (Uses Repository Interfaces)
Infrastructure Layer (PersistenceModel.MongoDb)
├── Repositories (MongoDB Driver-based)
├── Specifications (MongoDB Filter Builder-based)
├── Mappings (BsonClassMap)
└── Conventions (Guid serialization)
↓
MongoDB Database
├── Collections (Documents)
├── Indexes
└── Transactions (MongoDB 4.0+)
Project Structure¶
ConnectSoft.MicroserviceTemplate.PersistenceModel.MongoDb/
├── Repositories/
│ ├── MicroserviceAggregateRootsMongoDbRepository.cs
│ └── MicroserviceAggregateRootsMongoDbKeyedRepository.cs
├── Specifications/
│ ├── MicroserviceAggregateRootsMongoDbQueryableSpecification.cs
│ └── MicroserviceAggregateRootsMongoDbQueryableKeyedSpecification.cs
├── Mappings/
│ └── MicroserviceAggregateRootEntityMap.cs
├── Conventions/
│ └── MongoGuidMemberConvention.cs
└── MongoGuidConventions.cs
ConnectSoft.MicroserviceTemplate.DatabaseModel.MongoDb.Migrations/
└── MicroserviceMongoDbMigration.cs
Configuration¶
Service Registration¶
MongoDB is registered via extension methods:
// MicroserviceRegistrationExtensions.cs
#if UseMongoDb
services.AddMongoDbPersistence(configuration);
#endif
#if Migrations && UseMongoDb
services.AddMongoDbMigrator(configuration, typeof(MicroserviceMongoDbMigration));
#endif
#if (UseNHibernate || UseMongoDb)
services.AddPersistenceModel();
#endif
Connection Configuration¶
MongoDB connection is configured in appsettings.json:
{
"PersistenceModel": {
"MongoDb": {
"MongoDbConnectionStringKey": "ConnectSoft.MicroserviceTemplateMongoDb",
"DatabaseName": "MICROSERVICE_DATABASE"
}
},
"ConnectionStrings": {
"ConnectSoft.MicroserviceTemplateMongoDb": "mongodb://localhost:27017"
}
}
Configuration Options:
| Option | Type | Default | Description |
|---|---|---|---|
MongoDbConnectionStringKey |
string |
"ConnectSoft.MicroserviceTemplateMongoDb" |
Key for connection string in ConnectionStrings section |
DatabaseName |
string |
"MICROSERVICE_DATABASE" |
MongoDB database name |
Connection String Formats¶
Local Development:
Replica Set:
With Authentication:
Connection String Options:
- replicaSet: Replica set name
- authSource: Authentication database
- ssl: Enable SSL/TLS
- readPreference: Read preference (primary, secondary, etc.)
- maxPoolSize: Maximum connection pool size
MongoDB Registration Details¶
The AddMongoDbPersistence() extension method configures MongoDB:
// MongoDbExtensions.cs
internal static IServiceCollection AddMongoDbPersistence(
this IServiceCollection services,
IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
// Register MongoDB client, database, and unit of work
services.UseMongoDbPersistence(
configuration: configuration,
connectionStringKey: OptionsExtensions.PersistenceModelOptions.MongoDb.MongoDbConnectionStringKey,
databaseName: OptionsExtensions.PersistenceModelOptions.MongoDb.DatabaseName,
dependencyInjectionKey: MicroserviceConstants.MongoDbDIKey);
// Register repositories (keyed or non-keyed)
if (!string.IsNullOrEmpty(MicroserviceConstants.MongoDbDIKey))
{
services.AddKeyedScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsMongoDbKeyedRepository>(MicroserviceConstants.MongoDbDIKey);
services.AddKeyedScoped<IMicroserviceAggregateRootsSpecification, MicroserviceAggregateRootsMongoDbQueryableKeyedSpecification>(MicroserviceConstants.MongoDbDIKey);
}
else
{
services.AddScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsMongoDbRepository>();
services.AddScoped<IMicroserviceAggregateRootsSpecification, MicroserviceAggregateRootsMongoDbQueryableSpecification>();
}
// Register conventions and mappings
MongoGuidConventions.EnsureRegistered();
MicroserviceAggregateRootEntityMap.MapClasses();
return services;
}
Class Mappings¶
Overview¶
MongoDB uses BSON (Binary JSON) serialization to map C# classes to MongoDB documents. The template uses convention-based mapping with explicit configuration for complex scenarios.
Automatic Mapping¶
MongoDB driver automatically maps classes using conventions:
- Class Name → Collection Name:
MicroserviceAggregateRootEntity→"MicroserviceAggregateRoots"(pluralized) - Properties → Document Fields:
ObjectId→"ObjectId"(or"_id"if marked as ID) - Guid → BSON Binary: GUIDs are serialized as BSON binary with standard representation
Explicit Class Mapping¶
For explicit control, use BsonClassMap:
// MicroserviceAggregateRootEntityMap.cs
public static class MicroserviceAggregateRootEntityMap
{
public static void MapClasses()
{
if (!BsonClassMap.IsClassMapRegistered(typeof(MicroserviceAggregateRootEntity)))
{
BsonClassMap.RegisterClassMap<MicroserviceAggregateRootEntity>(classMap =>
{
// Auto-map all properties
classMap.AutoMap();
});
// Register interface serializer
var testObjectSerializer = BsonSerializer.LookupSerializer<MicroserviceAggregateRootEntity>();
BsonSerializer.RegisterSerializer(
new ImpliedImplementationInterfaceSerializer<IMicroserviceAggregateRoot, MicroserviceAggregateRootEntity>(testObjectSerializer));
}
}
}
Manual Mapping Examples¶
Map Property to Different Field Name:
BsonClassMap.RegisterClassMap<MyEntity>(classMap =>
{
classMap.AutoMap();
classMap.MapMember(x => x.InternalId).SetElementName("_id");
classMap.MapMember(x => x.CreatedDate).SetElementName("created_at");
});
Ignore Properties:
BsonClassMap.RegisterClassMap<MyEntity>(classMap =>
{
classMap.AutoMap();
classMap.UnmapMember(x => x.TemporaryProperty);
});
Map Collections:
BsonClassMap.RegisterClassMap<Order>(classMap =>
{
classMap.AutoMap();
classMap.MapMember(x => x.Items).SetElementName("order_items");
});
Attributes for Mapping¶
BsonId Attribute:
BsonElement Attribute:
BsonIgnore Attribute:
BsonRepresentation Attribute:
public class MyEntity
{
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } // Stored as string instead of binary
}
Conventions¶
Guid Serialization Convention¶
The template registers a convention to ensure GUIDs are serialized consistently:
// MongoGuidConventions.cs
public static class MongoGuidConventions
{
public static void EnsureRegistered()
{
if (Interlocked.Exchange(ref initialized, 1) == 1)
{
return;
}
var pack = new ConventionPack { new MongoGuidMemberConvention() };
ConventionRegistry.Register("ConnectSoft.Guid(Standard) Pack", pack, _ => true);
}
}
// MongoGuidMemberConvention.cs
public sealed class MongoGuidMemberConvention : ConventionBase, IMemberMapConvention
{
public void Apply(BsonMemberMap memberMap)
{
var t = memberMap.MemberType;
if (t == typeof(Guid))
{
memberMap.SetSerializer(new GuidSerializer(GuidRepresentation.Standard));
}
else if (Nullable.GetUnderlyingType(t) == typeof(Guid))
{
memberMap.SetSerializer(new NullableSerializer<Guid>(new GuidSerializer(GuidRepresentation.Standard)));
}
}
}
Benefits:
- Consistent GUID serialization across all entities
- Standard representation for interoperability
- Applied automatically to all Guid and Guid? properties
Collections¶
Collection Naming¶
Collections are named automatically based on entity type:
// Automatic collection name: "MicroserviceAggregateRoots"
var collection = database.GetCollection<MicroserviceAggregateRootEntity>("MicroserviceAggregateRoots");
Collection Name Convention:
- Entity class name pluralized
- Example: MicroserviceAggregateRootEntity → "MicroserviceAggregateRoots"
Custom Collection Names¶
Using Attribute:
In Mapping:
BsonClassMap.RegisterClassMap<MyEntity>(classMap =>
{
classMap.SetCollectionName("custom_collection_name");
classMap.AutoMap();
});
Collection Creation¶
Collections are created automatically on first insert, or explicitly via migrations:
// Via migration
[Migration(0, "Create collections")]
public class CreateCollectionsMigration : Migration
{
public override void Up()
{
this.Database.CreateCollection("MicroserviceAggregateRoots");
}
}
Repositories¶
MongoDB Repository Implementation¶
Repositories inherit from MongoDbRepository<TEntity, TIdentity>:
// MicroserviceAggregateRootsMongoDbRepository.cs
public class MicroserviceAggregateRootsMongoDbRepository(
IUnitOfWork unitOfWork,
IMongoDatabase mongoDatabase,
ISpecificationLocator specificationLocator,
ILogger<MongoDbRepository<IMicroserviceAggregateRoot, Guid>> logger)
: MongoDbRepository<IMicroserviceAggregateRoot, Guid>(unitOfWork, mongoDatabase, specificationLocator, logger),
IMicroserviceAggregateRootsRepository
{
}
Base Class Provides: - CRUD operations (Insert, Update, Delete, GetById, GetAll) - Query operations (Query, QueryAsync) - Specification pattern support - Unit of Work integration
Repository Operations¶
Insert:
var entity = new MicroserviceAggregateRootEntity { ObjectId = Guid.NewGuid() };
await repository.InsertAsync(entity, cancellationToken);
Update:
Delete:
await repository.DeleteAsync(entity, cancellationToken);
// Or by ID
await repository.DeleteAsync(id, cancellationToken);
Get by ID:
Get All:
Query:
Keyed Repositories¶
For multi-database scenarios, use keyed repositories:
// MicroserviceAggregateRootsMongoDbKeyedRepository.cs
public class MicroserviceAggregateRootsMongoDbKeyedRepository(
[FromKeyedServices(MicroserviceConstants.MongoDbDIKey)] IUnitOfWork unitOfWork,
[FromKeyedServices(MicroserviceConstants.MongoDbDIKey)] IMongoDatabase mongoDatabase,
[FromKeyedServices(MicroserviceConstants.MongoDbDIKey)] ISpecificationLocator specificationLocator,
[FromKeyedServices(MicroserviceConstants.MongoDbDIKey)] ILogger<MongoDbRepository<IMicroserviceAggregateRoot, Guid>> logger)
: MongoDbRepository<IMicroserviceAggregateRoot, Guid>(unitOfWork, mongoDatabase, specificationLocator, logger),
IMicroserviceAggregateRootsRepository
{
}
Usage with Keyed Services:
public class MyService
{
private readonly IMicroserviceAggregateRootsRepository mongoRepository;
public MyService(
[FromKeyedServices(MicroserviceConstants.MongoDbDIKey)]
IMicroserviceAggregateRootsRepository mongoRepository)
{
this.mongoRepository = mongoRepository;
}
}
Specifications and Queries¶
MongoDB Queryable Specification¶
Specifications translate to MongoDB filter builders:
// MicroserviceAggregateRootsMongoDbQueryableSpecification.cs
public class MicroserviceAggregateRootsMongoDbQueryableSpecification(
IMongoDatabase mongoDatabase,
ILogger<MongoDbQueryableSpecification<IMicroserviceAggregateRoot, Guid>> logger)
: MongoDbQueryableSpecification<IMicroserviceAggregateRoot, Guid>(mongoDatabase, logger),
IMicroserviceAggregateRootsSpecification
{
}
Using Specifications¶
// In retriever
public async Task<IReadOnlyList<IMicroserviceAggregateRoot>> GetActiveAggregatesAsync(
CancellationToken ct)
{
var specification = this.repository.Specify<IMicroserviceAggregateRootsSpecification>()
.Where(x => x.SomeValue == "Active")
.OrderByDescending(x => x.CreatedOn);
return await specification.ToListAsync(ct);
}
Direct MongoDB Queries¶
For complex queries, access MongoDB directly:
public class MyService
{
private readonly IMongoDatabase mongoDatabase;
public MyService(IMongoDatabase mongoDatabase)
{
this.mongoDatabase = mongoDatabase;
}
public async Task<List<MyEntity>> ComplexQueryAsync()
{
var collection = mongoDatabase.GetCollection<MyEntity>("MyEntities");
var filter = Builders<MyEntity>.Filter.And(
Builders<MyEntity>.Filter.Eq(x => x.Status, "Active"),
Builders<MyEntity>.Filter.Gte(x => x.CreatedOn, DateTimeOffset.UtcNow.AddDays(-30))
);
var sort = Builders<MyEntity>.Sort.Descending(x => x.CreatedOn);
return await collection
.Find(filter)
.Sort(sort)
.Limit(100)
.ToListAsync();
}
}
Aggregation Pipelines¶
For complex aggregations:
var pipeline = new BsonDocument[]
{
new BsonDocument("$match", new BsonDocument("Status", "Active")),
new BsonDocument("$group", new BsonDocument
{
{ "_id", "$Category" },
{ "count", new BsonDocument("$sum", 1) }
}),
new BsonDocument("$sort", new BsonDocument("count", -1))
};
var results = await collection.Aggregate<BsonDocument>(pipeline).ToListAsync();
Indexes¶
Creating Indexes¶
Indexes are created via migrations:
[Migration(1, "Create indexes")]
public class CreateIndexesMigration : Migration
{
public override void Up()
{
var collection = this.Database.GetCollection<BsonDocument>("MicroserviceAggregateRoots");
// Single field index
var singleFieldIndex = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("ObjectId"),
new CreateIndexOptions { Unique = true });
collection.Indexes.CreateOne(singleFieldIndex);
// Compound index
var compoundIndex = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("Status")
.Descending("CreatedOn"),
new CreateIndexOptions { Unique = false });
collection.Indexes.CreateOne(compoundIndex);
}
}
Index Types¶
Single Field Index:
var index = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("Status"));
Compound Index:
var index = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys
.Ascending("Status")
.Descending("CreatedOn"));
Text Index:
var index = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Text("Name").Text("Description"));
Geospatial Index:
var index = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Geo2DSphere("Location"));
TTL Index:
var index = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("ExpiresAt"),
new CreateIndexOptions { ExpireAfter = TimeSpan.Zero });
Index Options¶
var index = new CreateIndexModel<BsonDocument>(
Builders<BsonDocument>.IndexKeys.Ascending("Email"),
new CreateIndexOptions
{
Unique = true,
Sparse = true,
Background = false,
Name = "IX_Email_Unique"
});
Migrations¶
MongoDB Migrations Overview¶
MongoDB migrations use CSharpMongoMigrations to manage collection creation, indexes, and schema evolution.
See Database Migrations for comprehensive documentation.
Migration Example¶
// MicroserviceMongoDbMigration.cs
[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¶
// MongoDbExtensions.cs
#if Migrations
internal static IServiceCollection AddMongoDbMigrator(
this IServiceCollection services,
IConfiguration configuration,
Type assemblyType)
{
services.UseMongoDbMigrator(
configuration: configuration,
connectionStringKey: OptionsExtensions.PersistenceModelOptions.MongoDb.MongoDbConnectionStringKey,
databaseName: OptionsExtensions.PersistenceModelOptions.MongoDb.DatabaseName,
assemblyType: assemblyType);
return services;
}
#endif
Migrations run automatically and synchronously during service registration when UseMongoDbMigrator() is called. The LoggingMigrationRunner is used internally to provide structured logging and OpenTelemetry tracing for migration operations.
Unit of Work Pattern¶
Transaction Management¶
MongoDB supports multi-document transactions (MongoDB 4.0+). The Unit of Work pattern provides transaction management:
public class DefaultMicroserviceAggregateRootsProcessor : IMicroserviceAggregateRootsProcessor
{
private readonly IUnitOfWork unitOfWork;
private readonly IMicroserviceAggregateRootsRepository repository;
public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
CreateMicroserviceAggregateRootInput input,
CancellationToken token = default)
{
using var transaction = await this.unitOfWork.BeginTransactionAsync(token);
try
{
var aggregate = new MicroserviceAggregateRootEntity
{
ObjectId = input.ObjectId,
SomeValue = input.SomeValue
};
await this.repository.InsertAsync(aggregate, token);
await transaction.CommitAsync(token);
return aggregate;
}
catch
{
// Transaction rolls back automatically on disposal if not committed
throw;
}
}
}
Transaction Requirements: - MongoDB 4.0+ (replica set or sharded cluster) - Replica set for transactions (not standalone) - WiredTiger storage engine
Health Checks¶
MongoDB Health Check¶
MongoDB health check verifies database connectivity:
// HealthChecksExtensions.cs
#if UseMongoDb
builder.AddMongoDb(
mongoDatabase: serviceProvider =>
{
if (!string.IsNullOrEmpty(MicroserviceConstants.MongoDbDIKey))
{
return serviceProvider.GetRequiredKeyedService<IMongoClient>(MicroserviceConstants.MongoDbDIKey)
.GetDatabase(OptionsExtensions.PersistenceModelOptions.MongoDb.DatabaseName);
}
else
{
return serviceProvider.GetRequiredService<IMongoClient>()
.GetDatabase(OptionsExtensions.PersistenceModelOptions.MongoDb.DatabaseName);
}
},
name: "ConnectSoft.MicroserviceTemplate MongoDb persistence health check",
tags: new string[] { "application mongodb", "ready" });
#endif
Health Check Endpoint:
- /health/ready: Includes MongoDB check
- Returns Healthy if MongoDB is accessible
- Returns Unhealthy if connection fails
Observability¶
OpenTelemetry Integration¶
MongoDB operations are automatically traced via OpenTelemetry:
// OpenTelemetryExtensions.cs
#if UseMongoDb
.AddSource("MongoDB.Driver.Core.Extensions.DiagnosticSources")
#endif
Traced Operations: - Database commands - Query execution - Insert/Update/Delete operations - Connection pool operations
Logging¶
MongoDB driver logs are integrated with application logging:
// Configure MongoDB logging
services.AddSingleton<ILogger<MongoClient>>(serviceProvider =>
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<MongoClient>());
Best Practices¶
Do's¶
-
Use Repository Pattern
-
Create Indexes for Query Performance
-
Use Specifications for Complex Queries
-
Handle Transactions Properly
-
Use Appropriate Collection Names
-
Register Conventions Early
Don'ts¶
-
Don't Mix Direct MongoDB Access with Repository Pattern
-
Don't Create Indexes on Every Insert
-
Don't Ignore Transaction Requirements
-
Don't Use Large Document Sizes
-
Don't Skip Index Creation
Troubleshooting¶
Issue: Connection Failures¶
Symptoms: Cannot connect to MongoDB instance.
Solutions: 1. Verify connection string format 2. Check MongoDB server is running 3. Verify network connectivity (firewall, ports) 4. Check authentication credentials 5. Verify replica set configuration (if using transactions)
Issue: Collection Not Found¶
Symptoms: MongoCommandException with "collection not found".
Solutions: 1. Create collection via migration 2. Collections are auto-created on first insert (if not disabled) 3. Verify collection name matches mapping
Issue: Transaction Errors¶
Symptoms: Transaction operations fail with "transaction not supported".
Solutions: 1. Verify MongoDB version is 4.0+ 2. Ensure replica set (not standalone) for transactions 3. Check WiredTiger storage engine 4. Verify transaction is properly committed or rolled back
Issue: Performance Issues¶
Symptoms: Slow queries, high CPU usage.
Solutions:
1. Create Indexes: Add indexes for frequently queried fields
2. Use Projections: Select only needed fields
3. Limit Results: Use Limit() and Skip() for pagination
4. Optimize Queries: Use Explain() to analyze query plans
5. Review Index Usage: Check if indexes are being used
Issue: Serialization Errors¶
Symptoms: BsonSerializationException during serialization.
Solutions:
1. Verify class mapping is registered
2. Check property types are supported
3. Ensure conventions are registered before mapping
4. Verify BsonId attribute on ID property
5. Check for circular references (use [BsonIgnore])
Issue: Index Creation Failures¶
Symptoms: Index creation fails in migrations.
Solutions: 1. Verify index definition syntax 2. Check for duplicate index names 3. Ensure sufficient disk space 4. Verify index options are valid 5. Check index name length (MongoDB limit)
Summary¶
MongoDB in the ConnectSoft Microservice Template provides:
- ✅ Document-Based Storage: Flexible schema for document-heavy aggregates
- ✅ Repository Pattern: Consistent abstraction matching SQL persistence
- ✅ Specification Pattern: Type-safe query building with MongoDB filters
- ✅ Class Mappings: Automatic mapping with explicit configuration support
- ✅ Conventions: Consistent GUID serialization and other conventions
- ✅ Migrations: Versioned collection and index management
- ✅ Transactions: Multi-document transaction support (MongoDB 4.0+)
- ✅ Health Checks: Built-in connectivity verification
- ✅ Observability: OpenTelemetry tracing for all operations
- ✅ Polyglot Persistence: Use alongside NHibernate for different aggregates
By following these patterns, teams can:
- Store Documents Efficiently: Leverage MongoDB's document model for flexible data structures
- Maintain Consistency: Use repository pattern for persistence-agnostic business logic
- Query Effectively: Use specifications and indexes for performant queries
- Scale Horizontally: Leverage MongoDB's built-in sharding and replication
- Monitor Operations: Integrate with observability infrastructure for full visibility
MongoDB integration ensures that microservices can efficiently store and query document-based data while maintaining consistency with the template's architectural patterns and best practices.