Answer
NHibernate’s second‑level cache is not automatically stale‑safe for reads that occur on an async ISession unless the write transaction is flushed and committed (or the cache provider participates in the transaction). If the transaction is properly committed, the cache is invalidated and a subsequent async read will see the updated value; no manual eviction is required. Switching to a transactional cache provider (e.g., Redis with transactions) does not remove the need to commit the NHibernate transaction – the provider only guarantees that its own remove/update calls are executed within the same transaction.
Confirmed facts
- The second‑level cache lives in the ISessionFactory; all sessions (sync or async) share it.
- Invalidation happens when ISession.Flush() or Transaction.Commit() calls the ICacheProvider’s Remove/Update methods for the affected entity/collection/region.
- FlushMode.Auto does not trigger a flush automatically in async code; you must call Session.Flush() or Transaction.Commit() explicitly.
Likely explanation for stale reads
If you observe stale data after an async write, the most common cause is that the transaction never reached the flush/commit stage (e.g., the using block disposed before Commit, or FlushMode never fired). In that case the second‑level cache was not updated, so the async read returns the cached old value.
Steps to guarantee consistency for async workloads
- Configure the session factory to expose statistics (optional for verification):
var cfg = new Configuration();
cfg.SetProperty("hibernate.generate_statistics", "true");
- In each async method, open an ISession, begin a transaction, perform the write, then explicitly commit:
await using var session = factory.OpenSessionAsync();
await using var tx = session.BeginTransactionAsync();
// … modify entities …
await tx.CommitAsync(); // ensures flush and cache invalidation
- If you prefer FlushMode.Auto, call Session.Flush() before committing, or set FlushMode.Always and rely on the commit to flush.
- After the commit, a subsequent async read (new ISession) will either hit the updated cache (if the provider supports transactional invalidation) or miss and reload from the DB, both yielding the current value.
- Manual eviction (session.Evict(entity) or factory.EvictEntityRegion) is only needed when you cannot guarantee a commit or when you deliberately want to bypass the cache.
Verification
Enable NHibernate statistics and log second‑level cache activity. After an async write and commit, look for log lines such as:
Updating second-level cache for …
Removing from second-level cache for …
- Statistics showing an increase in second‑level cache put/miss counts.
If those messages are absent, the transaction likely did not reach the flush/commit point.
Missing diagnostic detail
To confirm whether the transaction is being committed, please verify that you are calling Transaction.CommitAsync() (or Commit()) and that no exception is swallowed before the commit. If you can confirm the commit occurs, the recommendation is to rely on the cache; otherwise, ensure explicit commit or manual eviction.