Let's be clear about what this sample demonstrates: nothing special.
That's the point.
ASP.NET Core Identity is Microsoft's standard library for user authentication. OpenIddict is a popular OAuth 2.0 / OpenID Connect server. Both rely heavily on Entity Framework Core for persistence. Both expect a real database with migrations, transactions, and relational integrity.
WitDatabase delivers all of that. You take any existing tutorial for ASP.NET Identity, any OpenIddict guide, any EF Core migration workflow — and it just works. No special adapters. No workarounds. No "almost compatible" disclaimers.
The sample includes:
- User registration and login with password hashing and lockout
- Role-based authorization (Administrator, User)
- OAuth 2.0 endpoints for token issuance
- EF Core migrations for schema management
- User management UI with full CRUD
All running on a single .witdb file. No SQL Server. No PostgreSQL. No external database to install or configure.
If you've ever thought "I'd use an embedded database, but I need real EF Core support" — this sample is for you.
The One-Line Change
Here's the complete diff when switching from SQL Server to WitDatabase:
Before (SQL Server):
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
After (WitDatabase):
services.AddDbContext<ApplicationDbContext>(options =>
options.UseWitDb(connectionString));
That's it. One method call changes. Everything else in your application stays exactly the same:
- Your
DbContextclass — unchanged - Your entity models — unchanged
- Your Identity configuration — unchanged
- Your migrations — regenerated, but same workflow
- Your LINQ queries — unchanged
- Your
SaveChangesAsync()calls — unchanged - Your application code — unchanged
Connection String Comparison
Different providers, same pattern:
| Provider | Connection String |
|---|---|
| SQL Server | Server=localhost;Database=MyApp;... |
| PostgreSQL | Host=localhost;Database=MyApp;... |
| SQLite | Data Source=myapp.db |
| WitDatabase | Data Source=myapp.witdb |
WitDatabase's connection string is the simplest — just point to a file. No server address, no credentials, no port numbers. The file is created automatically if it doesn't exist.
You can also add options:
Data Source=myapp.witdb;Encryption=aes-gcm;Password=secret
But for most cases, just the file path is enough.
What This Means
If you have an existing ASP.NET Core application with Identity, you can:
- Add the
OutWit.Database.EntityFrameworkNuGet package - Change
UseSqlServer()toUseWitDb() - Regenerate migrations
- Run
No code changes beyond that one line. Your controllers, services, ViewModels, Razor views — all unchanged. The database is now an embedded file instead of an external server.
What This Sample Demonstrates
This isn't a minimal example. It's a realistic application using standard ASP.NET patterns:
| Component | What It Does | WitDatabase Role |
|---|---|---|
| ASP.NET Core Identity | User accounts, password hashing, lockout | Stores users, roles, claims |
| OpenIddict | OAuth 2.0 / OpenID Connect server | Stores tokens, authorizations, applications |
| EF Core Migrations | Schema versioning | Applies migrations like any database |
| IHostedService | Startup initialization | Seeds roles and admin user |
| MVC + Razor Views | User interface | N/A (just the UI layer) |
| Role-based Authorization | Access control | Stores role assignments |
Each of these components uses EF Core in the standard way. None of them know or care that WitDatabase is the underlying store. They see a DbContext, they call SaveChangesAsync(), they run LINQ queries — and it works.
Project Setup
# Clone and run
git clone https://github.com/dmitrat/WitDatabase.git
cd WitDatabase/Samples/OutWit.Database.Samples.IdentityServer
dotnet run
Open https://localhost:7200 in your browser.
Default Credentials
| Field | Value |
|---|---|
admin@example.com |
|
| Password | Admin123! |
The admin account is created automatically on first run. Log in to access the user management interface.
What Happens on First Run
- The application starts
DatabaseInitializerServiceruns as a hosted service- EF Core migrations are applied (creates all tables)
- Default roles are created (Administrator, User)
- Admin user is created and assigned Administrator role
- The application is ready
All of this happens automatically. No manual database setup, no SQL scripts to run.
Project Structure
[[Svg Src="./witdatabase-identity-project-structure.svg" Alt="witdatabase-identity-project-structure"]]
This is a standard ASP.NET Core MVC structure. The only WitDatabase-specific code is in Program.cs — one line that says UseWitDb() instead of UseSqlServer().
DbContext: Standard EF Core
The ApplicationDbContext looks exactly like any Identity project:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Rename Identity tables (optional, but cleaner)
builder.Entity<ApplicationUser>().ToTable("Users");
builder.Entity<ApplicationRole>().ToTable("Roles");
builder.Entity<IdentityUserRole<int>>().ToTable("UserRoles");
// ... more table renames
// Custom configuration
builder.Entity<ApplicationUser>(entity =>
{
entity.Property(e => e.FirstName).HasMaxLength(100);
entity.HasIndex(e => e.Email).IsUnique();
});
}
}
Notice what's not here: anything WitDatabase-specific. This is standard EF Core code. The IdentityDbContext base class, the OnModelCreating overrides, the Fluent API configuration — all of it works identically whether you're using SQL Server, PostgreSQL, SQLite, or WitDatabase.
Tables Created by Identity
When migrations run, EF Core creates these tables:
| Table | Purpose |
|---|---|
| Users | User accounts (email, password hash, etc.) |
| Roles | Role definitions (Administrator, User, etc.) |
| UserRoles | Many-to-many: which users have which roles |
| UserClaims | Custom claims attached to users |
| UserLogins | External login providers (Google, Facebook, etc.) |
| UserTokens | Password reset tokens, 2FA tokens, etc. |
| RoleClaims | Custom claims attached to roles |
Plus, OpenIddict adds its own tables:
| Table | Purpose |
|---|---|
| OpenIddictApplications | OAuth client applications |
| OpenIddictAuthorizations | Granted authorizations |
| OpenIddictScopes | Defined scopes (openid, profile, etc.) |
| OpenIddictTokens | Issued access/refresh tokens |
All of these are created automatically by EF Core migrations. You don't write CREATE TABLE statements — you define C# models, and the tooling generates the schema.
Registration in Program.cs
The only place WitDatabase appears is in Program.cs:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseWitDb(connectionString);
options.UseOpenIddict<int>();
});
The UseOpenIddict<int>() call registers OpenIddict's entity sets. It's not WitDatabase-specific — you'd write the same line with any database provider.
Custom Identity Models: Nothing Special
Extending Identity's user and role classes is a common pattern. Here's the custom user:
public class ApplicationUser : IdentityUser<int>
{
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string DisplayName =>
Loading...
quot;{FirstName} {LastName}".Trim();
public string? AvatarUrl { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? LastLoginAt { get; set; }
public bool IsActive { get; set; } = true;
}
And the custom role:
public class ApplicationRole : IdentityRole<int>
{
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
This is the standard way to extend Identity. You inherit from IdentityUser<TKey> or IdentityRole<TKey>, add your properties, and EF Core handles the rest.
The int type parameter means primary keys are integers instead of GUIDs. This is common for performance reasons and is required by OpenIddict's default configuration.
Again — nothing here is WitDatabase-specific. This code would work identically with SQL Server.
Migrations: Just Works
EF Core migrations work exactly as expected. The workflow is identical to any other database provider.
Creating Migrations
# Create the initial migration
dotnet ef migrations add InitialCreate
# Create subsequent migrations after model changes
dotnet ef migrations add AddUserProfileFields
EF Core compares your current model to the last migration snapshot and generates the difference. The output looks like:
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
The migration files appear in your Migrations folder — just like with SQL Server or PostgreSQL.
Applying Migrations
You can apply migrations via CLI:
dotnet ef database update
Or in code (as this sample does):
await context.Database.MigrateAsync(cancellationToken);
The code approach is common for applications that need to self-initialize — the database is created and migrated on first run.
What Happens Under the Hood
When you apply migrations, EF Core:
- Connects to the database (or creates the file if it doesn't exist)
- Checks the
__EFMigrationsHistory table for applied migrations
- Runs any pending migrations in order
- Records each migration in the history table
WitDatabase supports all of this. The __EFMigrationsHistory table is created automatically, just like with other providers.
Full Migration Support
Operation
Command
Support
Add migration
dotnet ef migrations add Name
✅
Apply migrations
dotnet ef database update
✅
Remove last migration
dotnet ef migrations remove
✅
Generate SQL script
dotnet ef migrations script
✅
Revert to specific migration
dotnet ef database update MigrationName
✅
List migrations
dotnet ef migrations list
✅
The full EF Core migration workflow is available. You can develop locally, script migrations for deployment, and manage schema versions — all with standard tooling.
Identity Configuration: Copy-Paste from Any Tutorial
The Identity setup is textbook ASP.NET Core. You can copy this from Microsoft's documentation or any Identity tutorial — it's not WitDatabase-specific at all.
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
// Password policy
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 8;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
// User settings
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
What Each Setting Does
Password Policy — defines the rules for acceptable passwords. The sample requires 8+ characters with uppercase, lowercase, digits, and symbols. This is stored nowhere in the database — it's validation logic that runs when users create or change passwords.
Lockout Settings — protects against brute-force attacks. After 5 failed login attempts, the account is locked for 5 minutes. The lockout state is stored in the Users table (columns like LockoutEnd and AccessFailedCount).
Unique Email — ensures no two users can register with the same email. This is enforced by a unique index in the database.
The Key Line
.AddEntityFrameworkStores<ApplicationDbContext>()
This tells Identity to use your DbContext for persistence. Identity doesn't know what database you're using — it just calls EF Core methods. EF Core talks to WitDatabase. The abstraction is clean.
Cookie Configuration
builder.Services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.AccessDeniedPath = "/Account/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromDays(14);
options.SlidingExpiration = true;
});
This configures where users are redirected for login, how long sessions last, and whether the expiration slides forward on activity. Again — standard ASP.NET Core configuration, nothing database-specific.
OpenIddict Configuration: Standard Setup
OpenIddict adds OAuth 2.0 and OpenID Connect capabilities to your application. If you're not familiar with these protocols, here's the short version:
- OAuth 2.0 — lets third-party applications access your API on behalf of users
- OpenID Connect — adds user identity on top of OAuth 2.0 (login with your service)
The Configuration
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>()
.ReplaceDefaultEntities<int>();
})
.AddServer(options =>
{
// Enable OAuth flows
options.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.AllowClientCredentialsFlow();
// Define endpoints
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token");
// Define available scopes
options.RegisterScopes("openid", "profile", "email", "roles");
// Development certificates (use real certs in production!)
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Integrate with ASP.NET Core
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
Understanding the Flows
Flow
Use Case
Authorization Code
Web apps where users log in (most common)
Client Credentials
Server-to-server communication, no user involved
Refresh Token
Get new access tokens without re-login
The Authorization Code flow is what happens when you click "Login with Google" or "Login with GitHub" — your app redirects to the auth server, the user logs in, and the app receives a code to exchange for tokens.
Available Endpoints
Endpoint
URL
Purpose
Authorization
/connect/authorize
Start the login flow
Token
/connect/token
Exchange code for tokens
These are standard OAuth 2.0 endpoints. Any OAuth client library can work with them.
Where WitDatabase Fits
OpenIddict needs to store:
- Applications — registered OAuth clients
- Authorizations — which users authorized which apps
- Tokens — issued access and refresh tokens
- Scopes — available permission scopes
All of this goes into the database through EF Core. OpenIddict calls SaveChangesAsync(), EF Core translates to SQL, WitDatabase stores the data. OpenIddict doesn't know or care what database is underneath — it just uses the DbContext.
The sample uses development certificates for simplicity. In production, you'd configure proper X.509 certificates for signing and encryption — but that's an OpenIddict concern, not a database concern.
Database Seeding: IHostedService Pattern
The sample seeds initial data using a hosted service:
public class DatabaseInitializerService : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
// Apply migrations
await context.Database.MigrateAsync(cancellationToken);
// Seed roles
await SeedRolesAsync(roleManager);
// Seed admin user
await SeedAdminUserAsync(userManager);
}
}
Role seeding uses the standard RoleManager:
if (!await roleManager.RoleExistsAsync("Administrator"))
{
await roleManager.CreateAsync(new ApplicationRole("Administrator")
{
Description = "Full system access"
});
}
User seeding uses the standard UserManager:
var admin = await userManager.FindByEmailAsync("admin@example.com");
if (admin == null)
{
admin = new ApplicationUser
{
UserName = "admin@example.com",
Email = "admin@example.com",
FirstName = "Admin",
IsActive = true,
EmailConfirmed = true
};
await userManager.CreateAsync(admin, "Admin123!");
await userManager.AddToRoleAsync(admin, "Administrator");
}
This is the recommended pattern for ASP.NET Core applications. The seeding logic uses Identity's APIs, not raw SQL. Those APIs work through EF Core, which works with WitDatabase.
What You Get
When you run the sample, you have a complete identity server:
- Login page with email/password authentication
- Logout with proper cookie cleanup
- User management (list, create, edit, delete, activate/deactivate)
- Role-based access (only Administrators can manage users)
- OAuth 2.0 endpoints for external applications
- Account lockout after failed login attempts
- Password policy enforcement
All persisted in a single identity_server.witdb file. Copy the file to back up everything. Delete it to start fresh.
When to Use This
Scenario
Fit
Development & testing
✅ Perfect — no database setup
Prototypes & demos
✅ Perfect — self-contained
Single-server deployments
✅ Good — simple architecture
Small internal tools
✅ Good — minimal maintenance
High-traffic production
⚠️ Consider — evaluate performance needs
Multi-server clusters
❌ Not ideal — use shared database
For development, the benefits are clear: clone the repo, run dotnet run, and you have a working identity server. No Docker, no connection strings to external databases, no setup at all.
Summary
This sample proves a simple point: WitDatabase is a real EF Core database provider.
Feature
Status
EF Core DbContext
✅ Works
EF Core Migrations
✅ Works
ASP.NET Core Identity
✅ Works
UserManager / RoleManager
✅ Works
OpenIddict
✅ Works
OAuth 2.0 flows
✅ Works
Role-based authorization
✅ Works
You can take existing Identity code, change one line (UseSqlServer → UseWitDb), and run it. Migrations work. Queries work. Transactions work. It's not a compatibility layer or a subset — it's a full implementation.
Get the Code
git clone https://github.com/dmitrat/WitDatabase.git
cd WitDatabase/Samples/OutWit.Database.Samples.IdentityServer
dotnet run
Questions? Open an issue on GitHub.