Switch startup DB upgrades to EF migrations
This commit is contained in:
@@ -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