Unlocking NHibernate’s Second‑Level Cache: How, When, and What to Watch Out For
Learn how to enable, configure, and verify NHibernate’s Second‑Level Cache, including provider setup, TTL settings, and common pitfalls for production use.
20 Jun 2026, 22:59 UTC

What the Second‑Level Cache Gives You
NHibernate’s Second‑Level Cache (SLC) stores entity, collection, and query results across sessions, reducing round‑trips to the database for repeat reads. It is optional, configurable, and works with a variety of cache providers (EhCache, Infinispan, etc.). The core benefit is faster read‑heavy workloads, but it also introduces complexity around consistency and eviction.
Enabling SLC in a .NET Application
Enable the cache by adding the following properties to your NHibernate configuration (app.config, web.config, or a fluent builder):
<property name="cache.use_second_level_cache">true</property>
<property name="cache.region.factory_class">NHibernate.Caches.EhCache.EhCacheRegionFactory, NHibernate.Caches.EhCache</property>
<!-- Optional: enable query cache separately -->
<property name="cache.use_query_cache">true</property>
Replace NHibernate.Caches.EhCache.EhCacheRegionFactory, NHibernate.Caches.EhCache with the fully‑qualified factory class of your chosen provider. Ensure the provider assembly is referenced in the project.
Per‑Entity Cache Region Settings
Each entity can have its own region name and TTL (time‑to‑live). In a mapping file or Fluent API you can specify:
<class name="Customer" table="Customers" cache="read-write" region="CustomerRegion">
…
</class>
Or with Fluent:
public class CustomerMap : ClassMap<Customer>
{
public CustomerMap()
{
Table("Customers");
Cache.ReadWrite().Region("CustomerRegion");
}
}
Provider‑specific TTL and max‑entries are set in the cache provider’s config file (e.g., ehcache.xml for EhCache):
<cache name="CustomerRegion"
maxEntriesLocalHeap="1000"
timeToLiveSeconds="300"
eternal="false"/>
Verifying the Cache is Active
After building the ISessionFactory, you can programmatically confirm SLC is enabled:
var factory = cfg.BuildSessionFactory();
Console.WriteLine($"SLC enabled: {factory.GetCache().IsEnabled()}");
To see statistics, enable them in the config and query:
<property name="cache.use_second_level_cache">true</property>
<property name="cache.use_query_cache">true</property>
<property name="cache.use_statistics">true</property>
factory.GetStatistics().Enabled = true;
Console.WriteLine(factory.GetStatistics().SecondLevelCacheHitCount);
Run the following test pattern:
- Open
ISessionA, loadCustomer(1), close A. - Open new
ISessionB, loadCustomer(1)again. - Check logs or statistics for a cache hit.
Common Pitfalls and Mitigations
- Provider not configured: A missing
cache.region.factory_classor missing assembly reference throwsConfigurationException. Verify the provider assembly is referenced. - Stale data due to long TTL: If the database is updated outside NHibernate, the cache may return old values. Keep TTL short for frequently changing data or use
cache.use_query_cachewith parameterized queries to force refresh. - Immutable entity over‑caching: Mark entities as
mutable="false"in the mapping to prevent unnecessary cache entries. - Distributed environments: Non‑distributed providers (e.g., in‑process EhCache) will give each process its own cache, leading to inconsistencies. Use a distributed provider like Infinispan or Redis‑based EhCache.
- Query cache confusion: Query caching is orthogonal to SLC. A query cache hit only occurs if the exact HQL string and parameters match. Use named queries or cacheable HQL fragments to maximize reuse.
- Tenant‑aware caching: For multi‑tenant apps, append the tenant ID to region names (e.g.,
CustomerRegion_Tenant42) to isolate data.
Eviction and Manual Cache Management
When you need to force a refresh, use the cache API:
factory.GetCache().EvictEntityRegion("CustomerRegion");
factory.GetCache().EvictCollectionRegion("Orders_CustomerRegion");
Or clear the entire cache:
factory.GetCache().Clear();
Always prefer automatic eviction via provider policies (LRU, TTL) unless you have a specific scenario.
Limitations to Keep in Mind
- SLC does not cache lazy‑loaded collections until they are accessed; subsequent accesses still hit the database.
- Dirty checking and first‑level cache still operate per session. Modifications made in a session are not visible to other sessions until the session is flushed and the cache is updated.
- Cache statistics can add overhead; enable them only for debugging.
- Some providers require additional configuration (e.g., network ports for Infinispan). Verify connectivity before deployment.
Practical Checklist Before Production
- Reference the provider assembly and add it to the project.
- Set
cache.use_second_level_cachetotrueand specifycache.region.factory_class. - Configure provider‑specific region settings (TTL, max entries).
- Enable statistics only in staging to monitor hit/miss ratios.
- Run a cache‑hit test: load, close, reload, and verify hit count.
- Validate eviction policies by updating an entity and reloading.
- For multi‑tenant, confirm region names include tenant identifiers.
- Deploy a small load test to confirm cache behaves as expected under concurrency.
Conclusion
Second‑Level Cache in NHibernate can dramatically reduce database traffic for read‑heavy scenarios, but it requires careful configuration and awareness of consistency trade‑offs. By enabling the cache, configuring provider settings, validating through statistics, and guarding against common pitfalls, you can harness the full power of NHibernate’s caching while keeping data fresh and reliable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.