At the moment there is no built-in support for changing the database schema at runtime. Luckily, Entity Framework Core (EF) provides us with the right tools to implement it by ourselves.

The demos are on GitHub: github:PawelGerr/EntityFrameworkCore-Demos

Given are a database context DemoDbContext and an entity Product.

public class DemoDbContext : DbContext
{
  public DbSet<Product> Products { get; set; }

  public DemoDbContext (DbContextOptions<DemoDbContext> options)
    : base(options)
  {
  }
}

public class Product
{
  public Guid Id { get; set; }
}

There are 2 ways to change the schema, either by applying the TableAttribute or by implementing the interface IEntityTypeConfiguration<TEntity>.

The first option won't help us because the schema is hard-coded.

[Table("Products", Schema = "demo")]
public class Product
{
  public Guid Id { get; set; }
}

The second option gives us the ability to provide the schema from DbContext to the EF model configuration. At first we implement the entity configuration for Product.

public class ProductEntityConfiguration : IEntityTypeConfiguration<Product>
{
  private readonly string _schema;

  public ProductEntityConfiguration(string schema)
  {
    _schema = schema;
  }

  public void Configure(EntityTypeBuilder<Product> builder)
  {
    if (!String.IsNullOrWhiteSpace(_schema))
      builder.ToTable(nameof(DemoDbContext.Products), _schema);

    builder.HasKey(product => product.Id);
  }
}

Now we use the entity configuration in OnModelCreating and pass the schema to it via constructor. Additionally, we create the interface IDbContextSchema containing just the schema (i.e. a string) to be able to inject it into DemoDbContext.

public interface IDbContextSchema
{
  string Schema { get; }
}

// DbContext implements IDbContextSchema as well
// so we know it is "schema-aware"
public class DemoDbContext : DbContext, IDbContextSchema
{
 public string Schema { get; }

 public DbSet<Product> Products { get; set; }

 public DemoDbContext(DbContextOptions<DemoDbContext> options,
                      IDbContextSchema schema = null)
 : base(options)
 {
   Schema = schema?.Schema;
 }

 protected override void OnModelCreating(ModelBuilder modelBuilder)
 {
   base.OnModelCreating(modelBuilder);

   modelBuilder.ApplyConfiguration(new ProductEntityConfiguration(Schema));
 }
}

We are almost done. The last task is to change how EF is caching database model definitions. By default just the type of the DbContext is used but we need to differentiate the models not just by type but by the schema as well. For that we implement the interface IModelCacheKeyFactory.

public class DbSchemaAwareModelCacheKeyFactory : IModelCacheKeyFactory
{
  public object Create(DbContext context)
  {
    return new {
        Type = context.GetType(),
        Schema = context is IDbContextSchema schema ? schema.Schema : null
    };
  }
}

No we have to replace the default implementation with ours and to register the IDbContextSchema. In current example the IDbContextSchema is just a singleton but it can be provided by anything we want like read from a database or derived from a JWT bearer token during an HTTP request, etc.

IServiceCollection services = ...;

services
  .AddDbContext<DemoDbContext>(
      builder => builder.UseSqlServer("...")
                  .ReplaceService<IModelCacheKeyFactory, DbSchemaAwareModelCacheKeyFactory>())
  .AddSingleton<IDbContextSchema>(new DbContextSchema("demo"));

--------------------------------------------

// just a helper class
public class DbContextSchema : IDbContextSchema
{
  public string Schema { get; }

  public DbContextSchema(string schema)
  {
    Schema = schema ?? throw new ArgumentNullException(nameof(schema));
  }
}

Voila! 

PS: There is one special use case for that feature - isolation of integration tests due to missing support of ambient transactions. For that we need schema-aware migrations we will look at in the next blog post.

Stay tuned!

Related Articles

entity framework core
Unnecessary Fuzzy Searches may hurt your Entity Framework Core Performance
After talking about performance issues like N+1 Queries and the Cartesian Explosion that made its comeback in Entity Framework Core 3, we will today look at a performance issue that is not tied to any Entity Framework version but is rather a general one. What do I mean by…
Pawel Gerr
entity framework core
The performance issue "Cartesian Explosion" made its comeback in Entity Framework Core 3
In Entity Framework Core 3.0/3.1 the SQL statement generation underwent significant changes. As we have seen in the previous post these changes removed both the implicit client-side evaluation and the N+1 Query Problem (which is good!). Unfortunately, these changes (re)introduced…
Pawel Gerr
entity framework core
Is "N+1 Queries" still a performance issue in Entity Framework Core 3?
In a previous post we saw that EF 2.1 is highly susceptible to the N+1 queries problem. After the release of a new version of Entity Framework Core (EF) the first question coming to mind is: "Is it still a big issue in EF 3.1?" And if the answer is no, is there anything else we…
Pawel Gerr
entity framework core
Entity Framework Core 3.0 - "Hidden" GROUP BY Capabilities (Part 2)
In the previous blog post we used a navigational property to work around the limitations of the extension method . The problem is, there is not always such a property we can use for grouping. Especially, when following domain driven design practices, a bidirectional navigation is…
Pawel Gerr