Entity Framework Core: Inheritance - Table-per-Type (TPT) is not supported, is it? (Part 2 - Database First)
Entity Framework Core: Use TransactionScope with Caution!

Entity Framework Core: Changing Database Schema at Runtime

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.com/PawelGerr/Presentation-EntityFrameworkCore

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!

 

Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Your Information

(Name and email address are required. Email address will not be displayed with the comment.)