Using configurable composite command in multi-tenant ASP.NET Core application
My previous posts about tenant-based dependency injection and using composite command in ASP.NET Core culminated with idea to use configurable composite commands in multi-tenant ASP.NET Core applications. Configurable composite commands make it easy to tweak save and update processes that contain multiple steps of what some can be custom and their activation is based on tenant configuration. Here’s how to build thost composite commands.
- Global query filters in Entity Framework Core 2.0
- Implementing tenant providers on ASP.NET Core
- Implementing database per tenant strategy on ASP.NET Core
- Handling missing tenants in ASP.NET Core
- Unit testing multi-tenant database provider
- Defensive database context for multi-tenant ASP.NET Core applications
- Tenant-based dependency injection in multi-tenant ASP.NET Core applications
- Using configurable composite command in multi-tenant ASP.NET Core application
Source code for this and my other multi-tenancy blog posts is available at Github reporitory gpeipman/AspNetCoreMultitenant.
Additional features scenario
Let’s imagine we have multi-tenant SaaS application. Tenants may be added and configured by service administrators and by customers through public portal.
It’s typical that some users get things done with basic features while others need advanced features too.
Optional features we have are here:
- Intelligent product thumbnails – by default squared thumbnail is generated by taking center part of rectangular photo whatever there is. We can use external service like Azure Cognitive Services to generate intelligent thumbnails where product is correctly set to focus. It’s cool service, check out my blog post Create thumbnails using Azure Cognitive Services to find out how it works.
- Send new product notifications – notify customers about new product and post information automatically to social media channels too.
Users who don’t need these features will get default thumbnails they can replace if needed and notifying their customers is task they will complete by their own.
Extending tenant definition
To keep things simple let’s suppose we don’t have many optional paid features and we can add these to tenant definition as properties. It means we need to define those properties in tenants store (in my case tenants.json file) for tenants that have those features enabled.
{
"Id": 3,
"Host": "bigcorp:5000",
"DatabaseType": 2,
"ConnectionString": "Server=localhost;Database=multitenant;Uid=demo;Pwd=demo",
"Name": "Big corp",
"StorageType": "AzureBlob",
"StorageConnectionString": "<storage connection string>",
"UseAdvancedProductThumbnails": true,
"SendProductNotifications": true
}
These changes must be reflected also in Tenant class.
public class Tenant
{
public int Id { get; set; }
public int DatabaseType { get; set; }
public string Host { get; set; }
public string ConnectionString { get; set; }
public string Name { get; set; }
public string StorageType { get; set; }
public string StorageConnectionString { get; set; }
public bool UseAdvancedProductThumbnails { get; set; }
public bool SendProductNotifications { get; set; }
}
Building configurable composite command
As saving of product can get more complex we move product saving logic away from service class and host it in composite command. I’m using here same approach as I did in my blog post Using composite command in ASP.NET Core and let framework-level dependency injection to build commands and composite command. It’s something like shown here.
public class SaveProductCommand : CompositeCommandBase<ProductEditModel>
{
public SaveProductCommand(SaveProductToDatabaseCommand saveToDb,
SaveProductImagesCommand saveImages,
SaveProductThumbnailsCommand saveThumbnails,
SaveAdvancedProductThumbnails saveAdvancedProductThumbnails,
NotifyCustomersOfProductCommand notifyCustomers)
{
Children.Add(saveToDb);
Children.Add(saveImages);
Children.Add(saveThumbnails);
Children.Add(notifyCustomers);
}
}
Why dependency injection? Commands in composite need instances to be injected and dependency injection is best option for this. Every command gets its own dependencies and composite gets bunch of commands it needs to operate. Simple and clean.
To get our job done we need to find out what commands we should add to composite. For this we need ITenantProvider to be injected to composite command. We can ask current tenant and based on tenant configuration we can add child commands to composite.
public class SaveProductCommand : CompositeCommandBase<ProductEditModel>
{
public SaveProductCommand(ITenantProvider tenantProvider,
SaveProductToDatabaseCommand saveToDb,
SaveProductImagesCommand saveImages,
SaveProductThumbnailsCommand saveThumbnails,
SaveAdvancedProductThumbnails saveAdvancedProductThumbnails,
NotifyCustomersOfProductCommand notifyCustomers)
{
var tenant = tenantProvider.GetTenant();
Children.Add(saveToDb);
Children.Add(saveImages);
if (tenant.UseAdvancedProductThumbnails)
{
Children.Add(saveAdvancedProductThumbnails);
}
else
{
Children.Add(saveThumbnails);
}
if (tenant.SendProductNotifications)
{
Children.Add(notifyCustomers);
}
}
}
Now we have tenant-based composite command to save products and it considers also advanced features that tenant owner has activated.
Wrapping up
Adding information about custom features to tenant definition and using configurable composite command we were able to create turn custom features on and off based on tenant configuration. Our main working horse here was composite command that contains all steps needed to save product information added by user. As commands in composite have different dependencies we used framework-level dependency injection to inject ready-made instances of commands to constructor of composite command keeping composite command lightweight this way. We also injected tenant provider to composite command and we used it to check what custom features (child commands) we should add to composite. As the end result we can build no flexible composite commands that are configurable based on current tenant.
Hi,
I’m currently using a similar way to handle multitenant applications, but why would you want to store all these options/tenant specific features in the ‘master’ database (blobstore in your case).
We handle it by only having the host => DB-id mapping in the master database, and every setting for the tenant is stored in the tenantXX db itself.
I’d say all that tenant information (address, phone and so on) belongs in a Tenant table in the tenantdb itself.
thoughts?
I think it’s more the question of technical design and also a little bit the matter of taste. I went here with central tenant store because this is what is needed when tenants management is not small and easy and there are querying over tenants needed. It’s not the mandatory design. If you can live with something smaller and simpler then go with it. There’s no need to make things more complicated than needed.
Pingback:The Morning Brew - Chris Alcock » The Morning Brew #2903
Would it make sense to use ASP.NET Core Feature flags instead your own implementation ? You could persist the feature flags on tenant datastore with similar solution they are using in here : https://docs.microsoft.com/en-us/azure/azure-app-configuration/use-feature-flags-dotnet-core
Feature flags is also an option. I think it’s a matter of taste if one prefers to use it.