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.