We are starting a new web project using C# / MVC4 and Entity Framework 5 for data access. I've decided to go with an n-layered approach for the structure of the project and I would like some feedback on my design decisions.
This is how the solution is structured:
- Project.Model (Class Library): Contains EF .edmx, entity models, and viewmodels
- Project.DAL (Class Library): Contains EF DbContext and Repository classes
- Project.BLL (Class Library): Contains business logic classes
- Project (MVC Project)
DAL
The Data Access Layer is only concerned with simple CRUD like operations. I've decided to go with a repository approach. Here are the Repository interfaces:
public interface IRepository
{
}
public interface IRepository<T> : IRepository, IDisposable
where T : class, new()
{
T Add(T item);
T Get(object id);
T Get(Expression<Func<T, bool>> predicate);
IQueryable<T> GetAll();
IQueryable<T> GetAll(Expression<Func<T, bool>> predicate);
void Update(T item);
void Delete(T item);
}
After doing some research on using Entity Framework in web projects, the general consensus is that there should only be one DbContext
/ObjectContext
per request. So to create and dispose the single context for each request, I've written an HttpModule
that injects the DbContext
into the HttpContext
.
public class DbContextModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += context_BeginRequest;
context.EndRequest += context_EndRequest;
}
public void Dispose()
{
}
private void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext httpContext = application.Context;
httpContext.Items.Add(Repository.ContextKey, new ProjectEntities());
}
private void context_EndRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext httpContext = application.Context;
var entities = (ProjectEntities)httpContext.Items[Repository.ContextKey];
entities.Dispose();
entities = null;
application.Context.Items.Remove(Repository.ContextKey);
}
}
Next is the Repository
base class. Note that the constructor utilizes the injected DbContext from the HttpModule
above.
public abstract class Repository<T> : IRepository<T> where T : class, new()
{
protected Repository()
{
if (HttpContext.Current == null)
{
throw new Exception("Cannot create repository - current HttpContext is null.");
}
_entities = (ProjectEntities)HttpContext.Current.Items[Repository.ContextKey];
if (_entities == null)
{
throw new Exception("Cannot create repository - no DBContext in the current HttpContext.");
}
}
private ProjectEntities _entities;
public T Add(T item)
{
_entities.Set<T>().Add(item);
_entities.SaveChanges();
return item;
}
public T Get(object id)
{
return _entities.Set<T>().Find(id);
}
public T Get(Expression<Func<T, bool>> predicate)
{
return _entities.Set<T>().AsQueryable().FirstOrDefault(predicate);
}
public IQueryable<T> GetAll()
{
return _entities.Set<T>().AsQueryable();
}
public IQueryable<T> GetAll(Expression<Func<T, bool>> predicate)
{
return _entities.Set<T>().AsQueryable().Where(predicate);
}
public void Update(T item)
{
_entities.Entry(item).State = EntityState.Modified;
_entities.SaveChanges();
}
public void Delete(T item)
{
_entities.Set<T>().Remove(item);
_entities.SaveChanges();
}
}
And a simple example of an implementation:
public class AdminRepository : Repository<Admin>
{
public Admin GetByEmail(string email)
{
return Get(x => x.Email == email);
}
}
BLL
The Business Logic Layer encapsulates all business logic. To keep constraints, I've written the base Logic
class like this:
public abstract class Logic<TRepository> where TRepository : class, IRepository, new()
{
private static Expression<Func<TRepository>> _x = () => new TRepository();
private static Func<TRepository> _compiled = _x.Compile();
protected Logic()
{
Repository = _compiled();
}
protected internal TRepository Repository { get; private set; }
}
The constructor automatically creates the needed Repository
class, so no additional code is needed in child classes to instantiate the repository. Here is a simple example of an implementation:
public class AdminLogic : Logic<AdminRepository>
{
public ADMIN Add(ADMIN admin)
{
return Repository.Add(admin);
}
public ADMIN Get(object id)
{
return Repository.Get(id);
}
public ADMIN GetByEmail(string email)
{
return Repository.GetByEmail(email);
}
public IQueryable<ADMIN> GetAll()
{
return Repository.GetAll();
}
public void Update(ADMIN admin)
{
Repository.Update(admin);
}
}
This example is more of a pass-through for the DAL repository, but adding a business logic layer won't be a problem. I'm choosing to return IQueryable<T>
from the BLL because we are using some third party tools that require an IQueryable<T>
for deferred execution.
Project (MVC Project)
Finally, here is what a simple controller action will look like:
public ActionResult Index(int? page)
{
// Instantiate logic object
AdminLogic logic = new AdminLogic();
// Call GetAll() and use AutoMapper to project the results to the viewmodel
IQueryable<AdminModel> admins = logic.GetAll().Project().To<AdminModel>();
// Paging (using PagedList https://github.com/TroyGoode/PagedList)
IPagedList<AdminModel> paged = admins.ToPagedList(page ?? 1, 25);
return View(paged);
}
Everything works as expected, and tests show that the EF context is properly disposed and the overall speed is good.
Is this a pretty good way to go about this?