更改IdentityServer4 Entity Framework表名

4
我正在尝试更改IdentityServer4的PersistedGrantDb和ConfigurationDb创建的默认表名称,并使Entity Framework生成正确的SQL。例如,我希望将数据映射到名为mytesttable的表中,而不是使用实体IdentityServer4.EntityFramework.Entities.ApiResource使用表ApiResources
根据文档,这很简单,只需在DBContext'sOnModelCreating方法中为要重新映射的每个实体添加ToTable调用,以覆盖TableName = EntityName的默认行为。问题在于,这确实创建了一个名为mytesttable的表,但是Entity Framework在运行时创建的SQL仍然在查询中使用ApiResources,导致查询失败。
我已经采取的步骤是创建了一个从IdentityServer的ConfigurationDbContext派生的DBContext,以便能够重写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");
    }
}

我还实现了一个 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... */
        }
}

我很高兴地告诉你,使用NpgSQL提供程序,它可以愉快地漫游并在我的PostGRES数据库中创建必要的表,其中包括以mytesttable命名的表代替实体IdentityServer4.EntityFramework.Entities.ApiResourceApiResources。然而,当我从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

任何帮助都会很感激。

1
好的 - 原来你可以在Startup中返回的对象上公开的AddIdentityServerAddConfigurationStoreAddOperationalStore配置方法中更改实体目标的表名。所以; services.AddIdentityServer().AddConfigurationStore(options => { options.ApiResource.Name = "mytesttable"; }) - 所有IdentityServer实体->表名映射都是通过这种方式公开的。还需要在DBContexts的OnModelCreating中添加必要的ToTable()调用,这样就可以完美地使用与它们支持的实体不同的表名了。 - Yumbelie
有趣的是,我今天正在解决完全相同的问题。你应该将这个作为答案并接受。 - ctc
我已经写了一个更全面的答案并在下面发布了。 - Yumbelie
1个回答

5

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

首先,可以通过AddOperationalStoreAddConfigurationStore方法来更改Entity Framework查询中使用的表名,这些方法挂在AddIdentityServer中间件方法上。提供给配置方法的委托的options参数公开了表名,例如:options.{EntityName}.Name = {WhateverTableNameYouWantToUse} - 或者 options.ApiResource.Name = mytesttable。您还可以通过调整Schema属性来针对每个表覆盖模式。

下面的示例使用反射更新所有实体,以使用以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.
}

第二部分是修改由实体框架生成的IdentityServer实体所生成的模式。为了完成此操作,您有两个选择;您可以从IdentityServer提供的ConfigurationDbContextPersistedGrantDbContext中派生出一个,然后覆盖OnModelCreating方法以将每个IdentityServer实体重新映射到修改后的表名,然后创建您的初始迁移或更新迁移,如此处记载(使用流利API语法)或者您可以根据教程添加迁移部分从提供的IdentityServer DBContext的ConfigurationDbContextPersistedGrantDbContext创建初始迁移,然后仅在所有表名和这些表名的引用上使用文本编辑器进行查找和替换在已创建的迁移文件中。

无论您选择哪种方法,仍需要使用dotnet ef migrations ...命令行语法来创建初始迁移文件(如添加迁移所示)或带有表更改的修改集,完成后运行IdentityServer项目,模式将在目标数据库中创建。
注意:通过dotnet ef migrations语法(也称为设计时)调用OnModelCreating,如果您在DBContext上调用Database.Migrate(),也会在运行时调用 - 例如:MyDbContextInstance.Database.Migrate()(或异步等效方法)。
如果要使用自定义DBContext以便可以自定义OnModelCreating,则需要添加一些设计时类,这些类在从命令行调用dotnet ef时使用,并将新上下文添加到Startup
为了完整起见,以下是一个hacky的粗略示例,其中上下文目标是PostGres数据库(如果不同,请使用UseNpgsql或其他后备存储代替UseSQLServer),连接字符串名称为IDPDataDBConnectionString在appsettings.json文件中,此示例中的自定义DB上下文是从IdentityServer的ConfigurationDbContext派生的MyTestDbContext

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

将此添加到Startup

 services.AddDbContext<MyTestDbContext>(options =>
        {
            options.UseNpgsql(_configuration.GetConnectionString("IDPDataDBConnectionString"));
        }); 

请注意,设计时类的使用还允许您将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);
    }
}

}

顺便说一下;如果你已经有一个包含IdentityServer表的数据库,你可以手动重命名它们,忽略Entity Framework迁移 - 然后你只需要在Startup中进行AddConfigurationStoreAddOperationalStore的更改。


这个新的NuGet包对IdentityServer4有什么影响?你能否提供一个更新后的答案吗? - J86

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接