Handling data access in multi tenant site

不打扰是莪最后的温柔 提交于 2019-12-03 22:58:38

I choose to use action filters to do this. It may not be the most elegant solution, but it is the cleanest of the solutions we've tried so far.

I keep the tenant (in our case, it's a team) in the URL like this: https://myapp.com/{team}/tasks/details/1234

I use custom bindings to map {team} into an actual Team object so my action methods look like this:

[AjaxAuthorize, TeamMember, TeamTask("id")]
public ActionResult Details(Team team, Task id)

The TeamMember attribute verifies that the currently logged in user actually belongs to the team. It also verifies that the team actually exists:

public class TeamMemberAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    base.OnActionExecuting(filterContext);
    var httpContext = filterContext.RequestContext.HttpContext;

    Team team = filterContext.ActionParameters["team"] as Team;
    long userId = long.Parse(httpContext.User.Identity.Name);

    if (team == null || team.Members.Where(m => m.Id == userId).Count() == 0)
    {
        httpContext.Response.StatusCode = 403;
        ViewResult insufficientPermssions = new ViewResult();
        insufficientPermssions.ViewName = "InsufficientPermissions";
        filterContext.Result = insufficientPermssions;
    }
  }
}

Similarly, the TeamTask attribute ensures that the task in question actually belongs to the team.

Since my app is using subdomains (sub1.app.com, sub2.app.com.....) I basically choose to:

a) use something like the following code to cache info about tenants and

b) to call an action filter on each controller as suggested by Ragesh & Doc:

(Following code is from the blog on : http://www.developer.com/design/article.php/10925_3801931_2/Introduction-to-Multi-Tenant-Architecture.htm )

// <summary>
// This class is used to manage the Cached AppSettings
// from the Database
// </summary>
public class AppSettings
{
// <summary>
// This indexer is used to retrieve AppSettings from Memory
// </summary>
public string this[string Name]
{
  get
  {
     //See if we have an AppSettings Cache Item
     if (HttpContext.Current.Cache["AppSettings"] == null)
     {
        int? TenantID = 0;
        //Look up the URL and get the Tenant Info
        using (ApplContext dc =
           new ApplContext())
        {
           Site result =
                  dc.Sites
                  .Where(a => a.Host ==
                     HttpContext.Current.Request.Url.
                        Host.ToLower())
                  .FirstOrDefault();
           if (result != null)
           {
              TenantID = result.SiteID;
           }
        }
        AppSettings.LoadAppSettings(TenantID);
     }

     Hashtable ht =
       (Hashtable)HttpContext.Current.Cache["AppSettings"];
     if (ht.ContainsKey(Name))
     {
        return ht[Name].ToString();
     }
     else
     {
        return string.Empty;
     }
  }
}

// <summary>
// This Method is used to load the app settings from the
// database into memory
// </summary>
public static void LoadAppSettings(int? TenantID)
{
  Hashtable ht = new Hashtable();

  //Now Load the AppSettings
  using (ShoelaceContext dc =
     new ShoelaceContext())
  {

      //settings are turned off
      // no specific settings per user needed currently
     //var results = dc.AppSettings.Where(a =>
     //   a.in_Tenant_Id == TenantID);

     //foreach (var appSetting in results)
     //{
     //   ht.Add(appSetting.vc_Name, appSetting.vc_Value);
     //}
                ht.Add("TenantID", TenantID);

  }

  //Add it into Cache (Have the Cache Expire after 1 Hour)
  HttpContext.Current.Cache.Add("AppSettings",
     ht, null,
     System.Web.Caching.Cache.NoAbsoluteExpiration,
     new TimeSpan(1, 0, 0),
     System.Web.Caching.CacheItemPriority.NotRemovable, null);

     }
  }

If you want to execute common code like this on every Action in the Controller, you can do this:

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    base.OnActionExecuting(filterContext);
    // do your magic here, you can check the session and/or call the database
}

We have developed a multi tenant application using ASP.NET MVC as well and including the tenant ID in every query is a completely acceptable and really necessary thing to do. I'm not sure where you are hosting your application but if you can use SQL Azure they have a new product called Federations that allows you to easily manage multi tenant data. One nice feature is that when you open the connection you can specify the tenant ID and all queries executed thereafter will only effect that tenants data. It is essentially just including their tenant ID in every request for you so you don't have to do it manually. (Note that federating data is not a new concept, Microsoft just released their own implementation of it recently)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!