EF Cross-Provider Compatibility Guide¶
SkyCMS supports four database providers through a single codebase: MS SQL Server, MySQL, SQLite, and Azure Cosmos DB. This guide documents the query restrictions, patterns, and provider detection strategy that maintain compatibility across all four.
Audience: Developers
Provider Detection¶
Strategy Pattern¶
CosmosDbOptionsBuilder uses a strategy pattern to automatically detect the database provider from a connection string:
| Strategy | Detection Pattern | Provider |
|---|---|---|
CosmosDbConfigurationStrategy |
AccountEndpoint= in connection string |
Azure Cosmos DB |
SqlServerConfigurationStrategy |
Server= or Data Source= (case-insensitive) |
MS SQL Server |
MySqlConfigurationStrategy |
server= (lowercase) with MySQL-specific patterns |
MySQL |
SqliteConfigurationStrategy |
.db file extension or SQLite-specific data source |
SQLite |
The first matching strategy configures the DbContextOptionsBuilder. If no strategy matches, an exception is thrown.
Usage¶
var options = CosmosDbOptionsBuilder.GetDbOptions<ApplicationDbContext>(connectionString);
This is used throughout the application — both in the Editor and Publisher — to configure EF Core without hardcoding a specific provider.
Cosmos DB Query Restrictions¶
Cosmos DB's EF Core provider has significant limitations compared to relational providers. All LINQ queries in the application must respect these restrictions.
1. No Cross-Container Joins¶
Cosmos DB does not support joins between different entity types stored in separate containers.
// WRONG — Cosmos DB cannot execute this:
var result = from article in db.Articles
join author in db.Authors on article.AuthorId equals author.Id
select new { article, author };
// CORRECT — Sequential queries with client-side correlation:
var articles = await db.Articles
.Where(a => a.Status == publishedStatus)
.ToListAsync();
var authorIds = articles.Select(a => a.AuthorId).Distinct().ToList();
var authors = await db.Authors
.Where(a => authorIds.Contains(a.Id))
.ToListAsync();
var authorLookup = authors.ToDictionary(a => a.Id);
// Correlate in memory
2. No Inline Casts in Expression Trees¶
The Cosmos DB EF Core provider cannot translate inline casts (e.g., (int)SomeEnum.Value) inside lambda expressions.
// WRONG — Cosmos DB cannot translate the cast:
var articles = await db.Articles
.Where(a => a.StatusCode == (int)ArticleStatus.Published)
.ToListAsync();
// CORRECT — Pre-compute the cast into a local variable:
int publishedStatus = (int)ArticleStatus.Published;
var articles = await db.Articles
.Where(a => a.StatusCode == publishedStatus)
.ToListAsync();
This applies to all enum-to-int conversions and any similar cast expressions within LINQ predicates.
3. Provider-Specific Warning Suppression¶
ApplicationDbContext.OnConfiguring() detects the provider and suppresses appropriate warnings:
| Provider | Suppressed Warning | Reason |
|---|---|---|
| Cosmos DB | CosmosEventId.SyncNotSupported |
Cosmos requires async operations |
| Relational | RelationalEventId.PendingModelChangesWarning |
Multi-provider migrations coexist in the same assembly |
Migration Strategy¶
Shared Migration Assembly¶
Migrations live in the Common project and are shared across relational providers (SQL Server, MySQL, SQLite). Cosmos DB does not use EF migrations — its schema is managed at runtime.
Adding Migrations¶
Use the AddMigrationScript.ps1 script to create new migrations:
.\AddMigrationScript.ps1
The script targets the correct project and context. When running against different providers, the migration is applied according to the provider's capabilities.
Database Initialization¶
ApplicationDbContext.EnsureDatabaseExists() has two overloads:
- Relational — Standard
EnsureCreated()or migration apply. - Cosmos DB — Checks for required containers (
Identity,Articles) using the Cosmos client API directly.
FlexDb Multi-Provider Identity¶
The AspNetCore.Identity.FlexDb library extends ASP.NET Core Identity to work across all four providers:
- Uses
CosmosDbOptionsBuilderfor provider auto-detection. - Identity tables (
AspNetUsers,AspNetRoles, etc.) work on all providers. - Passkey support (
IdentityUserPasskey<TKey>) is tested across all providers. - Default authentication scheme:
IdentityConstants.ApplicationSchemewith login path/Account/Login.
Registration¶
builder.Services.AddCosmosIdentity<ApplicationDbContext, IdentityUser, IdentityRole, string>(options =>
{
// Identity options configured here
});
Cross-Provider Testing¶
The AspNetCore.Identity.FlexDb.Tests project validates compatibility across all providers:
[DataTestMethod]
[DataRow(TestDatabaseProvider.Cosmos)]
[DataRow(TestDatabaseProvider.SqlServer)]
[DataRow(TestDatabaseProvider.MySql)]
[DataRow(TestDatabaseProvider.Sqlite)]
public async Task PasskeyLifecycle_UserManager_Test(TestDatabaseProvider provider)
{
// Runs the same test against all 4 providers
}
Over 24 interoperability tests cover:
- User creation, update, deletion
- Role assignment and verification
- Passkey registration and authentication
- Email confirmation flows
- Password hashing and validation
Best Practices for Contributors¶
- Always pre-compute casts — Never use inline casts in LINQ predicates.
- Avoid joins — Use sequential queries with client-side correlation instead.
- Test against multiple providers — Use
[DataRow]parameterized tests with all provider values. - Use async everywhere — Cosmos DB does not support synchronous operations.
- Don't assume relational features — Features like
LIKE,DATEPART,GROUP BYwith aggregates may not translate to Cosmos DB. - Check provider before using provider-specific features — Guard with
Database.ProviderNamechecks when unavoidable.
See Also¶
- Tenant Isolation Reference — How ApplicationDbContext filters by tenant
- Publisher Architecture — How the Publisher configures EF
- Blog Architecture — Cosmos DB compatibility in blog queries
- Multi-Tenancy Deep Dive — Architectural overview