Addendum: Multi-tenant LightSwitch applications: favor convention over configuration.

Wut wut, did I not already cover the generic multi-tenant filter in my previous blog post? Well how observant of you, dear reader, I most certainly did. However, in this post I’d like to elaborate a bit about a hidden ‘convention’ in PowerProductivityStudio.Server, the nuget package that you install on your LightSwitch server project to weave generic code entry points into your application.

See, when we (me and my alter ego) initially built PPS, we already had the idea in mind to use these generic code entry points to facilitate multi tenancy. Multi-tenancy is where a single deployed instance of a (LightSwitch) app is used by multiple groups of users (each group is a tenant), who share the hosting costs of the server and the database. This means that their data is in the same tables, and must thus generally be filtered so that tenants cannot view, let alone alter, data that belongs to another tenant.

Additionally, we figured that some default data would sometimes be common between different tenants as well. This data can be seen by any of the tenants, but changed only by the administrator.

In a nutshell, we needed an easy way for any LightSwitch project to:

  • filter data based on the TenantId of that data
  • show data that belongs to tenant “0” (default, shared data)
  • prohibit any changes/creations to data that belongs to tenant “0” (default, shared data)
  • make sure when a user creates a record, that data is correctly labeled with the correct TenantId
  • make sure when a user tries to update or delete data that belongs to a different tenant, this is blocked by the application.

Now, if these requirements sound like something you require (pardon my English vocabulary on Mondays…), you’ll be glad to find out that PPS has a hidden convention.

Here’s how that works:

Step one: add an integer “TenantId” column to any data that needs filtering, for example:

Image 183

Note: I suggest turning off ‘Show by default’ and setting the TenantId property as nullable. This way, the client will not ever have to bother with the property.

Now, just as a test, go to the Project entity where you added the TenantId property, and hit the “Write Code” > “Projects_Filter” button.

Image 190

A method stub will be generated that has one argument, and a code comment with a suggestion on how you can implement a filter here.

Put a breakpoint in the generated method stub and run the application. By default, you’ll notice that the ‘Filter’ argument is null…

Image 191

Instead of manually coding a lambda expression that filters records though, we’re going to use a convention in PowerProductivityStudio…

Wait, in whut whut?

Step two: install the PowerProductivityStudio.Server nuget package on your LightSwitch server project. PPS is a nuget package that contains a code weaving task. This task will add generic ‘code entry’ point to your application, so you can write code when ‘any entity is created/updated/deleted/queried’, …  This way you can create a generic filter at runtime, like in my previous post. However instead of writing a generic filter, we’ll take it one step further than my previous post again.  To install PowerProductivityStudio.Server, right-click on your server project and select ‘Manage nuget packages’. Search for ‘PowerProducitivityStudio.Server’ and install it.

Step three: tell PPS about your current tenant. To do this, add any class to your LightSwitch server application and implement the PowerProducitivityStudio.Extensibility.IMultiTenantService interface, for example:

using System.Web;
using Microsoft.LightSwitch;

namespace LightSwitchApplication
{
    public class MyMultiTenantService : PowerProductivityStudio.Extensibility.IMultiTenantService
    {
        public int GetCurrentTenantId()
        {
            return 1;
        } 
    }
}

Step four: launch your application!

If you still have your breakpoint in the ‘Projects_Filter’ method, you’ll notice that the query no longer is ‘null’, but actually has a valid ‘Multi-Tenant-Filter’ now…

Image 192

Succes? Let’s see…Image 184

All done… We just found an easy way for any LightSwitch project to:

  • filter data based on the TenantId of that data
  • show data that belongs to tenant “0” (default, shared data)
  • prohibit any changes/creations to data that belongs to tenant “0” (default, shared data)
  • make sure when a user creates a record, that data is correctly labeled with the correct TenantId
  • make sure when a user tries to update or delete data that belongs to a different tenant, this is blocked by the application.

Too easy? To short of a blog post?

Well, I could elaborate a bit more about my progress in learning Spanish (I can conjugate 12 verbs now, and know 27 different ways to order a beer), or I could help you a bit with that multi-tenant filter implementation.

First suggestion I would have for your multi-tenant filter (I know you’re not interested in my Spanish progress) would be to not hardcode it to “return 1;”.  You could set up a table that stores all tenants and their corresponding HostURL (for SharePoint provider hosted apps, like in the last post), or add a related table that stores which login (/user) belongs to which tenant.

Then, when PPS asks you for the tenant, just look that up using the ServerApplicationContext:

using System.Web;
using Microsoft.LightSwitch;

namespace LightSwitchApplication
{
    public class MyMultiTenantService : PowerProductivityStudio.Extensibility.IMultiTenantService
    { 
        public int GetCurrentTenantId()
        {
            var user = ServerApplicationContext.Current.DataWorkspace.ApplicationData.Users.Where(u => u.Login == ServerApplicationContext.Current.Application.User.Name).Single();
            return user.Tenant.Id;
        } 
    }
}

Sweet, but this will bog your system down quite a bit, since any data that is being retrieved/edited, will result in at least one extra call. Below is a revised version that will cache the current tenant id for a particular user in the HttpContext.Current.Cache. In addition, it also takes scenarios in mind where there might not be an existing ServerApplicationContext (if you use Web API for your fancy word document generation, for example), and shaves some milliseconds of the query by retrieving the User and it’s Tenant at the same time.

using System.Web;
using Microsoft.LightSwitch;

namespace LightSwitchApplication
{
    public class MyMultiTenantService : PowerProductivityStudio.Extensibility.IMultiTenantService
    { 
        public int GetCurrentTenantId()
        {
            int tenantId;
            if (HttpContext.Current == null //can happen if you have custom Web API calls
                || HttpContext.Current.Session["CurrentTenantId"] == null)
            {
                if (ServerApplicationContext.Current != null)
                {
                    var user = ServerApplicationContext.Current.DataWorkspace.ApplicationData.Users.Include("Tenant").Where(u => u.Login == ServerApplicationContext.Current.Application.User.Name).Single(); 
                    tenantId = user.Tenant.Id; 
                }
                else //can happen if you have custom Web API calls
                {
                    using (var context = ServerApplicationContext.CreateContext())
                    {
                        var user = ServerApplicationContext.Current.DataWorkspace.ApplicationData.Users.Include("Tenant").Where(u => u.Login == ServerApplicationContext.Current.Application.User.Name).Single();
                        tenantId = user.Tenant.Id;
                    }
                }
                HttpContext.Current.Session["CurrentTenantId"] = tenantId;
            }
            else
            {
                tenantId = (int)HttpContext.Current.Session["CurrentTenantId"];
            }
            return tenantId;
        }
    }
}

Badam tsss!

Just one more thing: PPS weaves the multi tenant filter just before the call is made to your “Projects_Filter” or “WhateverEntity_Filter” method.  There’s good news and bad news to that timing: if the multi-tenant filter that PPS creates does not fit your needs, you can easily overwrite it with whatever custom code you want, and re-assign the filter parameter. The bad news: if you already have a filter implemented (let’s say you only want projects with an ‘a’ in the name to appear), you’ll need to merge your custom filter and the existing filter together:

Image 194

 

Now, if you’ll actually try this, you’ll get a compilation exception, because combining two expressions is not supported unless you add this little helper class somewhere in your server project  😉

Badam tsss again 😉

Sample project has been uploaded to my OneDrive, and as always…

Keep rocking LS!

 

Advertisements

23 thoughts on “Addendum: Multi-tenant LightSwitch applications: favor convention over configuration.

  1. OMG!!! You really are a LightSwitch superstar!!!

    Gracias por este post… y si, estoy pendiente de su progreso en el idioma español!!!

    Saludos desde Colombia!!!

  2. Hi Jan, tons of thanks for this!

    An issue I have is that I can’t build unfortunately. Here’s the error, wondering if you could shed some light?

    Many thanks!

    Ian

    Error 32 The “NotifyUserCodeWeaverTask” task could not be loaded from the assembly D:\NETXtensive LTD\Development\_SourceControl\Repositories\NETX\_Core Systems\LsCoreProject\LsCoreProject.Server\..\..\packages\PowerProductivityStudio.server.2014.2.7\tools\PowerProductivityStudio.MSBuild.dll. Could not load file or assembly ‘file:///D:\NETXtensive LTD\Development\_SourceControl\Repositories\NETX\_Core Systems\packages\PowerProductivityStudio.server.2014.2.7\tools\PowerProductivityStudio.MSBuild.dll’ or one of its dependencies. The system cannot find the file specified. Confirm that the declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask. LsCoreProject.Server

    • Hey Ian!
      Oh no, that’s sad to hear. Can you check if the file ‘D:\NETXtensive LTD\Development\_SourceControl\Repositories\NETX\_Core Systems\packages\PowerProductivityStudio.server.2014.2.7\tools\PowerProductivityStudio.MSBuild.dll’ exists? In that folder, there should be 7 assemblies (4 mono.cecil and 3 PPS ones).

      Thanks!

      Jan

      • I’m getting the same error. Have confirmed that those 7 dlls are in their place… BUT… The hard-coded path is one level off.

        The error provides the following path:
        C:\Users\kncncar\Source\Repos\AbstractingDb\packages\PowerProductivityStudio.server.2015.1.1\tools\

        But the actual path is:

        C:\Users\kncncar\Source\Repos\AbstractingDb\AbstractingServices\packages\PowerProductivityStudio.server.2015.1.1\tools

        This is of course because I had a check box ticked off “Create a folder for a solution” when I created the LS solution.

        Is that path hardcoded, or could I change it through configuration somewhere?

        Awesome stuff, by the way! Hopefully can get it to run with your help.

      • UPDATE: I see that I can change the path in the Targets file. And it gets past that error, but there is then another one (likely from the same cause):

        The “PowerProductivityStudio.MSBuild.NotifyUserCodeWeaverTask” task was not found. Check the following: 1.) The name of the task in the project file is the same as the name of the task class. 2.) The task class is “public” and implements the Microsoft.Build.Framework.ITask interface. 3.) The task is correctly declared with in the project file, or in the *.tasks files located in the “C:\Program Files (x86)\MSBuild\14.0\bin” directory. AbstractingServices.Server

    • Hey AC 🙂
      PowerProductivityStudio.server is a nuget package. Nuget packages are a way to deliver DLL’s, code, configuration, …
      Right-click on your server project, and select ‘Manage Nuget Packages’. Search for PowerProductivityStudio.Server and hit the install button 😉

      Keep rocking LS

      Jan

  3. Nice Jan!
    Here’s an idea for your addendum to the addendum of the addendum 😉
    Scenario: Multi-tenant middle-tier with optional single tenant databases: Lookup database conn string in Users table by HostURL and cache it. Then use _InitializingConnection method of data service to change SQL dbs. Do this in addition to a TenantID filter and you’ll have the option to share database or not. YOU keep rocking LS!
    Josh

      • Yes
        I would put con strings in an external Db to be shared by all tenants.
        Can’t wait for you to pull it off

        There are no blogs on initializingcon ection method yet

      • Next release maybe add to MultiTenantService the method GetConnString(TenantId, DataService) which weaves resulting string into _InitializingConnection. Let the dev decide whether to lookup in external db, from web.config, hardcode, etc. eso es muy bueno!

      • Perhaps instead of an integer, I could have the dev return an object with both an optional TenantId and an optional ConnectionString property? That way the dev could implement single-DB multi-tenancy, a DB for each tenant, or a combination of all, with a single method…
        Hmmmmmmmmmmmm…. Tempting.
        But I already got a weekend project for this weekend so it’ll be a while 🙂
        Have a great one Josh 🙂

      • In case this is tempting enough to blow off your other project, don’t forget to consider multi datasource projects. For each tenant there could be several dataservices thus several conn strings, thus the need to pass in dataservice object or name. Also it would only work for SQL data since OData connections are not dynamic – yet. Maybe you could hack that too while you’re at it. 🙂
        Have a nice weekend!

  4. Jan,

    Haven’t tried this yet, but
    Q: Are you saying the MultiTenantProvider has the filter handler ‘built-in’? As long as I have a property ‘TenantId’ and implement GetCurrentTenantId(), then I don’t have to do the FilterRequestOccured like in your previous post?

    TIA,
    Josh

    • Yep, that is indeed the very short and correct version =)
      You can check by implementing a “MyEntity_Filter” method and putting a breakpoint. Before you install PPS.Server and implement that interface, the query is null by default. After installing and implementing, the filter is “entity => entity.TenantId == 0 || entity.TenantId == 15” if your GetCurrentTenantId returned 15 😉
      Keep rocking LS 🙂

      Jan

      • Hey Josh,

        I updated the blog post to describe the timing where PPS weaves this multi-tenant filter, and the caveats with that timing. At first I was simply going to do a new blog post: “Addendum to the Addendum about the Addendum about Michael’s post”, but an update of this post seemed less hassle, lol!

        Keep rocking LS!

        Jan

  5. Hi Jan, great work!
    I’m trying to recreate your working example (MultiTenancyByConvention) but am running into this error:
    ‘An unhandled exception of type ‘System.StackOverflowException’ occurred in mscorlib.dll’
    I’m getting the error on:
    var user = ServerApplicationContext.Current.DataWorkspace.ApplicationData.Users.Include(“Tenant”).Where(u => u.Login == ServerApplicationContext.Current.Application.User.Name).Single();
    Any chance you can give me a push in the right direction?

      • Hey Peter!
        I was messing around and doing some maintenance, must have messed it up.
        Will fix it and make sure the latest version is working, thanks for the heads up and post-back!!!
        Keep rocking LS!
        Jan

  6. Hi Jan,

    This is pretty awesome stuff. Thank you very much!

    There is a thing or two I would like to have explained to me.
    My scenario is that an entity Company can have multiple users and each company can have Projects (another entity).
    So MyCompany should only be allowed to see projects belonging to said company. I have set up the relevant relationships in LS’ datamodel. So here goes the question: Do I really need to add a TenantId column when LS already creates an ID column to point to the owner Company entity? Seems a little redundant. Do I have to call my entity Tenant instead of Company?
    Is there a way for me to change the name of the int cloumn that PPSS is looking for?
    What am I missing? 😉

    Thanks,

    Sune

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s