1

I'm building a multi-tenant MVC app where there's a single app pool and single database. I have a Tenant table, and each of my models has a TenantId identified.

Each Tenant has a string "Url" that identifies the full URL used to access that tenant's data.

I can access this from my BaseController with the following (rough approximation):

HttpRequest request = HttpContext.Current.Request;
Uri requestUrl = request.Url;
_tenant = _tenantService.GetTenantByUrl(requestUrl);

Now, I'm at a point where I need to pass the Tenant into the service layer to perform business logic. One way I can do this is to go across every single method across all services (~200 methods) and add a Tenant parameter. I'd have to touch every call to the service layer, and every service layer method. This would work, but it's tedious and muddles the code.

For example, one of my methods before:

    public void DeleteUserById(int userId)
    {
        using (var db = CreateContext())
        {
            var user = db.Users.FirstOrDefault(u => u.UserId.Equals(userId));
            InternalDeleteUser(db, user);
        }
    }

After (if I pass in the Tenant):

    public void DeleteUserById(Tenant tenant, int userId)
    {
        using (var db = CreateContext())
        {
            var user = tenant.Users.FirstOrDefault(u => u.UserId.Equals(userId));
            InternalDeleteUser(db, user);
        }
    }

What I'm trying to achieve (by setting the tenant from my BaseController, one layer up):

    public void DeleteUserById(int userId)
    {
        using (var db = CreateContext())
        {
            var user = _tenant.Users.FirstOrDefault(u => u.UserId.Equals(userId));
            InternalDeleteUser(db, user);
        }
    }

Is there any way I can use my BaseService (all other services inherit from this) or some other pattern to define the Tenant from the Controller, and have the service methods pick it up, without passing it as a parameter to each one? This way I need only touch the base controller (or maybe even global.asax), and nothing else.

Put simply: How can I make an object accessible to all services by defining it from an MVC controller, without passing it directly to the service?

SB2055
  • 12,272
  • 32
  • 97
  • 202
  • Do you perform some sort of user authentication? I would find it natural to associate the TenantId with the current user. See [this stack overflow question](http://stackoverflow.com/questions/1064271/asp-net-mvc-set-custom-iidentity-or-iprincipal) for instruction on how to add custom fields to the current identity/principal object. Then you could access the TenantId from your service layer code like so: `((MyCustomPrincipal)HttpContext.Current.User).TenantId`. – Eiríkur Fannar Torfason Nov 03 '13 at 18:44
  • @EiríkurFannarTorfason I have custom membership set up, and the user is currently paired with the tenant, however not all queries involve the user, so I don't think this approach would work. – SB2055 Nov 03 '13 at 18:49
  • Well, there's always the `Session` object. – Eiríkur Fannar Torfason Nov 03 '13 at 19:01
  • @EiríkurFannarTorfason I'm playing with Session now - 'Session.Add("Tenant", "test");' from my Global.asax.cs - though I can't figure out how to access this from my Service layer without passing HttpContext (yikes). Thoughts? – SB2055 Nov 03 '13 at 19:39
  • `HttpContext.Current.Session["Tenant"]` should do it. – Eiríkur Fannar Torfason Nov 03 '13 at 19:56
  • @EiríkurFannarTorfason - I tried setting this value in both Application_BeginRequest in global.asax as well as Application.AcquireRequestState, but I still get a null Session. How would you set this value? – SB2055 Nov 04 '13 at 21:31
  • According to what I've read, the Session object should be available when the AcquireRequestState event is raised. – Eiríkur Fannar Torfason Nov 05 '13 at 10:00

2 Answers2

3

I guess what you´re saying about having a base service (see Layer Supertype) makes sense. That base class will have a dependency on an interface defined in the same service layer (e.g. IUserSession, IContext or whatever) and that interface will have a method or property that will return your Tenant.

The implementation of this interface will reside in your web application and it will do something as what you described, obtaining the data from the HttpContext.

If you have a background process, console application or whatever that does not run on a web context, you will have a different implementation that will create the Tenant based on any other criteria that you want.

So to summarize, you will have in your service layer:

abstract class BaseService 
{    
    protected IContext Context {get; private set;}

    public BaseService(IContext context)
    {
            Context = context;
    }
}

public interface IContext
{
    Tenant GetTenant();
}

Then in your web layer you´ll have:

public IWebContext : IContext
{
    public Tenant GetTenant()
    {
        //your code to return create the tenant based on the url.
    }
}

Hope this helps.

uvita
  • 4,124
  • 1
  • 27
  • 23
0

I have the same 'problem' since I'm building a multi tenant app as well. However, I solved it quite simple, IMO: every repository/service has defined a TenantId property, that must be set when that service is used. TenantId is a value object and it will throw if null.

Now, the point is any of the repos/services can be used outside the request, for example in a background thread or app. I am using a message driven approach so any required info (like tenant id) is part of the message and thus available for the consumer of the service (the message handler). Another benefit is testability.

I advice against coupling your service to a request specific object like HttpContext, Session or Cache.

MikeSW
  • 16,140
  • 3
  • 39
  • 53
  • Thanks Mike. I've also implemented the TenantId property on my BaseService. How do you acquire the TenantId from, say, your Controllers? Since a tenant should be determined by the URL, would you extract it within your BaseController? – SB2055 Nov 04 '13 at 21:33
  • I extract the tenant id in a httpmodule then cache it in HttpContext.Items. Then an extension method to make it easier to access it. – MikeSW Nov 04 '13 at 21:35
  • Awesome - looks like I've got some more learning to do :). Thanks for the help – SB2055 Nov 04 '13 at 21:44