using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using MiGu.DB.Abstractions.Entities; using MiGu.DB.Abstractions.Exceptions; using MiGu.DB.Abstractions.Persistence; using MiGu.DB.Abstractions.Runtime; using MiGu.DB.Kernel.Context; namespace MiGu.DB.Kernel.Repositories; public class Repository : IRepository where TEntity : class, IEntity { protected readonly MiGuDbContext Db; protected DbSet Set => Db.Set(); public Repository(MiGuDbContext db) => Db = db; public virtual IQueryable Query(bool asNoTracking = true) => asNoTracking ? Set.AsNoTracking() : Set.AsQueryable(); public virtual Task FindAsync(TKey id, CancellationToken ct = default) => Set.FindAsync([id], ct).AsTask(); public virtual Task AddAsync(TEntity entity, CancellationToken ct = default) => Set.AddAsync(entity, ct).AsTask(); public virtual void Update(TEntity entity) => Set.Update(entity); } public sealed class EditableRepository : Repository, IEditableRepository where TEntity : class, IEntity, ISoftDeletable, IVersioned, ILockable { private readonly IActorContextAccessor _actors; public EditableRepository(MiGuDbContext db, IActorContextAccessor actors) : base(db) => _actors = actors; public async Task GetEditableAsync(Guid id, long? expectedVersion, CancellationToken ct = default) { var entity = await Set.FirstOrDefaultAsync(x => x.Id.Equals(id), ct) ?? throw new EntityNotFoundException(typeof(TEntity).Name, id); if (entity.IsLock) throw new EntityLockedException(typeof(TEntity).Name, id); if (expectedVersion.HasValue && entity.Version != expectedVersion.Value) throw new ConcurrencyConflictException(typeof(TEntity).Name, id, expectedVersion); return entity; } public async Task SoftDeleteAsync(Guid id, long? expectedVersion, CancellationToken ct = default) { var entity = await GetEditableAsync(id, expectedVersion, ct); entity.IsDeleted = true; entity.DeletedAt = DateTimeOffset.UtcNow; entity.DeletedBy = _actors.Current.Name; if (entity is IAuditable auditable) auditable.UpdatedBy = _actors.Current.Name; } public async Task EnsureUniqueAsync(Expression> predicate, string errorMessage, CancellationToken ct = default) { if (await Set.AnyAsync(predicate, ct)) throw new InvalidOperationException(errorMessage); } } public sealed class HistoryRepository : Repository, IHistoryRepository where TEntity : class, IEntity, IHistoryEntry { public HistoryRepository(MiGuDbContext db) : base(db) { } public Task AppendAsync(TEntity entry, CancellationToken ct = default) => AddAsync(entry, ct); } public sealed class UnitOfWork : IUnitOfWork { private readonly MiGuDbContext _db; public UnitOfWork(MiGuDbContext db) => _db = db; public async Task SaveChangesAsync(CancellationToken ct = default) { try { return await _db.SaveChangesAsync(ct); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.FirstOrDefault(); throw new ConcurrencyConflictException( entry?.Entity.GetType().Name ?? "Unknown", entry?.Property("Id")?.CurrentValue); } } public Task ExecuteInTransactionAsync(Func action, CancellationToken ct = default) { var strategy = _db.Database.CreateExecutionStrategy(); return strategy.ExecuteAsync(async () => { await using var tx = await _db.Database.BeginTransactionAsync(ct); try { await action(ct); await _db.SaveChangesAsync(ct); await tx.CommitAsync(ct); } catch { await tx.RollbackAsync(ct); throw; } }); } }