A flexible, multi-database implementation of ASP.NET Core Identity that automatically selects the appropriate database provider based on your connection string. Supports Azure Cosmos DB, SQL Server, MySQL, and SQLite with seamless switching between providers.
Part of the SkyCMS Project - An open-source, multi-tenant content management system built on ASP.NET Core.
- What's New
- Overview
- Supported Database Providers
- Quick Start
- Architecture
- Configuration Options
- Security Features
- Database Migration
- Performance Considerations
- Advanced Usage
- NuGet Package Information
- Troubleshooting
- Contributing
- License
- .NET 10 support with C# 14.0 features
- Enhanced Strategy Pattern with improved provider detection
- Thread-safe stateless strategy implementations
- Improved documentation with comprehensive XML comments
- Performance optimizations for all providers
- Better error messages with detailed provider information
AspNetCore.Identity.FlexDb eliminates the need to choose a specific database provider at compile time. Simply provide a connection string, and the library automatically configures the correct Entity Framework provider using the Strategy Pattern.
- Zero Code Changes: Switch databases by changing connection strings only
- Rapid Development: No provider-specific configuration needed
- Multi-Environment: Use MySQL for dev, SQL Server for staging, Cosmos DB for production
- Single Package: All providers in one NuGet package
- Extensible: Add custom providers by implementing
IDatabaseConfigurationStrategy - Secure: Built-in personal data encryption and protection
| Feature | Description |
|---|---|
| Automatic Provider Detection | Intelligently selects database provider from connection string patterns |
| Multi-Database Support | Cosmos DB, SQL Server, and MySQL out of the box |
| Strategy Pattern | Clean, extensible architecture for adding new providers |
| Azure Integration | Native support for Azure Cosmos DB and Azure SQL Database |
| Backward Compatibility | Supports legacy Cosmos DB configurations |
| Personal Data Protection | Built-in encryption for sensitive user data |
| Thread-Safe | All operations are thread-safe and concurrent-friendly |
| Well Documented | Comprehensive XML documentation for all public APIs |
Best for: Global-scale, cloud-native applications requiring low latency and high availability
| Aspect | Details |
|---|---|
| Provider Priority | 10 (highest) |
| Detection Pattern | AccountEndpoint= |
| Features | Multi-region replication, automatic scaling, NoSQL flexibility |
| Connection String | AccountEndpoint=https://account.documents.azure.com:443/;AccountKey=key;Database=dbname; |
| Use Cases | Global apps, serverless architectures, document-based data models |
Optimizations:
- Optimized partition key strategy for user data
- Minimized RU consumption
- Efficient batch operations
Best for: Enterprise applications requiring ACID compliance and relational integrity
| Aspect | Details |
|---|---|
| Provider Priority | 20 |
| Detection Pattern | Server= or User ID= |
| Features | Full ACID compliance, advanced indexing, enterprise features |
| Connection String | Server=server;Initial Catalog=database;User ID=user;Password=password; |
| Use Cases | Enterprise apps, complex reporting, existing SQL Server infrastructure |
Best for: Open-source projects, cost-effective hosting, LAMP stack integration
| Aspect | Details |
|---|---|
| Provider Priority | 30 |
| Detection Pattern | uid= or user id= (with server=) |
| Features | Open source, wide hosting support, good performance |
| Connection String | Server=server;Port=3306;uid=user;pwd=password;database=dbname; |
| Use Cases | Linux hosting, open-source projects, budget-conscious deployments |
FlexDb uses a sophisticated detection system to automatically select the appropriate database provider based on connection string patterns.
Implementation classes:
- Strategy selection entry point: CosmosDbOptionsBuilder
- Strategy contract: IDatabaseConfigurationStrategy
- Provider inference helpers: Utilities
- Cosmos DB detection: CosmosDbConfigurationStrategy
- SQL Server detection: SqlServerConfigurationStrategy
- MySQL detection: MySqlConfigurationStrategy
- SQLite detection: SqliteConfigurationStrategy
If you need to debug provider detection or fully understand how FlexDb chooses a provider, start with CosmosDbOptionsBuilder.
The detection flow is implemented there in four steps:
DefaultStrategiesloads the built-in provider strategies in priority order:CosmosDbConfigurationStrategySqlServerConfigurationStrategyMySqlConfigurationStrategySqliteConfigurationStrategy
ConfigureDbOptions(DbContextOptionsBuilder optionsBuilder, string connectionString)delegates to the overload that accepts a strategy collection, passingDefaultStrategies.ConfigureDbOptions(DbContextOptionsBuilder optionsBuilder, string connectionString, IEnumerable<IDatabaseConfigurationStrategy> strategies)orders the strategies byPriority, then evaluates each strategy'sCanHandle(connectionString)method.- The first strategy whose
CanHandle(...)returnstrueis selected, and then itsConfigure(...)method is executed to wire up the EF Core provider.
In other words, the actual detection decision is not hardcoded in a single if/else chain in production code. Instead, FlexDb asks each registered strategy whether it can handle the connection string, picks the first match by priority, and then lets that strategy configure the provider.
When debugging provider selection, these are the most useful files to inspect:
- CosmosDbOptionsBuilder: entry point for provider selection, strategy ordering, first-match lookup, and final
Configure(...)call. - IDatabaseConfigurationStrategy: contract that defines
Priority,ProviderName,CanHandle(...), andConfigure(...). - CosmosDbConfigurationStrategy: Cosmos DB connection-string detection and EF Core Cosmos configuration.
- SqlServerConfigurationStrategy: SQL Server detection and EF Core SQL Server configuration.
- MySqlConfigurationStrategy: MySQL detection and EF Core MySQL configuration.
- SqliteConfigurationStrategy: SQLite detection and EF Core SQLite configuration.
- Utilities: helper methods such as
InferDatabaseProvider(...)andInferDatabaseProviderShortName(...), which use the same strategy-selection mechanism and are useful when you want to inspect what provider FlexDb thinks a connection string maps to.
If detection is not behaving as expected, this is the quickest way to trace it:
- Open CosmosDbOptionsBuilder and inspect
DefaultStrategiesto confirm which strategies are registered and in what order. - Inspect
ConfigureDbOptions(...)to see how the incoming connection string is passed through the ordered strategies. - Open the relevant strategy class and inspect its
CanHandle(...)implementation to verify the exact string patterns it expects. - If a provider is selected unexpectedly, compare its
Priorityagainst the other matching strategies. Lower priority numbers are evaluated first. - If you only want to inspect the inferred provider without configuring EF Core, use the helper methods in Utilities.
Detection Algorithm:
public class ProviderDetection
{
public static IDatabaseConfigurationStrategy DetectProvider(string connectionString)
{
// Priority 10: Cosmos DB (highest priority)
if (connectionString.Contains("AccountEndpoint=", StringComparison.OrdinalIgnoreCase))
{
return new CosmosDbConfigurationStrategy();
}
// Priority 20: SQL Server
if (connectionString.Contains("Server=", StringComparison.OrdinalIgnoreCase) ||
connectionString.Contains("User ID=", StringComparison.OrdinalIgnoreCase))
{
return new SqlServerConfigurationStrategy();
}
// Priority 30: MySQL
if (connectionString.Contains("server=", StringComparison.OrdinalIgnoreCase) &&
connectionString.Contains("database=", StringComparison.OrdinalIgnoreCase))
{
return new MySqlConfigurationStrategy();
}
// Priority 40: SQLite (lowest priority)
if (connectionString.Contains("Data Source=", StringComparison.OrdinalIgnoreCase) &&
connectionString.EndsWith(".db", StringComparison.OrdinalIgnoreCase))
{
return new SqliteConfigurationStrategy();
}
throw new NotSupportedException($"Unable to detect database provider from connection string");
}
}Strategy Interface:
public interface IDatabaseConfigurationStrategy
{
int Priority { get; } // Lower number = higher priority
string ProviderName { get; }
bool CanHandle(string connectionString);
void ConfigureDbContext(DbContextOptionsBuilder options, string connectionString);
void ConfigureModel(ModelBuilder modelBuilder);
}Custom Strategy Example:
using AspNetCore.Identity.FlexDb.Strategies;
public class PostgreSqlConfigurationStrategy : IDatabaseConfigurationStrategy
{
public int Priority => 25; // Between SQL Server and MySQL
public string ProviderName => "PostgreSQL";
public bool CanHandle(string connectionString)
{
return connectionString.Contains("Host=") &&
connectionString.Contains("Username=");
}
public void ConfigureDbContext(DbContextOptionsBuilder options, string connectionString)
{
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorCodesToAdd: null);
});
}
public void ConfigureModel(ModelBuilder modelBuilder)
{
// PostgreSQL-specific model configuration
modelBuilder.HasDefaultSchema("identity");
}
}
// Register custom strategy
services.AddCosmosIdentity<IdentityUser, IdentityRole, string>(
connectionString,
customStrategies: new[] { new PostgreSqlConfigurationStrategy() });Override Detection:
// Force a specific provider
services.AddDbContext<CosmosIdentityDbContext<IdentityUser, IdentityRole, string>>(
options =>
{
// Bypass detection, use SQL Server directly
options.UseSqlServer(connectionString);
});Strategy Selection Logging:
public class LoggingStrategySelector
{
private readonly ILogger _logger;
public IDatabaseConfigurationStrategy SelectStrategy(
string connectionString,
IEnumerable<IDatabaseConfigurationStrategy> strategies)
{
var selected = strategies
.Where(s => s.CanHandle(connectionString))
.OrderBy(s => s.Priority)
.FirstOrDefault();
if (selected == null)
{
_logger.LogError("No strategy found for connection string pattern");
throw new InvalidOperationException("Unable to determine database provider");
}
_logger.LogInformation(
"Selected {Provider} strategy (Priority: {Priority})",
selected.ProviderName,
selected.Priority);
return selected;
}
}Personal Data Encryption:
FlexDb includes PersonalDataConverter to automatically encrypt sensitive user information.
using AspNetCore.Identity.FlexDb;
using Microsoft.AspNetCore.Identity;
public class ApplicationUser : IdentityUser
{
[PersonalData]
[ProtectedPersonalData] // Automatically encrypted
public string? PhoneNumber { get; set; }
[PersonalData]
[ProtectedPersonalData]
public string? SocialSecurityNumber { get; set; }
}
// Configuration
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"./keys"))
.SetApplicationName("SkyCMS");
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<CosmosIdentityDbContext<ApplicationUser, IdentityRole, string>>()
.AddDefaultTokenProviders()
.AddPersonalDataProtection<PersonalDataConverter, PersonalDataProtectionKeyProvider>();
}
}How PersonalDataConverter Works:
public class PersonalDataConverter : IPersonalDataConverter
{
private readonly IDataProtector _protector;
public string? Protect(string? data)
{
if (string.IsNullOrEmpty(data))
return data;
return _protector.Protect(data);
}
public string? Unprotect(string? data)
{
if (string.IsNullOrEmpty(data))
return data;
return _protector.Unprotect(data);
}
}Retry Policies:
FlexDb includes built-in retry logic for transient failures.
using AspNetCore.Identity.FlexDb;
public class RetryConfiguration
{
public static async Task<T> ExecuteWithRetryAsync<T>(
Func<Task<T>> operation,
int maxRetries = 3,
int delayMilliseconds = 100)
{
return await Retry.ExecuteAsync(
operation,
maxRetries,
delayMilliseconds,
onRetry: (ex, attempt) =>
{
Console.WriteLine($"Retry {attempt} after error: {ex.Message}");
});
}
}
// Usage in repository
public async Task<IdentityUser?> FindByIdAsync(string userId)
{
return await RetryConfiguration.ExecuteWithRetryAsync(async () =>
{
return await _dbContext.Users.FindAsync(userId);
});
}Cosmos DB Retry Policy:
services.AddDbContext<CosmosIdentityDbContext<IdentityUser, IdentityRole, string>>(
options => options.UseCosmos(
connectionString,
databaseName,
cosmosOptions =>
{
cosmosOptions.MaxRetryCount(3);
cosmosOptions.MaxRetryWaitTimeOnRateLimitedRequests(TimeSpan.FromSeconds(30));
}));SQL Server Retry Policy:
services.AddDbContext<CosmosIdentityDbContext<IdentityUser, IdentityRole, string>>(
options => options.UseSqlServer(
connectionString,
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: new[] { 4060, 40197, 40501, 40613, 49918 });
}));GDPR Compliance - Data Export:
public async Task<Dictionary<string, string>> ExportPersonalDataAsync(string userId)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return new Dictionary<string, string>();
var personalData = new Dictionary<string, string>();
var personalDataProps = typeof(IdentityUser).GetProperties()
.Where(prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
foreach (var prop in personalDataProps)
{
var value = prop.GetValue(user)?.ToString() ?? "null";
personalData.Add(prop.Name, value);
}
return personalData;
}Optimizations:
- Connection pooling
- Optimized indexes on email and username
- Retry policies for transient failures
Best for: Lightweight, file-based storage for testing or small applications
| Aspect | Details |
|---|---|
| Provider Priority | 40 (lowest) |
| Detection Pattern | Data Source= |
| Features | Simple file-based storage, no server required |
| Connection String | Data Source=app.db; |
| Use Cases | Testing, development, small standalone apps |
Install the NuGet package:
dotnet add package AspNetCore.Identity.FlexDbusing AspNetCore.Identity.FlexDb;
using AspNetCore.Identity.FlexDb.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Connection string determines the provider automatically
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
CosmosDbOptionsBuilder.ConfigureDbOptions(options, connectionString));
builder.Services.AddCosmosIdentity<ApplicationDbContext, IdentityUser, IdentityRole, string>(
options =>
{
options.Password.RequiredLength = 8;
options.User.RequireUniqueEmail = true;
},
cookieExpireTimeSpan: TimeSpan.FromDays(30),
slidingExpiration: true
);
var app = builder.Build();
app.MapGet("/healthz", () => Results.Ok("ok"));
app.Run();
public class ApplicationDbContext : CosmosIdentityDbContext<IdentityUser, IdentityRole, string>
{
public ApplicationDbContext(DbContextOptions options) : base(options) { }
}Tip: The key name DefaultConnection is arbitrary—use any name, as long as you pass the same connection string into ConfigureDbOptions.
using AspNetCore.Identity.FlexDb;
using AspNetCore.Identity.FlexDb.Extensions;
using Microsoft.AspNetCore.Identity;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Connection string determines the provider automatically
var connectionString = Configuration.GetConnectionString("DefaultConnection");
// Add FlexDb Identity with automatic provider detection
services.AddDbContext<ApplicationDbContext>(options =>
CosmosDbOptionsBuilder.ConfigureDbOptions(options, connectionString));
services.AddCosmosIdentity<ApplicationDbContext, IdentityUser, IdentityRole, string>(
options =>
{
options.Password.RequiredLength = 8;
options.User.RequireUniqueEmail = true;
});
}
}
// Your DbContext
public class ApplicationDbContext : CosmosIdentityDbContext<IdentityUser, IdentityRole, string>
{
public ApplicationDbContext(DbContextOptions options) : base(options)
{
}
}{
"ConnectionStrings": {
"CosmosDb": "AccountEndpoint=https://myaccount.documents.azure.com:443/;AccountKey=mykey;Database=MyDatabase;",
"SqlServer": "Server=tcp:myserver.database.windows.net,1433;Initial Catalog=MyDatabase;User ID=myuser;Password=mypassword;",
"MySQL": "Server=myserver;Port=3306;uid=myuser;pwd=mypassword;database=MyDatabase;",
"SQLite": "Data Source=app.db;"
}
}The main database context that extends Entity Framework's IdentityDbContext:
public class CosmosIdentityDbContext<TUser, TRole, TKey> : IdentityDbContext<TUser, TRole, TKey>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>Features:
- Provider-Specific Configuration: Automatically adapts to database provider
- Cosmos DB Optimizations: Special handling for document database patterns
- Backward Compatibility: Support for legacy Cosmos DB configurations
- Entity Configuration: Optimized mappings for each database type
Automatic database provider configuration utility:
public static class CosmosDbOptionsBuilder
{
public static DbContextOptions<TContext> GetDbOptions<TContext>(string connectionString)
public static void ConfigureDbOptions(DbContextOptionsBuilder optionsBuilder, string connectionString)
}Provider Detection Logic:
- Cosmos DB: Detects
AccountEndpoint=pattern - SQL Server: Detects
User IDpattern - MySQL: Detects
uid=pattern - SQLite: Detects
Data Source=pattern
Custom store implementations for multi-provider support:
- CosmosUserStore: User management with provider-specific optimizations
- CosmosRoleStore: Role management across database types
- IdentityStoreBase: Common functionality and error handling
Abstracted data access layer:
public interface IRepository
{
string ProviderName { get; }
TEntity GetById<TEntity>(string id) where TEntity : class, new();
IQueryable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class, new();
Task<int> SaveChangesAsync();
}services.AddCosmosIdentity<ApplicationDbContext, IdentityUser, IdentityRole, string>(
options =>
{
// Password requirements
options.Password.RequiredLength = 8;
options.Password.RequireDigit = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
// User settings
options.User.RequireUniqueEmail = true;
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// Sign-in settings
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
},
cookieExpireTimeSpan: TimeSpan.FromDays(30),
slidingExpiration: true
);public class ApplicationDbContext : CosmosIdentityDbContext<IdentityUser, IdentityRole, string>
{
public ApplicationDbContext(DbContextOptions options)
: base(options, backwardCompatibility: false)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Additional custom configuration
builder.Entity<IdentityUser>()
.HasPartitionKey(u => u.Id);
}
}Enable encryption for sensitive user data:
services.Configure<IdentityOptions>(options =>
{
options.Stores.ProtectPersonalData = true;
});
services.AddSingleton<IPersonalDataProtector, MyPersonalDataProtector>();- Personal Data Encryption: Automatic encryption of PII fields
- Secure Key Management: Integration with ASP.NET Core Data Protection
- Provider-Agnostic: Works across all supported database types
- Cookie Authentication: Configurable expiration and sliding windows
- Two-Factor Authentication: Built-in 2FA support
- External Providers: OAuth integration ready
- Role-Based Access: Traditional role management
- Claims-Based Security: Fine-grained permission system
- Policy-Based Authorization: Flexible authorization policies
FlexDb makes it easy to migrate between database providers:
- Update Connection String: Change to target database format
- Migrate Data: Use Entity Framework migrations or custom migration logic
- No Code Changes: Application code remains unchanged
// From Cosmos DB to SQL Server
// Old: "AccountEndpoint=...;AccountKey=...;Database=MyDb;"
// New: "Server=...;Initial Catalog=MyDb;User ID=...;Password=...;"
// Entity Framework handles the provider switch automatically
await context.Database.MigrateAsync();- Partition Key Strategy: Optimized partitioning for user data
- Query Efficiency: Minimized RU consumption
- Bulk Operations: Efficient batch processing
- Connection Pooling: Optimized client connections
- Indexing Strategy: Appropriate indexes for Identity queries
- Connection Pooling: Efficient connection management
- Query Optimization: Optimized LINQ to SQL translations
public class ApplicationUser : IdentityUser<string>
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime CreatedDate { get; set; }
}
public class ApplicationRole : IdentityRole<string>
{
public string Description { get; set; }
}
public class ApplicationDbContext : CosmosIdentityDbContext<ApplicationUser, ApplicationRole, string>
{
public ApplicationDbContext(DbContextOptions options) : base(options) { }
}public class CustomRepository : IRepository
{
private readonly ApplicationDbContext _context;
public CustomRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<ApplicationUser> GetUserByEmailAsync(string email)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.NormalizedEmail == email.ToUpper());
}
}public class MultiTenantDbContext : CosmosIdentityDbContext<IdentityUser, IdentityRole, string>
{
private readonly string _tenantId;
public MultiTenantDbContext(DbContextOptions options, string tenantId)
: base(options)
{
_tenantId = tenantId;
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Add tenant filtering
builder.Entity<IdentityUser>().HasQueryFilter(u => u.TenantId == _tenantId);
}
}- Package ID:
AspNetCore.Identity.FlexDb - Target Framework: .NET 10.0
- Repository: SkyCMS on GitHub
- Project Home: AspNetCore.Identity.FlexDb
- License: MIT License
- Part of: SkyCMS Project - Multi-tenant content management system
- Dependencies:
- Microsoft.AspNetCore.Identity.EntityFrameworkCore (10.0.5)
- Microsoft.EntityFrameworkCore.Cosmos (10.0.5)
- Microsoft.EntityFrameworkCore.SqlServer (10.0.5)
- Microting.EntityFrameworkCore.MySql (10.0.5)
- AspNetCore.Identity.CosmosDb (10.0.5.1)
// Ensure connection string format matches expected patterns
// Cosmos DB: Must include "AccountEndpoint="
// SQL Server: Must include "User ID"
// MySQL: Must include "uid="
// SQLite: Must include "Data Source="// Ensure database and containers exist
await context.Database.EnsureCreatedAsync();// For provider switches, consider custom migration logic
public async Task MigrateFromCosmosToSql()
{
// Export data from Cosmos DB
// Transform data structure if needed
// Import to SQL Server
}- Use appropriate partition keys
- Optimize queries to avoid cross-partition operations
- Monitor RU consumption
- Ensure proper indexing on email and username fields
- Use connection pooling
- Consider read replicas for read-heavy workloads
- Fork the repository
- Create a feature branch
- Implement changes with tests
- Submit a pull request
- Follow .NET coding standards
- Include unit tests for new providers
- Update documentation for new features
- Ensure backward compatibility
This project is licensed under the MIT License.
MIT License
Copyright (c) Moonrise Software, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- SkyCMS: Multi-tenant content management system using this identity provider
- SkyCMS Editor: Content editing interface
- SkyCMS Publisher: Public website engine
- Issues: GitHub Issues
- Documentation: SkyCMS Documentation
- NuGet: Package Page
AspNetCore.Identity.FlexDb - One Identity Provider, Multiple Database Options