something

This commit is contained in:
2025-10-01 01:56:05 +03:00
parent ece9cedb37
commit 40342a0e14
112 changed files with 5468 additions and 5468 deletions

View File

@@ -1,224 +1,224 @@
using LctMonolith.Models.Database;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Main EF Core database context for gamification module (PostgreSQL provider expected).
/// </summary>
public class AppDbContext : IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
// Rank related entities
public DbSet<Rank> Ranks => Set<Rank>();
public DbSet<RankMissionRule> RankMissionRules => Set<RankMissionRule>();
public DbSet<RankSkillRule> RankSkillRules => Set<RankSkillRule>();
// Mission related entities
public DbSet<MissionCategory> MissionCategories => Set<MissionCategory>();
public DbSet<Mission> Missions => Set<Mission>();
public DbSet<PlayerMission> PlayerMissions => Set<PlayerMission>();
public DbSet<MissionSkillReward> MissionSkillRewards => Set<MissionSkillReward>();
public DbSet<MissionItemReward> MissionItemRewards => Set<MissionItemReward>();
public DbSet<MissionRankRule> MissionRankRules => Set<MissionRankRule>();
// Skill related entities
public DbSet<Skill> Skills => Set<Skill>();
public DbSet<PlayerSkill> PlayerSkills => Set<PlayerSkill>();
// Dialogue related entities
public DbSet<Dialogue> Dialogues => Set<Dialogue>();
public DbSet<DialogueMessage> DialogueMessages => Set<DialogueMessage>();
public DbSet<DialogueMessageResponseOption> DialogueMessageResponseOptions => Set<DialogueMessageResponseOption>();
// Store and inventory
public DbSet<StoreItem> StoreItems => Set<StoreItem>();
public DbSet<UserInventoryItem> UserInventoryItems => Set<UserInventoryItem>();
public DbSet<Transaction> Transactions => Set<Transaction>();
// System entities
public DbSet<EventLog> EventLogs => Set<EventLog>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<Notification> Notifications => Set<Notification>();
// Core profile / player chain
public DbSet<Player> Players => Set<Player>();
public DbSet<Profile> Profiles => Set<Profile>();
protected override void OnModelCreating(ModelBuilder b)
{
base.OnModelCreating(b);
// Player configuration
b.Entity<Player>()
.HasIndex(p => p.UserId)
.IsUnique();
b.Entity<Player>()
.HasOne<AppUser>()
.WithOne()
.HasForeignKey<Player>(p => p.UserId)
.IsRequired();
// Rank configurations
b.Entity<Rank>()
.HasIndex(r => r.ExpNeeded)
.IsUnique();
b.Entity<Rank>()
.HasIndex(r => r.Title)
.IsUnique();
// Skill configurations
b.Entity<Skill>()
.HasIndex(s => s.Title)
.IsUnique();
// MissionCategory configurations
b.Entity<MissionCategory>()
.HasIndex(mc => mc.Title)
.IsUnique();
// Mission configurations
b.Entity<Mission>()
.HasOne(m => m.MissionCategory)
.WithMany(mc => mc.Missions)
.HasForeignKey(m => m.MissionCategoryId)
.IsRequired();
b.Entity<Mission>()
.HasOne(m => m.ParentMission)
.WithMany(m => m.ChildMissions)
.HasForeignKey(m => m.ParentMissionId)
.IsRequired(false);
// Dialogue relationship for Mission
b.Entity<Mission>()
.HasOne(m => m.Dialogue)
.WithOne(d => d.Mission)
.HasForeignKey<Mission>(m => m.DialogueId)
.IsRequired(false);
// MissionRankRule configurations
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Mission)
.WithMany(m => m.MissionRankRules)
.HasForeignKey(mrr => mrr.MissionId);
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Rank)
.WithMany(r => r.MissionRankRules)
.HasForeignKey(mrr => mrr.RankId);
// MissionSkillReward configurations
b.Entity<MissionSkillReward>()
.HasKey(x => new { x.MissionId, x.SkillId });
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Mission)
.WithMany(m => m.MissionSkillRewards)
.HasForeignKey(msr => msr.MissionId);
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Skill)
.WithMany(s => s.MissionSkillRewards)
.HasForeignKey(msr => msr.SkillId);
// MissionItemReward configurations
b.Entity<MissionItemReward>()
.HasOne(mir => mir.Mission)
.WithMany(m => m.MissionItemRewards)
.HasForeignKey(mir => mir.MissionId);
// RankMissionRule composite key
b.Entity<RankMissionRule>().HasKey(x => new { x.RankId, x.MissionId });
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Rank)
.WithMany(r => r.RankMissionRules)
.HasForeignKey(rmr => rmr.RankId);
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Mission)
.WithMany(m => m.RankMissionRules)
.HasForeignKey(rmr => rmr.MissionId);
// RankSkillRule composite key
b.Entity<RankSkillRule>().HasKey(x => new { x.RankId, x.SkillId });
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Rank)
.WithMany(r => r.RankSkillRules)
.HasForeignKey(rsr => rsr.RankId);
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Skill)
.WithMany(s => s.RankSkillRules)
.HasForeignKey(rsr => rsr.SkillId);
// PlayerSkill composite key
b.Entity<PlayerSkill>().HasKey(x => new { x.PlayerId, x.SkillId });
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Player)
.WithMany(p => p.PlayerSkills)
.HasForeignKey(ps => ps.PlayerId);
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Skill)
.WithMany(s => s.PlayerSkills)
.HasForeignKey(ps => ps.SkillId);
// PlayerMission composite key
b.Entity<PlayerMission>().HasKey(x => new { x.PlayerId, x.MissionId });
b.Entity<PlayerMission>()
.HasOne(pm => pm.Player)
.WithMany(p => p.PlayerMissions)
.HasForeignKey(pm => pm.PlayerId);
b.Entity<PlayerMission>()
.HasOne(pm => pm.Mission)
.WithMany(m => m.PlayerMissions)
.HasForeignKey(pm => pm.MissionId);
// Dialogue configurations
b.Entity<Dialogue>()
.HasOne(d => d.Mission)
.WithOne(m => m.Dialogue)
.HasForeignKey<Dialogue>(d => d.MissionId)
.IsRequired();
// DialogueMessage configurations
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InitialDialogue)
.WithMany()
.HasForeignKey(dm => dm.InitialDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InterimDialogue)
.WithMany()
.HasForeignKey(dm => dm.InterimDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.EndDialogue)
.WithMany()
.HasForeignKey(dm => dm.EndDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// DialogueMessageResponseOption configurations
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.ParentDialogueMessage)
.WithMany(dm => dm.DialogueMessageResponseOptions)
.HasForeignKey(dmro => dmro.ParentDialogueMessageId)
.IsRequired();
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.DestinationDialogueMessage)
.WithMany()
.HasForeignKey(dmro => dmro.DestinationDialogueMessageId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// Refresh token index unique
b.Entity<RefreshToken>().HasIndex(x => x.Token).IsUnique();
// ---------- Performance indexes ----------
b.Entity<PlayerSkill>().HasIndex(ps => ps.SkillId);
b.Entity<EventLog>().HasIndex(e => new { e.UserId, e.Type, e.CreatedAt });
b.Entity<StoreItem>().HasIndex(i => i.IsActive);
b.Entity<Transaction>().HasIndex(t => new { t.UserId, t.CreatedAt });
b.Entity<Notification>().HasIndex(n => new { n.UserId, n.IsRead, n.CreatedAt });
}
}
using LctMonolith.Models.Database;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Main EF Core database context for gamification module (PostgreSQL provider expected).
/// </summary>
public class AppDbContext : IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
// Rank related entities
public DbSet<Rank> Ranks => Set<Rank>();
public DbSet<RankMissionRule> RankMissionRules => Set<RankMissionRule>();
public DbSet<RankSkillRule> RankSkillRules => Set<RankSkillRule>();
// Mission related entities
public DbSet<MissionCategory> MissionCategories => Set<MissionCategory>();
public DbSet<Mission> Missions => Set<Mission>();
public DbSet<PlayerMission> PlayerMissions => Set<PlayerMission>();
public DbSet<MissionSkillReward> MissionSkillRewards => Set<MissionSkillReward>();
public DbSet<MissionItemReward> MissionItemRewards => Set<MissionItemReward>();
public DbSet<MissionRankRule> MissionRankRules => Set<MissionRankRule>();
// Skill related entities
public DbSet<Skill> Skills => Set<Skill>();
public DbSet<PlayerSkill> PlayerSkills => Set<PlayerSkill>();
// Dialogue related entities
public DbSet<Dialogue> Dialogues => Set<Dialogue>();
public DbSet<DialogueMessage> DialogueMessages => Set<DialogueMessage>();
public DbSet<DialogueMessageResponseOption> DialogueMessageResponseOptions => Set<DialogueMessageResponseOption>();
// Store and inventory
public DbSet<StoreItem> StoreItems => Set<StoreItem>();
public DbSet<UserInventoryItem> UserInventoryItems => Set<UserInventoryItem>();
public DbSet<Transaction> Transactions => Set<Transaction>();
// System entities
public DbSet<EventLog> EventLogs => Set<EventLog>();
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<Notification> Notifications => Set<Notification>();
// Core profile / player chain
public DbSet<Player> Players => Set<Player>();
public DbSet<Profile> Profiles => Set<Profile>();
protected override void OnModelCreating(ModelBuilder b)
{
base.OnModelCreating(b);
// Player configuration
b.Entity<Player>()
.HasIndex(p => p.UserId)
.IsUnique();
b.Entity<Player>()
.HasOne<AppUser>()
.WithOne()
.HasForeignKey<Player>(p => p.UserId)
.IsRequired();
// Rank configurations
b.Entity<Rank>()
.HasIndex(r => r.ExpNeeded)
.IsUnique();
b.Entity<Rank>()
.HasIndex(r => r.Title)
.IsUnique();
// Skill configurations
b.Entity<Skill>()
.HasIndex(s => s.Title)
.IsUnique();
// MissionCategory configurations
b.Entity<MissionCategory>()
.HasIndex(mc => mc.Title)
.IsUnique();
// Mission configurations
b.Entity<Mission>()
.HasOne(m => m.MissionCategory)
.WithMany(mc => mc.Missions)
.HasForeignKey(m => m.MissionCategoryId)
.IsRequired();
b.Entity<Mission>()
.HasOne(m => m.ParentMission)
.WithMany(m => m.ChildMissions)
.HasForeignKey(m => m.ParentMissionId)
.IsRequired(false);
// Dialogue relationship for Mission
b.Entity<Mission>()
.HasOne(m => m.Dialogue)
.WithOne(d => d.Mission)
.HasForeignKey<Mission>(m => m.DialogueId)
.IsRequired(false);
// MissionRankRule configurations
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Mission)
.WithMany(m => m.MissionRankRules)
.HasForeignKey(mrr => mrr.MissionId);
b.Entity<MissionRankRule>()
.HasOne(mrr => mrr.Rank)
.WithMany(r => r.MissionRankRules)
.HasForeignKey(mrr => mrr.RankId);
// MissionSkillReward configurations
b.Entity<MissionSkillReward>()
.HasKey(x => new { x.MissionId, x.SkillId });
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Mission)
.WithMany(m => m.MissionSkillRewards)
.HasForeignKey(msr => msr.MissionId);
b.Entity<MissionSkillReward>()
.HasOne(msr => msr.Skill)
.WithMany(s => s.MissionSkillRewards)
.HasForeignKey(msr => msr.SkillId);
// MissionItemReward configurations
b.Entity<MissionItemReward>()
.HasOne(mir => mir.Mission)
.WithMany(m => m.MissionItemRewards)
.HasForeignKey(mir => mir.MissionId);
// RankMissionRule composite key
b.Entity<RankMissionRule>().HasKey(x => new { x.RankId, x.MissionId });
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Rank)
.WithMany(r => r.RankMissionRules)
.HasForeignKey(rmr => rmr.RankId);
b.Entity<RankMissionRule>()
.HasOne(rmr => rmr.Mission)
.WithMany(m => m.RankMissionRules)
.HasForeignKey(rmr => rmr.MissionId);
// RankSkillRule composite key
b.Entity<RankSkillRule>().HasKey(x => new { x.RankId, x.SkillId });
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Rank)
.WithMany(r => r.RankSkillRules)
.HasForeignKey(rsr => rsr.RankId);
b.Entity<RankSkillRule>()
.HasOne(rsr => rsr.Skill)
.WithMany(s => s.RankSkillRules)
.HasForeignKey(rsr => rsr.SkillId);
// PlayerSkill composite key
b.Entity<PlayerSkill>().HasKey(x => new { x.PlayerId, x.SkillId });
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Player)
.WithMany(p => p.PlayerSkills)
.HasForeignKey(ps => ps.PlayerId);
b.Entity<PlayerSkill>()
.HasOne(ps => ps.Skill)
.WithMany(s => s.PlayerSkills)
.HasForeignKey(ps => ps.SkillId);
// PlayerMission composite key
b.Entity<PlayerMission>().HasKey(x => new { x.PlayerId, x.MissionId });
b.Entity<PlayerMission>()
.HasOne(pm => pm.Player)
.WithMany(p => p.PlayerMissions)
.HasForeignKey(pm => pm.PlayerId);
b.Entity<PlayerMission>()
.HasOne(pm => pm.Mission)
.WithMany(m => m.PlayerMissions)
.HasForeignKey(pm => pm.MissionId);
// Dialogue configurations
b.Entity<Dialogue>()
.HasOne(d => d.Mission)
.WithOne(m => m.Dialogue)
.HasForeignKey<Dialogue>(d => d.MissionId)
.IsRequired();
// DialogueMessage configurations
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InitialDialogue)
.WithMany()
.HasForeignKey(dm => dm.InitialDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.InterimDialogue)
.WithMany()
.HasForeignKey(dm => dm.InterimDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
b.Entity<DialogueMessage>()
.HasOne(dm => dm.EndDialogue)
.WithMany()
.HasForeignKey(dm => dm.EndDialogueId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// DialogueMessageResponseOption configurations
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.ParentDialogueMessage)
.WithMany(dm => dm.DialogueMessageResponseOptions)
.HasForeignKey(dmro => dmro.ParentDialogueMessageId)
.IsRequired();
b.Entity<DialogueMessageResponseOption>()
.HasOne(dmro => dmro.DestinationDialogueMessage)
.WithMany()
.HasForeignKey(dmro => dmro.DestinationDialogueMessageId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
// Refresh token index unique
b.Entity<RefreshToken>().HasIndex(x => x.Token).IsUnique();
// ---------- Performance indexes ----------
b.Entity<PlayerSkill>().HasIndex(ps => ps.SkillId);
b.Entity<EventLog>().HasIndex(e => new { e.UserId, e.Type, e.CreatedAt });
b.Entity<StoreItem>().HasIndex(i => i.IsActive);
b.Entity<Transaction>().HasIndex(t => new { t.UserId, t.CreatedAt });
b.Entity<Notification>().HasIndex(n => new { n.UserId, n.IsRead, n.CreatedAt });
}
}

View File

@@ -1,47 +1,47 @@
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Development database seeder for initial ranks, competencies, sample store items.
/// Idempotent: checks existence before inserting.
/// </summary>
public static class DbSeeder
{
public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default)
{
await db.Database.EnsureCreatedAsync(ct);
if (!await db.Ranks.AnyAsync(ct))
{
var ranks = new List<Rank>
{
new() { Title = "Искатель", ExpNeeded = 0 },
new() { Title = "Пилот-кандидат", ExpNeeded = 500 },
new() { Title = "Принятый в экипаж", ExpNeeded = 1500 }
};
db.Ranks.AddRange(ranks);
Log.Information("Seeded {Count} ranks", ranks.Count);
}
if (!await db.Skills.AnyAsync(ct))
{
var comps = new[]
{
"Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации"
}.Select(n => new Skill { Title = n });
db.Skills.AddRange(comps);
Log.Information("Seeded competencies");
}
if (!await db.StoreItems.AnyAsync(ct))
{
db.StoreItems.AddRange(new StoreItem { Name = "Футболка Алабуга", Price = 100 }, new StoreItem { Name = "Брелок Буран", Price = 50 });
Log.Information("Seeded store items");
}
await db.SaveChangesAsync(ct);
}
}
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Database.Data;
/// <summary>
/// Development database seeder for initial ranks, competencies, sample store items.
/// Idempotent: checks existence before inserting.
/// </summary>
public static class DbSeeder
{
public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default)
{
await db.Database.EnsureCreatedAsync(ct);
if (!await db.Ranks.AnyAsync(ct))
{
var ranks = new List<Rank>
{
new() { Title = "Искатель", ExpNeeded = 0 },
new() { Title = "Пилот-кандидат", ExpNeeded = 500 },
new() { Title = "Принятый в экипаж", ExpNeeded = 1500 }
};
db.Ranks.AddRange(ranks);
Log.Information("Seeded {Count} ranks", ranks.Count);
}
if (!await db.Skills.AnyAsync(ct))
{
var comps = new[]
{
"Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации"
}.Select(n => new Skill { Title = n });
db.Skills.AddRange(comps);
Log.Information("Seeded competencies");
}
if (!await db.StoreItems.AnyAsync(ct))
{
db.StoreItems.AddRange(new StoreItem { Name = "Футболка Алабуга", Price = 100 }, new StoreItem { Name = "Брелок Буран", Price = 50 });
Log.Information("Seeded store items");
}
await db.SaveChangesAsync(ct);
}
}

View File

@@ -1,19 +1,19 @@
namespace LctMonolith.Models.Database;
public enum EventType
{
SkillProgress = 1,
MissionStatusChanged = 2,
RankChanged = 3,
ItemPurchased = 4,
ArtifactObtained = 5,
RewardGranted = 6,
ProfileChanged = 7,
AuthCredentialsChanged = 8,
ItemReturned = 9,
ItemSold = 10
}
#if false
// Moved to Models/EventType.cs
#endif
namespace LctMonolith.Models.Database;
public enum EventType
{
SkillProgress = 1,
MissionStatusChanged = 2,
RankChanged = 3,
ItemPurchased = 4,
ArtifactObtained = 5,
RewardGranted = 6,
ProfileChanged = 7,
AuthCredentialsChanged = 8,
ItemReturned = 9,
ItemSold = 10
}
#if false
// Moved to Models/EventType.cs
#endif

View File

@@ -1,56 +1,56 @@
using System.Linq.Expressions;
using LctMonolith.Database.Data;
using Microsoft.EntityFrameworkCore;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository implementation for common CRUD and query composition.
/// </summary>
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
protected readonly AppDbContext Context;
protected readonly DbSet<TEntity> Set;
public GenericRepository(AppDbContext context)
{
Context = context;
Set = context.Set<TEntity>();
}
public IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes)
{
IQueryable<TEntity> query = Set;
if (filter != null) query = query.Where(filter);
if (includes != null)
{
foreach (var include in includes)
query = query.Include(include);
}
if (orderBy != null) query = orderBy(query);
return query;
}
public async Task<TEntity?> GetByIdAsync(object id) => await Set.FindAsync(id) ?? null;
public ValueTask<TEntity?> FindAsync(params object[] keyValues) => Set.FindAsync(keyValues);
public async Task AddAsync(TEntity entity, CancellationToken ct = default) => await Set.AddAsync(entity, ct);
public async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default) => await Set.AddRangeAsync(entities, ct);
public void Update(TEntity entity) => Set.Update(entity);
public void Remove(TEntity entity) => Set.Remove(entity);
public async Task RemoveByIdAsync(object id, CancellationToken ct = default)
{
var entity = await Set.FindAsync([id], ct);
if (entity == null) throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found");
Set.Remove(entity);
}
}
using System.Linq.Expressions;
using LctMonolith.Database.Data;
using Microsoft.EntityFrameworkCore;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository implementation for common CRUD and query composition.
/// </summary>
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
protected readonly AppDbContext Context;
protected readonly DbSet<TEntity> Set;
public GenericRepository(AppDbContext context)
{
Context = context;
Set = context.Set<TEntity>();
}
public IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes)
{
IQueryable<TEntity> query = Set;
if (filter != null) query = query.Where(filter);
if (includes != null)
{
foreach (var include in includes)
query = query.Include(include);
}
if (orderBy != null) query = orderBy(query);
return query;
}
public async Task<TEntity?> GetByIdAsync(object id) => await Set.FindAsync(id) ?? null;
public ValueTask<TEntity?> FindAsync(params object[] keyValues) => Set.FindAsync(keyValues);
public async Task AddAsync(TEntity entity, CancellationToken ct = default) => await Set.AddAsync(entity, ct);
public async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default) => await Set.AddRangeAsync(entities, ct);
public void Update(TEntity entity) => Set.Update(entity);
public void Remove(TEntity entity) => Set.Remove(entity);
public async Task RemoveByIdAsync(object id, CancellationToken ct = default)
{
var entity = await Set.FindAsync([id], ct);
if (entity == null) throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found");
Set.Remove(entity);
}
}

View File

@@ -1,25 +1,25 @@
using System.Linq.Expressions;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository abstraction for aggregate root / entity access. Read operations return IQueryable for composition.
/// </summary>
public interface IGenericRepository<TEntity> where TEntity : class
{
IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes);
Task<TEntity?> GetByIdAsync(object id);
ValueTask<TEntity?> FindAsync(params object[] keyValues);
Task AddAsync(TEntity entity, CancellationToken ct = default);
Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
void Update(TEntity entity);
void Remove(TEntity entity);
Task RemoveByIdAsync(object id, CancellationToken ct = default);
}
using System.Linq.Expressions;
namespace LctMonolith.Database.Repositories;
/// <summary>
/// Generic repository abstraction for aggregate root / entity access. Read operations return IQueryable for composition.
/// </summary>
public interface IGenericRepository<TEntity> where TEntity : class
{
IQueryable<TEntity> Query(
Expression<Func<TEntity, bool>>? filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
params Expression<Func<TEntity, object>>[] includes);
Task<TEntity?> GetByIdAsync(object id);
ValueTask<TEntity?> FindAsync(params object[] keyValues);
Task AddAsync(TEntity entity, CancellationToken ct = default);
Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
void Update(TEntity entity);
void Remove(TEntity entity);
Task RemoveByIdAsync(object id, CancellationToken ct = default);
}

View File

@@ -1,40 +1,40 @@
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work aggregates repositories and transaction boundary.
/// </summary>
public interface IUnitOfWork
{
IGenericRepository<AppUser> Users { get; }
IGenericRepository<Player> Players { get; }
IGenericRepository<MissionCategory> MissionCategories { get; } // added
IGenericRepository<Rank> Ranks { get; }
IGenericRepository<RankMissionRule> RankMissionRules { get; }
IGenericRepository<RankSkillRule> RankSkillRules { get; }
IGenericRepository<Mission> Missions { get; }
IGenericRepository<PlayerMission> PlayerMissions { get; }
IGenericRepository<MissionSkillReward> MissionSkillRewards { get; }
IGenericRepository<Skill> Skills { get; }
IGenericRepository<PlayerSkill> PlayerSkills { get; }
IGenericRepository<StoreItem> StoreItems { get; }
IGenericRepository<UserInventoryItem> UserInventoryItems { get; }
IGenericRepository<Transaction> Transactions { get; }
IGenericRepository<EventLog> EventLogs { get; }
IGenericRepository<RefreshToken> RefreshTokens { get; }
IGenericRepository<Notification> Notifications { get; }
IGenericRepository<MissionItemReward> MissionItemRewards { get; } // added
IGenericRepository<MissionRankRule> MissionRankRules { get; } // added
IGenericRepository<Dialogue> Dialogues { get; }
IGenericRepository<DialogueMessage> DialogueMessages { get; }
IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions { get; }
IGenericRepository<Profile> Profiles { get; }
Task<int> SaveChangesAsync(CancellationToken ct = default);
Task BeginTransactionAsync(CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
}
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work aggregates repositories and transaction boundary.
/// </summary>
public interface IUnitOfWork
{
IGenericRepository<AppUser> Users { get; }
IGenericRepository<Player> Players { get; }
IGenericRepository<MissionCategory> MissionCategories { get; } // added
IGenericRepository<Rank> Ranks { get; }
IGenericRepository<RankMissionRule> RankMissionRules { get; }
IGenericRepository<RankSkillRule> RankSkillRules { get; }
IGenericRepository<Mission> Missions { get; }
IGenericRepository<PlayerMission> PlayerMissions { get; }
IGenericRepository<MissionSkillReward> MissionSkillRewards { get; }
IGenericRepository<Skill> Skills { get; }
IGenericRepository<PlayerSkill> PlayerSkills { get; }
IGenericRepository<StoreItem> StoreItems { get; }
IGenericRepository<UserInventoryItem> UserInventoryItems { get; }
IGenericRepository<Transaction> Transactions { get; }
IGenericRepository<EventLog> EventLogs { get; }
IGenericRepository<RefreshToken> RefreshTokens { get; }
IGenericRepository<Notification> Notifications { get; }
IGenericRepository<MissionItemReward> MissionItemRewards { get; } // added
IGenericRepository<MissionRankRule> MissionRankRules { get; } // added
IGenericRepository<Dialogue> Dialogues { get; }
IGenericRepository<DialogueMessage> DialogueMessages { get; }
IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions { get; }
IGenericRepository<Profile> Profiles { get; }
Task<int> SaveChangesAsync(CancellationToken ct = default);
Task BeginTransactionAsync(CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
}

View File

@@ -1,110 +1,110 @@
using LctMonolith.Database.Data;
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore.Storage;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work implementation encapsulating repositories and DB transaction scope.
/// </summary>
public class UnitOfWork : IUnitOfWork, IAsyncDisposable
{
private readonly AppDbContext _ctx;
private IDbContextTransaction? _tx;
public UnitOfWork(AppDbContext ctx)
{
_ctx = ctx;
}
private IGenericRepository<AppUser>? _users;
private IGenericRepository<Rank>? _ranks;
private IGenericRepository<RankMissionRule>? _rankMissionRules;
private IGenericRepository<RankSkillRule>? _rankSkillRules;
private IGenericRepository<Mission>? _missions;
private IGenericRepository<PlayerMission>? _playerMissions;
private IGenericRepository<MissionSkillReward>? _missionSkillRewards;
private IGenericRepository<Skill>? _skills;
private IGenericRepository<PlayerSkill>? _playerSkills;
private IGenericRepository<StoreItem>? _storeItems;
private IGenericRepository<UserInventoryItem>? _userInventoryItems;
private IGenericRepository<Transaction>? _transactions;
private IGenericRepository<EventLog>? _eventLogs;
private IGenericRepository<RefreshToken>? _refreshTokens;
private IGenericRepository<Notification>? _notifications;
private IGenericRepository<Player>? _players;
private IGenericRepository<MissionCategory>? _missionCategories;
private IGenericRepository<MissionItemReward>? _missionItemRewards;
private IGenericRepository<MissionRankRule>? _missionRankRules;
private IGenericRepository<Dialogue>? _dialogues;
private IGenericRepository<DialogueMessage>? _dialogueMessages;
private IGenericRepository<DialogueMessageResponseOption>? _dialogueMessageResponseOptions;
private IGenericRepository<Profile>? _profiles;
public IGenericRepository<AppUser> Users => _users ??= new GenericRepository<AppUser>(_ctx);
public IGenericRepository<Rank> Ranks => _ranks ??= new GenericRepository<Rank>(_ctx);
public IGenericRepository<RankMissionRule> RankMissionRules => _rankMissionRules ??= new GenericRepository<RankMissionRule>(_ctx);
public IGenericRepository<RankSkillRule> RankSkillRules => _rankSkillRules ??= new GenericRepository<RankSkillRule>(_ctx);
public IGenericRepository<Mission> Missions => _missions ??= new GenericRepository<Mission>(_ctx);
public IGenericRepository<PlayerMission> PlayerMissions => _playerMissions ??= new GenericRepository<PlayerMission>(_ctx);
public IGenericRepository<MissionSkillReward> MissionSkillRewards => _missionSkillRewards ??= new GenericRepository<MissionSkillReward>(_ctx);
public IGenericRepository<Skill> Skills => _skills ??= new GenericRepository<Skill>(_ctx);
public IGenericRepository<PlayerSkill> PlayerSkills => _playerSkills ??= new GenericRepository<PlayerSkill>(_ctx);
public IGenericRepository<StoreItem> StoreItems => _storeItems ??= new GenericRepository<StoreItem>(_ctx);
public IGenericRepository<UserInventoryItem> UserInventoryItems => _userInventoryItems ??= new GenericRepository<UserInventoryItem>(_ctx);
public IGenericRepository<Transaction> Transactions => _transactions ??= new GenericRepository<Transaction>(_ctx);
public IGenericRepository<EventLog> EventLogs => _eventLogs ??= new GenericRepository<EventLog>(_ctx);
public IGenericRepository<RefreshToken> RefreshTokens => _refreshTokens ??= new GenericRepository<RefreshToken>(_ctx);
public IGenericRepository<Notification> Notifications => _notifications ??= new GenericRepository<Notification>(_ctx);
public IGenericRepository<Player> Players => _players ??= new GenericRepository<Player>(_ctx);
public IGenericRepository<MissionCategory> MissionCategories => _missionCategories ??= new GenericRepository<MissionCategory>(_ctx);
public IGenericRepository<MissionItemReward> MissionItemRewards => _missionItemRewards ??= new GenericRepository<MissionItemReward>(_ctx);
public IGenericRepository<MissionRankRule> MissionRankRules => _missionRankRules ??= new GenericRepository<MissionRankRule>(_ctx);
public IGenericRepository<Dialogue> Dialogues => _dialogues ??= new GenericRepository<Dialogue>(_ctx);
public IGenericRepository<DialogueMessage> DialogueMessages => _dialogueMessages ??= new GenericRepository<DialogueMessage>(_ctx);
public IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions => _dialogueMessageResponseOptions ??= new GenericRepository<DialogueMessageResponseOption>(_ctx);
public IGenericRepository<Profile> Profiles => _profiles ??= new GenericRepository<Profile>(_ctx);
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _ctx.SaveChangesAsync(ct);
public async Task BeginTransactionAsync(CancellationToken ct = default)
{
if (_tx != null) throw new InvalidOperationException("Transaction already started");
_tx = await _ctx.Database.BeginTransactionAsync(ct);
}
public async Task CommitAsync(CancellationToken ct = default)
{
if (_tx == null) return;
try
{
await _ctx.SaveChangesAsync(ct);
await _tx.CommitAsync(ct);
}
catch
{
await RollbackAsync(ct);
throw;
}
finally
{
await _tx.DisposeAsync();
_tx = null;
}
}
public async Task RollbackAsync(CancellationToken ct = default)
{
if (_tx == null) return;
await _tx.RollbackAsync(ct);
await _tx.DisposeAsync();
_tx = null;
}
public async ValueTask DisposeAsync()
{
if (_tx != null) await _tx.DisposeAsync();
}
}
using LctMonolith.Database.Data;
using LctMonolith.Database.Repositories;
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore.Storage;
using EventLog = LctMonolith.Models.Database.EventLog;
namespace LctMonolith.Database.UnitOfWork;
/// <summary>
/// Unit of Work implementation encapsulating repositories and DB transaction scope.
/// </summary>
public class UnitOfWork : IUnitOfWork, IAsyncDisposable
{
private readonly AppDbContext _ctx;
private IDbContextTransaction? _tx;
public UnitOfWork(AppDbContext ctx)
{
_ctx = ctx;
}
private IGenericRepository<AppUser>? _users;
private IGenericRepository<Rank>? _ranks;
private IGenericRepository<RankMissionRule>? _rankMissionRules;
private IGenericRepository<RankSkillRule>? _rankSkillRules;
private IGenericRepository<Mission>? _missions;
private IGenericRepository<PlayerMission>? _playerMissions;
private IGenericRepository<MissionSkillReward>? _missionSkillRewards;
private IGenericRepository<Skill>? _skills;
private IGenericRepository<PlayerSkill>? _playerSkills;
private IGenericRepository<StoreItem>? _storeItems;
private IGenericRepository<UserInventoryItem>? _userInventoryItems;
private IGenericRepository<Transaction>? _transactions;
private IGenericRepository<EventLog>? _eventLogs;
private IGenericRepository<RefreshToken>? _refreshTokens;
private IGenericRepository<Notification>? _notifications;
private IGenericRepository<Player>? _players;
private IGenericRepository<MissionCategory>? _missionCategories;
private IGenericRepository<MissionItemReward>? _missionItemRewards;
private IGenericRepository<MissionRankRule>? _missionRankRules;
private IGenericRepository<Dialogue>? _dialogues;
private IGenericRepository<DialogueMessage>? _dialogueMessages;
private IGenericRepository<DialogueMessageResponseOption>? _dialogueMessageResponseOptions;
private IGenericRepository<Profile>? _profiles;
public IGenericRepository<AppUser> Users => _users ??= new GenericRepository<AppUser>(_ctx);
public IGenericRepository<Rank> Ranks => _ranks ??= new GenericRepository<Rank>(_ctx);
public IGenericRepository<RankMissionRule> RankMissionRules => _rankMissionRules ??= new GenericRepository<RankMissionRule>(_ctx);
public IGenericRepository<RankSkillRule> RankSkillRules => _rankSkillRules ??= new GenericRepository<RankSkillRule>(_ctx);
public IGenericRepository<Mission> Missions => _missions ??= new GenericRepository<Mission>(_ctx);
public IGenericRepository<PlayerMission> PlayerMissions => _playerMissions ??= new GenericRepository<PlayerMission>(_ctx);
public IGenericRepository<MissionSkillReward> MissionSkillRewards => _missionSkillRewards ??= new GenericRepository<MissionSkillReward>(_ctx);
public IGenericRepository<Skill> Skills => _skills ??= new GenericRepository<Skill>(_ctx);
public IGenericRepository<PlayerSkill> PlayerSkills => _playerSkills ??= new GenericRepository<PlayerSkill>(_ctx);
public IGenericRepository<StoreItem> StoreItems => _storeItems ??= new GenericRepository<StoreItem>(_ctx);
public IGenericRepository<UserInventoryItem> UserInventoryItems => _userInventoryItems ??= new GenericRepository<UserInventoryItem>(_ctx);
public IGenericRepository<Transaction> Transactions => _transactions ??= new GenericRepository<Transaction>(_ctx);
public IGenericRepository<EventLog> EventLogs => _eventLogs ??= new GenericRepository<EventLog>(_ctx);
public IGenericRepository<RefreshToken> RefreshTokens => _refreshTokens ??= new GenericRepository<RefreshToken>(_ctx);
public IGenericRepository<Notification> Notifications => _notifications ??= new GenericRepository<Notification>(_ctx);
public IGenericRepository<Player> Players => _players ??= new GenericRepository<Player>(_ctx);
public IGenericRepository<MissionCategory> MissionCategories => _missionCategories ??= new GenericRepository<MissionCategory>(_ctx);
public IGenericRepository<MissionItemReward> MissionItemRewards => _missionItemRewards ??= new GenericRepository<MissionItemReward>(_ctx);
public IGenericRepository<MissionRankRule> MissionRankRules => _missionRankRules ??= new GenericRepository<MissionRankRule>(_ctx);
public IGenericRepository<Dialogue> Dialogues => _dialogues ??= new GenericRepository<Dialogue>(_ctx);
public IGenericRepository<DialogueMessage> DialogueMessages => _dialogueMessages ??= new GenericRepository<DialogueMessage>(_ctx);
public IGenericRepository<DialogueMessageResponseOption> DialogueMessageResponseOptions => _dialogueMessageResponseOptions ??= new GenericRepository<DialogueMessageResponseOption>(_ctx);
public IGenericRepository<Profile> Profiles => _profiles ??= new GenericRepository<Profile>(_ctx);
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _ctx.SaveChangesAsync(ct);
public async Task BeginTransactionAsync(CancellationToken ct = default)
{
if (_tx != null) throw new InvalidOperationException("Transaction already started");
_tx = await _ctx.Database.BeginTransactionAsync(ct);
}
public async Task CommitAsync(CancellationToken ct = default)
{
if (_tx == null) return;
try
{
await _ctx.SaveChangesAsync(ct);
await _tx.CommitAsync(ct);
}
catch
{
await RollbackAsync(ct);
throw;
}
finally
{
await _tx.DisposeAsync();
_tx = null;
}
}
public async Task RollbackAsync(CancellationToken ct = default)
{
if (_tx == null) return;
await _tx.RollbackAsync(ct);
await _tx.DisposeAsync();
_tx = null;
}
public async ValueTask DisposeAsync()
{
if (_tx != null) await _tx.DisposeAsync();
}
}