A high-performance, Dapper-based implementation of ASP.NET Core Identity stores for SQL Server. This package provides a lightweight alternative to Entity Framework-based Identity stores.
- Full implementation of ASP.NET Core Identity stores using Dapper
- Support for users, roles, claims, logins, and tokens
- Customizable table names
- Compatible with existing ASP.NET Identity database schemas
- Significantly faster than Entity Framework for most operations
dotnet add package DapperIdentityRun the SQL script included in the package to create the required tables:
-- See Scripts/CreateTables.sql for the full scriptOr if you already have an existing ASP.NET Identity database (created by EF), this package is compatible with those tables.
In your Program.cs or Startup.cs:
using DapperIdentity.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add Dapper Identity with default options
builder.Services.AddDapperIdentity(
builder.Configuration.GetConnectionString("DefaultConnection")!
);
// Or with custom table names
builder.Services.AddDapperIdentity(
builder.Configuration.GetConnectionString("DefaultConnection")!,
options =>
{
options.UsersTableName = "MyUsers";
options.RolesTableName = "MyRoles";
options.UserClaimsTableName = "MyUserClaims";
options.UserRolesTableName = "MyUserRoles";
options.UserLoginsTableName = "MyUserLogins";
options.UserTokensTableName = "MyUserTokens";
options.RoleClaimsTableName = "MyRoleClaims";
}
);If you need more control over Identity configuration:
using DapperIdentity.Extensions;
using DapperIdentity.Models;
builder.Services.AddIdentity<DapperIdentityUser, DapperIdentityRole>(options =>
{
// Configure Identity options
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddDapperStores(builder.Configuration.GetConnectionString("DefaultConnection")!)
.AddDefaultTokenProviders();| Option | Default | Description |
|---|---|---|
UsersTableName |
AspNetUsers |
Name of the users table |
RolesTableName |
AspNetRoles |
Name of the roles table |
UserClaimsTableName |
AspNetUserClaims |
Name of the user claims table |
UserRolesTableName |
AspNetUserRoles |
Name of the user roles junction table |
UserLoginsTableName |
AspNetUserLogins |
Name of the user logins table |
UserTokensTableName |
AspNetUserTokens |
Name of the user tokens table |
RoleClaimsTableName |
AspNetRoleClaims |
Name of the role claims table |
IUserStore<DapperIdentityUser>IUserEmailStore<DapperIdentityUser>IUserPasswordStore<DapperIdentityUser>IUserPhoneNumberStore<DapperIdentityUser>IUserTwoFactorStore<DapperIdentityUser>IUserSecurityStampStore<DapperIdentityUser>IUserClaimStore<DapperIdentityUser>IUserLoginStore<DapperIdentityUser>IUserRoleStore<DapperIdentityUser>IUserLockoutStore<DapperIdentityUser>IUserAuthenticationTokenStore<DapperIdentityUser>IQueryableUserStore<DapperIdentityUser>
IRoleStore<DapperIdentityRole>IRoleClaimStore<DapperIdentityRole>IQueryableRoleStore<DapperIdentityRole>
public class AccountController : Controller
{
private readonly UserManager<DapperIdentityUser> _userManager;
private readonly SignInManager<DapperIdentityUser> _signInManager;
public AccountController(
UserManager<DapperIdentityUser> userManager,
SignInManager<DapperIdentityUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpPost]
public async Task<IActionResult> Register(RegisterModel model)
{
var user = new DapperIdentityUser
{
UserName = model.Email,
Email = model.Email
};
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return View(model);
}
}// Create a role
var role = new DapperIdentityRole { Name = "Admin" };
await _roleManager.CreateAsync(role);
// Add user to role
await _userManager.AddToRoleAsync(user, "Admin");
// Check if user is in role
var isAdmin = await _userManager.IsInRoleAsync(user, "Admin");await _userManager.AddClaimAsync(user, new Claim("Department", "Engineering"));
var claims = await _userManager.GetClaimsAsync(user);DapperIdentity is designed for high performance. Below are benchmark results comparing DapperIdentity against Entity Framework Core Identity stores.
BenchmarkDotNet v0.14.0
.NET 8.0.22, X64 RyuJIT AVX2
| Method | Mean | Allocated |
|---|---|---|
| Dapper: Create User | 418.7 us | 10.09 KB |
| EF: Create User | 15,442.2 us | 117.50 KB |
| Dapper: Find User By Id | 312.4 us | 4.82 KB |
| EF: Find User By Id | 1,245.8 us | 22.45 KB |
| Dapper: Find User By Name | 298.6 us | 4.65 KB |
| EF: Find User By Name | 1,189.3 us | 21.87 KB |
| Dapper: Find User By Email | 305.2 us | 4.72 KB |
| EF: Find User By Email | 1,198.7 us | 21.95 KB |
| Dapper: Create Role | 287.5 us | 3.24 KB |
| EF: Create Role | 8,456.3 us | 65.32 KB |
| Dapper: Find Role By Id | 245.8 us | 2.85 KB |
| EF: Find Role By Id | 856.4 us | 14.23 KB |
| Dapper: Update User | 625.4 us | 9.87 KB |
| EF: Update User | 2,847.6 us | 45.67 KB |
| Operation | Dapper | EF Core | Improvement |
|---|---|---|---|
| Create User | 418.7 us | 15.4 ms | ~37x faster |
| Find User By Id | 312.4 us | 1.25 ms | ~4x faster |
| Find User By Name | 298.6 us | 1.19 ms | ~4x faster |
| Create Role | 287.5 us | 8.46 ms | ~29x faster |
| Update User | 625.4 us | 2.85 ms | ~4.5x faster |
Note: EF Core performance degrades over time due to change tracking overhead, which is especially visible in write operations. Dapper maintains consistent performance.
To run the benchmarks yourself:
cd DapperIdentity.Benchmarks
dotnet run -c ReleaseThe package uses the standard ASP.NET Identity schema. If you're migrating from EF Identity, no schema changes are required.
-- Users table
CREATE TABLE AspNetUsers (
Id NVARCHAR(450) PRIMARY KEY,
UserName NVARCHAR(256),
NormalizedUserName NVARCHAR(256),
Email NVARCHAR(256),
NormalizedEmail NVARCHAR(256),
EmailConfirmed BIT NOT NULL,
PasswordHash NVARCHAR(MAX),
SecurityStamp NVARCHAR(MAX),
ConcurrencyStamp NVARCHAR(MAX),
PhoneNumber NVARCHAR(MAX),
PhoneNumberConfirmed BIT NOT NULL,
TwoFactorEnabled BIT NOT NULL,
LockoutEnd DATETIMEOFFSET,
LockoutEnabled BIT NOT NULL,
AccessFailedCount INT NOT NULL
);
-- See Scripts/CreateTables.sql for complete schemaDapperIdentity allows you to add custom fields to your user model by inheriting from DapperIdentityUser and creating a custom user store.
using DapperIdentity.Models;
public class ApplicationUser : DapperIdentityUser
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateTime? DateOfBirth { get; set; }
public string? ProfilePictureUrl { get; set; }
}Add the new columns to your AspNetUsers table:
ALTER TABLE AspNetUsers ADD
FirstName NVARCHAR(100) NULL,
LastName NVARCHAR(100) NULL,
DateOfBirth DATE NULL,
ProfilePictureUrl NVARCHAR(500) NULL;Inherit from DapperUserStore and override the methods that need to handle your custom fields:
using System.Data;
using Dapper;
using DapperIdentity.Stores;
using Microsoft.AspNetCore.Identity;
public class ApplicationUserStore : DapperUserStore<ApplicationUser>
{
public ApplicationUserStore(IDbConnection connection, DapperIdentityOptions options)
: base(connection, options)
{
}
public override async Task<IdentityResult> CreateAsync(
ApplicationUser user,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var sql = $@"
INSERT INTO {Options.UsersTableName}
(Id, UserName, NormalizedUserName, Email, NormalizedEmail,
EmailConfirmed, PasswordHash, SecurityStamp, ConcurrencyStamp,
PhoneNumber, PhoneNumberConfirmed, TwoFactorEnabled,
LockoutEnd, LockoutEnabled, AccessFailedCount,
FirstName, LastName, DateOfBirth, ProfilePictureUrl)
VALUES
(@Id, @UserName, @NormalizedUserName, @Email, @NormalizedEmail,
@EmailConfirmed, @PasswordHash, @SecurityStamp, @ConcurrencyStamp,
@PhoneNumber, @PhoneNumberConfirmed, @TwoFactorEnabled,
@LockoutEnd, @LockoutEnabled, @AccessFailedCount,
@FirstName, @LastName, @DateOfBirth, @ProfilePictureUrl)";
await Connection.ExecuteAsync(sql, user);
return IdentityResult.Success;
}
public override async Task<IdentityResult> UpdateAsync(
ApplicationUser user,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var sql = $@"
UPDATE {Options.UsersTableName} SET
UserName = @UserName,
NormalizedUserName = @NormalizedUserName,
Email = @Email,
NormalizedEmail = @NormalizedEmail,
EmailConfirmed = @EmailConfirmed,
PasswordHash = @PasswordHash,
SecurityStamp = @SecurityStamp,
ConcurrencyStamp = @ConcurrencyStamp,
PhoneNumber = @PhoneNumber,
PhoneNumberConfirmed = @PhoneNumberConfirmed,
TwoFactorEnabled = @TwoFactorEnabled,
LockoutEnd = @LockoutEnd,
LockoutEnabled = @LockoutEnabled,
AccessFailedCount = @AccessFailedCount,
FirstName = @FirstName,
LastName = @LastName,
DateOfBirth = @DateOfBirth,
ProfilePictureUrl = @ProfilePictureUrl
WHERE Id = @Id";
await Connection.ExecuteAsync(sql, user);
return IdentityResult.Success;
}
}If you want a cleaner inheritance pattern, the package provides a generic base class. You'll need to ensure the DapperUserStore<TUser> class exists:
// This may already be in the package - check DapperIdentity.Stores namespace
public class DapperUserStore<TUser> : DapperUserStore
where TUser : DapperIdentityUser, new()
{
public DapperUserStore(IDbConnection connection, DapperIdentityOptions options)
: base(connection, options)
{
}
// Override methods to use TUser instead of DapperIdentityUser
}using DapperIdentity.Extensions;
using Microsoft.Data.SqlClient;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")!;
// Register the database connection
builder.Services.AddScoped<IDbConnection>(_ =>
new SqlConnection(connectionString));
// Configure DapperIdentity options
builder.Services.Configure<DapperIdentityOptions>(options =>
{
// Optionally customize table names
});
// Register Identity with your custom user type
builder.Services.AddIdentity<ApplicationUser, DapperIdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
})
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<DapperRoleStore>()
.AddDefaultTokenProviders();If you only need a few extra fields and don't want to modify the schema, consider using claims:
// Adding custom data as claims
await _userManager.AddClaimAsync(user, new Claim("FirstName", "John"));
await _userManager.AddClaimAsync(user, new Claim("LastName", "Doe"));
await _userManager.AddClaimAsync(user, new Claim("Department", "Engineering"));
// Retrieving custom data
var claims = await _userManager.GetClaimsAsync(user);
var firstName = claims.FirstOrDefault(c => c.Type == "FirstName")?.Value;This approach:
- Requires no schema changes
- Works with the default
DapperIdentityUser - Is flexible and extensible
- Claims are automatically included in authentication tokens
MIT License
Contributions are welcome! Please feel free to submit a Pull Request.