feat random bullshit GO!
This commit is contained in:
95
LctMonolith/Infrastructure/Data/AppDbContext.cs
Normal file
95
LctMonolith/Infrastructure/Data/AppDbContext.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using LctMonolith.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace LctMonolith.Infrastructure.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) { }
|
||||
|
||||
public DbSet<Rank> Ranks => Set<Rank>();
|
||||
public DbSet<RankRequiredMission> RankRequiredMissions => Set<RankRequiredMission>();
|
||||
public DbSet<RankRequiredCompetency> RankRequiredCompetencies => Set<RankRequiredCompetency>();
|
||||
|
||||
public DbSet<Mission> Missions => Set<Mission>();
|
||||
public DbSet<UserMission> UserMissions => Set<UserMission>();
|
||||
public DbSet<MissionCompetencyReward> MissionCompetencyRewards => Set<MissionCompetencyReward>();
|
||||
public DbSet<MissionArtifactReward> MissionArtifactRewards => Set<MissionArtifactReward>();
|
||||
|
||||
public DbSet<Competency> Competencies => Set<Competency>();
|
||||
public DbSet<UserCompetency> UserCompetencies => Set<UserCompetency>();
|
||||
|
||||
public DbSet<Artifact> Artifacts => Set<Artifact>();
|
||||
public DbSet<UserArtifact> UserArtifacts => Set<UserArtifact>();
|
||||
|
||||
public DbSet<StoreItem> StoreItems => Set<StoreItem>();
|
||||
public DbSet<UserInventoryItem> UserInventoryItems => Set<UserInventoryItem>();
|
||||
public DbSet<Transaction> Transactions => Set<Transaction>();
|
||||
|
||||
public DbSet<EventLog> EventLogs => Set<EventLog>();
|
||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder b)
|
||||
{
|
||||
base.OnModelCreating(b);
|
||||
|
||||
// Rank required mission composite key
|
||||
b.Entity<RankRequiredMission>().HasKey(x => new { x.RankId, x.MissionId });
|
||||
b.Entity<RankRequiredMission>()
|
||||
.HasOne(x => x.Rank).WithMany(r => r.RequiredMissions).HasForeignKey(x => x.RankId);
|
||||
b.Entity<RankRequiredMission>()
|
||||
.HasOne(x => x.Mission).WithMany(m => m.RanksRequiring).HasForeignKey(x => x.MissionId);
|
||||
|
||||
// Rank required competency composite key
|
||||
b.Entity<RankRequiredCompetency>().HasKey(x => new { x.RankId, x.CompetencyId });
|
||||
b.Entity<RankRequiredCompetency>()
|
||||
.HasOne(x => x.Rank).WithMany(r => r.RequiredCompetencies).HasForeignKey(x => x.RankId);
|
||||
b.Entity<RankRequiredCompetency>()
|
||||
.HasOne(x => x.Competency).WithMany(c => c.RankRequirements).HasForeignKey(x => x.CompetencyId);
|
||||
|
||||
// UserMission composite key
|
||||
b.Entity<UserMission>().HasKey(x => new { x.UserId, x.MissionId });
|
||||
b.Entity<UserMission>()
|
||||
.HasOne(x => x.User).WithMany(u => u.Missions).HasForeignKey(x => x.UserId);
|
||||
b.Entity<UserMission>()
|
||||
.HasOne(x => x.Mission).WithMany(m => m.UserMissions).HasForeignKey(x => x.MissionId);
|
||||
|
||||
// UserCompetency composite key
|
||||
b.Entity<UserCompetency>().HasKey(x => new { x.UserId, x.CompetencyId });
|
||||
b.Entity<UserCompetency>()
|
||||
.HasOne(x => x.User).WithMany(u => u.Competencies).HasForeignKey(x => x.UserId);
|
||||
b.Entity<UserCompetency>()
|
||||
.HasOne(x => x.Competency).WithMany(c => c.UserCompetencies).HasForeignKey(x => x.CompetencyId);
|
||||
|
||||
// Mission competency reward composite key
|
||||
b.Entity<MissionCompetencyReward>().HasKey(x => new { x.MissionId, x.CompetencyId });
|
||||
|
||||
// Mission artifact reward composite key
|
||||
b.Entity<MissionArtifactReward>().HasKey(x => new { x.MissionId, x.ArtifactId });
|
||||
|
||||
// UserArtifact composite key
|
||||
b.Entity<UserArtifact>().HasKey(x => new { x.UserId, x.ArtifactId });
|
||||
|
||||
// UserInventory composite key
|
||||
b.Entity<UserInventoryItem>().HasKey(x => new { x.UserId, x.StoreItemId });
|
||||
|
||||
// Refresh token index unique
|
||||
b.Entity<RefreshToken>().HasIndex(x => x.Token).IsUnique();
|
||||
|
||||
// ---------- Added performance indexes ----------
|
||||
b.Entity<AppUser>().HasIndex(u => u.RankId);
|
||||
b.Entity<Mission>().HasIndex(m => m.MinRankId);
|
||||
b.Entity<UserMission>().HasIndex(um => new { um.UserId, um.Status });
|
||||
b.Entity<UserCompetency>().HasIndex(uc => uc.CompetencyId); // for querying all users by competency
|
||||
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 });
|
||||
}
|
||||
}
|
||||
48
LctMonolith/Infrastructure/Data/DbSeeder.cs
Normal file
48
LctMonolith/Infrastructure/Data/DbSeeder.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using LctMonolith.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LctMonolith.Infrastructure.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() { Name = "Искатель", Order = 0, RequiredExperience = 0 },
|
||||
new() { Name = "Пилот-кандидат", Order = 1, RequiredExperience = 500 },
|
||||
new() { Name = "Принятый в экипаж", Order = 2, RequiredExperience = 1500 }
|
||||
};
|
||||
db.Ranks.AddRange(ranks);
|
||||
Log.Information("Seeded {Count} ranks", ranks.Count);
|
||||
}
|
||||
|
||||
if (!await db.Competencies.AnyAsync(ct))
|
||||
{
|
||||
var comps = new[]
|
||||
{
|
||||
"Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации"
|
||||
}.Select(n => new Competency { Name = n });
|
||||
db.Competencies.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);
|
||||
}
|
||||
}
|
||||
56
LctMonolith/Infrastructure/Repositories/GenericRepository.cs
Normal file
56
LctMonolith/Infrastructure/Repositories/GenericRepository.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LctMonolith.Infrastructure.Data;
|
||||
|
||||
namespace LctMonolith.Infrastructure.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace LctMonolith.Infrastructure.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);
|
||||
}
|
||||
|
||||
34
LctMonolith/Infrastructure/UnitOfWork/IUnitOfWork.cs
Normal file
34
LctMonolith/Infrastructure/UnitOfWork/IUnitOfWork.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using LctMonolith.Domain.Entities;
|
||||
using LctMonolith.Infrastructure.Repositories;
|
||||
|
||||
namespace LctMonolith.Infrastructure.UnitOfWork;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of Work aggregates repositories and transaction boundary.
|
||||
/// </summary>
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
IGenericRepository<AppUser> Users { get; }
|
||||
IGenericRepository<Rank> Ranks { get; }
|
||||
IGenericRepository<RankRequiredMission> RankRequiredMissions { get; }
|
||||
IGenericRepository<RankRequiredCompetency> RankRequiredCompetencies { get; }
|
||||
IGenericRepository<Mission> Missions { get; }
|
||||
IGenericRepository<UserMission> UserMissions { get; }
|
||||
IGenericRepository<MissionCompetencyReward> MissionCompetencyRewards { get; }
|
||||
IGenericRepository<MissionArtifactReward> MissionArtifactRewards { get; }
|
||||
IGenericRepository<Competency> Competencies { get; }
|
||||
IGenericRepository<UserCompetency> UserCompetencies { get; }
|
||||
IGenericRepository<Artifact> Artifacts { get; }
|
||||
IGenericRepository<UserArtifact> UserArtifacts { 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; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||
Task BeginTransactionAsync(CancellationToken ct = default);
|
||||
Task CommitAsync(CancellationToken ct = default);
|
||||
Task RollbackAsync(CancellationToken ct = default);
|
||||
}
|
||||
99
LctMonolith/Infrastructure/UnitOfWork/UnitOfWork.cs
Normal file
99
LctMonolith/Infrastructure/UnitOfWork/UnitOfWork.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using LctMonolith.Domain.Entities;
|
||||
using LctMonolith.Infrastructure.Data;
|
||||
using LctMonolith.Infrastructure.Repositories;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace LctMonolith.Infrastructure.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<RankRequiredMission>? _rankRequiredMissions;
|
||||
private IGenericRepository<RankRequiredCompetency>? _rankRequiredCompetencies;
|
||||
private IGenericRepository<Mission>? _missions;
|
||||
private IGenericRepository<UserMission>? _userMissions;
|
||||
private IGenericRepository<MissionCompetencyReward>? _missionCompetencyRewards;
|
||||
private IGenericRepository<MissionArtifactReward>? _missionArtifactRewards;
|
||||
private IGenericRepository<Competency>? _competencies;
|
||||
private IGenericRepository<UserCompetency>? _userCompetencies;
|
||||
private IGenericRepository<Artifact>? _artifacts;
|
||||
private IGenericRepository<UserArtifact>? _userArtifacts;
|
||||
private IGenericRepository<StoreItem>? _storeItems;
|
||||
private IGenericRepository<UserInventoryItem>? _userInventoryItems;
|
||||
private IGenericRepository<Transaction>? _transactions;
|
||||
private IGenericRepository<EventLog>? _eventLogs;
|
||||
private IGenericRepository<RefreshToken>? _refreshTokens;
|
||||
private IGenericRepository<Notification>? _notifications;
|
||||
|
||||
public IGenericRepository<AppUser> Users => _users ??= new GenericRepository<AppUser>(_ctx);
|
||||
public IGenericRepository<Rank> Ranks => _ranks ??= new GenericRepository<Rank>(_ctx);
|
||||
public IGenericRepository<RankRequiredMission> RankRequiredMissions => _rankRequiredMissions ??= new GenericRepository<RankRequiredMission>(_ctx);
|
||||
public IGenericRepository<RankRequiredCompetency> RankRequiredCompetencies => _rankRequiredCompetencies ??= new GenericRepository<RankRequiredCompetency>(_ctx);
|
||||
public IGenericRepository<Mission> Missions => _missions ??= new GenericRepository<Mission>(_ctx);
|
||||
public IGenericRepository<UserMission> UserMissions => _userMissions ??= new GenericRepository<UserMission>(_ctx);
|
||||
public IGenericRepository<MissionCompetencyReward> MissionCompetencyRewards => _missionCompetencyRewards ??= new GenericRepository<MissionCompetencyReward>(_ctx);
|
||||
public IGenericRepository<MissionArtifactReward> MissionArtifactRewards => _missionArtifactRewards ??= new GenericRepository<MissionArtifactReward>(_ctx);
|
||||
public IGenericRepository<Competency> Competencies => _competencies ??= new GenericRepository<Competency>(_ctx);
|
||||
public IGenericRepository<UserCompetency> UserCompetencies => _userCompetencies ??= new GenericRepository<UserCompetency>(_ctx);
|
||||
public IGenericRepository<Artifact> Artifacts => _artifacts ??= new GenericRepository<Artifact>(_ctx);
|
||||
public IGenericRepository<UserArtifact> UserArtifacts => _userArtifacts ??= new GenericRepository<UserArtifact>(_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 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user