c# - 更改 IdentityServer4 Entity Framework 表名

标签 c# postgresql entity-framework entity-framework-core identityserver4

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

根据documentation这应该像添加 ToTable 一样简单我想在 DBContext's 中重新映射的每个实体的调用OnModelCreating方法来覆盖 TableName = EntityName 的默认行为。问题是这确实创建了一个表 mytesttable但是 Entity Framework 在运行时创建的 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");
    }
}

我还实现了 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 的表代替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

任何帮助表示赞赏。

最佳答案

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

所以,首先; AddOperationalStore 公开了更改 Entity Framework 查询中使用的表名的能力。和 AddConfigurationStore挂断 AddIdentityServer 的方法中间件方法。 options提供给配置方法的委托(delegate)参数公开表名,例如:options.{EntityName}.Name = {WhateverTableNameYouWantToUse} - 或 options.ApiResource.Name = mytesttable .您也可以通过调整 Schema 来覆盖每个表的架构。属性(property)。

下面的示例使用反射来更新所有实体以使用以 idn_ 为前缀的表名, 所以 idn_ApiResources , idn_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.
}

第二部分是修改 Entity Framework 从 IdentityServer 实体生成的模式。要做到这一点,你有两个选择;您可以从 IdentityServer 提供的 DBContext 之一派生; ConfigurationDbContextPeristedGrantDbContext然后覆盖 OnModelCreating方法将每个 IdentityServer 实体重新映射到修改后的表名,然后将初始迁移或更新迁移创建为 documented here (流利的 Api 语法),您可以从提供的 IdentityServer DBContext 的 ConfigurationDbContext 创建初始迁移和 PersistedGrantDbContext根据教程 Adding Migrations部分,然后在创建的迁移文件中对所有表名和对这些表名的引用使用文本编辑器进行查找和替换。

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

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

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

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

复制粘贴代码,调整路径为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如果您这样做(参见 here 了解更多信息)。
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 表的数据库,您可以手动重命名它们而忽略 EntityFrameworks 迁移 - 您唯一需要的是 Startup 中的更改至AddConfigurationStoreAddOperationalStore .

关于c# - 更改 IdentityServer4 Entity Framework 表名,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51483885/

相关文章:

c# - 使用性能计数器跟踪 Windows 服务

c# - 格式化 HTML 表单布局

c# - 无法让最小起订量返回值

mysql - 从Mysql到postgresql的数据迁移

sql - 大型 PostgreSQL 表 : better to add a column or create a new table to store metadata?

c# - Linq to Entity 比较字符串忽略空格

entity-framework - Entity Framework 代码首先创建 "discriminator"列

c# - Leadtools - OcrException - ocr 未启用

c# - 我可以应用 DebuggerHidden 之类的属性来跳过函数及其调用的所有函数吗?

postgresql - 编码 "UTF8"的字符 0xe28093 在 "LATIN1"中没有等效项