更改 IdentityServer4 实体框架表名称

Yum*_*lie 4 c# postgresql entity-framework entity-framework-core identityserver4

我正在尝试更改由 PersistedGrantDb 和 ConfigurationDb 为 IdentityServer4 创建的默认表名,并让实体框架生成正确的 SQL。例如; 我希望将数据映射到名为IdentityServer4.EntityFramework.Entities.ApiResource的表中ApiResources,而不是使用使用表的实体mytesttable

根据文档,这应该就像为ToTable我想在DBContext's OnModelCreating方法中重新映射的每个实体添加调用一样简单,以覆盖 TableName = EntityName 的默认行为。问题是这确实创建了一个表,mytesttable但实体框架在运行时创建的 SQL 仍然ApiResources在查询中使用,因此失败。

我采取的步骤是我创建了一个DBContext从 IdentityServer 派生的ConfigurationDbContext,以便能够覆盖OnModelCreating和自定义表名:

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


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

        Console.WriteLine("OnModelCreating invoking...");

        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<IdentityServer4.EntityFramework.Entities.ApiResource>().ToTable("mytesttable");

        base.OnModelCreating(modelBuilder);

        Console.WriteLine("...OnModelCreating invoked");
    }
}
Run Code Online (Sandbox Code Playgroud)

我还实现了一个DesignTimeDbContextFactoryBase<MyTestDBContext>类来MyTestDbContext在设计时通过dotnet ef migrations命令行语法调用时制造实例。

这有效并且调用了dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c MyTestDbContext -o Data/Migrations/IdentityServer/MyTestContext在我的程序集中创建初始迁移。

然后我启动 IdentityServer 实例,调用Startup包含以下逻辑的测试方法:

private static void InitalizeDatabase(IApplicationBuilder app)
{
        using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
         {

            serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

            var context = serviceScope.ServiceProvider.GetRequiredService<MyTestDbContext>();
            context.Database.Migrate();

            /* Add some test data here... */
        }
}
Run Code Online (Sandbox Code Playgroud)

这愉快地通过徘徊,并用我的Postgres数据库创建必要的表NpgSQL供应商,包括命名的表mytesttable来代替ApiResources对实体IdentityServer4.EntityFramework.Entities.ApiResource。但是,当我从 IdentityServer 实例调用命令时,生成的 SQL 仍在引用ApiResources而不是mytesttable

  Failed executing DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
  SELECT x."Id", x."Description", x."DisplayName", x."Enabled", x."Name"
  FROM "ApiResources" AS x
  ORDER BY x."Id"
  Npgsql.PostgresException (0x80004005): 42P01: relation "ApiResources" does not exist
Run Code Online (Sandbox Code Playgroud)

任何帮助表示赞赏。

Yum*_*lie 5

这个答案有两个部分;首先需要在 IdentityServer 的配置中调整表名,以便它使用新的表名生成查询。其次; 实体框架生成的模式需要修改,以便它知道为身份框架实体创建不同命名的表。继续阅读...

所以,首先; 改变实体框架查询使用的表名的能力被暴露AddOperationalStoreAddConfigurationStore方法是挂完AddIdentityServer中间件方法。在options供给到配置方法委托的参数暴露的表名,例如:options.{EntityName}.Name = {WhateverTableNameYouWantToUse}-或options.ApiResource.Name = mytesttable。您还可以通过调整Schema属性来覆盖每个表的架构。

下面使用反射的示例更新所有的entites与前缀使用表名idn_,所以idn_ApiResourcesidn_ApiScopes等:

services.AddIdentityServer()
.AddConfigurationStore(options => {
                // Loop through and rename each table to 'idn_{tablename}' - E.g. `idn_ApiResources`
                foreach(var p in options.GetType().GetProperties()) {
                if (p.PropertyType == typeof(IdentityServer4.EntityFramework.Options.TableConfiguration))
                {
                    object o = p.GetGetMethod().Invoke(options, null);
                    PropertyInfo q = o.GetType().GetProperty("Name");

                    string tableName = q.GetMethod.Invoke(o, null) as string;
                    o.GetType().GetProperty("Name").SetMethod.Invoke(o, new object[] { $"idn_{tableName}" });

                }
            }

         // Configure DB Context connection string and migrations assembly where migrations are stored  
            options.ConfigureDbContext = builder => builder.UseNpgsql(_configuration.GetConnectionString("IDPDataDBConnectionString"),
                sql => sql.MigrationsAssembly(typeof(IdentityServer.Data.DbContexts.MyTestDbContext).GetTypeInfo().Assembly.GetName().Name));
}
.AddOperationalStore(options => { 

 // Copy and paste from AddConfigurationStore logic above.
}
Run Code Online (Sandbox Code Playgroud)

第二部分是从 IdentityServer 实体中修改实体框架生成的模式。要做到这一点,您有两个选择;您可以从 IdentityServer 提供的 DBContext 之一派生;ConfigurationDbContext或者PeristedGrantDbContext,然后覆盖该OnModelCreating方法以将每个 IdentityServer 实体重新映射到修改后的表名称,然后按照此处记录的(Fluent Api 语法)创建初始迁移或更新迁移,或者您可以从提供的 IdentityServer DBContextConfigurationDbContextPersistedGrantDbContext根据教程添加迁移部分,然后在创建的迁移文件中对所有表名和对这些表名的引用进行查找和替换。

无论您选择哪种方法,您仍然需要使用dotnet ef migrations ...命令行语法来创建初始迁移文件(如添加迁移中所示)或具有表更改的修改集,完成此操作后,运行您的 IdentityServer 项目,架构将是在目标数据库中创建。

笔记; 如果您调用DBContext - 例如(或异步等效方法),OnModelCreating则通过dotnet ef migrations语法(又名在设计时)和运行时调用。Database.Migrate()MyDbContextInstance.Database.Migrate()

如果要使用自定义 DBContext 以便进行自定义OnModelCreating,则需要添加一些设计时类,这些类dotnet ef在从命令行调用并将新上下文添加到Startup.

为了完整起见,下面是一个 hacky 粗略示例,其中上下文目标是 PostGres 数据库(如果它不同,则使用它UseSQLServer来代替UseNpgsql或任何您的后备存储)并且连接字符串名称IDPDataDBConnectionString在 appsettings.json 文件和自定义数据库中在这种情况下MyTestDbContext,上下文是从 IdentityServer 的ConfigurationDbContext.

复制并粘贴代码,调整路径appsettings.json(或重构),然后从命令行执行dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c MyTestDbContext -o Data/Migrations/IdentityServer/ConfigurationDbCreatedWithMyTestContext,您应该看到实体框架使用您在OnModelCreating派生上下文中放置的任何覆盖生成架构迁移文件。下面的示例还包括一些Console.WriteLine调用,以便更轻松地跟踪正在发生的事情。

将此添加到Startup

 services.AddDbContext<MyTestDbContext>(options =>
        {
            options.UseNpgsql(_configuration.GetConnectionString("IDPDataDBConnectionString"));
        }); 
Run Code Online (Sandbox Code Playgroud)

请注意,如果您愿意,使用设计时类还允许您将 IdentityServer 数据库迁移文件分离到单独的类库中。Startup如果您这样做,请确保将其定位(有关更多信息,请参见此处)。

namespace MyIdentityServer.DataClassLibrary.DbContexts
{

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


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

        Console.WriteLine("OnModelCreating invoking...");

        base.OnModelCreating(modelBuilder);

        // Map the entities to different tables here
        modelBuilder.Entity<IdentityServer4.EntityFramework.Entities.ApiResource>().ToTable("mytesttable");

        Console.WriteLine("...OnModelCreating invoked");
    }

}
public class MyTestContextDesignTimeFactory : DesignTimeDbContextFactoryBase<MyTestDbContext>
{

    public MyTestContextDesignTimeFactory()
        : base("IDPDataDBConnectionString", typeof(MyTestContextDesignTimeFactory).GetTypeInfo().Assembly.GetName().Name)
    {
    }

    protected override MyTestDbContext CreateNewInstance(DbContextOptions<MyTestDbContext> options)
    {
        var x = new DbContextOptions<ConfigurationDbContext>();

        Console.WriteLine("Here we go...");

        var optionsBuilder = newDbContextOptionsBuilder<ConfigurationDbContext>();

        optionsBuilder.UseNpgsql("IDPDataDBConnectionString", postGresOptions => postGresOptions.MigrationsAssembly(typeof(MyTestContextDesignTimeFactory).GetTypeInfo().Assembly.GetName().Name));

        DbContextOptions<ConfigurationDbContext> ops = optionsBuilder.Options;

        return new MyTestDbContext(ops, new ConfigurationStoreOptions());
    }
}




/* Enable these if you just want to host your data migrations in a separate assembly and use the IdentityServer supplied DbContexts 

public class ConfigurationContextDesignTimeFactory : DesignTimeDbContextFactoryBase<ConfigurationDbContext>
{

    public ConfigurationContextDesignTimeFactory()
        : base("IDPDataDBConnectionString", typeof(ConfigurationContextDesignTimeFactory).GetTypeInfo().Assembly.GetName().Name)
    {
    }

    protected override ConfigurationDbContext CreateNewInstance(DbContextOptions<ConfigurationDbContext> options)
    {
        return new ConfigurationDbContext(options, new ConfigurationStoreOptions());
    }
}

public class PersistedGrantContextDesignTimeFactory : DesignTimeDbContextFactoryBase<PersistedGrantDbContext>
{
    public PersistedGrantContextDesignTimeFactory()
        : base("IDPDataDBConnectionString", typeof(PersistedGrantContextDesignTimeFactory).GetTypeInfo().Assembly.GetName().Name)
    {
    }

    protected override PersistedGrantDbContext CreateNewInstance(DbContextOptions<PersistedGrantDbContext> options)
    {
        return new PersistedGrantDbContext(options, new OperationalStoreOptions());
    }
}
*/

public abstract class DesignTimeDbContextFactoryBase<TContext> :
IDesignTimeDbContextFactory<TContext> where TContext : DbContext
{
    protected string ConnectionStringName { get; }
    protected String MigrationsAssemblyName { get; }
    public DesignTimeDbContextFactoryBase(string connectionStringName, string migrationsAssemblyName)
    {
        ConnectionStringName = connectionStringName;
        MigrationsAssemblyName = migrationsAssemblyName;
    }

    public TContext CreateDbContext(string[] args)
    {
        return Create(
            Directory.GetCurrentDirectory(),
            Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"),
            ConnectionStringName, MigrationsAssemblyName);
    }
    protected abstract TContext CreateNewInstance(
        DbContextOptions<TContext> options);

    public TContext CreateWithConnectionStringName(string connectionStringName, string migrationsAssemblyName)
    {
        var environmentName =
            Environment.GetEnvironmentVariable(
                "ASPNETCORE_ENVIRONMENT");

        var basePath = AppContext.BaseDirectory;

        return Create(basePath, environmentName, connectionStringName, migrationsAssemblyName);
    }

    private TContext Create(string basePath, string environmentName, string connectionStringName, string migrationsAssemblyName)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(basePath)
            .AddJsonFile(@"c:\change\this\path\to\appsettings.json")
            .AddJsonFile($"appsettings.{environmentName}.json", true)
            .AddEnvironmentVariables();

        var config = builder.Build();

        var connstr = config.GetConnectionString(connectionStringName);

        if (String.IsNullOrWhiteSpace(connstr) == true)
        {
            throw new InvalidOperationException(
                "Could not find a connection string named 'default'.");
        }
        else
        {
            return CreateWithConnectionString(connstr, migrationsAssemblyName);
        }
    }

    private TContext CreateWithConnectionString(string connectionString, string migrationsAssemblyName)
    {
        if (string.IsNullOrEmpty(connectionString))
            throw new ArgumentException(
         $"{nameof(connectionString)} is null or empty.",
         nameof(connectionString));

        var optionsBuilder =
             new DbContextOptionsBuilder<TContext>();

        Console.WriteLine(
            "MyDesignTimeDbContextFactory.Create(string): Connection string: {0}",
            connectionString);

        optionsBuilder.UseNpgsql(connectionString, postGresOptions => postGresOptions.MigrationsAssembly(migrationsAssemblyName));

        DbContextOptions<TContext> options = optionsBuilder.Options;

        Console.WriteLine("Instancing....");

        return CreateNewInstance(options);
    }
}

}
Run Code Online (Sandbox Code Playgroud)

边注; 如果您已经拥有一个包含 IdentityServer 表的数据库,您可以忽略 EntityFrameworks 迁移手动重命名它们 - 然后您唯一需要的是StartupAddConfigurationStore和的更改AddOperationalStore