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.
This article is divided into the following sections:
- Adding Required NuGet Packages
- Configuring Migrations for the IdentityServer4 Configuration
- Creating Migrations
- Conclusion
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.
Great series and very helpful. I found it much better than IdendityServer’s own examples and documentation. The only thing missing is how to use the data that was migrated, such as the clients, instead of the in memory configurations.
Hello Marc. Thanks a lot for reading our series and for the comment as well. Related to your suggestion, we basically like to keep our in memory config class with the database, so everything could be retreived from the in-memory class. But, if you want to query the database, you can just inject the ConfigurationDbContext as you would do with the custom DbContext class and query the DbSet properties (Clients etc.) from it.
Actually, I got it working. I don’t know why, but none of the data from the in memory config had been migrated into the database. I’m pretty sure I ran it last week, but I was doing this on Friday. Maybe I was more focused on the weekend ahead and didn’t actually run it.
When using the in memory configuration though, the clients are hard coded, which means a recompile is necessary when adding new clients, or am I missing something?
You are right. If you modify the configuration, you should delete the db and start the app. But this can me modified, if you just modify the MigrationManager class.