Switch startup DB upgrades to EF migrations
This commit is contained in:
4
FAQ.md
4
FAQ.md
@@ -26,6 +26,10 @@ Backend state is persisted via EF Core + SQLite.
|
||||
|
||||
To start with a clean backend state, stop the app and remove the corresponding SQLite file.
|
||||
|
||||
## Do I need to run manual DB migrations after updating the app?
|
||||
|
||||
Usually no. Startup now uses EF Core migration-based schema upgrades (`Database.Migrate`) and applies pending migrations automatically.
|
||||
|
||||
## Does the backend read SQLite on every API call?
|
||||
|
||||
No. The backend loads state from SQLite once during startup into in-memory state and serves requests from memory. Successful state mutations are then written back to SQLite.
|
||||
|
||||
@@ -35,7 +35,7 @@ Backend state persistence:
|
||||
- EF Core with SQLite (`Microsoft.EntityFrameworkCore.Sqlite`)
|
||||
- Development DB: `RpgRoller/App_Data/rpgroller.development.db`
|
||||
- Default DB: `RpgRoller/App_Data/rpgroller.db`
|
||||
- Database schema is created automatically on startup (`EnsureCreated`)
|
||||
- Database schema is created/upgraded automatically on startup via EF Core migrations (`Database.Migrate`)
|
||||
- Runtime state is loaded once at startup into memory and written back to SQLite on successful state changes
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using RpgRoller.Hosting;
|
||||
using RpgRoller.Data;
|
||||
|
||||
namespace RpgRoller.Tests;
|
||||
|
||||
@@ -27,4 +31,99 @@ public sealed class HostingCoverageTests
|
||||
Assert.Contains(services, d => d.ServiceType == typeof(RpgRoller.Services.IGameService));
|
||||
Assert.Contains(services, d => d.ServiceType == typeof(RpgRoller.Services.IDiceRoller));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SqliteSchemaUpgrader_MigratesLegacySkillsSchema()
|
||||
{
|
||||
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-legacy-upgrade-{Guid.NewGuid():N}.db");
|
||||
var connectionString = $"Data Source={dbPath}";
|
||||
|
||||
using (var connection = new SqliteConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText =
|
||||
"""
|
||||
CREATE TABLE "Users" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY,
|
||||
"Username" TEXT NOT NULL,
|
||||
"UsernameNormalized" TEXT NOT NULL,
|
||||
"PasswordHash" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NOT NULL,
|
||||
"ActiveCharacterId" TEXT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Sessions" (
|
||||
"Token" TEXT NOT NULL CONSTRAINT "PK_Sessions" PRIMARY KEY,
|
||||
"UserId" TEXT NOT NULL,
|
||||
"CreatedAtUtc" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Campaigns" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Campaigns" PRIMARY KEY,
|
||||
"GmUserId" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"Ruleset" TEXT NOT NULL,
|
||||
"Version" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Characters" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Characters" PRIMARY KEY,
|
||||
"OwnerUserId" TEXT NOT NULL,
|
||||
"CampaignId" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "Skills" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Skills" PRIMARY KEY,
|
||||
"CharacterId" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DiceRollDefinition" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "RollLogEntries" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_RollLogEntries" PRIMARY KEY,
|
||||
"CampaignId" TEXT NOT NULL,
|
||||
"CharacterId" TEXT NOT NULL,
|
||||
"SkillId" TEXT NOT NULL,
|
||||
"RollerUserId" TEXT NOT NULL,
|
||||
"Visibility" TEXT NOT NULL,
|
||||
"Result" INTEGER NOT NULL,
|
||||
"Breakdown" TEXT NOT NULL,
|
||||
"TimestampUtc" TEXT NOT NULL
|
||||
);
|
||||
""";
|
||||
_ = command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
var options = new DbContextOptionsBuilder<RpgRollerDbContext>()
|
||||
.UseSqlite(connectionString)
|
||||
.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning))
|
||||
.Options;
|
||||
|
||||
using (var db = new RpgRollerDbContext(options))
|
||||
{
|
||||
SqliteSchemaUpgrader.ApplyPendingChanges(db);
|
||||
}
|
||||
|
||||
using var verifyConnection = new SqliteConnection(connectionString);
|
||||
verifyConnection.Open();
|
||||
|
||||
using var tableInfoCommand = verifyConnection.CreateCommand();
|
||||
tableInfoCommand.CommandText = "PRAGMA table_info('Skills');";
|
||||
using var tableInfoReader = tableInfoCommand.ExecuteReader();
|
||||
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
while (tableInfoReader.Read())
|
||||
{
|
||||
columns.Add(tableInfoReader.GetString(1));
|
||||
}
|
||||
|
||||
Assert.Contains("WildDice", columns);
|
||||
Assert.Contains("AllowFumble", columns);
|
||||
|
||||
using var historyCommand = verifyConnection.CreateCommand();
|
||||
historyCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226084000_InitialSchema';";
|
||||
var historyCount = Convert.ToInt32(historyCommand.ExecuteScalar());
|
||||
Assert.Equal(1, historyCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using RpgRoller.Contracts;
|
||||
@@ -32,7 +33,9 @@ public abstract class ApiTestBase : IClassFixture<WebApplicationFactory<Program>
|
||||
services.RemoveAll<RpgRollerDbContext>();
|
||||
|
||||
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db");
|
||||
services.AddDbContextFactory<RpgRollerDbContext>(options => options.UseSqlite($"Data Source={dbPath}"));
|
||||
services.AddDbContextFactory<RpgRollerDbContext>(options =>
|
||||
options.UseSqlite($"Data Source={dbPath}")
|
||||
.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning)));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ public static class ApplicationInitializationExtensions
|
||||
using var scope = app.Services.CreateScope();
|
||||
var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<RpgRollerDbContext>>();
|
||||
using var db = dbFactory.CreateDbContext();
|
||||
db.Database.EnsureCreated();
|
||||
SqliteSchemaUpgrader.ApplyPendingChanges(db);
|
||||
_ = scope.ServiceProvider.GetRequiredService<IGameService>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using RpgRoller.Data;
|
||||
using RpgRoller.Domain;
|
||||
using RpgRoller.Services;
|
||||
@@ -18,7 +19,9 @@ public static class ServiceCollectionExtensions
|
||||
EnsureSqliteDataDirectory(sqliteConnectionString, environment.ContentRootPath);
|
||||
|
||||
services.AddSingleton<IPasswordHasher<UserAccount>, PasswordHasher<UserAccount>>();
|
||||
services.AddDbContextFactory<RpgRollerDbContext>(options => options.UseSqlite(sqliteConnectionString));
|
||||
services.AddDbContextFactory<RpgRollerDbContext>(options =>
|
||||
options.UseSqlite(sqliteConnectionString)
|
||||
.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning)));
|
||||
services.AddSingleton<IDiceRoller, RandomDiceRoller>();
|
||||
services.AddSingleton<IGameService, GameService>();
|
||||
|
||||
|
||||
104
RpgRoller/Hosting/SqliteSchemaUpgrader.cs
Normal file
104
RpgRoller/Hosting/SqliteSchemaUpgrader.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RpgRoller.Data;
|
||||
|
||||
namespace RpgRoller.Hosting;
|
||||
|
||||
public static class SqliteSchemaUpgrader
|
||||
{
|
||||
private const string InitialMigrationId = "20260226084000_InitialSchema";
|
||||
private const string ProductVersion = "10.0.2";
|
||||
|
||||
public static void ApplyPendingChanges(RpgRollerDbContext db)
|
||||
{
|
||||
if (db.Database.IsSqlite())
|
||||
{
|
||||
EnsureLegacySchemaHistory(db);
|
||||
}
|
||||
|
||||
db.Database.Migrate();
|
||||
}
|
||||
|
||||
private static void EnsureLegacySchemaHistory(RpgRollerDbContext db)
|
||||
{
|
||||
db.Database.OpenConnection();
|
||||
try
|
||||
{
|
||||
if (TableExists(db, "__EFMigrationsHistory"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TableExists(db, "Skills"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ColumnExists(db, "Skills", "WildDice") || !ColumnExists(db, "Skills", "AllowFumble"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var createHistoryCommand = db.Database.GetDbConnection().CreateCommand();
|
||||
createHistoryCommand.CommandText =
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
||||
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
||||
"ProductVersion" TEXT NOT NULL
|
||||
);
|
||||
""";
|
||||
_ = createHistoryCommand.ExecuteNonQuery();
|
||||
|
||||
using var insertHistoryCommand = db.Database.GetDbConnection().CreateCommand();
|
||||
insertHistoryCommand.CommandText =
|
||||
"""
|
||||
INSERT OR IGNORE INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ($migrationId, $productVersion);
|
||||
""";
|
||||
|
||||
var migrationParameter = insertHistoryCommand.CreateParameter();
|
||||
migrationParameter.ParameterName = "$migrationId";
|
||||
migrationParameter.Value = InitialMigrationId;
|
||||
insertHistoryCommand.Parameters.Add(migrationParameter);
|
||||
|
||||
var productVersionParameter = insertHistoryCommand.CreateParameter();
|
||||
productVersionParameter.ParameterName = "$productVersion";
|
||||
productVersionParameter.Value = ProductVersion;
|
||||
insertHistoryCommand.Parameters.Add(productVersionParameter);
|
||||
|
||||
_ = insertHistoryCommand.ExecuteNonQuery();
|
||||
}
|
||||
finally
|
||||
{
|
||||
db.Database.CloseConnection();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TableExists(RpgRollerDbContext db, string tableName)
|
||||
{
|
||||
using var command = db.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$name LIMIT 1;";
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = "$name";
|
||||
parameter.Value = tableName;
|
||||
command.Parameters.Add(parameter);
|
||||
|
||||
return command.ExecuteScalar() is not null;
|
||||
}
|
||||
|
||||
private static bool ColumnExists(RpgRollerDbContext db, string tableName, string columnName)
|
||||
{
|
||||
using var command = db.Database.GetDbConnection().CreateCommand();
|
||||
command.CommandText = $"PRAGMA table_info('{tableName}');";
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var currentColumnName = reader.GetString(1);
|
||||
if (string.Equals(currentColumnName, columnName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
111
RpgRoller/Migrations/20260226084000_InitialSchema.cs
Normal file
111
RpgRoller/Migrations/20260226084000_InitialSchema.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using RpgRoller.Data;
|
||||
|
||||
namespace RpgRoller.Migrations;
|
||||
|
||||
[DbContext(typeof(RpgRollerDbContext))]
|
||||
[Migration("20260226084000_InitialSchema")]
|
||||
public sealed class InitialSchema : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "Users" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY,
|
||||
"Username" TEXT NOT NULL,
|
||||
"UsernameNormalized" TEXT NOT NULL,
|
||||
"PasswordHash" TEXT NOT NULL,
|
||||
"DisplayName" TEXT NOT NULL,
|
||||
"ActiveCharacterId" TEXT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "Sessions" (
|
||||
"Token" TEXT NOT NULL CONSTRAINT "PK_Sessions" PRIMARY KEY,
|
||||
"UserId" TEXT NOT NULL,
|
||||
"CreatedAtUtc" TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "Campaigns" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Campaigns" PRIMARY KEY,
|
||||
"GmUserId" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"Ruleset" TEXT NOT NULL,
|
||||
"Version" INTEGER NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "Characters" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Characters" PRIMARY KEY,
|
||||
"OwnerUserId" TEXT NOT NULL,
|
||||
"CampaignId" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "Skills" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_Skills" PRIMARY KEY,
|
||||
"CharacterId" TEXT NOT NULL,
|
||||
"Name" TEXT NOT NULL,
|
||||
"DiceRollDefinition" TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
ALTER TABLE "Skills" ADD COLUMN "WildDice" INTEGER NOT NULL DEFAULT 1;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
ALTER TABLE "Skills" ADD COLUMN "AllowFumble" INTEGER NOT NULL DEFAULT 1;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "RollLogEntries" (
|
||||
"Id" TEXT NOT NULL CONSTRAINT "PK_RollLogEntries" PRIMARY KEY,
|
||||
"CampaignId" TEXT NOT NULL,
|
||||
"CharacterId" TEXT NOT NULL,
|
||||
"SkillId" TEXT NOT NULL,
|
||||
"RollerUserId" TEXT NOT NULL,
|
||||
"Visibility" TEXT NOT NULL,
|
||||
"Result" INTEGER NOT NULL,
|
||||
"Breakdown" TEXT NOT NULL,
|
||||
"TimestampUtc" TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""CREATE UNIQUE INDEX IF NOT EXISTS "IX_Users_UsernameNormalized" ON "Users" ("UsernameNormalized");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Sessions_UserId" ON "Sessions" ("UserId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Campaigns_GmUserId" ON "Campaigns" ("GmUserId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Characters_OwnerUserId" ON "Characters" ("OwnerUserId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Characters_CampaignId" ON "Characters" ("CampaignId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Skills_CharacterId" ON "Skills" ("CharacterId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_RollLogEntries_CampaignId" ON "RollLogEntries" ("CampaignId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_RollLogEntries_RollerUserId" ON "RollLogEntries" ("RollerUserId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_RollLogEntries_SkillId" ON "RollLogEntries" ("SkillId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_RollLogEntries_CharacterId" ON "RollLogEntries" ("CharacterId");""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""DROP TABLE IF EXISTS "RollLogEntries";""");
|
||||
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Skills";""");
|
||||
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Characters";""");
|
||||
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Campaigns";""");
|
||||
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Sessions";""");
|
||||
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Users";""");
|
||||
}
|
||||
}
|
||||
78
RpgRoller/Migrations/RpgRollerDbContextModelSnapshot.cs
Normal file
78
RpgRoller/Migrations/RpgRollerDbContextModelSnapshot.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using RpgRoller.Data;
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Migrations;
|
||||
|
||||
[DbContext(typeof(RpgRollerDbContext))]
|
||||
internal sealed class RpgRollerDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity<Campaign>(entity =>
|
||||
{
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
entity.Property(x => x.Ruleset).HasConversion<string>().IsRequired();
|
||||
entity.Property(x => x.Version).IsRequired();
|
||||
entity.HasIndex(x => x.GmUserId);
|
||||
entity.ToTable("Campaigns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Character>(entity =>
|
||||
{
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
entity.HasIndex(x => x.OwnerUserId);
|
||||
entity.HasIndex(x => x.CampaignId);
|
||||
entity.ToTable("Characters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<RollLogEntry>(entity =>
|
||||
{
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.Visibility).HasConversion<string>().IsRequired();
|
||||
entity.Property(x => x.Breakdown).IsRequired().HasMaxLength(256);
|
||||
entity.Property(x => x.TimestampUtc).IsRequired();
|
||||
entity.HasIndex(x => x.CampaignId);
|
||||
entity.HasIndex(x => x.RollerUserId);
|
||||
entity.HasIndex(x => x.SkillId);
|
||||
entity.HasIndex(x => x.CharacterId);
|
||||
entity.ToTable("RollLogEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Skill>(entity =>
|
||||
{
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
entity.Property(x => x.DiceRollDefinition).IsRequired().HasMaxLength(128);
|
||||
entity.Property(x => x.WildDice).IsRequired();
|
||||
entity.Property(x => x.AllowFumble).IsRequired();
|
||||
entity.HasIndex(x => x.CharacterId);
|
||||
entity.ToTable("Skills");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UserAccount>(entity =>
|
||||
{
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.Username).IsRequired().HasMaxLength(64);
|
||||
entity.Property(x => x.UsernameNormalized).IsRequired().HasMaxLength(64);
|
||||
entity.Property(x => x.PasswordHash).IsRequired();
|
||||
entity.Property(x => x.DisplayName).IsRequired().HasMaxLength(128);
|
||||
entity.HasIndex(x => x.UsernameNormalized).IsUnique();
|
||||
entity.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UserSession>(entity =>
|
||||
{
|
||||
entity.HasKey(x => x.Token);
|
||||
entity.Property(x => x.Token).HasMaxLength(64);
|
||||
entity.Property(x => x.CreatedAtUtc).IsRequired();
|
||||
entity.HasIndex(x => x.UserId);
|
||||
entity.ToTable("Sessions");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user