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.
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
.
Top comments (11)
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);
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 theHttpContext.Items
dictionary. Updating this to .NET Core 3 is on my list of things to do.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."
I have not, however, I'll be updating it to Core 3.1 soon and writing the next part.
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
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.
Looking forward to the next part of the article.
Thanks for this starter!
Hey Andrew, really helpful post!
Any suggestion about how to get the tenant if I use something like
domain.com/tenant1
domain.com/tenant2
Buenas, cuando saldra
Hello, when will the second part come out, and when will I migrate to use 3.1
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.
Great article! Looking forward to your next article!