This is going to be the first in a series of short posts on how data access is handled in Fail Tracker. Future posts will get into how the strategy works with unit testing as well as how advanced topics, such as row-level security, are handled. Read on to find out how Fail Tracker utilizes a simple repository layer around NHibernate for all data access.
[more]
The Problem With Utilizing ISession Directly
I’ve been prone to favor ivory-tower architecture on past projects. It’s challenging, but with Fail Tracker I’ve tried to force myself to use what’s “in the box” so to speak and to keep with the simplest thing that could possibly work. This means I’ve only added infrastructure and plumbing when there was an immediate need for it or when working with what was in the box was clearly going to cause maintenance headaches. To that end, I attempted to utilize NHibernate’s ISession directly from my controllers as recommend by Ayende. Unfortunately, I ran into a barrier almost immediately. I wanted to utilize LINQ to NHibernate for all queries, but its implement as an extension method on top of ISession. This was a complete non-starter for me since I’m an avid practitioner of Test Driven Development, and there’s no good way to mock behavior on extension methods.
The Repository Pattern
Instead of using ISession, I opted for a simple repository layer that would serve as a very thin wrapper around ISession. It provides exactly two operations: adding an entity, and querying the entities:
public interface IRepository<TEntity> { void Save(TEntity entity); IQueryable<TEntity> Query(); }
This probably looks simpler than what you typically think of when you hear “repository,” but it is in fact a repository. According to the formal definition, a repository exposes a collection-like interface, not a series of GetByX methods as is more commonly seen these days.
Anywho, I gained the ability to easily mock the contents of the repository by exposing an IQueryable directly on the interface. This made TDD quite painless as I’ll demonstrate in future posts. Another nice benefit is interface segregation. Check out the ISession interface:
public interface ISession : IDisposable { EntityMode ActiveEntityMode { get; } FlushMode FlushMode { get; set; } CacheMode CacheMode { get; set; } ISessionFactory SessionFactory { get; } IDbConnection Connection { get; } bool IsOpen { get; } bool IsConnected { get; } bool DefaultReadOnly { get; set; } ITransaction Transaction { get; } ISessionStatistics Statistics { get; } void Flush(); IDbConnection Disconnect(); void Reconnect(); void Reconnect(IDbConnection connection); IDbConnection Close(); void CancelQuery(); bool IsDirty(); bool IsReadOnly(object entityOrProxy); void SetReadOnly(object entityOrProxy, bool readOnly); object GetIdentifier(object obj); bool Contains(object obj); void Evict(object obj); object Load(Type theType, object id, LockMode lockMode); object Load(string entityName, object id, LockMode lockMode); object Load(Type theType, object id); T Load<T>(object id, LockMode lockMode); T Load<T>(object id); object Load(string entityName, object id); void Load(object obj, object id); void Replicate(object obj, ReplicationMode replicationMode); void Replicate(string entityName, object obj, ReplicationMode replicationMode); object Save(object obj); void Save(object obj, object id); object Save(string entityName, object obj); void SaveOrUpdate(object obj); void SaveOrUpdate(string entityName, object obj); void Update(object obj); void Update(object obj, object id); void Update(string entityName, object obj); object Merge(object obj); object Merge(string entityName, object obj); void Persist(object obj); void Persist(string entityName, object obj); [Obsolete("Use Merge(object) instead")] object SaveOrUpdateCopy(object obj); [Obsolete("No direct replacement. Use Merge instead.")] object SaveOrUpdateCopy(object obj, object id); void Delete(object obj); void Delete(string entityName, object obj); int Delete(string query); int Delete(string query, object value, IType type); int Delete(string query, object[] values, IType[] types); void Lock(object obj, LockMode lockMode); void Lock(string entityName, object obj, LockMode lockMode); void Refresh(object obj); void Refresh(object obj, LockMode lockMode); LockMode GetCurrentLockMode(object obj); ITransaction BeginTransaction(); ITransaction BeginTransaction(IsolationLevel isolationLevel); ICriteria CreateCriteria<T>() where T : class; ICriteria CreateCriteria<T>(string alias) where T : class; ICriteria CreateCriteria(Type persistentClass); ICriteria CreateCriteria(Type persistentClass, string alias); ICriteria CreateCriteria(string entityName); ICriteria CreateCriteria(string entityName, string alias); IQueryOver<T,T> QueryOver<T>() where T : class; IQueryOver<T,T> QueryOver<T>(Expression> alias) where T : class; IQuery CreateQuery(string queryString); IQuery CreateFilter(object collection, string queryString); IQuery GetNamedQuery(string queryName); ISQLQuery CreateSQLQuery(string queryString); void Clear(); object Get(Type clazz, object id); object Get(Type clazz, object id, LockMode lockMode); object Get(string entityName, object id); T Get<T>(object id); T Get<T>(object id, LockMode lockMode); string GetEntityName(object obj); NHibernate.IFilter EnableFilter(string filterName); NHibernate.IFilter GetEnabledFilter(string filterName); void DisableFilter(string filterName); IMultiQuery CreateMultiQuery(); ISession SetBatchSize(int batchSize); ISessionImplementor GetSessionImplementation(); IMultiCriteria CreateMultiCriteria(); ISession GetSession(EntityMode entityMode); }
That’s anything but segregated! If all I tell you is that a class consumes ISession, you’d be hard-pressed to tell me what that class is actually doing. Contrast that with a class consuming IRepository, where you can be sure the class is using at most two methods.
Wiring It Up
Fail Tracker relies heavily upon StructureMap to wire things up at runtime. It takes advantage of StructureMap’s ability to resolve generic types, so any class that depends on an IRepository of *any* type is resolved to this generic implementation:
public class NHibernateRepository<TEntity> : IRepository<TEntity> { private readonly ISession _session; public NHibernateRepository(ISession session) { _session = session; } public void Save(TEntity entity) { _session.Save(entity); _session.Flush(); } public IQueryable<TEntity> Query() { return _session.Query<TEntity>(); } }
The wiring “magic” is just a single call in a registry:
public class NHibernateRegistry : Registry { public NHibernateRegistry() { For<ISession>().HttpContextScoped().Use(NHibernateBootstrapper.GetSession); //"Magic!" For(typeof(IRepository<>)).Use(typeof(NHibernateRepository<>)); } }
Up Next
As I said, I’m a big fan of TDD. In the next post, I’ll show you how I’m leveraging SpecsFor to simplify testing components that depend on IRepository in Fail Tracker.