Dmytro Shved

Deep dive into DbContext in ASP.NET Core Identity

· Dmytro Shved

Do you know for sure what’s going on under the hood when you create your ApplicationDbContext using ASP.NET Core Identity in EF Core? Where do the AspNetUsers and AspNetRoles tables actually come from? What really happens when you’re inherit from IdentityDbContext<ApplicationUser>?

 1public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
 2{
 3    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
 4
 5    public DbSet<TodoList> TodoLists => Set<TodoList>();
 6
 7    public DbSet<TodoItem> TodoItems => Set<TodoItem>();
 8
 9    protected override void OnModelCreating(ModelBuilder builder)
10    {
11        base.OnModelCreating(builder);
12        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
13    }
14}

Can you tell what exactly happens within the chain of inheritance? What path does the ModelBuilder builder take? What does that simple inheritance from IdentityDbContext<ApplicationUser> actually brings to the table?

Keep that statement in mind: We’re passing the ModelBuilder builder object into this entire chain of inheritance, so EF Core builds out the entire schema. Didn’t click? Keep reading :)

IdentityDbContext<ApplicationUser>
    ↑
ApplicationDbContext

Well, let’s dive in!


ASP.NET Core Identity is a membership system that provides common features like user registration, login, role management, and claims-based authentication.

For all this to become a reality, we need a Users table and a Roles table - along with several supporting tables. So, in addition to registering DbSet properties to interact with respective tables, our ApplicationDbContext must provide us with the entire schema and their respective columns:

So, Identity should somehow create those tables for us, right? Right :)

lets look at how it’ll create them for us.


First of all, following the inheritance chain:

IdentityDbContext<ApplicationUser>
    ↑
ApplicationDbContext<IdentityUser>

Navigate to the source code of the:

IdentityDbContext<ApplicationUser> 

We’ll get into IdentityDbContext.cs. For now, just understand that IdentityDbContext class is responsible for generating the Roles tables and their respective columns for our IdentityUser.

The common misconception is that IdentityDbContext handles only roles. In reality IdentityDbContext is responsible for extending the user-centric schema with roles.

That file includes 5 versions of the IdentityDbContext class. At the top of the file you’ll see non-generic

IdentityDbContext

This is the most basic IdentityDbContext class, you’d use this version in case you don’t want to set your custom columns into the IdentityUser.

But in our example we’re using the generic version:

IdentityDbContext<TUser>

Notice that non-generic IdentityDbContext and generic IdentityDbContext<TUser> both inherit from

IdentityDbContext<TUser, TRole, TKey>

But each of them handles the types of base class differently.

The non-generic IdentityDbContext

IdentityDbContext : IdentityDbContext<IdentityUser, IdentityRole, string>

sets the default type for IdentityUser, IdentityRole and primary key type as string.

Conversely, the generic IdentityDbContext<TUser>

1IdentityDbContext<TUser> : IdentityDbContext<TUser, IdentityRole, string> 
2where TUser : IdentityUser
3// ☝️ Set the default type for TUser

sets the generic type for TUser that must be a type of IdentityUser, so we can pass our own implementation of IdentityUser as we did right at the beginning of this post:

1//                Our custom user of type IdentityUser     👇
2public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
3{
4   // ...
5}

Look carefully at the code in this file, you’ll see that the chain of inheritance looks like this:

   ... (We'll dive deeper into that chain in a second)
    ↑
IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken, TUserPasskey>
    ↑
IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
    ↑
IdentityDbContext<TUser, TRole, TKey>
    ↑ 
IdentityDbContext<TUser> & IdentityDbContext
    ↑
ApplicationDbContext (which inherits from IdentityDbContext<TUser>)

But why do we need to have all of those variations of the IdentityDbContext class? Can’t we just have non-generic one?

-> The point of having different variations of the IdentityDbContext class is the ability to customize our roles in the way we want.

Each variation of IdentityDbContext adds more specificity:

Now, let’s check the most flexible IdentityDbContext class (the top class in the chain above) and its inner logic.

If you follow the implementation of the OnModelCreating() from the initial code:

 1public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
 2{
 3    // ...
 4
 5    protected override void OnModelCreating(ModelBuilder builder)
 6    {
 7        //👇 Dive here
 8        base.OnModelCreating(builder);
 9        builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
10    }
11}

The chain looks like this:

✅ - OnModelCreating() method is present

❌ - OnModelCreating() method is not present

       ... (We'll dive deeper into that chain in a second)
        ↑
✅ IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken, TUserPasskey>
        ↑
❌ IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
        ↑
❌ IdentityDbContext<TUser, TRole, TKey> 
        ↑ 
❌ IdentityDbContext<TUser> 
        ↑
✅ ApplicationDbContext

You’ll land in the topmost class with an override - back to our most flexible IdentityDbContext:

IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken, TUserPasskey>

Notice that it has a logic for those methods:

Keep in mind that all of those methods are being overriden by this class, meaning its base class IdentityUserContext - defines the virtual versions of these methods, providing the base implementation.

But why do we need to have 3 different versions of the same OnModelCreatingVersion_()? Can’t we just use one version?

-> These three versions of OnModelCreatingVersion_() are all about versioning the Identity database schema to avoid breaking older applications during upgrades.

Imagine you created your database schema several years ago using an older version of Identity. Later, you upgrade your project to a newer version with updated DbContext types.

By keeping separate schema versions, Identity ensures that older databases continue to work correctly even after update.

By default Identity uses the latest OnModelCreatingVersion3() version inside of the IdentityDbContext<...> parent class - IdentityUserContext

Although it is technically possible to target an earlier version, using the latest one is strongly recommended for new projects. We’ll take a look at the source code of IdentityUserContext to understand how the schema version is selected in a second.

Now, lets examine that topmost IdentityDbContext<...> class.

I’ve intentionally ommitted most of the source code, as the key part is the OnModelCreating() and OnModelCreatingVersion3() logic inside of it.

 1public abstract class IdentityDbContext<TUser, TRole, TKey, TUserClaim, ...> 
 2: IdentityUserContext<TUser, TKey, TUserClaim, ...>
 3//   ☝️ Parent class
 4{
 5    //...
 6    
 7    protected override void OnModelCreating(ModelBuilder builder)
 8    {
 9        base.OnModelCreating(builder);// 👈 Delegate to the parent
10    }
11
12    internal override void OnModelCreatingVersion3(ModelBuilder builder)
13    {
14        base.OnModelCreatingVersion3(builder); // 👈 Delegate to the parent and perform its own logic below
15
16        builder.Entity<TUser>(b =>
17        {
18            // fluent API that mutates the builder object.
19        });
20
21        // ...
22    }
23}

First, notice that in OnModelCreating(ModelBuilder builder), this class simply delegates the execution to the base implementation:

1base.OnModelCreating(builder); 
2//☝️ Delegate to the parent

The same pattern appears in OnModelCreatingVersion3(ModelBuilder builder):

1 internal override void OnModelCreatingVersion3(ModelBuilder builder)
2    {
3        base.OnModelCreatingVersion3(builder);
4        
5        // Perform its own implementation after the parent logic
6    }

As well as OnModelCreatingVersion3() other versions: 2 and 1 are delegating the execution to the parent.

This is important as it shows that the actual version-selection logic and core configuration are defined in the base class (IdentityUserContext), while IdentityDbContext only extends that configuration providing the roles schema.

Now, before diving into the IdentityUserContext class, lets revisit our chain of inheritance and to understand where IdentityDbContext fits with its parent classs - IdentityUserContext

   ...
    ↑
IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken, TUserPasskey>
    ↑
IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken, TUserPasskey>
    ↑
IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
    ↑
IdentityDbContext<TUser, TRole, TKey>
    ↑ 
IdentityDbContext<TUser> & IdentityDbContext
    ↑
ApplicationDbContext

Now, lets move one level up and examine the parent class - IdentityUserContext.

As the name suggests, this class is responsible for configuring the user-related part of the Identity schema. In contrast, IdentityDbContext builds on top of it and adds support for roles and related entities.

When you navigate to the source code of IdentityUserContext, you’ll find multiple generic variations of the class. Similar to IdentityDbContext, they form an internal inheritance chain:

   👇 Pparent class
 DbContext
    ↑
IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken, TUserPasskey>
    ↑
IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
    ↑
IdentityUserContext<TUser, TKey>
    ↑
IdentityUserContext<TUser>

Now, lets take a look at the implementation of the

As in the previous examples, most of the source code is omitted. The focus here is on OnModelCreating() and the version-selection logic.

First, lets examine OnModelCreating():

1protected override void OnModelCreating(ModelBuilder builder)
2{
3    var version = GetStoreOptions()?.SchemaVersion ?? IdentitySchemaVersions.Version1;
4    OnModelCreatingVersion(builder, version);
5}

This line defines which schema version Identity will use:

1var version = GetStoreOptions()?.SchemaVersion 
2              ?? IdentitySchemaVersions.Version1;

If no schema version is explicitly configured, Identity falls back to a default value and ultimately uses the latest supported schema version (currently Version3).

This means that in most cases, you do not need to manually specify the schema version — the framework will automatically select the appropriate one.

Next, let’s examine OnModelCreatingVersion(), where the internal version-selection logic is implemented:

 1internal virtual void OnModelCreatingVersion(ModelBuilder builder, Version schemaVersion)
 2{
 3    if (schemaVersion >= IdentitySchemaVersions.Version3)
 4    {
 5        OnModelCreatingVersion3(builder);
 6    }
 7    else if (schemaVersion >= IdentitySchemaVersions.Version2)
 8    {
 9        OnModelCreatingVersion2(builder);
10    }
11    else
12    {
13        OnModelCreatingVersion1(builder);
14    }
15}

Here, the schemaVersion value (resolved in the previous step) determines which version-specific method will be executed. By default (and in our case), it leads to a call to OnModelCreatingVersion3(builder).

Since OnModelCreatingVersion3() is a virtual method, the actual implementation that runs first is the override in IdentityDbContext:

1internal override void OnModelCreatingVersion3(ModelBuilder builder)
2{
3    base.OnModelCreatingVersion3(builder); // 👈 Delegate to the parent
4
5    // ...
6}

This means execution flows back to IdentityDbContext, where additional configuration is applied on top of the base identity schema.

When OnModelCreatingVersion3() is executed, the call to base.OnModelCreatingVersion3() ensures that the base class (IdentityUserContext) contributes its configuration to the same ModelBuilder builder object.

IdentityUserContext adds the core user-related entities, while IdentityDbContext extends the same model with role-related entities such as roles, user roles, and role claims.

After the base class logic is executed, IdentityDbContext continues execution and applies its own configuration on top of the existing model.

In this way, the same ModelBuilder instance is incrementally configured as it “flows” through the inheritance chain of contexts.


Lets revisit the full version of our chain of inheritance, so you can actually see where each actual class fits:

DbContext
    ↑
IdentityUserContext<...>  <────────────── IdentityDbContext<...>
    ↑                                           ↑
IdentityUserContext<TUser, TKey, ...>       IdentityDbContext<TUser, TRole, TKey, ...>
    ↑                                           ↑
IdentityUserContext<TUser, TKey>            IdentityDbContext<TUser, TRole, TKey>
    ↑                                           ↑
IdentityUserContext<TUser>                  IdentityDbContext<TUser> & IdentityDbContext
                                                ↑
                                            ApplicationDbContext

This entire process demonstrates how the ModelBuilder is progressively configured through multiple layers of Identity before being finalized into a metadata model, which EF Core later uses to generate SQL for the application.

I strongly recommend to read the article about EF Core’s Internal Data Representation

That’s it for today, thanks for reading :)

#asp.net core #ef core #asp.net core identity

Reply to this post by email ↪