I am about to >explode< :) due to amount of reading on this subject... My head hurts, and I need some honest opinions... There is a similar question/review that I noticed, but I believe my approach is bit different so I wanted to ask.
We are embarking on a really big project that will use ASP.NET MVC 4 with Web API and would like to use Entity Framework Code First. Before I post code for review, please consider following
- My project will be big (possibly 100+ domain entities)
- There needs to SOLID approach to architecture
- By now, I understand that Entity Framework
DbContext
is (entirely or to some extent) implementing Unit of Work and Repository patterns, so I could skip all this ;) - I am really concerned using EF injected directly into Controller constructors (testability, separation of concerns, etc, etc)
- I am also aware that there is more than one way to implement UoW and Repository patterns.
- I am not worried or wanting to build abstractions that will allow me to "swap" ORM (e.g. swap Entity Framework for NHiberante or such).
My Approach
- Repositories are generic and have a base class that implement much of the standard logic.
- Repositories need
DbContext
via constructor (which is provided byUnitOfWork
) UnitOfWork
is responsible for managing access to all repositories and insuring context is shared among them.UnitOfWork
is disposable, Repositories are not...- In order to "hide"
DbContext
,UnitOfWork
is constructed usingIDbContextFactory
.
Questions
- This seems to work for me, and the advantage I see is that every Controller just needs UoW injected which is nice. Some Controllers need 2-3 repositories in addition to domain services so this makes things nice...I think...
- Over time, UoW will grow with repositories (there could be 65+ aggregate roots each having a repo). Any ides on how to better manage this? Should I somehow inject Repositories instead od new()-ing them up in the
UnitOfWork
? I'd love to be able to create a IoC module (Autofac is my poison) to wire up all repos (somehow) - Is use of
IDbContextFactory
an overkill or should I just injectDbContext
to constructor ofUnitOfWork
instead? Right now, my web app has no direct dependency on Entity Framework it only depends n DAL (which in turn depends on EF). On the other hand DbContextFactory new()es upMyAppDbContext
and is not handled by IoC - Does anyone notices any other "code smell" ?
- Some of the questions are in code NOTEs to make them more relevant...
Ok here is the code with 2 repositories and sample use (all namespaces are omitted for brevity sake)
IDbContextFactory and DbContextFactory
/// <summary>
/// Creates instance of specific DbContext
/// </summary>
public interface IDbContextFactory //: IDisposable //NOTE: Since UnitOfWork is disposable I am not sure if context factory has to be also...
{
DbContext GetDbContext();
}
public class DbContextFactory : IDbContextFactory
{
private readonly DbContext _context;
public DbContextFactory()
{
// the context is new()ed up instead of being injected to avoid direct dependency on EF
// not sure if this is good approach...but it removes direct dependency on EF from web tier
_context = new MyAppDbContext();
}
public DbContext GetDbContext()
{
return _context;
}
// see comment in IDbContextFactory inteface...
//public void Dispose()
//{
// if (_context != null)
// {
// _context.Dispose();
// GC.SuppressFinalize(this);
// }
//}
}
IRepository, Repository, and 2 specific Repositories wiht extra interfaces (Vehicle and Inventory)
public interface IRepository<T> where T : class
{
/// <summary>
/// Get the total objects count.
/// </summary>
int Count { get; }
/// <summary>
/// Gets all objects from database
/// </summary>
IQueryable<T> All();
/// <summary>
/// Gets object by primary key.
/// </summary>
/// <param name="id"> primary key </param>
/// <returns> </returns>
T GetById(object id);
/// <summary>
/// Gets objects via optional filter, sort order, and includes
/// </summary>
/// <param name="filter"> </param>
/// <param name="orderBy"> </param>
/// <param name="includeProperties"> </param>
/// <returns> </returns>
IQueryable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = "");
/// <summary>
/// Gets objects from database by filter.
/// </summary>
/// <param name="predicate"> Specified a filter </param>
IQueryable<T> Filter(Expression<Func<T, bool>> predicate);
/// <summary>
/// Gets objects from database with filting and paging.
/// </summary>
/// <param name="filter"> Specified a filter </param>
/// <param name="total"> Returns the total records count of the filter. </param>
/// <param name="index"> Specified the page index. </param>
/// <param name="size"> Specified the page size </param>
IQueryable<T> Filter(Expression<Func<T, bool>> filter, out int total, int index = 0, int size = 50);
/// <summary>
/// Gets the object(s) is exists in database by specified filter.
/// </summary>
/// <param name="predicate"> Specified the filter expression </param>
bool Contains(Expression<Func<T, bool>> predicate);
/// <summary>
/// Find object by keys.
/// </summary>
/// <param name="keys"> Specified the search keys. </param>
T Find(params object[] keys);
/// <summary>
/// Find object by specified expression.
/// </summary>
/// <param name="predicate"> </param>
T Find(Expression<Func<T, bool>> predicate);
/// <summary>
/// Create a new object to database.
/// </summary>
/// <param name="entity"> Specified a new object to create. </param>
T Create(T entity);
/// <summary>
/// Deletes the object by primary key
/// </summary>
/// <param name="id"> </param>
void Delete(object id);
/// <summary>
/// Delete the object from database.
/// </summary>
/// <param name="entity"> Specified a existing object to delete. </param>
void Delete(T entity);
/// <summary>
/// Delete objects from database by specified filter expression.
/// </summary>
/// <param name="predicate"> </param>
void Delete(Expression<Func<T, bool>> predicate);
/// <summary>
/// Update object changes and save to database.
/// </summary>
/// <param name="entity"> Specified the object to save. </param>
void Update(T entity);
}
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _dbContext;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext dbContext)
{
_dbContext = dbContext;
_dbSet = _dbContext.Set<T>();
}
public virtual int Count
{
get { return _dbSet.Count(); }
}
public virtual IQueryable<T> All()
{
return _dbSet.AsQueryable();
}
public virtual T GetById(object id)
{
return _dbSet.Find(id);
}
public virtual IQueryable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = "")
{
IQueryable<T> query = _dbSet;
if (filter != null)
{
query = query.Where(filter);
}
if (!String.IsNullOrWhiteSpace(includeProperties))
{
foreach (var includeProperty in includeProperties.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
}
if (orderBy != null)
{
return orderBy(query).AsQueryable();
}
else
{
return query.AsQueryable();
}
}
public virtual IQueryable<T> Filter(Expression<Func<T, bool>> predicate)
{
return _dbSet.Where(predicate).AsQueryable();
}
public virtual IQueryable<T> Filter(Expression<Func<T, bool>> filter, out int total, int index = 0, int size = 50)
{
int skipCount = index*size;
var resetSet = filter != null ? _dbSet.Where(filter).AsQueryable() : _dbSet.AsQueryable();
resetSet = skipCount == 0 ? resetSet.Take(size) : resetSet.Skip(skipCount).Take(size);
total = resetSet.Count();
return resetSet.AsQueryable();
}
public bool Contains(Expression<Func<T, bool>> predicate)
{
return _dbSet.Count(predicate) > 0;
}
public virtual T Find(params object[] keys)
{
return _dbSet.Find(keys);
}
public virtual T Find(Expression<Func<T, bool>> predicate)
{
return _dbSet.FirstOrDefault(predicate);
}
public virtual T Create(T entity)
{
var newEntry = _dbSet.Add(entity);
return newEntry;
}
public virtual void Delete(object id)
{
var entityToDelete = _dbSet.Find(id);
Delete(entityToDelete);
}
public virtual void Delete(T entity)
{
if (_dbContext.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
}
public virtual void Delete(Expression<Func<T, bool>> predicate)
{
var entitiesToDelete = Filter(predicate);
foreach (var entity in entitiesToDelete)
{
if (_dbContext.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
}
}
public virtual void Update(T entity)
{
var entry = _dbContext.Entry(entity);
_dbSet.Attach(entity);
entry.State = EntityState.Modified;
}
}
public class VehicleRepository : Repository<Vehicle>, IVehicleRepository
{
public VehicleRepository(DbContext dbContext) : base(dbContext)
{
}
}
public interface IVehicleRepository : IRepository<Vehicle>
{
//RFU
}
public interface IInventoryRepository : IRepository<InventoryItem>
{
IList<InventoryItem> GetByVehicleId(string vehicleId); // NOTE: InventoryItem.VehicleId != InventoryItem.Id
}
public class InventoryItemRepository : Repository<InventoryItem>, IInventoryItemRepository
{
public InventoryItemRepository(DbContext dbContext) : base(dbContext)
{
}
public IList<InventoryItem> GetByVehicleId(string vehicleId)
{
return Filter(vii => vii.Vehicle.Id == vehicleId).ToList();
}
}
IUnitOfWork, UnitOfWork
public interface IUnitOfWork : IDisposable
{
InventoryItemRepository InventoryItemRepository { get; }
VehicleRepository VehicleRepository { get; }
void Save();
}
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _dbContext;
private bool _disposed;
private InventoryItemRepository _inventoryItemRepository;
private VehicleRepository _vehicleRepository;
/// <summary>
/// NOTE: repository getters instantiate repositories as needed (lazily)...
/// i wish I knew of IoC "way" of wiring up repository getters...
/// </summary>
/// <param name="dbContextFactory"></param>
public UnitOfWork(IDbContextFactory dbContextFactory)
{
_dbContext = dbContextFactory.GetDbContext();
}
public void Save()
{
if (_dbContext.GetValidationErrors().Any())
{
// TODO: move validation errors into domain level exception and then throw it instead of EF related one
}
_dbContext.SaveChanges();
}
public InventoryItemRepository InventoryItemRepository
{
get { return _inventoryItemRepository ?? (_inventoryItemRepository = new InventoryItemRepository(_dbContext)); }
}
public VehicleRepository VehicleRepository
{
get { return _vehicleRepository ?? (_vehicleRepository = new VehicleRepository(_dbContext)); }
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_dbContext.Dispose();
}
}
_disposed = true;
}
}
Sample Usage in ASP.NET MVC4 + Web API
Global.asax.cs (Application_Start)
// relevant registration
builder.RegisterType<UnitOfWork>().As<IUnitOfWork>()
.WithParameter("dbContextFactory", new DbContextFactory())
.InstancePerHttpRequest()
.InstancePerApiRequest();
InventoryController
public class InventoryController : ApiController
{
private readonly InventoryItemMapper _mapper; // NOTE: maps viewModel to domain entities and vice versa using ValueInjector
private readonly IUnitOfWork _unitOfWork;
public InventoryController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork; // UoW (with all repos) is injected and ready for use...
_mapper = new VehicleInventoryItemMapper(); //TODO: this will be injected also...
}
public IEnumerable<InventoryViewModel> Get()
{
var inventoryItems = _unitOfWork.InventoryItemRepository.All().ToList();
var inventory = _mapper.MapToModel(inventoryItems);
return inventory;
}
}