Multi-tenancy is a complex topic with a generally understood definition, yet the devil is in the details. From a high level, Multi-tenancy is the idea that a single codebase can support many users in what they perceive as unique-to-them silos. Users have their tenants, which can provide isolation from others. Isolation can be logical or physical, specifically around dependencies such as data storage, authentication and authorization, and third-party services. For developers, multi-tenancy also makes the programming model more straightforward since most business logic can have a contextual baseline codified into the application’s infrastructure.
While the multi-tenancy approach is popular, it can be tricky to implement, especially within the ASP.NET Core pipeline, which heavily depends on dependency injection. In this post, we’ll see how to use the FinBuckle.Multitenant
package to gain a competitive advantage when developing multi-tenant applications.
What is FinBuckle.Multitenant?
FinBuckle.Multitenant
is an open-source .NET library designed to codify many best practices around multi-tenancy, taking into account many of the standard building blocks found in the .NET community. These building blocks include ASP.NET Core, dependency injection, identity management, and more. The package focuses on being “lightweight” and a drop-in dependency for your .NET (Core) solutions, providing a mechanism to support data isolation, tenancy resolution, and tenant-specific behaviors. How does the library do all that?
FinBuckle.Multitenant
has three components users should understand before starting: Tenants, Strategies, and Stores.
A Tenant is a logical concept specifying a boundary for a set of users. Within a tenant, you may have unique data storage, identity management, or any other aspect of the application. If your application is an apartment complex, each tenant would be an apartment.
Strategies help your application determine which tenant is currently in context. The library provides multiple strategies, including a URL base path strategy, a claim strategy, a session strategy, a TLD host strategy, a header strategy, and many more. Additionally, strategies can be combined to create a combination unique to your use case. You may also create custom strategies depending on your application’s unique scenario. Staying with the analogy of an apartment complex, a strategy for determining your apartment might be a key, facial recognition, NFC taps, or a friendly doorman recognizing you.
The final essential element of the library is a Store. Stores provide a record of all potential tenants that exist within your overall application. These stores are a data storage mechanism backed by a database, in-memory collections, HTTP endpoints, configuration files, or a distributed cache. Which works best depends on your particular use case and the number of potential tenants. In the final analogy, a store is the building’s rental office, which has the contracts for each apartment.
All three parts are integral to how FinBuckle.MultiTenant
works, but let’s see it used in an ASP.NET Core sample.
Getting Started with Multi-tenancy
Starting with an ASP.NET Core web application, we’ll first need to install the FinBuckle.Multitenant.AspNetCore
package.
dotnet add package FinBuckle.MultiTenant.AspNetCore
Once installed, we’ll need to configure all elements described in the previous section: Tenants, Strategies, and Stores. Let’s take a look at our Program.cs
file and how we hook the library into the ASP.NET Core infrastructure of the application.
using Finbuckle.MultiTenant;
using Multitenants;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddMultiTenant<TenantInfo>()
.WithRouteStrategy("tenant")
.WithDelegateStrategy(Tenants.QueryStringStrategy)
.WithInMemoryStore(Tenants.Register);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseMultiTenant();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
The library has both a services registration and a middleware component. Here you can see us adding the multi-tenancy with the TenantInfo
class being our tenant definition. You can implement your own ITenantInfo
instances, but the library provides a simple TenantInfo
type definition for an easy way to get started.
You may have also noticed our two strategies of RouteStrategy
and DelegateStrategy
. The RouteStrategy
is an included strategy that uses the endpoint’s route values to determine the tenant. In this sample, the route value’s key is “tenant”. We’ll see later how the DelegateStrategy
is implemented in our Tenants
static class, but it’s a custom method that takes an HttpContext
instance.
Finally, we are using an InMemoryStore
for this demo, with all the tenants hard-coded into our application. Let’s see what these references lead to.
using System.Diagnostics.CodeAnalysis;
using Finbuckle.MultiTenant;
using Finbuckle.MultiTenant.Stores;
namespace Multitenants;
public static class Tenants
{
public static readonly TenantInfo Default = new()
{
Id = 1.ToString(),
Name = "Default",
Identifier = "default"
};
public static readonly TenantInfo Other = new()
{
Id = 2.ToString(),
Name = "Other",
Identifier = "other"
};
private static readonly List<TenantInfo> All = new()
{
Default,
Other
};
public static void Register(InMemoryStoreOptions<TenantInfo> options)
{
options.Tenants.Add(Default);
options.Tenants.Add(Other);
}
public static Task<string?> QueryStringStrategy(object state)
{
if (state is not HttpContext httpContext)
return Task.FromResult<string?>(null);
var tenantContext = httpContext.GetMultiTenantContext<TenantInfo>();
//Someone already set the tenant. Likely another strategy
if (tenantContext is not null && tenantContext.HasResolvedTenant)
return Task.FromResult(tenantContext.TenantInfo!.Identifier);
var tenant = httpContext.Request.Query.TryGetValue("tenant", out var values)
&& TryGetTenant(values.ToString(), out var info)
? info
: Default;
return Task.FromResult(tenant.Identifier);
}
private static bool TryGetTenant(string identifier, [NotNullWhen(true)] out TenantInfo? tenant)
{
tenant = All.FirstOrDefault(x => x.Identifier == identifier);
return tenant is not null;
}
}
The most complex part of the Tenants
implementation is our QueryStringStrategy
, which provides a default
tenant fallback when an ASP.NET Core request does not specify the tenant.
Cool! Now that it’s all setup, where do we use the tenant information?
Well, an instance of TenantInfo
should always be in the services collection of your .NET application. That means you can ask your application to resolve the TenantInfo
as a dependency of any of your .NET services. This includes database classes, services, razor views, and more. In this case, we’ll inject our TenantInfo
into a Razor View.
@page "{tenant?}"
@model IndexModel
@inject Finbuckle.MultiTenant.TenantInfo Tenant
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>
Learn about
<a href="https://learn.microsoft.com/aspnet/core">
building Web apps with ASP.NET Core
</a>.
</p>
You are currently on the "@Tenant.Name" tenant.
</div>
Note that the route of this Razor page has a tenant
route value, matching our RouteStrategy
from before. The value is also optional, allowing our custom QueryStringStategy
to set the default tenant.
Running the page, you can now experiment with going to /
, /other
, and /?tenant=other
, all of which should switch between the hard-coded tenants. The value used is the Identifier
on your TenantInfo
instances, so be sure to use the Id here appropriately.
And that’s it! Wow, how easy was that? Adding multi-tenancy to a .NET application has never been easier.
Conclusion
FinBuckle.Multitenant
is a refreshingly complete solution built for the modern sensibilities of the newest .NET programming model. It has well-thought solutions for what becomes a quickly complex problem. The authors at FinBuckle have done a great job thinking about the different aspects of an application that might need tenancy information and providing mechanisms to retrieve the tenant in most conceivable situations. Whether you’re working with ASP.NET Core, distributed services, or authentication, you can retrieve the tenant information when and where you need it.
If you use FinBuckle.Multitenant
or are thinking about using it, be sure to go to FinBuckle’s GitHub sponsors page and show your support. Just a few dollars can make a difference in making projects like these sustainable.