Schema overwrite for EF Core

Posts   
 
    
jovball
User
Posts: 441
Joined: 23-Jan-2005
# Posted on: 22-Aug-2024 17:31:24   

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?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39753
Joined: 17-Aug-2003
# Posted on: 23-Aug-2024 07:27:45   

Not that I'm aware of. Perhaps the EF team knows different ways, but to my knowledge they don't have a built-in way to overwrite schemas at runtime using a simple api

Frans Bouma | Lead developer LLBLGen Pro
jovball
User
Posts: 441
Joined: 23-Jan-2005
# Posted on: 26-Aug-2024 05:02:43   

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 
}

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39753
Joined: 17-Aug-2003
# Posted on: 27-Aug-2024 07:59:29   

From the looks of it, it looks fine! simple_smile 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. simple_smile

Frans Bouma | Lead developer LLBLGen Pro