Skip to content

NHibernate in ConnectSoft Microservice Template

Purpose & Overview

NHibernate is an open-source Object-Relational Mapping (ORM) framework for .NET that provides a comprehensive solution for mapping .NET domain models to relational databases. In the ConnectSoft Microservice Template, NHibernate serves as one of the primary persistence technologies, alongside MongoDB, enabling type-safe database operations with SQL Server, PostgreSQL, MySQL, and other relational databases.

NHibernate provides:

  • ORM Mapping: Automatic mapping between .NET objects and database tables
  • Fluent NHibernate: Code-based mapping configuration (alternative to XML)
  • Session Management: Automatic session lifecycle and transaction management
  • Query Languages: HQL (Hibernate Query Language), Criteria API, LINQ provider
  • Second-Level Cache: Distributed caching support with Redis
  • Connection Pooling: Built-in connection pool management
  • Lazy Loading: Efficient data loading with lazy associations
  • Transaction Support: ACID-compliant transaction management
  • Schema Generation: Optional automatic schema generation (though FluentMigrator is preferred)

NHibernate Philosophy

NHibernate aims to reduce the impedance mismatch between object-oriented programming and relational databases. It provides a powerful yet flexible mapping framework that allows developers to work with objects while NHibernate handles the SQL translation and data persistence.

Architecture Overview

NHibernate in ConnectSoft Architecture

Application Layer
    ├── Processors (Commands/Writes)
    └── Retrievers (Queries/Reads)
    ↓ (Uses Repository Interface)
Infrastructure Layer
    ├── PersistenceModel.NHibernate
    │   ├── Repositories (NHibernate Implementation)
    │   ├── Mappings (Fluent NHibernate)
    │   └── Specifications (Query Specifications)
    ├── NHibernate Session Factory
    ├── Session Management (Per HTTP Request)
    └── Unit of Work (Transaction Management)
Database Layer
    └── SQL Server / PostgreSQL / MySQL

Key Integration Points

Layer Component Responsibility
PersistenceModel IMicroserviceAggregateRootsRepository Repository interface (abstraction)
PersistenceModel.NHibernate MicroserviceAggregateRootsRepository NHibernate repository implementation
PersistenceModel.NHibernate.Mappings MicroserviceAggregateRootEntityMap Fluent NHibernate entity mappings
ApplicationModel NHibernateExtensions NHibernate configuration and registration
Framework ConnectSoft.Extensions.PersistenceModel.NHibernate Base repository and session management

Configuration

Service Registration

NHibernate is registered via extension methods in the application startup:

// MicroserviceRegistrationExtensions.cs
public static IServiceCollection ConfigureMicroserviceServices(
    this IServiceCollection services, 
    IConfiguration configuration, 
    IWebHostEnvironment environment)
{
    // ... other service registrations ...

#if Migrations
    services.AddMicroserviceFluentMigrator(configuration, typeof(MicroserviceMigration).Assembly);
#endif

#if UseNHibernate
    services.AddNHibernatePersistenceModel(configuration);
#endif

#if (UseNHibernate || UseMongoDb)
    services.AddPersistenceModel();
#endif

    return services;
}

NHibernate Extension Method

The AddNHibernatePersistenceModel() method configures all NHibernate services:

// NHibernateExtensions.cs
internal static IServiceCollection AddNHibernatePersistenceModel(
    this IServiceCollection services, 
    IConfiguration configuration)
{
    ArgumentNullException.ThrowIfNull(services);

    string hibernateConfig = OptionsExtensions.PersistenceModelOptions.NHibernate.NHibernateConfigFile;

    services.AddNHibernateFromConfiguration(
        filePath: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, hibernateConfig),
        nhibernateMappingsAssembly: typeof(MicroserviceAggregateRootEntityMap).Assembly,
        systemConfiguration: configuration,
        dependencyInjectionKey: MicroserviceConstants.NHibernateDIKey);

    if (!string.IsNullOrEmpty(MicroserviceConstants.NHibernateDIKey))
    {
        // Keyed services (for multi-database scenarios)
        services.AddKeyedScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsKeyedRepository>(
            MicroserviceConstants.NHibernateDIKey);
        services.AddKeyedScoped<IMicroserviceAggregateRootsSpecification, MicroserviceAggregateRootsQueryableKeyedSpecification>(
            MicroserviceConstants.NHibernateDIKey);
    }
    else
    {
        // Standard services (single database scenario)
        services.AddScoped<IMicroserviceAggregateRootsRepository, MicroserviceAggregateRootsRepository>();
        services.AddScoped<IMicroserviceAggregateRootsSpecification, MicroserviceAggregateRootsQueryableSpecification>();
    }

    return services;
}

NHibernate Configuration File

NHibernate is configured via hibernate.cfg.xml:

<!-- hibernate.cfg.xml -->
<session-factory xmlns="urn:nhibernate-configuration-2.2" 
                 name="ConnectSoft.MicroserviceTemplateSqlServer">

  <!-- Database Dialect -->
  <property name="dialect">NHibernate.Dialect.MsSql2012Dialect</property>

  <!-- Database Driver -->
  <property name="connection.driver_class">
    NHibernate.Driver.MicrosoftDataSqlClientDriver
  </property>

  <!-- Connection String Key (from appsettings.json) -->
  <property name="connection.connection_string_name">
    ConnectSoft.MicroserviceTemplateSqlServer
  </property>

  <!-- Performance Settings -->
  <property name="adonet.batch_size">100</property>
  <property name="command_timeout">60</property>
  <property name="hbm2ddl.keywords">auto-quote</property>

  <!-- SQL Logging (development) -->
  <property name="show_sql">true</property>
  <property name="format_sql">true</property>

  <!-- Second-Level Cache (Redis) -->
  <!--#if (UseRedisAsNHibernateSecondLevelCache) -->
  <property name="cache.provider_class">
    NHibernate.Caches.StackExchangeRedis.RedisCacheProvider, 
    NHibernate.Caches.StackExchangeRedis
  </property>
  <property name="cache.default_expiration">300</property>
  <property name="cache.use_second_level_cache">true</property>
  <property name="cache.use_query_cache">true</property>
  <property name="cache.region_prefix">ConnectSoft.MicroserviceTemplate.Application</property>
  <property name="cache.configuration">localhost:6379,ConnectRetry=3,KeepAlive=180</property>
  <property name="cache.serializer">
    NHibernate.Caches.Util.JsonSerializer.JsonCacheSerializer, 
    NHibernate.Caches.Util.JsonSerializer
  </property>
  <!--#endif -->
</session-factory>

Configuration Options

Option Description Default
dialect Database-specific SQL dialect MsSql2012Dialect
connection.driver_class Database driver class MicrosoftDataSqlClientDriver
connection.connection_string_name Connection string key from appsettings.json Required
adonet.batch_size Batch size for bulk operations 100
command_timeout Command timeout (seconds) 60
show_sql Log SQL statements (development) true
format_sql Format SQL in logs true
cache.use_second_level_cache Enable second-level cache false
cache.use_query_cache Enable query cache false

appsettings.json Configuration

{
  "ConnectionStrings": {
    "ConnectSoft.MicroserviceTemplateSqlServer": "Server=localhost;Database=MicroserviceDB;Integrated Security=true;"
  },
  "PersistenceModel": {
    "NHibernate": {
      "NHibernateConfigFile": "hibernate.cfg.xml",
      "NHibernateConnectionStringKey": "ConnectSoft.MicroserviceTemplateSqlServer"
    }
  }
}

Supported Database Dialects

Database Dialect Class
SQL Server 2012+ NHibernate.Dialect.MsSql2012Dialect
SQL Server 2008 NHibernate.Dialect.MsSql2008Dialect
PostgreSQL NHibernate.Dialect.PostgreSQL83Dialect
MySQL NHibernate.Dialect.MySQLDialect
Oracle NHibernate.Dialect.Oracle12cDialect
SQLite NHibernate.Dialect.SQLiteDialect

Entity Mappings

Fluent NHibernate Mapping

Entity mappings are defined using Fluent NHibernate's ClassMap<T>:

// MicroserviceAggregateRootEntityMap.cs
namespace ConnectSoft.MicroserviceTemplate.PersistenceModel.NHibernate.Mappings
{
    using System;
    using ConnectSoft.MicroserviceTemplate.EntityModel.PocoEntities;
    using FluentNHibernate.Mapping;

    /// <summary>
    /// Defines a NHibernate mapping for a <see cref="MicroserviceAggregateRootEntity"/> entity.
    /// </summary>
    public class MicroserviceAggregateRootEntityMap : ClassMap<MicroserviceAggregateRootEntity>
    {
        public MicroserviceAggregateRootEntityMap()
        {
            // Schema and table names (with quoted identifiers)
            this.Schema("`" + PersistenceModelConstants.Schema + "`");
            this.Table("`MicroserviceAggregateRoots`");

            // Primary key mapping
            this.Id(m => m.ObjectId)
                .Column(nameof(MicroserviceAggregateRootEntity.ObjectId))
                .GeneratedBy.Assigned()
                .UnsavedValue(Guid.Empty);

            // Additional property mappings would go here
            // this.Map(m => m.SomeValue).Column("SomeValue").Not.Nullable();
            // this.Map(m => m.CreatedOn).Column("CreatedOn");
        }
    }
}

Mapping Examples

Simple Property Mapping

this.Map(m => m.Name)
    .Column("Name")
    .Length(100)
    .Not.Nullable();

Collection Mapping (One-to-Many)

this.HasMany(m => m.Items)
    .KeyColumn("AggregateRootId")
    .Cascade.AllDeleteOrphan()
    .Inverse();

Reference Mapping (Many-to-One)

this.References(m => m.Category)
    .Column("CategoryId")
    .Cascade.None();

Component Mapping (Value Object)

this.Component(m => m.Address, address =>
{
    address.Map(a => a.Street);
    address.Map(a => a.City);
    address.Map(a => a.PostalCode);
});

Cache Mapping

this.Cache.ReadWrite().Region("aggregate-root");

Mapping Auto-Discovery

NHibernate automatically discovers mappings in the specified assembly:

services.AddNHibernateFromConfiguration(
    nhibernateMappingsAssembly: typeof(MicroserviceAggregateRootEntityMap).Assembly,
    // ... other parameters ...
);

All classes inheriting from ClassMap<T> in this assembly are automatically registered.

Repository Implementation

Repository Base Class

Repositories inherit from GenericRepository<TEntity, TIdentity> provided by the framework:

// 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)
    }
}

Keyed Repository (Multi-Database)

For scenarios with multiple databases, repositories can use keyed services:

// MicroserviceAggregateRootsKeyedRepository.cs
public class MicroserviceAggregateRootsKeyedRepository(
    [FromKeyedServices(MicroserviceConstants.NHibernateDIKey)] IUnitOfWork unitOfWork,
    [FromKeyedServices(MicroserviceConstants.NHibernateDIKey)] ISpecificationLocator specificationLocator)
    : GenericRepository<IMicroserviceAggregateRoot, Guid>(unitOfWork, specificationLocator), 
      IMicroserviceAggregateRootsRepository
{
    // Same functionality as non-keyed repository
    // But uses keyed Unit of Work and Specification Locator
}

Repository Operations

The GenericRepository base class provides:

CRUD Operations

// Insert
await repository.InsertAsync(entity, cancellationToken);

// Update
await repository.UpdateAsync(entity, cancellationToken);

// Delete
await repository.DeleteAsync(entity, cancellationToken);
await repository.DeleteAsync(id, cancellationToken);

// Get by ID
var entity = await repository.GetByIdAsync(id, cancellationToken);

// Get all
var allEntities = await repository.GetAllAsync(cancellationToken);

Query Operations

// Expression-based query
var results = await repository.QueryAsync(
    x => x.Status == "Active" && x.CreatedOn >= startDate,
    cancellationToken);

// Specification-based query
var specification = repository.Specify<IActiveAggregatesSpecification>()
    .Where(x => x.CreatedOn >= startDate);

var results = await specification.ToListAsync(cancellationToken);

Session Management

Session Lifecycle

NHibernate sessions are managed per HTTP request (scoped):

  1. Session Creation: Session created at request start
  2. Session Usage: Repositories use session for operations
  3. Transaction Management: Unit of Work manages transactions
  4. Session Disposal: Session disposed at request end

Session Factory

The session factory is registered as a singleton:

// Provided by ConnectSoft.Extensions.PersistenceModel.NHibernate
services.AddNHibernateFromConfiguration(
    // Creates ISessionFactory (singleton)
    // Creates ISession (scoped per HTTP request)
);

Session Scoping

  • ISessionFactory: Singleton (created once, reused)
  • ISession: Scoped (one per HTTP request)
  • ITransaction: Managed by Unit of Work pattern

Unit of Work Pattern

Transaction Management

NHibernate integrates with the Unit of Work pattern for transaction management:

// Unit of Work is registered as scoped service
services.AddScoped<IUnitOfWork, NHibernateUnitOfWork>();

Using Unit of Work

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

    public async Task<IMicroserviceAggregateRoot> CreateMicroserviceAggregateRoot(
        CreateMicroserviceAggregateRootInput input,
        CancellationToken token = default)
    {
        // Begin transaction
        using var transaction = await this.unitOfWork.BeginTransactionAsync(token);

        try
        {
            var aggregate = new MicroserviceAggregateRootEntity
            {
                ObjectId = input.ObjectId,
                SomeValue = input.SomeValue
            };

            // Persist (uses NHibernate session)
            await this.repository.InsertAsync(aggregate, token);

            // Commit transaction
            await transaction.CommitAsync(token);

            return aggregate;
        }
        catch
        {
            // Transaction rolls back automatically on disposal if not committed
            throw;
        }
    }
}

ExecuteTransactional Helper

Some implementations provide a helper method:

unitOfWork.ExecuteTransactional(() =>
{
    repository.Insert(newEntity);
    repository.Update(existingEntity);
    // All operations are in same transaction
});

Querying

HQL (Hibernate Query Language)

NHibernate supports HQL for database-agnostic queries:

// HQL Query
var results = session.CreateQuery(
    "from MicroserviceAggregateRootEntity where Status = :status")
    .SetParameter("status", "Active")
    .List<MicroserviceAggregateRootEntity>();

LINQ Provider

NHibernate provides a LINQ provider:

var results = session.Query<MicroserviceAggregateRootEntity>()
    .Where(x => x.Status == "Active" && x.CreatedOn >= startDate)
    .OrderBy(x => x.CreatedOn)
    .ToList();

Criteria API

For dynamic queries:

var criteria = session.CreateCriteria<MicroserviceAggregateRootEntity>()
    .Add(Restrictions.Eq("Status", "Active"))
    .Add(Restrictions.Ge("CreatedOn", startDate))
    .AddOrder(Order.Desc("CreatedOn"));

var results = criteria.List<MicroserviceAggregateRootEntity>();

Specification Pattern

The template uses the Specification Pattern for complex queries (see Specification Pattern):

var specification = repository.Specify<IMicroserviceAggregateRootsSpecification>()
    .Where(x => x.Status == "Active")
    .And(x => x.CreatedOn >= startDate);

var results = await specification.ToListAsync(cancellationToken);

Second-Level Cache

Redis Cache Configuration

NHibernate supports second-level caching with Redis:

<!-- hibernate.cfg.xml -->
<property name="cache.provider_class">
  NHibernate.Caches.StackExchangeRedis.RedisCacheProvider, 
  NHibernate.Caches.StackExchangeRedis
</property>
<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache">true</property>
<property name="cache.region_prefix">ConnectSoft.MicroserviceTemplate.Application</property>
<property name="cache.configuration">localhost:6379,ConnectRetry=3,KeepAlive=180</property>
<property name="cache.default_expiration">300</property>

Entity Cache Configuration

Enable caching on entities via mapping:

// In ClassMap<T>
public MicroserviceAggregateRootEntityMap()
{
    // Enable second-level cache
    this.Cache
        .ReadWrite()  // Read-write cache strategy
        .Region("aggregate-root");  // Cache region
}

Query Cache

Enable caching on queries:

var results = session.CreateQuery(
    "from MicroserviceAggregateRootEntity where Status = :status")
    .SetParameter("status", "Active")
    .SetCacheable(true)  // Enable query cache
    .SetCacheRegion("active-aggregates")  // Cache region
    .List<MicroserviceAggregateRootEntity>();

Cache Strategies

Strategy Description Use Case
Read-Only Cache.ReadOnly() Immutable entities
Read-Write Cache.ReadWrite() Read-heavy, occasionally updated
Nonstrict-Read-Write Cache.NonstrictReadWrite() Rarely updated
Transactional Cache.Transactional() High consistency required

See Caching for detailed information.

Database Migrations

FluentMigrator Integration

The template uses FluentMigrator for database schema management (not NHibernate's schema generation):

// MicroserviceRegistrationExtensions.cs
#if Migrations
services.AddMicroserviceFluentMigrator(configuration, typeof(MicroserviceMigration).Assembly);
#endif

Migrations run at application startup:

// Startup middleware
#if UseNHibernate
application.RunMicroserviceFluentMigrations();
#endif

Migration Example

[Migration(20240101000000)]
public class CreateMicroserviceAggregateRootsTable : Migration
{
    public override void Up()
    {
        Create.Table("MicroserviceAggregateRoots")
            .InSchema("dbo")
            .WithColumn("ObjectId").AsGuid().PrimaryKey()
            .WithColumn("SomeValue").AsString(255).Nullable();
    }

    public override void Down()
    {
        Delete.Table("MicroserviceAggregateRoots").InSchema("dbo");
    }
}

Migration Strategy

While NHibernate supports automatic schema generation (hbm2ddl.auto), the template uses FluentMigrator for production-ready, version-controlled database migrations. This ensures schema changes are tracked, reversible, and can be applied to multiple environments consistently.

Logging

NHibernate Logging Integration

NHibernate integrates with Microsoft.Extensions.Logging:

// MicroserviceRegistrationExtensions.cs
#if UseNHibernate
loggerFactory.UseNHibernateLogging();
#endif

Logging Configuration

NHibernate logs can be filtered in appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "NHibernate": "Debug",
      "NHibernate.SQL": "Debug",
      "NHibernate.Impl": "Warning"
    }
  }
}

SQL Logging

Enable SQL statement logging:

<!-- hibernate.cfg.xml -->
<property name="show_sql">true</property>
<property name="format_sql">true</property>

Health Checks

NHibernate Health Check

NHibernate persistence is monitored via health checks:

// HealthChecksExtensions.cs
#if UseNHibernate
builder.AndAndConfigureNHibernatePersistenceHealthCheck(configuration);
#endif

The health check verifies: - Database connectivity - Session factory availability - Basic query execution

See Health Checks for detailed information.

OpenTelemetry Integration

SQL Instrumentation

NHibernate operations are automatically traced via SQL Client instrumentation:

// OpenTelemetryExtensions.cs
#if UseNHibernate
tracingBuilder.AddSqlClientInstrumentation(options =>
{
    options.RecordException = true;
    options.SetDbStatementForText = true;
    options.EnableConnectionLevelAttributes = true;
});
#endif

Traced operations include: - SQL query execution - Connection management - Transaction operations - Batch operations

Metrics

NHibernate metrics are available via OpenTelemetry: - Query execution time - Connection pool usage - Transaction duration - Cache hit/miss ratios

See Logging for detailed observability information.

Testing

Unit Testing Mappings

Mappings can be validated using Fluent NHibernate's PersistenceSpecification:

// MicroserviceNHibernateClassMappingsUnitTests.cs
[TestMethod]
public void CanCorrectlyMapMicroserviceAggregateRootEntityUsingFluentNHibernate()
{
    // Arrange
    var serviceProvider = CreateServiceProvider();
    var session = serviceProvider.GetRequiredService<ISessionSource>();

    // Act and Assert
    new PersistenceSpecification<MicroserviceAggregateRootEntity>(session)
        .CheckProperty(c => c.ObjectId, Guid.NewGuid())
        .VerifyTheMappings();
}

Integration Testing

[TestMethod]
public async Task Repository_Should_Save_And_Retrieve_Entity()
{
    // Arrange
    var repository = serviceProvider.GetRequiredService<IMicroserviceAggregateRootsRepository>();
    var unitOfWork = serviceProvider.GetRequiredService<IUnitOfWork>();
    var entity = new MicroserviceAggregateRootEntity { ObjectId = Guid.NewGuid() };

    // Act
    using var transaction = await unitOfWork.BeginTransactionAsync();
    await repository.InsertAsync(entity);
    await transaction.CommitAsync();

    // Assert
    var retrieved = await repository.GetByIdAsync(entity.ObjectId);
    Assert.IsNotNull(retrieved);
    Assert.AreEqual(entity.ObjectId, retrieved.ObjectId);
}

Best Practices

Do's

  1. Use Fluent NHibernate for Mappings
  2. Code-based mappings are more maintainable than XML
  3. Better IntelliSense and compile-time checking
  4. Easier refactoring

  5. Always Use Transactions

    // ✅ GOOD - Explicit transaction
    using var transaction = await unitOfWork.BeginTransactionAsync();
    await repository.InsertAsync(entity);
    await transaction.CommitAsync();
    

  6. Use Async Methods

    // ✅ GOOD - Async operations
    var entity = await repository.GetByIdAsync(id, cancellationToken);
    

  7. Enable Second-Level Cache for Read-Heavy Entities

    this.Cache.ReadWrite().Region("aggregate-root");
    

  8. Use Specifications for Complex Queries

  9. Encapsulates query logic
  10. Reusable and testable
  11. Maintains Clean Architecture boundaries

  12. Quote Identifiers for Schema/Table Names

    this.Schema("`" + PersistenceModelConstants.Schema + "`");
    this.Table("`MicroserviceAggregateRoots`");
    

Don'ts

  1. Don't Use Schema Generation in Production

    <!-- ❌ BAD - Never use in production -->
    <property name="hbm2ddl.auto">update</property>
    
    <!-- ✅ GOOD - Use FluentMigrator instead -->
    

  2. Don't Mix Session and Repository Abstractions

    // ❌ BAD - Leaks NHibernate to application layer
    public void SomeMethod(ISession session) { }
    
    // ✅ GOOD - Use repository interface
    public void SomeMethod(IMicroserviceAggregateRootsRepository repository) { }
    

  3. Don't Forget to Handle Null Returns

    // ❌ BAD - May throw NullReferenceException
    var entity = await repository.GetByIdAsync(id);
    var name = entity.Name;
    
    // ✅ GOOD - Check for null
    var entity = await repository.GetByIdAsync(id);
    if (entity == null)
    {
        throw new EntityNotFoundException(id);
    }
    

  4. Don't Use N+1 Queries

    // ❌ BAD - N+1 query problem
    var aggregates = await repository.GetAllAsync();
    foreach (var agg in aggregates)
    {
        var items = await LoadItemsForAggregate(agg.Id); // N queries!
    }
    
    // ✅ GOOD - Use eager loading or batch loading
    var aggregates = session.Query<MicroserviceAggregateRootEntity>()
        .FetchMany(a => a.Items)
        .ToList();
    

  5. Don't Expose ISession or ISessionFactory

  6. Always use repository interfaces
  7. Maintains Clean Architecture boundaries
  8. Enables easy swapping of persistence technologies

Troubleshooting

Issue: Session is Closed

Symptom: ObjectDisposedException or "Session is closed" errors.

Cause: Session disposed before operation completes, or accessing lazy-loaded properties outside session scope.

Solution:

// ✅ GOOD - Eager load associations within session
var entity = session.Query<Entity>()
    .Fetch(e => e.ChildCollection)
    .FirstOrDefault();

// ✅ GOOD - Map associations to DTOs before session closes
var dto = mapper.Map<EntityDto>(entity);
// Session can now close, DTO is detached

Issue: LazyInitializationException

Symptom: "Initializing proxy" exceptions when accessing lazy-loaded properties.

Cause: Accessing lazy-loaded properties outside active session.

Solution: - Use eager loading: .Fetch() or .FetchMany() - Map to DTOs before session closes - Use Session.Flush() if needed

Issue: Query Performance Issues

Symptom: Slow queries, high database load.

Causes and Solutions:

  1. N+1 Queries: Use eager loading or batch fetching

    var results = session.Query<Entity>()
        .FetchMany(e => e.Children)
        .ToList();
    

  2. Missing Indexes: Add database indexes for frequently queried columns

  3. Inefficient Queries: Use .Select() to limit columns:

    var summaries = session.Query<Entity>()
        .Select(e => new { e.Id, e.Name })
        .ToList();
    

  4. Large Result Sets: Use pagination:

    var page = session.Query<Entity>()
        .Skip(pageNumber * pageSize)
        .Take(pageSize)
        .ToList();
    

Issue: Transaction Not Committing

Cause: Missing Commit() call or exception before commit.

Solution:

using var transaction = await unitOfWork.BeginTransactionAsync();
try
{
    await repository.InsertAsync(entity);
    await transaction.CommitAsync(); // ✅ Must call Commit()
}
catch
{
    // Transaction rolls back on disposal if not committed
    throw;
}

Issue: Mapping Not Found

Symptom: "No persister for" errors.

Cause: Mapping class not discovered or assembly not registered.

Solution: Verify mapping class inherits from ClassMap<T> and is in the registered assembly:

services.AddNHibernateFromConfiguration(
    nhibernateMappingsAssembly: typeof(MicroserviceAggregateRootEntityMap).Assembly,
    // ...
);

Summary

NHibernate in the ConnectSoft Microservice Template provides:

  • ORM Mapping: Fluent NHibernate for type-safe entity mappings
  • Repository Pattern: Clean abstraction over data access
  • Session Management: Automatic per-request session lifecycle
  • Transaction Support: Unit of Work pattern for ACID compliance
  • Query Flexibility: HQL, LINQ, Criteria API, and Specifications
  • Caching: Second-level cache with Redis support
  • Migration Support: FluentMigrator integration for schema management
  • Observability: OpenTelemetry integration for tracing and metrics
  • Health Monitoring: Health checks for persistence layer
  • Testing Support: Mapping validation and integration test utilities

By following these patterns, microservices achieve:

  • Clean Architecture — Persistence details isolated from domain and application layers
  • Type Safety — Compile-time checking of queries and mappings
  • Performance — Efficient querying, caching, and batch operations
  • Maintainability — Code-based mappings, specifications, and repository abstractions
  • Scalability — Connection pooling, second-level cache, and optimized queries
  • Testability — Mockable repositories and in-memory test scenarios

The NHibernate integration ensures that ConnectSoft microservices can efficiently persist and retrieve domain entities while maintaining clean architectural boundaries and excellent observability.