Multitenancy the easy way

Multitenancy, if you got this far it is time to cash out, move into management or retire. It seriously isn't so bad, Azure has some design patterns you can follow lol.

We'll be focusing on a standalone single-tenant app with single-tenant database, using ASP.NET Identity without rewriting Identity or messing around with Entity Framework.

The Problem

ASP.NET Identity is hardcoded with values that make multitenancy hard but not impossible.

The Solution

Extend the User class with our tenant property, create a new index for uniqueness, implement our own logic for creating a user.

The How

Extending ApplicationUser

The first step is modifying the GenerateUserIdentityAsync method to resemble the code below.

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
    {
        var temp = this as IdentityUser;
        temp.UserName = this.UserName;
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }

Adding these properties with the attributes.

    [Required]
    [Index("IX_UserUniqueness", 1, IsUnique = true)]
    [MaxLength(256)]
    public string Tenant { get; set; }

    [Required]
    [Index("IX_UserUniqueness", 2, IsUnique = true)]
    [MaxLength(256)]
    public new string UserName { get; set; }

Entity Framework

Enable migrations by running this in the Package Manager Console.
enable-migrations

Create your first migration by running this in the Package Manager Console.
add-migration Initial

Your migration will contain code like below, comment out the Index line with just UserName ASP.NET Identity creates this by default.

        CreateTable(
            "dbo.AspNetUsers",
            c => new
            {
                Id = c.String(nullable: false, maxLength: 128),
                Tenant = c.String(nullable: false, maxLength: 256),
                UserName = c.String(nullable: false, maxLength: 256),
                Email = c.String(maxLength: 256),
                EmailConfirmed = c.Boolean(nullable: false),
                PasswordHash = c.String(),
                SecurityStamp = c.String(),
                PhoneNumber = c.String(),
                PhoneNumberConfirmed = c.Boolean(nullable: false),
                TwoFactorEnabled = c.Boolean(nullable: false),
                LockoutEndDateUtc = c.DateTime(),
                LockoutEnabled = c.Boolean(nullable: false),
                AccessFailedCount = c.Int(nullable: false),
            })
            .PrimaryKey(t => t.Id)
            .Index(t => new { t.Tenant, t.UserName }, unique: true, name: "IX_UserUniqueness");
            //.Index(t => t.UserName, unique: true, name: "UserNameIndex");

Create your database by running this in the Package Manager Console.
update-database


AccountController

You're going to have implement your own logic for creating and logging in users with their tenant property. An example is below.

Register

public async Task<ActionResult> Register(RegisterViewModel model)
    {
        if (ModelState.IsValid)
        {
            var hashedPassword = UserManager.PasswordHasher.HashPassword(model.Password);
            var user = new ApplicationUser()
            {
                UserName = model.Email,
                Email = model.Email,
                Tenant = model.Tenant,
                SecurityStamp = Guid.NewGuid().ToString(),
                PasswordHash = hashedPassword
            };
            var result = false;
            using (var db = new ApplicationDbContext())
            {
                db.Users.Add(user);

                var addUserResult = await db.SaveChangesAsync();

                if (addUserResult > 0)
                {
                    result = true;
                }
            }

            if (result)
            {
                await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);

                return RedirectToAction("Index", "Home");
            }

        }

        return View(model);
    }

Login

public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }


        using (var db = new ApplicationDbContext())
        {
            var user = await db.Users.SingleOrDefaultAsync(x => x.Email == model.Email && x.Tenant == model.Tenant);
            if(user == null)
            {
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);
            }
            if(!(await(UserManager.CheckPasswordAsync(user,model.Password))))
            {
                ModelState.AddModelError("", "Invalid login attempt.");
                return View(model);
            }
            await SignInManager.SignInAsync(user, model.RememberMe, true);               
            return RedirectToLocal(returnUrl);
        }

    }

Real World Use

Going forward in the lifecycle of your app, once your user has logged in. Their user id should be used instead of their email address where applicable.
An example project is on GitHub.