20
\$\begingroup\$

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?

\$\endgroup\$
1

3 Answers 3

17
\$\begingroup\$

Have a look on the Onion Architecture series by Jeffrey Palermo

How about this for a structure

  • Project.Domain (Class Library)

    • has no deppendencies on any other projects
    • represents the core business of your app/service
    • any external dependecies are abstracted away via DI (adapter pattern)
    • you might want to define an IDataContext/ISession that abstracts away the storage mechanism for example
    • contains both your entities and your business logic
  • Project.Reports (Class Library)

    • references Project.Domain
    • contains view models or projections or however you want to call them
    • these models are crafted to serve the views in the UI
    • i prefer to keep it separate from domain as ui requirements tend to change a lot more often then my core business
    • you might want to have distinct dedicated view models for different platforms
  • Project.Data.Sql (Class Library)

    • references Project.Domain
    • contains EF DbContext, mappings, migrations
    • the dbcontext implements the IDataContext/ISession defined in the domain
    • do you really need repositories? Repository is the new singleton
    • how about relying on the fact that the dbcontext already implements the unit of work and the repository patterns out of the box
  • Project.Service (Optional)

    • web api or wcf would fit in here
    • wires everything via a dependency injection container
  • Project.UI.Web (MVC Project)

    • wires what it needs via a dependency injection container
\$\endgroup\$
2
  • \$\begingroup\$ I like how you structured the project. Can you provide more details or articles? \$\endgroup\$
    – lbrahim
    Commented May 20, 2014 at 8:30
  • \$\begingroup\$ We have just released an application for a customer mostly using this guideline. However we've just faced a dilema : We need som kind of variable scope per request and the HttpCurrent is not an option in this kind of architecture when we are in Domain. I curious if you have any solution for this need ? \$\endgroup\$ Commented Sep 13, 2016 at 13:42
2
\$\begingroup\$

It looks like you have dependencies hard coded into each layer.

Service layer: How do you plan to inject a mocked repository into your Logic Layer when unit testing it? Wouldn't it make sense to inject these dependencies in via the constructor into your services once you've registered them in your dependency injection container?

public class AdminLogic 
    {
        private readonly IRepository<User> _userRepository;

        public AdminLogic(IRepository<User> userRepository)
        {
            _userRepository = userRepository;
        }

        public User GetByEmail(string email)
        {
            return _userRepository.GetByEmail(email);
        }
    }

Another issue is that sometimes a service class will need to consume many repositories in order to do its work..Not sure how you intend to handle those scenarios given that they appear to only use a single repo.

Presentation layer: Same again, inject the dependancy on the admin service and register it in your container and have your controller factory build your controllers for you.

 public class AdminController : Controller
{
    private readonly IAdminLogic _adminLogic;

    public AdminController(IAdminLogic adminLogic)
    {
        _adminLogic = adminLogic;
    }

    public ActionResult Index(int? page)
    {
        // Call GetAll() and use AutoMapper to project the results to the viewmodel
        IQueryable<AdminModel> admins = _adminLogic.GetAll().Project().To<AdminModel>();

        // Paging (using PagedList https://github.com/TroyGoode/PagedList)
        IPagedList<AdminModel> paged = admins.ToPagedList(page ?? 1, 25);

        return View(paged);
    }

}
\$\endgroup\$
0
\$\begingroup\$

I think you have a number of problems with this design.

1: Generic DAL and Repository classes. In my view this is a big mistake. you are guaranteed to need some GetModelsBySpecialCriteria Method at some point which will force you to jump through hoops trying to define it in your generic way. The reason for the repository is to hide all the stuff you have to do to get the data away from the calling class. Not to provide a query language for calling classes.

2: Generic BLL classes. It looks like you are just repeating the DAL layer? what if your logic requires multiple repositories?

Somewhere in your project you will have actual requirements to implement. these will be effectively random and open to constant change. You dont help yourself by creating an extra generic framework to cram them all into.

How To do it right: (short version)

You already have EF (although i hate it) this provides your generic query DB DAL layer. hide it behind a repository! calling classes should not have to construct queries.

Models : Should have no external dependencies.

Business logic : two choices. OOP - hide it in Model methods, ADM - put it in services, keep models as data structs.

Website : Instantiate Repositories, Get models, call services, display result

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.