A couple of months ago (is it August already, really?), Michael Washington published a detailed article on creating multi-tenant Cloud Business Applications. Multi-tenancy is a hot subject in the SAAS world, because it allows a single deployed server instance to serve multiple groups of users (called tenants because they usually pay for that service), which cuts down hosting costs and makes software updates so much easier.
The article is awesome, and after wasting Michael’s time because I thought there was a small security issue (which there isn’t) gets my absolute official seal of approval, along with a great A- rating!
Wait, A-? Why not A+? Well, the implementation relies on you writing a bit of code each time an entity is inserted, updated, or queried. Multiply this by 200-300 entities and you have a lot of typing to do, and a lot of chances to forget a particular entity. Unfortunately though, since LightSwitch has no generic way built-in to write code when ‘any’ entity is inserted, updated, or queried, there’s no way to built-in way to save your keystrokes…
Smooth transition: faithful readers of my blog will already know that when I say: no ‘built-in’ way, a small hack is just around the corner…
PS: this hack works for non-CBA LightSwitch apps as well.
Part 1: making the code more generic.
The name of the game is simple: when a LightSwitch application gets compiled, we will use a custom MSBuild task to open the server assembly, interpret the IL, and weave in calls to a generic event handler, then write code in our generic events.
Save yourself the headache (trust me, there’s a lot of headaches) involved in the actual hacking, and install a nuget package called powerproductivitystudio.server on your server project instead. This package will add a custom MSBuild task to open the server assembly when it gets compiled, which interprets the IL, and weaves in calls to a generic event handler for you. If you have seen my recent development on app-stitch, you’ll understand we use it ourselves extensively to drive events that happen in your LightSwitch app to the app-stitch event processor.
What you have left to do, is add a class where you can handle the generic LightSwitch events. The class can be added anywhere in your server project, it can have any name, as long as it implements ‘PowerProductivityStudio.Extensibility.IServerEventHandler’, it will be recognized by the PPS framework at runtime and the methods will be called appropriately. Here’s an empty stub to get you started:
public class MyGenericFilter : PowerProductivityStudio.Extensibility.IServerEventHandler
{
public void EntityCreatedEventOccured(Microsoft.LightSwitch.IEntityObject entityObject)
{
//Gets called when eny entity is created on the server.
}
public void EntityPermissionRequestOccured(string action, string entityPluralName, ref bool result)
{
//Gets called when the server is asked if a user has rights to view/edit/... an entity.
//Set result to true or false.
}
public void EntityValidatedEventOccured(Microsoft.LightSwitch.IDataService dataService, Microsoft.LightSwitch.IEntityObject entity, Microsoft.LightSwitch.IValidationResultsBuilder validationResultsBuilder)
{
//Gets called whenever an entity is validated on the server.
}
public void FilterRequestOccured(ref T originalFilter) where T : class
{
//Gets called whenever a set of entities is retrieved, and could be filtered.
}
public void ServerEventOccured(string action, Microsoft.LightSwitch.IEntityObject entity, Microsoft.LightSwitch.IDataService dws)
{
//Gets called whenever an entity is being inserted, updated, deleted or is inserting, updating or deleting.
}
}
To make Michael’s Task_Inserting and Tasks_Updating code work for every single entity, you can implement the last method as:
public void ServerEventOccured(string action, Microsoft.LightSwitch.IEntityObject entity, Microsoft.LightSwitch.IDataService dws)
{
if (action.EndsWith("Inserting") || action.EndsWith("Updating"))
{
if (entity.Details.Properties.Contains("HostURL"))
{
var hostUrl = "MyTenantID123"; //this.Application.SharePoint.HostUrl.Host;
entity.Details.Properties["HostURL"].Value = hostUrl;
}
}
}
The code checks if the entity has a property called ‘HostURL’ (Sometimes, multi-tenant apps have a set of commonly used data, which will not have the ‘HostURL’ property), and will set your tenant’s ID (the SharePoint Host URL in Michael’s example) on inserts and updates.
Almost done, and your app is forever multi-tenant… The last task is to make sure every entity that has a property called ‘HostURL’, is filtered so that each tenant can only access his/her data.
Michael does this by creating a filter (a lambda expression) and assigning it in the Tasks_Filter method. We’ll do exactly the same, only write some generic code to create the lambda expression at runtime, in the FilterRequestOccured method:
public void FilterRequestOccured<T>(ref T originalFilter) where T : class
{
//T is Expression<Func<Entity, bool>>
var entityType = typeof(T).GetGenericArguments()[0].GetGenericArguments()[0];
if(entityType.GetProperty("HostURL") == null)
return;
var hostUrl = "MyTenantID123"; //this.Application.SharePoint.HostUrl.Host;
// Only allow users to see records from their own site by creating a filter (Function) at runtime
// filter = e => e.HostURL == this.Application.SharePoint.HostUrl.Host;
var e = System.Linq.Expressions.Expression.Parameter(entityType, "e");
var myTenantId = System.Linq.Expressions.Expression.Constant(hostUrl);
var hostURLProperty = System.Linq.Expressions.Expression.Property(e, "HostURL");
var hostURLEqualsMyTenantId = System.Linq.Expressions.Expression.Equal(myTenantId, hostURLProperty);
Type funcType = typeof(Func<,>).MakeGenericType(entityType, typeof(bool));
var newFilter = System.Linq.Expressions.Expression.Lambda(funcType, hostURLEqualsMyTenantId, new[]{ e }) as T;
originalFilter = newFilter;
}
Congratulations, your app is now forever multi-tenant, and you can continue with Michael’s awesome post.
Part 2: app-stitch: multi-tenant business rules
Hold on to your hats though, there’s one more chapter to tell in the multi-tenant story: business rules. Business logic is usually hard-coded in the application, but sometimes one tenant has slightly different rules than another tenant.
To answer this need, we’ve made sure that app-stitch supports multi-tenant scenarios as well, so that each tenant can only see his/her own rules, but more importantly, to make sure that a business rule only fires on actions on data that actually belong to the tenant.
Once you have app-stitch installed, open the folder appstitch/appstitch.LightSwitch and open the class ItemUserAndContextProvider.cs. Each time app-stitch needs an AppStitchItem (rules/audited events/settings), information about the current user or a ServerApplicationContext instance, the ItemUserAndContextProvider class will provide just that.
Locate this piece of code:
Microsoft.LightSwitch.Server.IServerApplicationContext context;
using (GetServerApplicationContext(out context))
{
user = new AppStitch.UserInformation();
user.FullName = context.Application.User.Identity.Name;
user.CanView = true;
user.CanEdit = true;
user.IsDebugAdmin = context.Application.User.HasPermission(Permissions.SecurityAdministration);
}
This is where your application creates an ‘AppStitch.UserInformation’ instance to give app-stitch information about the current user. (Note: this is also the place where you could deny a user from creating or even viewing the app-stitch business rules)
What you have to do, is add the fact that this user belongs to a particular ‘group’. Rules created by a user will only be visible to other users in the same group, and will only apply to events (data that changes, …) by users in the same group. For this example, we’ll say that each tenant is a separate group:
Microsoft.LightSwitch.Server.IServerApplicationContext context;
using (GetServerApplicationContext(out context))
{
user = new AppStitch.UserInformation();
user.FullName = context.Application.User.Identity.Name;
user.CanView = true;
user.CanEdit = true;
user.IsDebugAdmin = context.Application.User.HasPermission(Permissions.SecurityAdministration);
//add this:
user.GroupId = "MyTenantID123"; //this.Application.SharePoint.HostUrl.Host;
}
In summary…
app-stitch relies on a nuget package that creates generic code entry points in your application. With a bit of generic code of yourself, you can make your data and your business rules multi-tenant-ready once and for all. If you want to see a sample, here’s the bits. 🙂
Keep rocking LS!
Jan
Like this:
Like Loading...