In all the previous parts of this series, we have been working with the in-memory IDP configuration. But, every time we wanted to change something in that configuration, we had to restart our Identity Server to load the new configuration. Well, in this article we are going to learn to migrate the IdentityServer4 configuration to the database using EntityFramework Core, so we could persist our configuration across multiple IdentityServer instances.

To download the source code for the client application, you can visit the Migrate the IdenityServer4 Configuration repository.

We highly recommend visiting the IdentityServer4 series page to learn about all the articles in this series because this article is strongly related to the previous ones.

So, let’s go to work.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Adding Required Nuget Packages

Let’s add several NuGet packages required for the IdentityServer4 configuration migration process.

The first package, we require is IdentityServer4.EntityFramework:

IdentityServer4 EntityFramework package - Migrate the IdentityServer4 Configuration

This package implements the required stores and services using two context classes: ConfigurationDbContext and PersistedGrantDbContext. It uses the first context for the configuration of clients, resources, and scopes. Additionally, it uses the second context for the temporary operational data like authorization codes, and refresh tokens.

The second library we require is Microsoft.EntityFrameworkCore.SqlServer:

EfCoreSqlServer package - Migrate the IdentityServer4 Configuration

As the package description states, it is a database provider for the EF Core.

Finally, we require Microsoft.EntityFrameworkCore.Tools to support our migrations:

EfCoreSqlTools package

That is it. We can move on to the configuration part.

Configuring Migrations for the IdentityServer4 Configuration

Let’s first modify the appsettings.json file:

"ConnectionStrings": {
    "sqlConnection": "server=.; database=CompanyEmployeeOAuth; Integrated Security=true"
  }

After this, we have to modify the Startup class. Let’s start with the constructor:

public IConfiguration Configuration { get; set; }

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

Take note that IConfiguration interface resides in the Microsoft.Extensions.Configuration namespace.

Then, let’s modify the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    var migrationAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

    services.AddIdentityServer()
        .AddTestUsers(InMemoryConfig.GetUsers())
        .AddDeveloperSigningCredential() //not something we want to use in a production environment
        .AddConfigurationStore(opt =>
        {
            opt.ConfigureDbContext = c => c.UseSqlServer(Configuration.GetConnectionString("sqlConnection"),
                sql => sql.MigrationsAssembly(migrationAssembly));
        })
        .AddOperationalStore(opt =>
        {
            opt.ConfigureDbContext = o => o.UseSqlServer(Configuration.GetConnectionString("sqlConnection"),
                sql => sql.MigrationsAssembly(migrationAssembly));
        });

    services.AddControllersWithViews();
}

So, we start by extracting the assembly name for our migrations. We need that because we have to inform EF Core that our project will contain the migration code. Additionally, EF Core needs this information because our project is in a different assembly than the one containing the DbContext classes.

After that, we replace the AddInMemoryClients, AddInMemoryIdentityResources, and AddInMemoryApiResources methods with the AddConfigurationStore and AddOperationalStore methods. Both methods require information about the connection string and migration assembly.

Nicely done.

Creating Migrations

As we have mentioned, we are working with two db context classes and for both of them, we have to create a separate migration:

PM> Add-Migration InitialPersistedGranMigration -c PersistedGrantDbContext -o Migrations/IdentityServer/PersistedGrantDb
PM> Add-Migration InitialConfigurationMigration -c ConfigurationDbContext -o Migrations/IdentityServer/ConfigurationDb

As we can see, we are using two flags for our migrations (- c and – o). The – c flag stands for Context and the – o flag stands for OutputDir. So basically, we have created migrations for each context class in the separate folder:

Migrations structure - Migrate the IdentityServer4 Configuration

Once we have our migration files, we are going to create a new Extensions folder with a new class to seed our data:

public static class MigrationManager
{
    public static IHost MigrateDatabase(this IHost host)
    {
        using (var scope = host.Services.CreateScope())
        {
                scope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

           using (var context = scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>())
           {
               try
               {
                   context.Database.Migrate();

                   if (!context.Clients.Any())
                   {
                       foreach (var client in InMemoryConfig.GetClients())
                       {
                           context.Clients.Add(client.ToEntity());
                       }
                       context.SaveChanges();
                    }

                    if (!context.IdentityResources.Any())
                    {
                        foreach (var resource in InMemoryConfig.GetIdentityResources())
                        {
                            context.IdentityResources.Add(resource.ToEntity());
                        }
                        context.SaveChanges();
                    }

                    if(!context.ApiScopes.Any())
                    {
                        foreach (var apiScope in InMemoryConfig.GetApiScopes())
                        {
                            context.ApiScopes.Add(apiScope.ToEntity());
                        }

                        context.SaveChanges();
                    }

                    if (!context.ApiResources.Any())
                    {
                        foreach (var resource in InMemoryConfig.GetApiResources())
                        {
                            context.ApiResources.Add(resource.ToEntity());
                        }
                        context.SaveChanges();
                    }
                }
                catch (Exception ex)
                {
                    //Log errors or do anything you think it's needed
                    throw;
                }
            }
        }

        return host;
    }
}

So, we create scope and use it to migrate all the tables from the  PersistedGrantDbContext class. Soon after that, we create a context for the ConfigurationDbContext class and use the Migrate method to apply migration. Then we go through all the clients, identity resources, and api resources, add each of them to the context and call the SaveChanges method.

To learn more about automatic migrations with Entity Framework Core, we strongly recommend reading Migrations and Seed Data with the EF Core article.

Now, all we have to do is to modify Program.cs class:

public static void Main(string[] args)
{
    CreateHostBuilder(args)
        .Build()
        .MigrateDatabase()
        .Run();
}

That’s all it takes.

Now, let’s start our Identity Server application. As soon as it is started, we can inspect our database:

IdentityServer4 Config Database

Additionally, you can inspect the content of some tables like dbo.ApiResources or dbo.Clients and you are going to find our in-memory configuration in these tables for sure.

As a final step, we can start both additional applications and check if everything works as it did prior migrations. Of course, it should work, but this time, we use the configuration from the database.

Conclusion

Excellent job.

We have seen how to migrate our in-memory configuration to the MS SQL database in a few easy steps. Additionally, we have learned what libraries we require for the process and how to prepare migrations for both context classes.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!