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 DbContext class — 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:

  1. Add the OutWit.Database.EntityFramework NuGet package
  2. Change UseSqlServer() to UseWitDb()
  3. Regenerate migrations
  4. 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
Email 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

  1. The application starts
  2. DatabaseInitializerService runs as a hosted service
  3. EF Core migrations are applied (creates all tables)
  4. Default roles are created (Administrator, User)
  5. Admin user is created and assigned Administrator role
  6. 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:

  1. Connects to the database (or creates the file if it doesn't exist)
  2. Checks the __EFMigrationsHistory table for applied migrations
  3. Runs any pending migrations in order
  4. 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.

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 (UseSqlServerUseWitDb), 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.