Provider-Agnostic Database Support in Identity Base 0.7.1
Published: November 10, 2025
Identity Base was built on PostgreSQL. Every DbContext, every configuration option, every shipped migration assumed Npgsql. If you needed SQL Server support, you faced a choice: fork the library to swap providers, maintain a parallel set of migrations, or abandon Identity Base entirely. The library's internal database choice became your application's constraint.
Identity Base 0.7.1 removes the PostgreSQL dependency entirely. The library no longer ships provider-specific code or migrations. Instead, your host application chooses the database provider - PostgreSQL, SQL Server, SQLite, or any EF Core-supported database - and generates migrations in your own codebase. Identity Base provides the domain logic; you provide the persistence strategy.
This separation required moving migration ownership from the library to the host. Since migrations are provider-specific (PostgreSQL migrations differ from SQL Server migrations), a provider-agnostic library can't ship them. The tradeoff is more setup work in exchange for complete control over your database infrastructure.
⚠️ Breaking Changes
Migration ownership moved to host: Identity Base 0.7.1 no longer ships EF Core migrations. Applications must generate and manage their own migrations for each Identity Base DbContext (IdentityDbContext, AdminDbContext, RolesDbContext, OrganizationsDbContext).
DatabaseOptions removed: The provider-specific DatabaseOptions configuration class has been removed. Applications now configure DbContexts directly via a configureDbContext delegate or by registering contexts in dependency injection.
Configuration approach changed: Previous versions allowed passing a connection string directly to Identity Base. Now you must explicitly configure EF Core provider options (e.g., UseNpgsql, UseSqlServer) and handle migration generation.
The Problem with Provider Coupling
When a library hardcodes a database provider, it makes a choice that propagates through every layer of your infrastructure. Previous versions of Identity Base shipped with Npgsql dependencies baked into the package, referenced PostgreSQL-specific features in configuration, and distributed migrations that only worked with PostgreSQL.
Switching providers required library modifications. If your organization standardized on SQL Server, you couldn't just configure a different connection string. You'd need to fork Identity Base, replace all Npgsql references with Microsoft.EntityFrameworkCore.SqlServer, regenerate migrations for SQL Server's dialect, and maintain that fork as new versions released. The same applied to any other provider - Oracle, MySQL, SQLite for testing. Each required deep changes to library internals.
Infrastructure decisions became permanent early. Choosing Identity Base meant choosing PostgreSQL, often before you'd fully evaluated database requirements. If you later discovered SQL Server's Azure AD integration or Oracle's licensing made more sense for your deployment, migrating meant either replacing the entire authentication system or maintaining a fork indefinitely.
Provider-specific features leaked into APIs. Configuration classes referenced Npgsql types. Documentation assumed PostgreSQL conventions. Sample code used PostgreSQL connection strings. The entire library carried the assumption that PostgreSQL was the only reasonable choice, creating friction for teams with different constraints.
The coupling wasn't accidental - it was deliberately done in order to move fast in the beginning. Migrations were bundled in the packages so that the initial learning curve and setup was easier. However this proved to be a limitation, because EF Core migrations are provider-specific. A migration generated for PostgreSQL uses bytea for binary columns, jsonb for JSON, and PostgreSQL-specific index types. The same migration for SQL Server uses varbinary(max), nvarchar(max), and different index syntax. A library can't ship both simultaneously without maintaining parallel migration paths for every supported provider.
Previous versions chose to ship PostgreSQL migrations and accept the coupling. Identity Base 0.7.1 inverts that choice: remove the provider dependency, let hosts control providers, and move migrations to the host as a necessary consequence.
How Host-Controlled Configuration Works
The new approach inverts the dependency: Identity Base provides DbContext classes with entity configuration, but your host application owns the EF Core provider registration and migration generation.
Configuration via Delegate
When registering Identity Base services, you now pass a configureDbContext delegate that configures the provider and any options. The provider choice is entirely yours:
PostgreSQL:
builder.Services.AddIdentityBase(
configureDbContext: (_, options) =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Identity"))
);
builder.Services.AddOrganizations(
configureDbContext: (_, options) =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Identity"))
);
SQL Server:
builder.Services.AddIdentityBase(
configureDbContext: (_, options) =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Identity"))
);
builder.Services.AddOrganizations(
configureDbContext: (_, options) =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Identity"))
);
SQLite (useful for testing):
builder.Services.AddIdentityBase(
configureDbContext: (_, options) =>
options.UseSqlite("Data Source=identity-test.db")
);
The delegate receives the IServiceProvider and DbContextOptionsBuilder, letting you configure exactly how each context connects to your database. Want to enable provider-specific features like retry policies, query logging, or connection pooling? Configure them directly in the delegate:
builder.Services.AddIdentityBase(
configureDbContext: (_, options) =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("Identity"),
sqlOptions => sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null
)
)
);
This pattern extends to all Identity Base features. The AddIdentityBase, AddOrganizations, AddRoles, and AddAdmin methods all accept the same configureDbContext parameter.
Table Prefixes for Multi-Service Scenarios
Host control also enables custom table naming. The new IdentityDbNamingOptions class lets you define a table prefix that applies to all Identity Base tables and indexes:
builder.Services.AddIdentityBase(
configureDbContext: (_, options) =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Identity")),
configureNaming: naming => naming.UseTablePrefix("ProductService_")
);
With this configuration, Identity Base creates tables named ProductService_Users, ProductService_Sessions, etc., instead of the default Identity_ prefix. This allows multiple services to coexist in the same database without collision.
The naming options integrate directly into the DbContext's OnModelCreating phase, ensuring indexes and constraints follow the same prefix convention. You don't need to manually rename tables in migrations - the prefix applies automatically to all schema objects.
Why This Architecture Makes Sense
Removing the provider dependency follows the dependency inversion principle: high-level domain logic (user management, permissions, organizations) should not depend on low-level infrastructure details (PostgreSQL vs SQL Server). Identity Base defines what entities and relationships exist; your host application decides how they're persisted.
This architectural change solves the provider lock-in problem by pushing the provider decision to the boundary where you have full context. You know your infrastructure constraints - compliance requirements, existing database licenses, team expertise, cloud platform integrations. Identity Base no longer makes assumptions about those constraints.
The consequence is that migrations move to the host. Since migrations encode provider-specific SQL and Identity Base no longer depends on a provider, it can't generate migrations. Your host application generates them using the provider you've configured. This isn't a limitation - it's the mechanism that enables provider agnosticism.
This also aligns with how most .NET applications already handle database configuration. If you're using EF Core for your own application DbContext, you're already registering a provider in Program.cs and generating migrations. Identity Base no longer requires a different mental model - it behaves like any other DbContext in your application.
Generating and Applying Migrations
After configuring the DbContexts, you generate migrations using the standard EF Core tooling. Each Identity Base feature that uses a DbContext needs its own migration folder.
Creating Initial Migrations
From your host application's root directory:
# Generate migrations for Identity Base core
dotnet ef migrations add InitialIdentity \
--context IdentityDbContext \
--output-dir Migrations/Identity
# Generate migrations for Organizations feature
dotnet ef migrations add InitialOrganizations \
--context OrganizationsDbContext \
--output-dir Migrations/Organizations
# Generate migrations for Roles feature
dotnet ef migrations add InitialRoles \
--context RolesDbContext \
--output-dir Migrations/Roles
# Generate migrations for Admin feature
dotnet ef migrations add InitialAdmin \
--context AdminDbContext \
--output-dir Migrations/Admin
These commands generate migration files in your application's codebase, not in the Identity Base library. The migrations reflect your chosen provider (PostgreSQL, SQL Server, etc.) and any naming customizations you've configured.
Applying Migrations at Startup
Identity Base's startup migrator works with host-generated migrations. The migrator calls Database.Migrate() on each registered DbContext during application startup, applying any pending migrations before seeding data:
var app = builder.Build();
// Migrator runs host-generated migrations, then seeds default data
await app.Services.MigrateIdentityBaseDatabaseAsync();
app.Run();
The MigrateIdentityBaseDatabaseAsync extension method detects which features you've enabled and migrates only the corresponding DbContexts. If you've only registered AddIdentityBase and AddOrganizations, it migrates just those two contexts.
Design-Time Factory for Migrations
The EF Core tools require a way to instantiate DbContexts at design time (when running dotnet ef migrations add). If your configuration depends on IConfiguration or dependency injection, you'll need a design-time factory:
public class IdentityDbContextFactory
: IDesignTimeDbContextFactory<IdentityDbContext>
{
public IdentityDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<IdentityDbContext>();
// Read connection string from appsettings or environment
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
optionsBuilder.UseNpgsql(
configuration.GetConnectionString("Identity")
);
return new IdentityDbContext(
optionsBuilder.Options,
new IdentityDbNamingOptions { TablePrefix = "Identity_" }
);
}
}
Place this factory class in your host application. The EF Core tools will discover it automatically when generating migrations, ensuring the design-time context matches your runtime configuration.
Migrating from 0.7.0 to 0.7.1
If you're upgrading from a previous Identity Base version that used library-shipped migrations, you'll need to take ownership of the migration history.
Step 1: Remove Old Configuration
Previous versions used DatabaseOptions to configure the provider:
// Old approach - remove this
builder.Services.AddIdentityBase(new DatabaseOptions
{
ConnectionString = builder.Configuration.GetConnectionString("Identity")
});
Replace this with the new configureDbContext delegate:
// New approach
builder.Services.AddIdentityBase(
configureDbContext: (_, options) =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Identity"))
);
Step 2: Generate Initial Migrations
Run the dotnet ef migrations add commands shown earlier to generate migrations in your host application. These migrations will include all tables and schema elements that Identity Base needs.
Step 3: Handle Existing Databases
If you already have an Identity Base database from a previous version, you have two options:
Option A: Baseline the migration
If your existing database already contains all the tables that would be created by the initial migration, you can add the migration without applying it:
# Generate the migration
dotnet ef migrations add InitialIdentity --context IdentityDbContext
# Add migration record without running the migration
dotnet ef database update InitialIdentity --context IdentityDbContext --connection "your-connection-string"
This tells EF Core that the InitialIdentity migration has already been applied, even though you didn't actually run it. Future migrations will build on this baseline.
Option B: Fresh migration from scratch
If you're in early development or can recreate your database, simply drop the existing database and let the migrator create a new one with your host-generated migrations:
dotnet ef database drop --context IdentityDbContext
# Then run your app - the migrator will apply all pending migrations
Step 4: Verify Migration Application
After updating your configuration and generating migrations, run your application in a development environment. Check the logs for migration application messages:
[Information] Applying migration 'InitialIdentity'.
[Information] Applying migration 'InitialOrganizations'.
[Information] Identity Base database migration completed successfully.
Query your database to verify the tables exist with correct names (including any custom prefix you configured):
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name LIKE 'Identity_%';
Troubleshooting Common Issues
"No DbContext named 'IdentityDbContext' was found"
This error occurs when running dotnet ef migrations add without a design-time factory. The EF Core tools can't instantiate the DbContext because it depends on configuration from your DI container.
Fix: Create an IDesignTimeDbContextFactory<IdentityDbContext> implementation as shown in the "Design-Time Factory for Migrations" section above. The factory provides a way to construct the DbContext using only design-time configuration, without running the full application startup.
"Table 'Identity_Users' already exists" During Migration
If you're migrating an existing database and see table creation errors, you likely ran the migration against a database that already has Identity Base tables from a previous version.
Fix: Use the baseline approach from the migration guide. Generate the initial migration, then manually insert a record into the __EFMigrationsHistory table indicating that migration has already been applied:
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20251108000000_InitialIdentity', '9.0.0');
Replace the timestamp and version with values from your generated migration file. This tells EF Core the migration is already applied without actually running the table creation statements.
Design-Time Factory Uses Wrong Connection String
If your migrations generate correctly but create tables in an unexpected database, your design-time factory might be reading configuration from a different source than your runtime application.
Fix: Ensure your IDesignTimeDbContextFactory reads from the same configuration file as your application. If you use environment-specific configuration (appsettings.Development.json), explicitly load that file in the factory:
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json", optional: true)
.Build();
Table Prefix Not Applied to Indexes or Foreign Keys
If your custom table prefix applies to table names but not to indexes or foreign keys, you may be running migrations generated before configuring the prefix.
Fix: Delete the generated migration files, configure your naming options, then regenerate the migrations. The prefix applies during model creation, so it must be configured before running dotnet ef migrations add:
# Remove incorrect migrations
rm -rf Migrations/Identity
# Regenerate with prefix configured
dotnet ef migrations add InitialIdentity \
--context IdentityDbContext \
--output-dir Migrations/Identity
What This Enables
Removing provider coupling transforms Identity Base from a PostgreSQL-specific library into a database-agnostic component. The primary gain is freedom to choose your database based on your actual infrastructure constraints rather than library limitations.
Use any EF Core provider. PostgreSQL, SQL Server, SQLite, MySQL, Oracle - if EF Core supports it, Identity Base works with it. Teams with existing SQL Server infrastructure can now adopt Identity Base without introducing PostgreSQL to their stack. Organizations with Oracle licenses can use them. Development teams can run SQLite for fast local testing while deploying to PostgreSQL in production.
Align with organizational standards. If your company standardizes on SQL Server with Azure AD integration, or requires Oracle for compliance reasons, or has PostgreSQL expertise in-house, Identity Base no longer fights those decisions. The library fits your infrastructure instead of forcing you to accommodate its requirements.
Optimize for your specific provider. Configure connection resilience, retry policies, query logging, and performance features specific to your chosen database. Enable SQL Server's query store for performance analysis, or PostgreSQL's statement timeout, or SQLite's WAL mode for testing - all through the standard EF Core configuration you control.
Run multiple services in one database. The table prefix customization lets you namespace Identity Base tables per service (ProductService_Users, AnalyticsService_Users), enabling database consolidation when that matches your deployment strategy. This benefit exists independently of provider choice but complements it by removing another infrastructure constraint.
The library now focuses exclusively on identity and access management logic - user authentication, permissions, organizations, roles - while remaining silent on database choice. Infrastructure decisions stay where they belong: in your application, where you have full context about constraints, requirements, and tradeoffs.