In our multi-tenant application we have a need to customize the styles used per-tenant.
We currently plan to do so using LESS and variables in the following way on the client:
- Download dependent LESS files from server
- Call web service to get configuration object
- Form string of valid LESS with variables defined
- Use
less.js
compiler to compile LESS based on these variables and the fixed LESS files from step 1
This approach has a number of downsides:
- Clients can behave badly
- Some browsers have problems with
less.js
- Compilation takes time
We would instead like to take care of this work on the server, so that roughly speaking, this happens on the server instead:
- Client requests to download one big compiled stylesheet -
GET content/styles/{tenantName}.css
- Using
tenantName
the server fetches configuration
- Using a template and the appropriate variables (maybe
string.Format
or something more sophisticated)
- Server compiles LESS to CSS string
- Server returns CSS string with appropriate
Content-Type
Here are my questions:
- Is this an unusual or undesirable way to achieve said result?
- Short of setting up architecture for server-side JavaScript, how can I compile the LESS into CSS?
- What must I do in the controller action or in the route configuration to make the client think that the server is returning a regular old CSS file, complete with cache control, not modified?
You could use BundleTransformer to compile your LESS server side.
It can depend on how you want to serve the file. If you know all the tenants then just add add a bundle url for each tenant application to the bundle config.
var themeStyles = new CustomStyleBundle("~bundles/theme/tenant").Include("~/Content/theme.less");
themeStyles.Builder = new ThemeBuilder();
BundleTable.Bundles.Add(themeStyles);
If you don't and the tenants are flexible as was the case in our situation then add the following controller action for your themes.
[Route("bundles/theme/{id}")]
public ContentResult Theme(string id)
{
var tenantThemePath = string.Format("~/bundles/theme/{0}", id);
// Check that bundle has not already been added.
if (BundleTable.Bundles.All(x => x.Path != tenantThemePath))
{
var themeStyles = new CustomStyleBundle(tenantThemePath ).Include("~/Content/theme.less");
themeStyles.Builder = new ThemeBuilder();
BundleTable.Bundles.Add(themeStyles);
}
var context = new BundleContext(HttpContext, BundleTable.Bundles, institutionPath);
var response = BundleTable.Bundles.GetBundleFor(tenantThemePath).GenerateBundleResponse(context);
Response.Cache.SetCacheability(response.Cacheability);
return Content(response.Content, response.ContentType);
}
The ThemeBuilder implementation for BundleTransformer
public class ThemeBuilder : IBundleBuilder
{
public string BuildBundleContent(Bundle bundle, BundleContext context, IEnumerable<BundleFile> files)
{
var lessTranslator = bundle.Transforms.OfType<StyleTransformer>()
.Where(x => x != null)
.Select(x => x.Translators.OfType<LessTranslator>().FirstOrDefault())
.FirstOrDefault();
if (lessTranslator == null)
{
return string.Empty;
}
lessTranslator.GlobalVariables = GetThemeVariables();
return string.Empty;
}
private string GetThemeVariables()
{
// Simplified for brevity
// This will be translated to less variables by the BundleTransformer
// themeColour should correspond to a variable name in your less file.
return string.Format("themeColour={0}", themeColour);
}
}
You will need away of getting the theme colours out we stashed those variables in HttpContext stores so that we could pull them out using an extension method in the GetThemeVariables method.
I hope this helps.
UPDATE
I've expanded on my original answer and created a more reusable way of including themes.
Demo site here: http://bundletransformer-theme-builder.azurewebsites.net/
GitHub repo here: https://github.com/benembery/bundle-transformer-theme-builder