One Codebase, Three Databases: Runtime DB Provider Switching in EF Core 10

Supporting multiple database providers in a single deployable is not a thought experiment — it is a real requirement when your SaaS product runs inside client infrastructure you do not control. This is how we solved it in a production ASP.NET Core 10 microservice using EF Core's provider model without forking the codebase.

The Problem with Provider-Per-Branch Strategies

When a microservice needs to support SQL Server for one enterprise client, PostgreSQL for another running on AWS RDS, and MySQL for a third locked into a managed hosting plan, the naive solution is to maintain separate branches or separate build targets per provider. That approach collapses inside twelve months. Migrations drift, configuration diverges, and any cross-cutting change has to be applied three times with no guarantee of consistency.

The alternative is to treat the database provider as a runtime configuration concern, not a compile-time decision. EF Core's provider abstraction makes this viable, but the details — schema handling, migration isolation, connection string formats — require deliberate decisions early, or they accumulate as hidden technical debt.

Our microservice connects a video conferencing layer to a multi-tenant data store. Clients deploy it inside their own infrastructure. We had no control over whether they run SQL Server, PostgreSQL, or MySQL. The codebase had to work correctly against all three without shipping different binaries.

Architecture overview — one artifact, three engines
ASP.NET Core 10 Microservice Single published artifact — no separate builds
ProviderType: "SqlServer" | "PostgreSQL" | "MySql"
SQL Server Enterprise / on-prem
PostgreSQL Cloud / AWS RDS
MySQL Managed hosting schema = database — decide naming early

Provider registered at startup — no runtime branching after the first request is handled

Runtime Detection and Conditional Registration

The configuration entry point is a single structured block in appsettings.json. It carries everything the startup pipeline needs: the target provider type, timeout behavior, data protection settings, and a shared migrations history table name that stays consistent across all three engines. Nothing provider-specific leaks into the application configuration surface — the provider selection is the only environment-variable decision that matters.

At application startup, a provider detection helper reads the configured provider type and maps it to an internal enum. That enum value is passed into a generic DbContext registration extension, which conditionally wires in the correct provider package. No runtime branching occurs after startup. The correct provider is registered before the first request is handled, and the rest of the application is unaware of which engine is underneath.

The three provider packages coexist in the project file without conflict. Only the one matching the detected enum is activated. A single published artifact can be deployed against any of the three engines by changing one configuration value and supplying the appropriate connection string.

Schema Isolation, Migration Boundaries, and the MySQL Caveat

SQL Server and PostgreSQL both support named schemas, which we use to isolate table namespaces and separate database-level concerns across environments. Every table, index, and column mapping is defined exclusively through the Fluent API in ModelBuilder — no data annotations on domain entities. This keeps the domain layer entirely provider-agnostic.

MySQL does not support named schemas in the same way. In MySQL, a schema is effectively a database. The workaround is to treat the schema name as a database name during provisioning and adjust the connection string accordingly. This is not a bug in the abstraction — it is a documented limitation of the MySQL engine — but it forces a naming convention decision before the first migration runs. Teams that discover this late end up with inconsistent database names across environments.

Migration isolation is where EF Core's abstraction does not hold. Each provider has its own dedicated migration project. They share the same migrations history table name but are otherwise independent. Running dotnet ef database update against the wrong project against a live database is a real operational risk. Deployment scripts must enforce provider-to-project mapping explicitly.

Tradeoffs Worth Naming Directly

Timeout behavior is configured in one place, but it does not behave identically across providers. PostgreSQL and MySQL implement statement-level timeouts differently than SQL Server's command timeout semantics. Long-running queries need to be validated against each provider individually, not assumed to behave identically.

CI pipeline complexity increases proportionally. Testing all three providers correctly requires three database instances running in parallel — three Docker services, three sets of migrations, three connection strings in CI secrets. The tradeoff is real: you get a single codebase, but the pipeline surface area triples. Teams that underinvest in CI automation here end up testing one provider locally and discovering provider-specific failures only in staging.

Multi-tenant query isolation uses a tenant identifier filter applied at the repository layer. EF Core's LINQ translation handles this consistently across all three providers. There are no provider-specific workarounds needed for tenant scoping — this part of the abstraction holds cleanly.

What This Architecture Buys You

A single deployable that respects the client's existing database infrastructure is a meaningful competitive advantage in B2B SaaS, particularly when selling into enterprise accounts with locked-down environments. The cost is three migration projects, careful CI configuration, and early decisions about MySQL schema naming. Those costs are fixed and manageable.

At Smartnet, we apply this pattern where the deployment environment is genuinely heterogeneous — not as a default for every project. The architecture earns its complexity when it removes a real constraint. When it does, EF Core 10's provider model is robust enough to carry the load.

The webhook validation layer that runs on top of this data tier is covered in Securing Webhook Endpoints: How We Validate Twilio Callbacks in Telemedicine.

Running a .NET service across multiple database engines?

Smartnet designs and implements provider-agnostic data layers for SaaS platforms that deploy into heterogeneous client infrastructure.

Contact us →