Implementing the Repository and Unit of Work Patterns with Entity Framework
Data access in web applications often begins with direct management of database connections and SQL statements. As complexity grows, Object-Relational Mapping (ORM) tools like Entity Framework improve code reuse and maintainability. The latest Entity Framework versions support patterns that facilitate a loosely-coupled design. Using the Repository pattern alongside the Unit of Work pattern can significantly enhance application architecture, promote code reuse, and support unit testing.
Direct Entity Framework Usage
Entity Framework can quickly generate a relational model from a data base. A simple implementation for data access is shown below:
void CreateData()
{
using (AppDbContext db = new AppDbContext())
{
Creature creature = new Creature();
creature.Title = "Glimmer";
creature.Level = 75;
Artifact artifact = new Artifact();
artifact.Title = "Moonbeam";
artifact.Category = "Spell";
artifact.Power = 85;
creature.PrimaryArtifact = artifact;
db.Creatures.Add(creature);
db.SaveChanges();
}
}
This code instantiates the data context within a using block, ensuring proper disposal. The database connection is managed automatically during SaveChanges(). While straightforward, embedding complex business rules directly within such blocks can become cumbersome. Abstracting data persistence logic using the Repository pattern addresses this.
The Repository Pattern
The Repository pattern acts as a mediator between the domain and data mapping layers, providing a collection-like interface for accessing domain objects. It centralizes data access logic. When business logic involves multiple entities, each repository method might require its own data context, leading to multiple database connections.
void ProcessPage()
{
Artifact newArtifact = ArtifactHandler.CreateArtifact();
CreatureHandler.CreateCreature(newArtifact);
}
public static class CreatureHandler
{
public static void CreateCreature()
{
using (AppDbContext ctx = new AppDbContext())
{
// Business logic for Creature.
}
}
}
public static class ArtifactHandler
{
public static Artifact CreateArtifact()
{
using (AppDbContext ctx = new AppDbContext())
{
// Business logic for Artifact.
return new Artifact();
}
}
}
This approach opens and closes connections for each operation, which is inefficient. The Unit of Work pattern coordinates these operations using a single shared context.
Sharing Context with Unit of Work
The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates the writing out of changes. It allows multiple repositories to share a single data context, grouping operations into a single transaction.
public static class CreatureProcessor
{
public static void CreateCreature()
{
Creature creature = new Creature();
using (AppDbContext ctx = new AppDbContext())
{
WorkUnit unit = new WorkUnit(ctx);
Artifact artifact = unit.Artifacts.Retrieve(15);
creature.PrimaryArtifact = artifact;
unit.Creatures.Insert(creature);
unit.Finalize();
}
}
}
The context is shared across repositories. Database execusion is deferred until Finalize() is called. The core interfaces and classes are defined as follows:
public interface IWorkUnit
{
IDataStore<Creature> Creatures { get; }
IDataStore<Artifact> Artifacts { get; }
void Finalize();
}
public interface IDataStore<T> where T : class
{
T Retrieve(object identifier);
IEnumerable<T> GetAll();
IEnumerable<T> Filter(Expression<Func<T, bool>> predicate);
void Insert(T item);
void Delete(T item);
}
public class WorkUnit : IWorkUnit
{
private readonly ObjectContext _ctx;
private CreatureStore _creatureStore;
private ArtifactStore _artifactStore;
public WorkUnit(ObjectContext context)
{
if (context == null) throw new ArgumentNullException("context");
_ctx = context;
}
public IDataStore<Creature> Creatures
{
get
{
if (_creatureStore == null)
{
_creatureStore = new CreatureStore(_ctx);
}
return _creatureStore;
}
}
public IDataStore<Artifact> Artifacts
{
get
{
if (_artifactStore == null)
{
_artifactStore = new ArtifactStore(_ctx);
}
return _artifactStore;
}
}
public void Finalize()
{
_ctx.SaveChanges();
}
}
public abstract class DataStore<T> : IDataStore<T> where T : class
{
protected IObjectSet<T> _entitySet;
public DataStore(ObjectContext context)
{
_entitySet = context.CreateObjectSet<T>();
}
public abstract T Retrieve(object identifier);
public IEnumerable<T> GetAll()
{
return _entitySet;
}
public IEnumerable<T> Filter(Expression<Func<T, bool>> predicate)
{
return _entitySet.Where(predicate);
}
public void Insert(T item)
{
_entitySet.AddObject(item);
}
public void Delete(T item)
{
_entitySet.DeleteObject(item);
}
}
Concrete repository implementations provide entity-specific logic:
public class CreatureStore : DataStore<Creature>
{
public CreatureStore(ObjectContext ctx) : base(ctx) { }
public override Creature Retrieve(object id)
{
return _entitySet.SingleOrDefault(c => c.Id == (int)id);
}
}
public class ArtifactStore : DataStore<Artifact>
{
public ArtifactStore(ObjectContext ctx) : base(ctx) { }
public override Artifact Retrieve(object id)
{
return _entitySet.SingleOrDefault(a => a.Id == (int)id);
}
}
While this design shares context within a block, sharing it across different classes or method calls requires passing the WorkUnit object. A global, request-scoped context offers an alternative.
Global Context with Unit of Work
A global Unit of Work can be scoped to a single HTTP request. The context is disposed after the request completes, effectively wrapping the application logic in one transactional scope. Operations are queued until Commit() is called.
void ProcessRequest()
{
Artifact art = ArtifactService.Create();
CreatureService.Create(art);
WorkUnit.Commit();
}
public static class CreatureService
{
public static void Create(Artifact artifact)
{
Creature cr = new Creature();
// Apply business rules.
cr.PrimaryArtifact = artifact;
_creatureStore.Insert(cr);
}
}
public static class ArtifactService
{
public static Artifact Create()
{
Artifact art = new Artifact();
// Apply business rules.
_artifactStore.Insert(art);
return art;
}
}
The database execution occurs only upon Commit(). The global Unit of Work implementation manages context lifetime:
public static class WorkUnit
{
private const string ContextKey = "App.Data.WorkUnit.Key";
private static IWorkUnitFactory _factory;
private static readonly Hashtable _threadTable = new Hashtable();
public static void Commit()
{
IUnitOfWork unit = GetCurrentUnit();
unit?.Commit();
}
public static IUnitOfWork Current
{
get
{
IUnitOfWork unit = GetCurrentUnit();
if (unit == null)
{
_factory = DependencyResolver.GetInstance<IWorkUnitFactory>();
unit = _factory.Create();
StoreUnit(unit);
}
return unit;
}
}
private static IUnitOfWork GetCurrentUnit()
{
if (HttpContext.Current != null)
{
if (HttpContext.Current.Items.Contains(ContextKey))
{
return (IUnitOfWork)HttpContext.Current.Items[ContextKey];
}
return null;
}
else
{
Thread currentThread = Thread.CurrentThread;
if (string.IsNullOrEmpty(currentThread.Name))
{
currentThread.Name = Guid.NewGuid().ToString();
return null;
}
lock (_threadTable.SyncRoot)
{
return (IUnitOfWork)_threadTable[currentThread.Name];
}
}
}
private static void StoreUnit(IUnitOfWork unit)
{
if (HttpContext.Current != null)
{
HttpContext.Current.Items[ContextKey] = unit;
}
else
{
lock(_threadTable.SyncRoot)
{
_threadTable[Thread.CurrentThread.Name] = unit;
}
}
}
}
A concrete implementation for Entity Framework:
public class EFWorkUnit : IUnitOfWork, IDisposable
{
public ObjectContext Context { get; private set; }
public EFWorkUnit(ObjectContext ctx)
{
Context = ctx;
ctx.ContextOptions.LazyLoadingEnabled = true;
}
public void Commit()
{
Context.SaveChanges();
}
public void Dispose()
{
Context?.Dispose();
Context = null;
GC.SuppressFinalize(this);
}
}
A factory pattern decouples creation logic:
public class EFWorkUnitFactory : IWorkUnitFactory
{
private static Func<ObjectContext> _contextProvider;
private static readonly object _syncLock = new object();
public static void RegisterContextProvider(Func<ObjectContext> provider)
{
_contextProvider = provider;
}
public IUnitOfWork Create()
{
ObjectContext ctx;
lock (_syncLock)
{
ctx = _contextProvider();
}
return new EFWorkUnit(ctx);
}
}
Initialization in the application startup configures the factory:
EFWorkUnitFactory.RegisterContextProvider(() => new AppDbContext());
Entity Framework Repository Implementation
A generic repository base class leverages the global Unit of Work:
public class EFDataStore<T> : IDataStore<T> where T : class
{
private ObjectContext _ctx;
private IObjectSet<T> _entitySet;
protected ObjectContext DbContext
{
get
{
if (_ctx == null)
{
_ctx = GetCurrentWorkUnit<EFWorkUnit>().Context;
}
return _ctx;
}
}
protected IObjectSet<T> EntitySet
{
get
{
if (_entitySet == null)
{
_entitySet = this.DbContext.CreateObjectSet<T>();
}
return _entitySet;
}
}
public TUnit GetCurrentWorkUnit<TUnit>() where TUnit : IUnitOfWork
{
return (TUnit)WorkUnit.Current;
}
public IQueryable<T> GetQuery()
{
return EntitySet;
}
public IEnumerable<T> GetAll()
{
return GetQuery().ToList();
}
public IEnumerable<T> Find(Func<T, bool> predicate)
{
return this.EntitySet.Where<T>(predicate);
}
public T Single(Func<T, bool> predicate)
{
return this.EntitySet.SingleOrDefault<T>(predicate);
}
public T First(Func<T, bool> predicate)
{
return this.EntitySet.First<T>(predicate);
}
public virtual void Delete(T item)
{
this.EntitySet.DeleteObject(item);
}
public virtual void Insert(T item)
{
this.EntitySet.AddObject(item);
}
public void Attach(T item)
{
this.EntitySet.Attach(item);
}
public void Save()
{
this.DbContext.SaveChanges();
}
}
Business logic managers wrap repositoreis, utilizing the shared context:
public static class CreatureHandler
{
private static IDataStore<Creature> _store
{
get
{
return ServiceLocator.GetInstance<IDataStore<Creature>>();
}
}
public static List<Creature> GetAll()
{
return _store.GetAll().ToList();
}
public static void CreateCreature(Artifact artifact)
{
Creature cr = new Creature();
// Business logic.
cr.PrimaryArtifact = artifact;
_store.Insert(cr);
}
}
Application initialization and cleanup:
void Application_Start()
{
// Configure Dependency Injection container.
Container.Configure(c =>
{
c.For<IWorkUnitFactory>().Use<EFWorkUnitFactory>();
c.For(typeof(IDataStore<>)).Use(typeof(EFDataStore<>));
});
EFWorkUnitFactory.RegisterContextProvider(() => new AppDbContext());
}
void Application_EndRequest()
{
WorkUnit.Current?.Dispose();
}