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!