ASP.NET Identity performance tips

At load tests of web application using ASP.NET Identity 2.2.1 I turned on Microsoft SQL Server Query Store to monitor performance of T-SQL queries. Analyzing the results I suggest following tips to improve performance of ASP.NET Identity:

1. Disable checks of schema version

As for exec count, the following query was on the first place:

SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS where TABLE_NAME=@Table

After some search in source code of ASP.NET Identity I found internal IsIdentityV1Schema method in IdentityDbContext class. This method runs mentioned query to check version of schema, whether DB contains ASP.NET Identity 1.0 schema. To avoid this DB roundtrips you have to set parameter throwIfV1Schema to false in constructor of IdentityDbContext class as shown in code:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
    }
}

The interesting on this issue is fact, that this is set when you create web application project from default template in Visual Studio 2015, but I wrote this code manually, so I didn’t consider the importance of this parameter.

2. Avoid usage of T-SQL UPPER function

When selecting user by username the following query is executed:

SELECT TOP (1)
    [Extent1].[Id] AS [Id],
    [Extent1].[Email] AS [Email],
    [Extent1].[EmailConfirmed] AS [EmailConfirmed],
    [Extent1].[PasswordHash] AS [PasswordHash],
    [Extent1].[SecurityStamp] AS [SecurityStamp],
    [Extent1].[PhoneNumber] AS [PhoneNumber],
    [Extent1].[PhoneNumberConfirmed] AS [PhoneNumberConfirmed],
    [Extent1].[TwoFactorEnabled] AS [TwoFactorEnabled],
    [Extent1].[LockoutEndDateUtc] AS [LockoutEndDateUtc],
    [Extent1].[LockoutEnabled] AS [LockoutEnabled],
    [Extent1].[AccessFailedCount] AS [AccessFailedCount],
    [Extent1].[UserName] AS [UserName]
    FROM [dbo].[AspNetUsers] AS [Extent1]
    WHERE ((UPPER([Extent1].[UserName])) = (UPPER(@p__linq__0))) OR ((UPPER([Extent1].[UserName]) IS NULL) AND (UPPER(@p__linq__0) IS NULL))

Query contains UPPER function which disables usage of index created on UserName column. Execution plan for query is:

Query execution plan

Studying default implementation of UserStore class I found that method FindByNameAsync contains LINQ predicate with ToUpper method. This default behaviour can be overriden creating custom UserStore implementation:

public class ApplicationUserStore : UserStore<ApplicationUser>
{
    public ApplicationUserStore(ApplicationDbContext context)
        : base(context)
    {
    }

    public override Task<ApplicationUser> FindByEmailAsync(string email)
    {
        return GetUserAggregateAsync(u => u.Email == email);
    }

    public override Task<ApplicationUser> FindByNameAsync(string userName)
    {
        return GetUserAggregateAsync(u => u.UserName == userName);
    }
}

Than at creating UserManager only use custom UserStore implementation:

public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(IUserStore<ApplicationUser> store)
        : base(store)
    {
    }

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) 
    {
        var manager = new ApplicationUserManager(new ApplicationUserStore(context.Get<ApplicationDbContext>()));
        // ...
    }
}

After this customization UPPER function is no longer present in query:

SELECT TOP (1)
    [Extent1].[Id] AS [Id],
    [Extent1].[Email] AS [Email],
    [Extent1].[EmailConfirmed] AS [EmailConfirmed],
    [Extent1].[PasswordHash] AS [PasswordHash],
    [Extent1].[SecurityStamp] AS [SecurityStamp],
    [Extent1].[PhoneNumber] AS [PhoneNumber],
    [Extent1].[PhoneNumberConfirmed] AS [PhoneNumberConfirmed],
    [Extent1].[TwoFactorEnabled] AS [TwoFactorEnabled],
    [Extent1].[LockoutEndDateUtc] AS [LockoutEndDateUtc],
    [Extent1].[LockoutEnabled] AS [LockoutEnabled],
    [Extent1].[AccessFailedCount] AS [AccessFailedCount],
    [Extent1].[UserName] AS [UserName]
    FROM [dbo].[AspNetUsers] AS [Extent1]
    WHERE [Extent1].[UserName] = @p__linq__0

Execution plan for query is now:

Query execution plan

This solution assumes that DB uses case insensitive collation.

3. Index Email column

In my web application there was also many scenarios where users were queried by email. To support this scenario I add index for Email column. In Package Manager Console I run PowerShell commands to enable migrations and add new migration AspNetUsersEmailIndex:

Enable-Migrations
Add-Migration AspNetUsersEmailIndex

In generated skeleton of AspNetUsersEmailIndex migration class was added code to create index for Email column:

public partial class AspNetUsersEmailIndex : DbMigration
{
    public override void Up()
    {
        CreateIndex("dbo.AspNetUsers", "Email");
    }

    public override void Down()
    {
        DropIndex("dbo.AspNetUsers", new[] { "Email" });
    }
}

Running update database command I got T-SQL of migration:

Update-Database -TargetMigration AspNetUsersEmailIndex -Script

Generated T-SQL script contains CREATE INDEX command:

CREATE INDEX [IX_Email] ON [dbo].[AspNetUsers]([Email])