Changing Identity Table ID Types in ASP.NET

Tags: dotnet

I recently started a new personal project using MudBlazor in .NET 8. When setting up Identity I wanted it to match what I’m used to working with at work, which means making two changes:

  1. Change the ID types for all the Identity tables from nvarchar(450) to uniqueidentifier, or the Guid type in C#.
  2. Change the names of the Identity tables to match the naming convention that I’m used to, which means:

In the last week I’ve spent way too much time trying to figure out how to do this, so I wanted to document it somewhere that I can come back to later and hopefully help someone else out. One caveat is that I’m doing database-first with Entity Framework Core, so this might not be the best way to do it if you’re using code-first.

First, create a new ASP.NET project without authentication. I’m using MudBlazor, so I used the MudBlazor template, but this should work on any type of ASP.NET project. I’m also on .NET 8, so some of this might not work on older versions.

Add the Microsoft.AspNetCore.Identity.EntityFrameworkCore package to your project.

Add your connection string to appsettings.json. Here’s what the file should look like:

{
"ConnectionStrings": {
"DefaultConnection": "Server=SERVERNAME;Database=DATABASENAME;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

In your database, create the Identity tables in the format that you want to use. I modified the default Identity tables scripts to match my changes. There’s an example of one of the tables below, and you can find the rest of the script in this GitHub Gist.

CREATE TABLE [dbo].[Role] (
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](256) NULL,
[NormalizedName] [nvarchar](256) NULL,
[ConcurrencyStamp] [nvarchar](max) NULL,
CONSTRAINT [PK_Role] PRIMARY KEY CLUSTERED ([Id] ASC) WITH (
PAD_INDEX = OFF,
STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON,
OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF
) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
 
ALTER TABLE [dbo].[Role] ADD CONSTRAINT [DF_Role_Id] DEFAULT(newid())
FOR [Id]
GO

Next, add your custom Identity classes. This isn’t necessarily required, but it will be if you ever want to add custom columns to your Identity tables. I only created two classes: User and Role. I did it in one IdentityClasses.cs file:

public class User : IdentityUser<Guid> { }
public class Role : IdentityRole<Guid> { }

Our next step is to setup the database first scaffolding. I’m using the EF Core Power Tools Reverse Engineering tool. Before you do this, make sure you have at least one table in your database that is not part of Identity.

Right click your project and click on EF Core Power Tools -> Reverse Engineer. This will open a new window where you can select your database and tables. Make sure all the Identity tables are unchecked, and all other tables are checked.

On the next window I made a couple changes, but you can just click OK too.

  1. Change EntityTypes path to Data/Models
  2. Check “Customize code using templates”

Click OK and let it generate the code. You should see some new files in your project. These will correspond to the table(s) that you selected in the previous window, and there should not be any files for the Identity tables. Your new DbContext shouldn’t have any references to the Identity tables either. However, there will be some issues if you are using custom tables names like I am. To fix these, you’ll need to make some changes to your DbContext class.

First, change the class so that it’s inheriting from IdentityDbContext and using your custom Identity classes, as well as the Guid type parameter.:

public partial class DbContext : IdentityDbContext<User, Role, Guid>
{...}

Next, in the OnModelCreating method, map your Identity objects to the proper tables. In the code below, notice that I am mapping the User and Role objects to the User and Role tables, respectively, while using the default Identity objects for the rest of the tables. These default objects should use <Guid> as the type parameter.

// Rename Identity tables to remove AspNet prefix and make singular
modelBuilder.Entity<User>(entity =>
{
entity.ToTable(name: "User");
});
modelBuilder.Entity<Role>(entity =>
{
entity.ToTable(name: "Role");
});
modelBuilder.Entity<IdentityUserRole<Guid>>(entity =>
{
entity.ToTable("UserRole");
});
modelBuilder.Entity<IdentityUserClaim<Guid>>(entity =>
{
entity.ToTable("UserClaim");
});
modelBuilder.Entity<IdentityUserLogin<Guid>>(entity =>
{
entity.ToTable("UserLogin");
});
modelBuilder.Entity<IdentityRoleClaim<Guid>>(entity =>
{
entity.ToTable("RoleClaim");
});
modelBuilder.Entity<IdentityUserToken<Guid>>(entity =>
{
entity.ToTable("UserToken");
});

We can do one better here though. Rather than having to manually make these changes to our context every time we regenerate the code, we can use the t4 files that were created by the Reverse Engineer tool. I manually added the above code to the generator, but you could also make it more generic if you wanted. Here’s a gist of my current DbContext.t4 file. Note that I added some debug lines to the top - feel free to remove the following lines if you don’t want them:

<#@ template debug="true" #>
<#
System.Diagnostics.Debugger.Launch();
#>

Now you can right click the efpt.config.json file that was created by the Reverse Engineer tool and click “EF Core Power Tools - Refresh”. This will regenerate the code, and you should see the changes that you made to the t4 file in your DbContext class.

Last but not least, we need to update Program.cs to include Identity in our project. You’ll need to add some or all of the following lines - I used a Program.cs from a different project as an example, so you might not need all of this:

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
 
builder.Services.AddIdentity<User, Role>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<DbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();

builder.Services.AddIdentity<User, Role> is the most important bit here - we need to tell our project to use our custom User and Role classes.

Alright, we should have all the code changes needed to make our Identity tables work properly. Now we can scaffold our Identity files into our project using the Identity scaffolding tool. I did this with the CLI, but you can use the docs to do it in Visual Studio if you prefer.

dotnet aspnet-codegenerator identity -dc DbContext

Theoretically all of the scaffolded files should be using the User class that we created earlier. Try running your project and navigating to /Account/Register to see if it works. If it does, congratulations! If you have any issues… well, good luck. The documentation around this entire process is pretty sparse, and I had to do a lot of trial and error to get it to work. I hope this helps someone else out there!

Feel free to contact me on Mastodon if you have any questions or need help with this process.