Use in-memory runtime state with SQLite write-through

This commit is contained in:
2026-02-24 23:13:20 +01:00
parent f212636feb
commit 5c199b4468
4 changed files with 743 additions and 455 deletions

View File

@@ -238,26 +238,41 @@ public sealed class GameServiceTests
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), new CreateSkillRequest("Stealth", "2D+1")).Succeeded); Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")).Succeeded); Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")).Succeeded);
var ownerUser = harness.Db.Users.Single(u => u.UsernameNormalized == "OWNER"); using (var db = harness.CreateDbContext())
ownerUser.ActiveCharacterId = Guid.NewGuid(); {
harness.Db.SaveChanges(); var ownerUser = db.Users.Single(u => u.UsernameNormalized == "OWNER");
ownerUser.ActiveCharacterId = Guid.NewGuid();
db.SaveChanges();
}
var staleMe = GetValue(service.GetMe(ownerSession)); using var staleMeHarness = CreateHarnessFromPath(harness.DbPath, 2, 3, 4);
var staleMeService = staleMeHarness.Service;
var staleMe = GetValue(staleMeService.GetMe(ownerSession));
Assert.Null(staleMe.ActiveCharacterId); Assert.Null(staleMe.ActiveCharacterId);
Assert.Null(staleMe.CurrentCampaignId); Assert.Null(staleMe.CurrentCampaignId);
Assert.True(service.ActivateCharacter(ownerSession, ownerCharacter.Id).Succeeded); Assert.True(staleMeService.ActivateCharacter(ownerSession, ownerCharacter.Id).Succeeded);
var activeMe = GetValue(service.GetMe(ownerSession)); var activeMe = GetValue(staleMeService.GetMe(ownerSession));
Assert.Equal(ownerCharacter.Id, activeMe.ActiveCharacterId); Assert.Equal(ownerCharacter.Id, activeMe.ActiveCharacterId);
Assert.Equal(campaign.Id, activeMe.CurrentCampaignId); Assert.Equal(campaign.Id, activeMe.CurrentCampaignId);
var staleOwner = harness.Db.Users.Single(u => u.Id == ownerUser.Id); using (var db = harness.CreateDbContext())
staleOwner.ActiveCharacterId = Guid.NewGuid(); {
harness.Db.SaveChanges(); var staleOwner = db.Users.Single(u => u.UsernameNormalized == "OWNER");
staleOwner.ActiveCharacterId = Guid.NewGuid();
db.SaveChanges();
}
var staleCurrentCampaign = service.GetCurrentCampaignCharacters(ownerSession); using var staleCurrentHarness = CreateHarnessFromPath(harness.DbPath, 2, 3, 4);
var staleCurrentService = staleCurrentHarness.Service;
var staleCurrentCampaign = staleCurrentService.GetCurrentCampaignCharacters(ownerSession);
Assert.False(staleCurrentCampaign.Succeeded); Assert.False(staleCurrentCampaign.Succeeded);
Assert.Null(harness.Db.Users.Single(u => u.Id == ownerUser.Id).ActiveCharacterId); using (var db = harness.CreateDbContext())
{
Assert.Null(db.Users.Single(u => u.UsernameNormalized == "OWNER").ActiveCharacterId);
}
var skill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1"))); var skill = GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, new CreateSkillRequest("Stealth", "2D+1")));
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("", "2D+1")).Succeeded); Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("", "2D+1")).Succeeded);
@@ -265,11 +280,15 @@ public sealed class GameServiceTests
Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("Stealth", "bad")).Succeeded); Assert.False(service.UpdateSkill(ownerSession, skill.Id, new UpdateSkillRequest("Stealth", "bad")).Succeeded);
Assert.False(service.RollSkill(string.Empty, skill.Id, new RollSkillRequest("public")).Succeeded); Assert.False(service.RollSkill(string.Empty, skill.Id, new RollSkillRequest("public")).Succeeded);
var mutableSkill = harness.Db.Skills.Single(s => s.Id == skill.Id); using (var db = harness.CreateDbContext())
mutableSkill.DiceRollDefinition = "bad"; {
harness.Db.SaveChanges(); var mutableSkill = db.Skills.Single(s => s.Id == skill.Id);
mutableSkill.DiceRollDefinition = "bad";
db.SaveChanges();
}
Assert.False(service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("public")).Succeeded); using var invalidExpressionHarness = CreateHarnessFromPath(harness.DbPath, 2, 3, 4);
Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, new RollSkillRequest("public")).Succeeded);
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded); Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
} }
@@ -346,15 +365,28 @@ public sealed class GameServiceTests
private static ServiceHarness CreateHarness(IPasswordHasher<UserAccount> passwordHasher, params int[] rollValues) private static ServiceHarness CreateHarness(IPasswordHasher<UserAccount> passwordHasher, params int[] rollValues)
{ {
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-servicetests-{Guid.NewGuid():N}.db"); var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-servicetests-{Guid.NewGuid():N}.db");
return CreateHarnessFromPath(dbPath, passwordHasher, rollValues);
}
private static ServiceHarness CreateHarnessFromPath(string dbPath, params int[] rollValues)
{
return CreateHarnessFromPath(dbPath, new PasswordHasher<UserAccount>(), rollValues);
}
private static ServiceHarness CreateHarnessFromPath(string dbPath, IPasswordHasher<UserAccount> passwordHasher, params int[] rollValues)
{
var options = new DbContextOptionsBuilder<RpgRollerDbContext>() var options = new DbContextOptionsBuilder<RpgRollerDbContext>()
.UseSqlite($"Data Source={dbPath}") .UseSqlite($"Data Source={dbPath}")
.Options; .Options;
var db = new RpgRollerDbContext(options); using (var db = new RpgRollerDbContext(options))
db.Database.EnsureCreated(); {
db.Database.EnsureCreated();
}
var service = new GameService(db, passwordHasher, new FixedDiceRoller(rollValues)); var factory = new SqliteDbContextFactory(dbPath);
return new ServiceHarness(service, db); var service = new GameService(factory, passwordHasher, new FixedDiceRoller(rollValues));
return new ServiceHarness(service, factory, dbPath);
} }
private static T GetValue<T>(ServiceResult<T> result) private static T GetValue<T>(ServiceResult<T> result)
@@ -382,20 +414,47 @@ public sealed class GameServiceTests
private sealed class ServiceHarness : IDisposable private sealed class ServiceHarness : IDisposable
{ {
private readonly RpgRollerDbContext m_Db; private readonly SqliteDbContextFactory m_Factory;
public ServiceHarness(GameService service, RpgRollerDbContext db) public ServiceHarness(GameService service, SqliteDbContextFactory factory, string dbPath)
{ {
Service = service; Service = service;
m_Db = db; m_Factory = factory;
DbPath = dbPath;
} }
public GameService Service { get; } public GameService Service { get; }
public RpgRollerDbContext Db => m_Db; public string DbPath { get; }
public void Dispose()
{
m_Factory.Dispose();
}
public RpgRollerDbContext CreateDbContext()
{
return m_Factory.CreateDbContext();
}
}
private sealed class SqliteDbContextFactory : IDbContextFactory<RpgRollerDbContext>, IDisposable
{
private readonly DbContextOptions<RpgRollerDbContext> m_Options;
public SqliteDbContextFactory(string dbPath)
{
m_Options = new DbContextOptionsBuilder<RpgRollerDbContext>()
.UseSqlite($"Data Source={dbPath}")
.Options;
}
public RpgRollerDbContext CreateDbContext()
{
return new RpgRollerDbContext(m_Options);
}
public void Dispose() public void Dispose()
{ {
m_Db.Dispose();
} }
} }

View File

@@ -205,10 +205,11 @@ public sealed class UnitTest1 : IClassFixture<WebApplicationFactory<Program>>
services.AddSingleton<IDiceRoller>(new FixedDiceRoller(rollValues)); services.AddSingleton<IDiceRoller>(new FixedDiceRoller(rollValues));
services.RemoveAll<DbContextOptions<RpgRollerDbContext>>(); services.RemoveAll<DbContextOptions<RpgRollerDbContext>>();
services.RemoveAll<IDbContextFactory<RpgRollerDbContext>>();
services.RemoveAll<RpgRollerDbContext>(); services.RemoveAll<RpgRollerDbContext>();
var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db"); var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-tests-{Guid.NewGuid():N}.db");
services.AddDbContext<RpgRollerDbContext>(options => options.UseSqlite($"Data Source={dbPath}")); services.AddDbContextFactory<RpgRollerDbContext>(options => options.UseSqlite($"Data Source={dbPath}"));
})); }));
} }

View File

@@ -14,16 +14,18 @@ var sqliteConnectionString = builder.Configuration.GetConnectionString("RpgRolle
EnsureSqliteDataDirectory(sqliteConnectionString, builder.Environment.ContentRootPath); EnsureSqliteDataDirectory(sqliteConnectionString, builder.Environment.ContentRootPath);
builder.Services.AddSingleton<IPasswordHasher<UserAccount>, PasswordHasher<UserAccount>>(); builder.Services.AddSingleton<IPasswordHasher<UserAccount>, PasswordHasher<UserAccount>>();
builder.Services.AddDbContext<RpgRollerDbContext>(options => options.UseSqlite(sqliteConnectionString)); builder.Services.AddDbContextFactory<RpgRollerDbContext>(options => options.UseSqlite(sqliteConnectionString));
builder.Services.AddSingleton<IDiceRoller, RandomDiceRoller>(); builder.Services.AddSingleton<IDiceRoller, RandomDiceRoller>();
builder.Services.AddScoped<IGameService, GameService>(); builder.Services.AddSingleton<IGameService, GameService>();
var app = builder.Build(); var app = builder.Build();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var db = scope.ServiceProvider.GetRequiredService<RpgRollerDbContext>(); var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<RpgRollerDbContext>>();
using var db = dbFactory.CreateDbContext();
db.Database.EnsureCreated(); db.Database.EnsureCreated();
_ = scope.ServiceProvider.GetRequiredService<IGameService>();
} }
app.UseDefaultFiles(); app.UseDefaultFiles();

File diff suppressed because it is too large Load Diff