- Home
- LLBLGen Pro
- Architecture
Schema overwrite for EF Core
Joined: 23-Jan-2005
The LLBLGen runtime framework has had a method for overwriting catalog/schemas in the generated code at runtime for many years. I'm looking for a way to do the same things for EF Core and coming up empty. I've found posts on doing command interception and string replacement on the raw command but I'd like to avoid that track. Any ideas on how I might accomplish this in a cleaner way?
Joined: 23-Jan-2005
I have a working approach for doing this and would be happy to get some input from you on any ways to improve this. One of the things I realized since my original post is that this mostly applies to Oracle and perhaps Postgres due to how schemas are used (or mostly not used IMO) in SQL Server. My solution applies to all tables in a given schema (remap all "SchemaA" to SchemaB").
The premise here is that we have a common set of tables that might be used in multiple database instances but in different schemas. For this example, I'll be using the Quartz job scheduler tables with a custom job history table using a sequence for the PK value.
AppSettings.json - adding a second entry just as an example
{
"SchemaMappings": [
{
"DefaultName": "QUARTZ_SCHEDULER1",
"RemapTo": "QUARTZ"
},
{
"DefaultName": "AUDIT_DATA",
"RemapTo": "AUDIT_INFO"
}
],
}
Options class representing the json elements
public class SchemaRemapOption
{
public required string DefaultName { get; set; }
public required string RemapTo { get; set; }
}
A static class to load a mapping dictionary
public static class SchemaRemapper
{
private static readonly Serilog.ILogger _logger = Log.ForContext(typeof(SchemaRemapper));
private static Dictionary<string, string>? _schemaMappings;
public static Dictionary<string, string> SchemaMappings {
get
{
if (_schemaMappings == null)
{
SetRemapValues();
}
return _schemaMappings ?? new Dictionary<string, string>();
}
}
public static void SetRemapValues()
{
var environment = System.Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "production";
var baseDirectory = AppContext.BaseDirectory;
_logger.Information("Setting Schema remapping values based on environment {HostEnvironment} running in {BaseDirectory}",
environment, baseDirectory);
var builder = new ConfigurationBuilder()
.SetBasePath(baseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile("appsettings.{environment}.json", optional: true, reloadOnChange: true);
IConfigurationRoot configuration = builder.Build();
var schemaRemapList = configuration.GetSection("SchemaMappings").Get<List<SchemaRemapOption>>();
if (schemaRemapList == null)
{
_schemaMappings = new Dictionary<string, string>();
}
else
{
_schemaMappings = schemaRemapList.ToDictionary(x => x.DefaultName, x => x.RemapTo);
}
_logger.Information("Schema remap count: {SchemaRemapCount}", _schemaMappings.Count);
}
The last part is based off your suggestion to a different thread which proved very useful then and now - https://www.llblgen.com/tinyforum/Thread/28731. I haven't actually done the custom template yet, just tested the concept with hand-coded derived classes and a handful of tables.
A derived DBContext Builder class calling a derived ModelBuilder class, both based on the normal generated classes. The derived ModelBuilder class has a ResolveSchema method which is called with the generated (default) schema name and returns either the original schema or the remapped schema.
public partial class QuartzOracleDbContext : QuartzSchedulerDataContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
new QuartzOracleDbModelBuilder().BuildModel(modelBuilder);
OnModelCreatingComplete();
}
public partial class QuartzOracleDbModelBuilder : QuartzSchedulerModelBuilder
{
//get the SchemaMappings
public string ResolveSchemaName(string defaultSchema)
{
var dictionary = SchemaRemapper.SchemaMappings;
if (dictionary.ContainsKey(defaultSchema))
{
var newSchema = dictionary[defaultSchema];
Console.WriteLine($"Schema {defaultSchema} replaced with {newSchema}");
return newSchema;
}
return defaultSchema;
}
protected override void MapJobDetail(EntityTypeBuilder<JobDetail> config)
{
base.MapJobDetail(config);
config.ToTable("QRTZ_JOB_DETAILS", ResolveSchemaName("QUARTZ_SCHEDULER"));
}
protected override void MapJobHistory(EntityTypeBuilder<JobHistory> config)
{
base.MapJobHistory(config);
config.ToTable("QRTZ_JOB_HISTORY", ResolveSchemaName("QUARTZ_SCHEDULER"));
// standard template would generate this for an Oracle sequence call (works for identity sequence but not a standalone one)
// the schema needs to be resolved here too
//config.Property(t => t.Id).HasColumnName("ID").HasDefaultValueSql($"NEXTVAL FOR \"QUARTZ_SCHEDULER\".\"QRTZ_JOB_HISTORY_SEQ\" FROM DUAL");
config.Property(t => t.Id).HasColumnName("ID").ValueGeneratedOnAdd().HasValueGenerator((_, __) => new SequenceValueGenerator("QRTZ_JOB_HISTORY_SEQ", ResolveSchemaName("QUARTZ_SCHEDULER")));
}
protected override void MapJobHistoryMessage(EntityTypeBuilder<JobHistoryMessage> config)
{
base.MapJobHistoryMessage(config);
config.ToTable("QRTZ_JOB_HISTORY_MESSAGES", ResolveSchemaName("QUARTZ_SCHEDULER"));
config.Property(t => t.Id).HasColumnName("ID").ValueGeneratedOnAdd().HasValueGenerator((_, __) => new SequenceValueGenerator("QRTZ_JOB_HISTORY_MESSAGES_SEQ", ResolveSchemaName("QUARTZ_SCHEDULER")));
}
protected override void MapJobHistoryView(EntityTypeBuilder<JobHistoryView> config)
{
base.MapJobHistoryView(config);
config.ToTable("QRTZ_JOB_HISTORY_VIEW", ResolveSchemaName("QUARTZ_SCHEDULER"));
}
// snip - more models
}
Joined: 17-Aug-2003
From the looks of it, it looks fine! The modelbuilder builds the readonly store for modeling data that's used at runtime, AFAIK, so doing it at this level seems to be the best way.