Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to filter DbContext in a multi-tenant application using Entity Framework with MVC4

I am developing a multi-tenant web application using MVC4 and EF5. I previously asked this question regarding filtering my DbContext: Is it bad practice to filter by ID within the repository pattern.

Apparently, my approach was sound, but it was suggested that rather than handling all filtering in that single repository, I provide a context already filtered to tenant level using 'a repository or DBContextWrapper class that would feed your [my] normal repository'.

Unfortunately I am no MVC expert, so I started implementing this as best I could, but upon researching filtering in EF for other multi-tenant applications I found a question for a very similar case Multi-tenancy web application with filtered dbContext, though completely failed to understand the answer to it.

In my application, the CompanyID is a property of the User class, so should be taken directly from the authenticated user. Eg:

int CompanyID = db.Users.Single(u => u.Email == User.Identity.Name).CompanyID;

My current approach does appear to work, however I'm pretty sure I have gone about it the wrong way and/or have done it inefficiently based on what I've seen in other Questions about doing the same sort of thing. In another question Solutions for a simple multi tenant web application with entity framework reflection is used to do this, but I'm not able to work out whether it would apply in my case, or even how to use it.

I would be extremely appreciative if anyone can explain the best way of going about this, and the pros/cons of differing ways. Thanks :)

My current implementation is as follows:

DB

  • One database, multiple tenants.
  • All tables link back to Company table one way or another, although not all have a CompanyID field.

TestController.cs

public class TestController : Controller
{
    private BookingSystemEntities db = new BookingSystemEntities();
    public ActionResult Index()
    {
        var user = db.Users.Single(u => u.Email == User.Identity.Name);
        IBookingSystemRepository rep = new BookingSystemRepository(db, user);            
        return View(rep.GetAppointments(false));
    }

}

BookingSystemRepository.cs

public class BookingSystemRepository : IBookingSystemRepository
{
    private CompanyBookingSystemRepository db;

    public BookingSystemRepository(BookingSystemEntities context, User user)
    {
        this.db = new CompanyBookingSystemRepository(context, user);
    }

    public IEnumerable<Appointment> GetAppointments()
    { return GetAppointments(false); }

    public IEnumerable<Appointment> GetAppointments(bool includeDeleted)
    {
        return includeDeleted
            ? db.Appointments
            : db.Appointments.Where(a => a.Deleted.HasValue);
    }

    public IEnumerable<Client> GetClients()
    { return GetClients(false); }

    public IEnumerable<Client> GetClients(bool includeDeleted)
    {
        return includeDeleted
            ? db.Clients
            : db.Clients.Where(c => c.Deleted.HasValue);
    }

    public void Save()
    {
        db.SaveChanges();
    }

    public void Dispose()
    {
        if (db != null)
            db.Dispose();
    }
}

CompanyBookingSystemRepository.cs

public class CompanyBookingSystemRepository
{
    private BookingSystemEntities db;
    private User User;
    public IEnumerable<Appointment> Appointments { get { return db.Appointments.Where(a => a.User.CompanyID == User.CompanyID).AsEnumerable<Appointment>(); } }
    public IEnumerable<Client> Clients { get { return db.Clients.Where(a => a.CompanyID == User.CompanyID).AsEnumerable<Client>(); } }

    public CompanyBookingSystemRepository(BookingSystemEntities context, User user)
    {
        db = context;
        this.User = user;
    }

    public void SaveChanges()
    {
        db.SaveChanges();
    }

    public void Dispose()
    {
        if (db != null)
            db.Dispose();
    }
}
like image 701
Matthew Hudson Avatar asked Jun 19 '13 11:06

Matthew Hudson


1 Answers

I like your approach better than some of the other examples you've provided. Filtering based on the logged-in user should be the most effective way to make sure you're filtering your data appropriately assuming each tenant runs off the same codebase and domain. (If not, you could utilize those to filter as well.)

If you're concerned about database performance with filtering tables that don't have a CompanyID you can purposely denormalize your database to include that field in those tables.

The reflection approach you cited, although elegant, seems overly complicated and like a lot more overhead than including the CompanyID in your db call (especially since the db call is happening in both instances).

EDIT (after comment):

As for the rest of it, you seem to have written a lot of excess code that's not really necessary (at least not within the example cited above). I don't necessarily understand why you're distinguishing between a BookingSystemRepository and a CompanyBookingSystemRepository since from your code it seems like the former exists only to pass calls through to the latter, which just filters results using the UserID (is there ever a situation where you wouldn't filter those results?).

You could eliminate both of those classes (and the issue you cite in your comment) entirely by just changing your method to:

public class TestController : Controller
{
    private BookingSystemEntities db = new BookingSystemEntities();
    public ActionResult Index()
    {
        var user = db.Users.Single(u => u.Email == User.Identity.Name);
        var appointments = db.Appointments.Where(a => a.User.CompanyID == user.CompanyID).AsEnumerable();
        return View(appointments);
    }

    public override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }
}

From there, if you're worried about performance, you really should be doing all your filtering within the DB and then only call those procedures to return your data.

like image 197
Chris Searles Avatar answered Nov 01 '22 10:11

Chris Searles