Using NHibernate Stateless Sessions for High‑Performance Bulk Operations
Learn how to bypass the first‑level cache and leverage ADO.NET batching with NHibernate’s stateless session for fast inserts, updates, and deletes.
21 Apr 2026, 00:19 UTC

Problem: Slow bulk inserts with regular NHibernate sessions
When you need to load thousands of rows into a database, opening a normal ISession for each entity triggers the first‑level cache, dirty‑checking, and lazy‑loading proxies. This adds memory overhead and generates extra SELECT statements, slowing the operation down.
Thesis: A stateless session gives you plain ADO.NET speed while still letting you map entities with NHibernate
By using IStatelessSession you bypass the cache and interceptors, letting NHibernate translate your entity instances directly into SQL. Combined with ADO.NET batching you can send many statements in a single round‑trip, achieving bulk‑load performance comparable to hand‑written SQL.
Configuration
Enable ADO.NET batching in your NHibernate configuration (e.g., hibernate.cfg.xml or code‑based setup). The batch size controls how many statements are grouped before being sent to the database.
<property name="adonet.batch_size">20</property>
<!-- other usual settings: dialect, connection driver, etc. -->
No special changes are required for the stateless session itself; you obtain it from the same ISessionFactory.
Worked example: Bulk inserting Product entities
Assume a simple entity:
public class Product
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual decimal Price { get; set; }
}
Mapping (by code or XML) is unchanged from a regular session.
The following snippet shows the typical usage pattern. Run this code in a console app, unit test, or service where you have access to the NHibernate factory.
using (IStatelessSession stateless = factory.OpenStatelessSession())
using (ITransaction tx = stateless.BeginTransaction())
{
int batchSize = 20; // matches adonet.batch_size
int count = 0;
foreach (var product in productsToInsert) // IEnumerable
{
stateless.Insert(product);
count++;
if (count % batchSize == 0)
{
stateless.Flush(); // sends the batch to DB
stateless.Clear(); // detaches instances, frees references
}
}
// flush any remainder
if (count % batchSize != 0)
{
stateless.Flush();
}
tx.Commit();
}
Where to run: inside any .NET process that has initialized the ISessionFactory. Required permissions: the database user must have INSERT rights on the target table.
Trade‑offs and limitations
- No first‑level cache or dirty‑checking: Repeated inserts of the same entity instance will issue a new INSERT each time; there is no automatic update‑or‑insert logic.
- Associations must be handled manually: The stateless session does not traverse collections or trigger lazy loading. If your entity has references, you must insert or update those related rows yourself.
- Event listeners and interceptors are bypassed: Custom logic in
IPreInsertEventListener,IInterceptor, etc., will not run. You need to replicate that logic in your bulk‑load code. - No automatic identifier generation feedback: After
Insertyou do not get the generated identifier back; you must rely on database‑generated keys (e.g., identity) and retrieve them separately if needed.
Verification steps
- Check row count: after the bulk insert, run
SELECT COUNT(*) FROM Product;and confirm it matches the number of entities you passed toInsert. - Monitor SQL: enable NHibernate’s SQL logger (
log4netorMicrosoft.Extensions.Logging) and verify that during the bulk operation you see only INSERT statements (no SELECT for lazy properties). - Confirm batching: with the logger you should see groups of statements separated by batches of size
adonet.batch_size(e.g., 20 inserts per batch).
These checks give you confidence that the stateless session is behaving as expected without relying on unverified claims.
Closing
For scenarios where you need to move large volumes of data into (or out of) a relational database while still benefiting from NHibernate’s mapping layer, the stateless session is a practical choice. It strips away the overhead of the persistence context, lets you use ADO.NET batching, and keeps your code tied to your existing entity models. Just remember to handle associations, identifiers, and any custom event logic yourself, and verify the outcome with row counts and SQL logging.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.