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.
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.
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:
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:
As the package description states, it is a database provider for the EF Core.
Finally, we require Microsoft.EntityFrameworkCore.Tools to support our migrations:
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:
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:
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.