When to Use NHibernate’s StatelessSession for Bulk Writes
StatelessSession cuts memory and CPU for large sequential inserts, but drops caching, lazy loading, and automatic dirty checking. This guide shows when it pays off, how to map, and the pitfalls that affect caching, events, and concurrency.
19 Jul 2025, 08:26 UTC

Problem: High‑Volume Sequential Inserts in a Legacy Domain
Enterprise applications often need to ingest thousands of records per second—think nightly data feeds, CSV imports, or ETL pipelines. The standard ISession in NHibernate is powerful but heavy: it maintains a first‑level cache, tracks dirty state, and fires events for every entity. For a one‑off bulk load this overhead can swamp the database and the application’s memory.
Thesis: StatelessSession is a lightweight alternative that trades away NHibernate’s convenience for raw throughput.
NHibernate’s StatelessSession removes the identity map and automatic dirty checking, so each Insert or Update is sent straight to the database. Batching is still available, and transactions are fully supported. The result is a predictable, low‑memory path that can process hundreds of thousands of rows in minutes.
When It Makes Sense
- Linear, non‑relational writes – you’re loading a flat table or a set of parents without needing lazy associations.
- Entities have
Identityor client‑assigned identifiers; noSaveOrUpdateor merge logic is required. - You don’t rely on NHibernate events, interceptors, or automatic optimistic locking.
- Memory footprint is a hard constraint; the first‑level cache would otherwise grow to gigabytes.
- Batching is already enabled at the driver level (e.g.,
batch-size=50in the connection string).
What You Lose
- No first‑level cache: every
GetorLoadhits the database. - No lazy loading or cascade operations; associations must be explicitly fetched or set.
- No automatic dirty checking – an entity modified after
Insertwon’t be persisted unless you callUpdateagain. - Events such as
PreInsertEventListenerare not fired, so domain invariants that depend on them must be enforced elsewhere. - Second‑level cache and query cache are bypassed; any cached read models may become stale.
Concrete Example: Bulk Import of 100 000 Customer Records
Below is a minimal console program that demonstrates a typical pattern. Replace <YourConnectionString> with your database string and <Customer> with your mapped entity.
using System;
using System.Collections.Generic;
using System.Data;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
public class BulkImport
{
public static void Main()
{
var cfg = new Configuration();
cfg.Configure(); // reads hibernate.cfg.xml
cfg.SetProperty("connection.connection_string", "<YourConnectionString>");
cfg.SetProperty("adonet.batch_size", "50"); // enable driver batching
using var sessionFactory = cfg.BuildSessionFactory();
using var session = sessionFactory.OpenStatelessSession();
using var tx = session.BeginTransaction();
var customers = GenerateCustomers(100_000); // your data source
int count = 0;
foreach (var cust in customers)
{
session.Insert(cust); // no identity map, straight to DB
if (++count % 5_000 == 0) // commit every 5k rows
{
tx.Commit();
tx = session.BeginTransaction();
}
}
tx.Commit();
}
static IEnumerable<Customer> GenerateCustomers(int n)
{
for (int i = 1; i <= n; i++)
{
yield return new Customer
{
Id = i, // client‑assigned or identity
Name = $"Customer {i}",
Email = $"customer{i}@example.com"
};
}
}
}
Key points:
- All inserts are sent in batches of 50 (driver‑level).
- Transactions are committed every 5 000 rows to keep the transaction size reasonable.
- Because
StatelessSessionhas no first‑level cache, the memory footprint stays below a few megabytes regardless of the record count.
Mapping Considerations
When using StatelessSession, mapping must be “simple”:
- Do not map any lazy associations. NHibernate will throw if you try to access a lazy proxy inside a
StatelessSession. - Use
identityorassignedgenerator for the primary key.nativeorsequencegenerators that rely on NHibernate’s session state are not supported. - Disable
inverseon collections if you plan to bulk insert parents only; otherwise you’ll need to insert child rows explicitly.
Trade‑Offs and Risks
| Feature | StatelessSession | ISession |
|---|---|---|
| First‑level cache | None | Present |
| Lazy loading | Unsupported | Supported |
| Event listeners | None fired | All fired |
| Optimistic concurrency (version) | Manual increment required | Automatic |
| Batching | Supported via driver | Supported via session flush |
Because the session does not track changes, a developer can accidentally modify an entity after calling Insert and assume the change will be persisted. The safest pattern is to treat each entity as immutable once handed to StatelessSession or to call Update explicitly if needed. Always review the transaction boundaries: a long‑running transaction can lock rows and exhaust database resources.
Practical Checklist Before Switching
- Confirm that the mapping uses
identityorassignedgenerators. - Run a small test (e.g., 1 000 rows) and enable NHibernate SQL logging to verify that no
SELECTstatements are issued for lazy associations. - Verify that the driver supports true ADO.NET batching; otherwise the
adonet.batch_sizesetting will have no effect. - Ensure that any domain logic relying on events or optimistic locking is moved to a service layer or database trigger.
- Measure memory consumption and CPU usage against the
ISessionbaseline to quantify the benefit.
Conclusion: Choose Wisely
If your workload is a pure, high‑volume write with predictable identifiers, StatelessSession can deliver a 5–10× speedup and a fraction of the memory usage. If you need lazy loading, automatic dirty checking, or event hooks, stick with ISession and consider tuning batch size, disabling the second‑level cache, or using a dedicated write service.
In short: use StatelessSession when you can give up NHibernate’s full feature set for raw throughput; otherwise keep the rich session semantics and pay the memory cost.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.