loading...

Multi-Tenant Applications with ASP.NET Core 2.2 - Getting Started

agrothe profile image Andrew Grothe ・3 min read

As software as a service has been more ubiquitous, multi-tenancy has become a basic requirement of most web applications. This series will outline how I accomplish this in a .NET Core Web Application.

While this post is mostly a self-documentation exercise so I don't forget this next time, hopefully it helps someone else with similar needs. I'll be focusing on the latest stable version of .NET Core, currently 2.2 and hopefully updating for .NET Core 3 once it moves to GA.

I've started off with Visual Studio 2019 and used the ASP.NET Core Web Application template configured with Razor Pages and Individual User Accounts. While I like what ASP.NET Identity brings in terms of functionality, I don't like that they still use GUID for the primary keys.

First things first, I change the IdentityUser to use int or long as a primary key. You can follow this blog entry on extending the IdentityUser but add in your preferred data type.

So instead of

public class ApplicationUser : IdentityUser
{
    ...
}

use this

public class ApplicationUser : IdentityUser<long>
{
    ...
}

And everywhere you override IdentityUser insert IdentityUser<long> instead.

Now, while you can use any method you prefer to identity tenants in your application, I prefer to use sub-domains. The first thing we will do is inject the current sub-domain into our HttpContext.

Create a new class using Add => New Item by right clicking in the solution explorer. Find the Middleware Class entry and name it TenantInjector or whatever you prefer.

Alt Text

This gives us a basic Middleware class we can extend. Notice the Invoke method:

public Task Invoke(HttpContext httpContext)
{
    return _next(httpContext);
}

We will do two things here. First, make it async. Second, add some logic to make the current sub-domain available as CurrentTenant property in the HttpContext.

public async Task Invoke(HttpContext httpContext)
{
    var tenant = string.Empty;

    if (httpContext.Request.Host.Host.Contains("."))
    {
        tenant = httpContext.Request.Host.Host.Split('.')[0].ToLowerInvariant();
        httpContext.Items.Add("CURRENT_TENANT", tenant);
    }
    else
    {
        httpContext.Items.Add("CURRENT_TENANT", httpContext.Request.Host.Host);
    }

    await _next(httpContext);
}

Notice how we use async Task instead of just Task and replaced return with await.

In the rest of our logic, we extract the sub-domain if it exists or just use the TLD if no sub-domain is present.

So:

demo.domain.com    => tenant of "demo".
domain.com         => tenant of "domain".
localhost:4433     => tenant of "localhost".

To make development easier I add some entries to my HOST file:

127.0.0.1   demo.domain.com
127.0.0.1   demo2.domain.com

Now we can go over to our Startup.cs and add the following line to the Configure method:

app.UseTenantInjector();

So now we can access the current tenant in any of our controllers by using HttpContext.Items["CURRENT_TENANT"].ToString().

I do like dependency injection myself, so let's go ahead and refactor this to use a service.

Lets add a new interface ITenantResolver and class TenantResolver. They should look something like this:

public interface ITenantResolver
{
    string CurrentTenant();
}

public class TenantResolver : ITenantResolver
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantResolver(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    private string Current_Tenant { get; set; }

    public void SetTenant()
    {
        _httpContextAccessor.HttpContext.Items.TryGetValue(Const.CURRENT_TENANT, out object currentTenant);
        if (currentTenant != null)
        {
            Current_Tenant = currentTenant.ToString();
        }
    }

    public string CurrentTenant()
    {
        if (string.IsNullOrEmpty(Current_Tenant))
        {
            SetTenant();
        }
        return Current_Tenant;
    }
}

Wire it up to the DI Container:

services.AddScoped<ITenantResolver, TenantResolver>();

Now we can simply inject our TenantResolver wherever we need it.

@inject  ITenantResolver tenantResolver
<p>Current Tenant = @tenantResolver.CurrentTenant()</p>

In the next post we will modify our Login method to ensure the current user has access to the current domain by using Claims.

Discussion

pic
Editor guide
Collapse
garethdebruyn profile image
garethdebruyn

Thanks Andrew, this has really helped. Im trying to implement this in .net core 3.1.2
I get an error that says Const does not exist in the current context. What am I missing?

_httpContextAccessor.HttpContext.Items.TryGetValue(Const.CURRENT_TENANT, out object currentTenant);

Collapse
agrothe profile image
Andrew Grothe Author

Hi Gareth, Const is a class I used to keep my constants. Just replace Const.CURRENT_TENANT with "Current_Tenant" or any other string you which to use as a key in the HttpContext.Items dictionary. Updating this to .NET Core 3 is on my list of things to do.

Collapse
galdin profile image
Galdin Raphael

Short and sweet indeed. Thanks for sharing. I find the mention of the hosts file interesting, I've always stayed away from it but I guess I'm ready to give it a shot now :D

Collapse
agrothe profile image
Andrew Grothe Author

I like using the hosts file for development. Especially when building new WordPress sites. I can point a domain at the new site even before DNS propagates and get started on development right away.

Collapse
tmakaro profile image
Troy Makaro

Have you done the next post yet?

"In the next post we will modify our Login method to ensure the current user has access to the current domain by using Claims."

Collapse
agrothe profile image
Andrew Grothe Author

I have not, however, I'll be updating it to Core 3.1 soon and writing the next part.

Collapse
mokenyon profile image
Morgan Kenyon

Great article! Looking forward to your next article!

Collapse
scara1701 profile image
Gwen Demulder

Looking forward to the next part of the article.
Thanks for this starter!

Collapse
rodrigojuarez profile image
Rodrigo Juarez

Hey Andrew, really helpful post!

Any suggestion about how to get the tenant if I use something like

domain.com/tenant1
domain.com/tenant2

Collapse
nstubbe profile image
Niels Stubbe

Excellent article, thank you for sharing.

Collapse
bbaldallaque profile image
Bryant Baldallaque

Buenas, cuando saldra
Hello, when will the second part come out, and when will I migrate to use 3.1

Collapse
agrothe profile image
Andrew Grothe Author

Updated the original to include the DI part mentioned at the end. It was simple enough it didn't warrant a post of its own.