Support multiple DB providers in EF Core

April 5th 2024 EF Core .NET

In most projects, EF Core is only used with a single database provider. However, I'm currently working on a project, which requires us to transition from one database provider to another. And since we can't simply stop all other development until this process is complete, we need to support two different database providers in parallel, at least temporarily.

Fortunately, most of the code can remain the same for both database providers. For example, entity classes and their attributes:

public class Person(string firstName, string lastName)
{
    public int Id { get; set; }

    [MaxLength(50)]
    public string FirstName { get; set; } = firstName;

    [MaxLength(50)]
    public string LastName { get; set; } = lastName;
}

This doesn't work if each provider requires different property values for an attribute. For example, with SQL Server, we used a Table attribute to set the schema and table name for the entity:

[Table("People", Schema = "Sample")]

This wasn't an option for MySQL because schemas are equivalent to databases, so there can't be multiple schemas in a single database. We decided to prefix the table names instead:

[Table("Sample_People")]

But of course, there can't be two different Table attributes on a single entity, one for each database provider. To work around this limitation, we decided to use the fluent API in such cases instead of data annotation attributes. This allowed us to take advantage of inheritance and have a different OnModelCreating method for each provider.

Most of the database context code remained in the common abstract base class:

public abstract class SampleContextBase(DbContextOptions options) : DbContext(options)
{
    public DbSet<Person> People { get; set; }
}

Individual database context classes for each provider derive from this base class, each implementing their own OnModelCreating method:

public class SqlServerSampleContext(DbContextOptions<SqlServerSampleContext> options)
    : SampleContextBase(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Person>().ToTable("People", "Sample");
    }
}

public class MySqlSampleContext(DbContextOptions<MySqlSampleContext> options)
    : SampleContextBase(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Person>().ToTable("Sample_People");
    }
}

For dependency injection, we register the correct implementation depending on which type of database we want the running application instance to connect to:

switch (provider)
{
    case ProviderType.SqlServer:
        services.AddDbContext<SampleContextBase, SqlServerSampleContext>(options =>
        {
            options.UseSqlServer(
                "Server=(localdb)\\mssqllocaldb;Database=EfCoreMultipleProviders;Trusted_Connection=True;MultipleActiveResultSets=true"
            );
        });
        break;
    case ProviderType.MySql:
        services.AddDbContext<SampleContextBase, MySqlSampleContext>(options =>
        {
            var serverVersion = ServerVersion.Create(8, 0, 35, ServerType.MySql);
            options.UseMySql(
                "Server=localhost;Database=EfCoreMultipleProviders;User=root;Password=root",
                serverVersion
            );
        });
        break;
}

This allows the correct context to be injected when the base class is requested:

var context = serviceProvider.GetRequiredService<SampleContextBase>();

Two different database providers also means two separate sets of migrations. The process for achieving that is well documented. Having two context classes, one for each provider, is almost everything that's needed.

When generating migrations, you now need to specify the context class to use and the output directory for the generated files:

Add-Migration InitialCreate -Context SqlServerSampleContext -OutputDir SqlServer\Migrations
Add-Migration InitialCreate -Context MySqlSampleContext -OutputDir MySql/Migrations

Similarly, the context class must be specified when running the migrations:

Update-Database -Context SqlServerSampleContext
Update-Database -Context MySqlSampleContext

That's all there is to it. After running these commands, you'll have your database set up for both database types:

Database schema in SQL Server and MySQL

A working sample project is available in my GitHub repository. The class library contains the two contexts and the migrations created as described in this post. The test demonstrates how to conditionally register the correct context class for each provider and inject it as a dependency elsewhere in code.

Although EF Core samples usually don't show how to support multiple database providers in a single project, this isn't difficult to set up. With a minimum amount of additional plumbing code, it works well. You only need to use slightly more verbose commands to generate the migrations for both providers, and to run them.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License