13

I created a customized IConfigurationDbContext in order to 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 my custom IClientStore which is added to the container like this:

services.AddScoped<IClientStore, ClientStore>();

when I run IdentityConfigurationDbContext migration, I get this error:

System.InvalidOperationException: No database provider has been configured for this DbContext.

I tried doing 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 do I fix this issue, and complete my migration work?

8 Answers 8

9

You don't need to create a custom ConfigurationDbContext or event IDbContextFactory in order to switch to use different databases. With 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));
               })
               ...;

            ...
        }

        ...
    }
}

Separate Configuration/Operational Store into its own project/assembly?

What if you want to lay out your solution nicely and would like to separate the configuration store and operational store (as well as the identity user store) into their own class library/assembly?

Per the documentation, you can use -o to specify the output migration folder destination:

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 long path when doing migrations? Then you might think: how about a custom ConfigurationDbContext inherited from IdentityServer's, 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 errors

I think this is where people get into troubles. When you do Add-Migration, you would either encounter:

Unable to create an object of type AppConfigurationDbContext. For different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728.

or

Unable to resolve service for type Microsoft.EntityFrameworkCore.DbContextOptions<IdentityServer4.EntityFramework.DbContexts.ConfigurationDbContext> while attempting to activate DL.STS.Data.ConfigurationStore.EFCore.AppConfigurationDbContext.

I don't think, for now, there is a way to fix it.

Is there any other ways?

It turns out it's actually quite easy. It seems like you can't have your own DbContext inherited from IdentityServer's. So get rid of that, and create an extension method in that 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 on 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 being that separate library/assembly:

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

Am getting "No DbContext named 'ConfigurationDbContext' was found". Pls check my question stackoverflow.com/questions/61263843/…
This answer is fantastic and was exactly what I needed. I have an n-tier project where the contexts and migrations are kept separate from the web project (exactly as is described here). After following this answer the only thing to add is the exact scripts I ran to generate and apply the migrations. From the API project directory: dotnet ef migrations add AddConfigurationTables -s ../<Api Project Folder> --context ConfigurationDbContext -o Migrations\IdentityServer4 dotnet ef database update AddConfigurationTables -s ../<Api Project Folder> --context ConfigurationDbContext
2

with the recent release, the Identityserver framework does support custom implementation of configuration store, operation store. This will also work with migration

see below for instance

            public class CustomPersistsDbContext : DbContext, IPersistedGrantDbContext
                {
                }

In the OnModelCreating(ModelBuilder modelBuilder) I had to add the relations 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");
                    });
                }

On the services startup

 .AddOperationalStore<CustomPersistsDbContext>(options =>

Comments

1

I've tried a different approach. Instead of implementing IConfigurationDbContext I have inherited from IdentityServer4.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 the 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 cannot remember the source of that.

6 Comments

I did that, but got this error during migration: No parameterless constructor was found on 'IdentityConfigurationDbContext'. Either add a parameterless constructor to 'IdentityConfigurationDbContext' or add an implementation of 'IDbContextFactory<IdentityConfigurationDbContext>' in the same assembly as 'IdentityConfigurationDbContext'.
even though you inherited from ConfigurationDbContext
Yes, I also added a No parameterless constructor same thing.
Can't find those ex. methods.
hm this will not use CustomConfigurationDbContext unless AddConfigurationStore<CustomConfigurationDbContext > but then it errors out with "Unable to resolve service for type 'Microsoft.EntityFrameworkCore .DbContextOptions`1[IdentityServer4.EntityFramework.DbContexts.ConfigurationDbContext]' while attempting to activate.."
|
1

Adding an IDbContextFactory fixed the issue.

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());
        }
    }

3 Comments

Oh yes, you should have an implementation of IDbContextFactory<CustomConfigurationDbContext>
How did you register the CustomConfigurationDbContext in Startup.cs? I'm getting this error System.InvalidOperationException: 'No service for type CustomConfigurationDbContext' has been registered.'
check my answer above that is the only stuff I have added on startup
1

I may not be too late to this party but what you might be able to simply do is thus:

.AddConfigurationStore<CustomIdentityContext>(options =>
            {
                options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, b => b.MigrationsAssembly("tuitionpayer.IdentityEF"));
            })
            // this adds the operational data from DB (codes, tokens, consents)
            .AddOperationalStore<CustomIdentityContext>(options =>
            {
                options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, b => b.MigrationsAssembly("tuitionpayer.IdentityEF"));

                // this enables automatic token cleanup. this is optional.
                options.EnableTokenCleanup = true;
            });

And the DbContext will look something like:

public class CustomIdentityContext : IdentityDbContext<CustomUser, CustomRole, string>, IPersistedGrantDbContext, IConfigurationDbContext
{
    public CustomIdentityContext(DbContextOptions<CustomIdentityContext> options) : base(options)
    {
    }

    public DbSet<PersistedGrant> PersistedGrants { get; set; }
    public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
    public DbSet<Client> Clients { get; set; }
    public DbSet<ClientCorsOrigin> ClientCorsOrigins { get; set; }
    public DbSet<IdentityResource> IdentityResources { get; set; }
    public DbSet<ApiResource> ApiResources { get; set; }
    public DbSet<ApiScope> ApiScopes { get; set; }

    public Task<int> SaveChangesAsync() => base.SaveChangesAsync();
}

Also, note that the CustomUser and CustomRole are to Inherit from IdentityUser and IdentityRole respectively.

I hope this helps someone else.

1 Comment

I see you used Oracle in the question, you can easily replace the UseSqlServer in mine
0

I think that the easiest way to do it is to use the parameter T of ConfigurationDbContext as below. It works for me on net core 3.0

public class ConfigurationDataContext : ConfigurationDbContext<ConfigurationDataContext>
{
    public ConfigurationDataContext(DbContextOptions<ConfigurationDataContext> options, ConfigurationStoreOptions storeOptions)
    : base(options, storeOptions)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.ApplyConfigurationsFromAssembly(typeof(MyConfigurationsAssemby).Assembly);
    }
}

Comments

0

I used your solution with a little change. I am attaching below the code. in your test method OnModelCreating you declare two methods

...
modelBuilder.ConfigureClientContext(configurationStoreOptions);
modelBuilder.ConfigureResourcesContext(configurationStoreOptions);

modelBuilder.ConfigurePersistedGrantContext(operationalStoreOptions); // need to add

that refer to PersistedGrants and DeviceFlowCodes, this is good, but you need to add

ConfigurePersistedGrantContext also.

here too there is info👍

public class MYCustomDbContext : DbContext, IPersistedGrantDbContext, IConfigurationDbContext


public DbSet<PersistedGrant> PersistedGrants { get; set; }

public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }

........

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    if (modelBuilder is null)
       throw new ArgumentNullException(nameof(modelBuilder));

    ConfigurationStoreOptions storeConfigurationOptions = new ConfigurationStoreOptions();
    OperationalStoreOptions storeOperationalOptions = new OperationalStoreOptions();

    modelBuilder.ConfigureClientContext(configurationStoreOptions);
    modelBuilder.ConfigureResourcesContext(configurationStoreOptions);
    modelBuilder.ConfigurePersistedGrantContext(operationalStoreOptions);
}

Task<int> IPersistedGrantDbContext.SaveChangesAsync() => base.SaveChangesAsync();

public Task<int> SaveChangesAsync() => base.SaveChangesAsync();
public void ConfigureServices(IServiceCollection services)
{
......
 services.AddOperationalStore<MYCustomDbContext>(options => {
                    // this enables automatic token cleanup.
                    options.EnableTokenCleanup = true;
                    options.TokenCleanupInterval = 3600; }
.....
}

Comments

0

I made this with the last version of IdentityServer4.EntityFramework (4.1.2):

I created a class called ApplicationConfigurationDbContext inherit from ConfigurationDbContext<TContext>, where TContext is my class

public class ApplicationConfigurationDbContext : ConfigurationDbContext<ApplicationConfigurationDbContext>
{
    public ApplicationConfigurationDbContext(DbContextOptions<ApplicationConfigurationDbContext> options, ConfigurationStoreOptions storeOptions) : base(options, storeOptions)
    {

    }

    // My own entities...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // My own configurations...
    }
}

Then in Startup class I registered my DbContext and finally after calling AddIdentityServer() extension method I chained AddConfigurationStore() extension method too, that's it!.

public class Startup
{
    // Hide for brevity

    public void ConfigureServices(IServiceCollection services)
    {
        var builder = services.AddIdentityServer(options =>
        {
            // My options...
        })
        .AddConfigurationStore<ApplicationConfigurationDbContext>(options =>
        {
            options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
                sql => sql.MigrationsAssembly(migrationsAssembly));
        })
        .AddAspNetIdentity<ApplicationUser>();
    }
}

When I wish to add a migration I do this:

dotnet ef migrations add "Add_New_Table" --context "ApplicationConfigurationDbContext" --startup-project "My.Start.Project" --project "Target.Project" --output-dir "Migrations/ConfigurationDb"

More information about Entity Framework Core tools via .NET CLI here.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.