Using IdentityServer4 with custom DBContext config
I created a custom one IConfigurationDbContext
for using IDS4 with Oracle.
public class IdentityConfigurationDbContext : DbContext, IConfigurationDbContext {
private readonly ConfigurationStoreOptions storeOptions;
public IdentityConfigurationDbContext(DbContextOptions<IdentityServerDbContext> options)
: base(options) {
}
public IdentityConfigurationDbContext(DbContextOptions<ConfigurationDbContext> options, ConfigurationStoreOptions storeOptions)
: base(options) {
this.storeOptions = storeOptions ?? throw new ArgumentNullException(nameof(storeOptions));
}
public DbSet<Client> Clients { get; set; }
public DbSet<IdentityResource> IdentityResources { get; set; }
public DbSet<ApiResource> ApiResources { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.ConfigureClientContext(storeOptions);
modelBuilder.ConfigureResourcesContext(storeOptions);
base.OnModelCreating(modelBuilder);
}
}
in ConfigureService:
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddAspNetIdentity<ApplicationUser>();
I also have a custom IClientStore
one that is added to the container like this:
services.AddScoped<IClientStore, ClientStore>();
when i run migration IdentityConfigurationDbContext
i get this error:
System.InvalidOperationException: No database provider has been configured for this DbContext.
I tried to do this:
services.AddDbContext<IdentityConfigurationDbContext>(builder => builder.UseOracle(connectionString, options => {
options.MigrationsAssembly(migrationsAssembly);
options.MigrationsHistoryTable("EF_MIGRATION_HISTORY");
}));
Is this the right way to use a custom dbcontext with IDS4? and how to fix this issue and complete the migration work?
source to share
I've tried a different approach. Instead of implementing, IConfigurationDbContext
I inherited fromIdentityServer4.EntityFramework.DbContexts.ConfigurationDbContext
public class CustomConfigurationDbContext : ConfigurationDbContext
{
public CustomConfigurationDbContext(DbContextOptions<ConfigurationDbContext> options,
ConfigurationStoreOptions storeOptions)
: base(options, storeOptions)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
//...
base.OnConfiguring(optionsBuilder);
}
}
}
And in startup.cs
services.AddIdentityServer()
.AddTemporarySigningCredential()
.AddConfigurationStore(
builder => builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)))
.AddOperationalStore(
builder => builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly)))
.AddAspNetIdentity<ApplicationUser>();
It works like a charm. Disclaimer: This is not my idea. I just can't remember the source of this.
source to share
You don't need to create a custom ConfigurationDbContext
or event IDbContextFactory
to switch to use different databases. Since IdentityServer4.EntityFramework
version 2.3.2 you can do:
namespace DL.STS.Host
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
string connectionString = _configuration.GetConnectionString("appDbConnection");
string migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly
.GetName().Name;
services
.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
// I made up this extension method "UseOracle",
// but this is where you plug your database in
builder.UseOracle(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
...;
...
}
...
}
}
Serve config / live storage to your own project / assembly?
What if you want to neatly decompose your solution and want to split the configuration store and operational store (as well as the user ID store) into their own class library / assembly?
According to the documentation, you can use -o
to specify the destination of the migration output folder:
dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb
But who likes to memorize / type such a long way when doing migrations? Then you might be thinking: how about a custom ConfigurationDbContext
one inherited from IdentityServer and a separate project:
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Options;
using Microsoft.EntityFrameworkCore;
namespace DL.STS.Data.ConfigurationStore.EFCore
{
public class AppConfigurationDbContext : ConfigurationDbContext
{
public AppConfigurationDbContext(DbContextOptions<ConfigurationDbContext> options,
ConfigurationStoreOptions storeOptions) : base(options, storeOptions)
{
}
}
}
Common mistakes
I think this is where people get into trouble. When you do Add-Migration
, you may encounter:
Unable to create object of type
AppConfigurationDbContext
. For the various templates supported at design time see https://go.microsoft.com/fwlink/?linkid=851728 .
or
Failed to resolve service for type
Microsoft.EntityFrameworkCore.DbContextOptions<IdentityServer4.EntityFramework.DbContexts.ConfigurationDbContext>
when trying to activateDL.STS.Data.ConfigurationStore.EFCore.AppConfigurationDbContext
.
I don't think there is a way to fix this right now.
Are there other ways?
It turns out it's actually pretty simple. It looks like you cannot have your own DbContext
inherited from IdentityServer. So get rid of that and create an extension method in this separate library / assembly:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace DL.STS.Data.ConfigurationStore.EFCore.Extensions
{
public static class IdentityServerBuilderExtensions
{
public static IIdentityServerBuilder AddEFConfigurationStore(
this IIdentityServerBuilder builder, string connectionString)
{
string assemblyNamespace = typeof(IdentityServerBuilderExtensions)
.GetTypeInfo()
.Assembly
.GetName()
.Name;
builder.AddConfigurationStore(options =>
options.ConfigureDbContext = b =>
b.UseSqlServer(connectionString, optionsBuilder =>
optionsBuilder.MigrationsAssembly(assemblyNamespace)
)
);
return builder;
}
}
}
Then on Startup.cs
in your web project:
public void ConfigureServices(IServiceCollection services)
{
...
string connectionString = _configuration.GetConnectionString("appDbConnection");
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddEFConfigurationStore(connectionString)
...;
...
}
And when you do PM> Add-Migration AddConfigurationTables -Context ConfigurationDbContext
with the default project, which is a separate library / assembly:
source to share
Adding IDbContextFactory
fixes the problem.
public class IdentityConfigurationDbContextFactory : IDbContextFactory<IdentityConfigurationDbContext> {
public IdentityConfigurationDbContext Create(DbContextFactoryOptions options) {
var optionsBuilder = new DbContextOptionsBuilder<ConfigurationDbContext>();
var config = new ConfigurationBuilder()
.SetBasePath(options.ContentRootPath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{options.EnvironmentName}.json", true)
.Build();
optionsBuilder.UseOracle(config.GetConnectionString("DefaultConnection"));
return new IdentityConfigurationDbContext(optionsBuilder.Options, new ConfigurationStoreOptions());
}
}
source to share
In the latest release, the Identityserver framework supports a custom implementation of the configuration store, operations store. This will work with migrations too
see below for example
public class CustomPersistsDbContext : DbContext, IPersistedGrantDbContext
{
}
In OnModelCreating (ModelBuilder modelBuilder) I had to add relationships manually:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//Optional: The version of .NET Core, used by Ef Core Migration history table
modelBuilder.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
//.. Your custom code
//PersistentDbContext
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b =>
{
b.Property<string>("UserCode")
.ValueGeneratedOnAdd()
.HasMaxLength(200);
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200);
b.Property<DateTime>("CreationTime");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000);
b.Property<string>("DeviceCode")
.IsRequired()
.HasMaxLength(200);
b.Property<DateTime?>("Expiration")
.IsRequired();
b.Property<string>("SubjectId")
.HasMaxLength(200);
b.HasKey("UserCode");
b.HasIndex("DeviceCode")
.IsUnique();
b.HasIndex("UserCode")
.IsUnique();
b.ToTable("DeviceCodes");
});
modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b =>
{
b.Property<string>("Key")
.HasMaxLength(200);
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(200);
b.Property<DateTime>("CreationTime");
b.Property<string>("Data")
.IsRequired()
.HasMaxLength(50000);
b.Property<DateTime?>("Expiration");
b.Property<string>("SubjectId")
.HasMaxLength(200);
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50);
b.HasKey("Key");
b.HasIndex("SubjectId", "ClientId", "Type");
b.ToTable("PersistedGrants");
});
}
When starting services
.AddOperationalStore<CustomPersistsDbContext>(options =>
source to share