ASP.NET Core Identity, part 2 - extending to support multi-tenancy

08 September 2017

This article continues on from our previous article which introduced Identity, how it changes a project and how to change the primary key type. This article explores how we extend ASP.NET Core 2 Identity to support multi-tenancy. 


What is Multi-Tenancy?

Multi-tenancy (or multitenancy) is defined as "an architecture in which a single instance of a software application serves multiple customers"; for example we might have a single copy of our web application that is accessed via multiple websites, potentially showing a different branding and content in each, but all stored in the same single database.

We need to separate the users of each copy of our web application from the users of all others copies. It is also possible that a user may need to register in more than one of these sites using the same email address.

Identity requires that email addresses of users are unique, so we need to amend Identity and its database to support the concept of multiple sites, ensuring that registrant email addresses are unique for each site but can exist in the same database table multiple times. We do this by adding a TenantId field (which likely relates to a parent table defining the various different tenants used in the application).


How to extend Identity to support multi-tenancy

We are going to amend our ApplicationUser class to have a TenantId which will partition our users into those used in separate copies of our web app:

  1. Open the Models/ApplicationUser.cs file:
    1. Add a new property public int TenantId { get; set; }
      We could also define a Models/Tenant.cs class to represent a Tenant table in the database and include a relationship property for this in the ApplicationUser class, but to keep things simple in this example we will simply define the TenantId as a constant.
    2. Open the Package Manager Console in Visual Studio then run the commands:
      add-migration UserTenantId
    3. Identity creates an index on the AspNetUsers table named UserNameIndex which ensures the NormalizedUserName column is unique; this index needs to be modified to allow NormalizedUserName values to be unique across different TenantId partitions. I have tried all kinds of EntityFramework features (via the Fluent API in the OnModelCreating method of the Data/ApplicationDbContext.cs class) but cannot find a way to successfully change this index; therefore my current workaround is to edit the UserTenantId migration directly (I will modify this article if I eventually find a better way):
      1. Open the Data/Migrations/[DateAsNumber]_UserTenantId.cs file.
      2. Add the following code at the end of the Up method:
        migrationBuilder.DropIndex(
           name: "UserNameIndex",
           table: "AspNetUsers"
        );
        migrationBuilder.CreateIndex(
           name: "UserNameIndex",
           table: "AspNetUsers",
           columns: new[] { "NormalizedUserName", "TenantId" },
           unique: true,
           filter: "[NormalizedUserName] IS NOT NULL");
      3. Add the following code at the start of the Down method:
        migrationBuilder.DropIndex(
           name: "UserNameIndex",
           table: "AspNetUsers"
        );
        migrationBuilder.CreateIndex(
           name: "UserNameIndex",
           table: "AspNetUsers",
           columns: new[] { "NormalizedUserName", "TenantId" },
           unique: true,
           filter: "[NormalizedUserName] IS NOT NULL");
      4. Please note that you may need to repeat this code in any future migrations you add which apply changes to the ApplicationUser class (I would advise commenting this within the ApplicationUser class to ensure it is not overlooked in future changes).
    4. Apply the migration to the database changes by running update-database from the Package Manager Console.
  2. Open the Startup.cs file:
    1. Add the following line just after the opening curly bracket of the class:
      public const int TenantId = 1;
      This is the property we will use to partition our users (in a real project this is more likely to come from a config file or some form of Settings class).
    2. Change the AddIdentity line in the ConfigureServices method to:
      services.AddIdentity<ApplicationUser, IdentityRole<long>>()
        .AddUserManager<ApplicationUserManager<ApplicationUser>>()
         .AddRoleStore<RoleStore<IdentityRole<long>, ApplicationDbContext, long, IdentityUserRole<long>, IdentityRoleClaim<long>>>()
       .AddUserStore<ApplicationUserStore>()
        
      .AddUserValidator<ApplicationUserValidator<ApplicationUser>>()
         .AddDefaultTokenProviders();

      This is adding a custom UserManager, UserStore and UserValidator to handle checking email addresses with TenantId.
  3. Add a custom class Data/IdentityModel.cs to define our custom UserManager, UserStore and UserValidator classes (this is 105 lines of code so please download the project source code to view this). These custom classes simply override the default Identity code, adding TenantId into methods that Find and Validate Email/Name.
  4. Amend your Controllers/AccountController.cs to set the TenantId of the new AppicationUser during the [HttpPost] Register method:
    From: var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    To: var user = new ApplicationUser { UserName = model.Email, Email = model.Email, TenantId = Startup.TenantId };

Your code should now build and run. Try creating a user account, then amend the TenantId to 2 in Startup.cs, build and run again, then try registering a new user with the same email; this should work as the second user will be in a different partition of users (as defined by the different TenantID). Try creating another user with the same email address and it will fail as that email is already in use within the partition.

You can download the complete source code for this project here.