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:
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:
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])