using System.Linq.Expressions; using AutoMapper; using DigitalData.MessagingService.Application.Common.Interfaces.Repositories; using DigitalData.MessagingService.Domain.Exceptions; using DigitalData.MessagingService.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace DigitalData.MessagingService.Infrastructure.Repositories; /// /// Generic repository implementation with AutoMapper-based CRUD operations. /// IMPORTANT: Each operation auto-saves changes - NO explicit SaveChangesAsync needed! /// public class Repository(MessagingServiceDbContext Context, IMapper Mapper) : IRepository where TEntity : class { protected readonly DbSet DbSet = Context.Set(); // --- CREATE --- public async Task CreateAsync(TDto dto, CancellationToken cancellationToken = default) { var entity = Mapper.Map(dto); await DbSet.AddAsync(entity, cancellationToken); await Context.SaveChangesAsync(cancellationToken); return entity; } public async Task> CreateRangeAsync(IEnumerable dtos, CancellationToken cancellationToken = default) { var entities = Mapper.Map>(dtos); await DbSet.AddRangeAsync(entities, cancellationToken); await Context.SaveChangesAsync(cancellationToken); return entities; } // --- READ --- public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) { return await DbSet.FindAsync([id], cancellationToken); } public async Task> GetAllAsync(CancellationToken cancellationToken = default) { return await DbSet.ToListAsync(cancellationToken); } public async Task> FindAsync( Expression> predicate, int? skip = null, int? take = null, CancellationToken cancellationToken = default) { var query = DbSet.Where(predicate); if (skip.HasValue) query = query.Skip(skip.Value); if (take.HasValue) query = query.Take(take.Value); return await query.ToListAsync(cancellationToken); } public async Task FindFirstAsync( Expression> predicate, CancellationToken cancellationToken = default) { return await DbSet.FirstOrDefaultAsync(predicate, cancellationToken); } public async Task FindSingleAsync( Expression> predicate, CancellationToken cancellationToken = default) { return await DbSet.SingleOrDefaultAsync(predicate, cancellationToken); } public async Task CountAsync( Expression>? predicate = null, CancellationToken cancellationToken = default) { return predicate == null ? await DbSet.CountAsync(cancellationToken) : await DbSet.CountAsync(predicate, cancellationToken); } public async Task AnyAsync( Expression> predicate, CancellationToken cancellationToken = default) { return await DbSet.AnyAsync(predicate, cancellationToken); } // --- UPSERT --- /// /// Upsert: if no record matches the predicate, creates a new entity; /// if one or more match, updates the FIRST match. /// Returns the entity and a flag indicating whether it was created (true) or updated (false). /// Auto-saves changes. /// public async Task<(TEntity Entity, bool Created)> UpsertAsync( Expression> predicate, TDto dto, CancellationToken cancellationToken = default) { var entity = await DbSet.FirstOrDefaultAsync(predicate, cancellationToken); if (entity is null) { entity = Mapper.Map(dto); await DbSet.AddAsync(entity, cancellationToken); await Context.SaveChangesAsync(cancellationToken); return (entity, true); } Mapper.Map(dto, entity); await Context.SaveChangesAsync(cancellationToken); return (entity, false); } /// /// Upsert (single-safe): if no record matches the predicate, creates a new entity; /// if exactly one matches, updates it. Throws InvalidOperationException if 2+ match. /// Auto-saves changes. /// public async Task<(TEntity Entity, bool Created)> UpsertSingleAsync( Expression> predicate, TDto dto, CancellationToken cancellationToken = default) { var entity = await DbSet.SingleOrDefaultAsync(predicate, cancellationToken); if (entity is null) { entity = Mapper.Map(dto); await DbSet.AddAsync(entity, cancellationToken); await Context.SaveChangesAsync(cancellationToken); return (entity, true); } Mapper.Map(dto, entity); await Context.SaveChangesAsync(cancellationToken); return (entity, false); } // --- UPDATE --- /// /// Updates a SINGLE entity that matches the predicate. /// Throws NotFoundException if 0 or 2+ records match. /// Auto-saves changes. /// public async Task UpdateSingleAsync( Expression> predicate, TDto dto, CancellationToken cancellationToken = default) { var entity = await DbSet.SingleOrDefaultAsync(predicate, cancellationToken) ?? throw new NotFoundException($"No {typeof(TEntity).Name} found matching the predicate."); Mapper.Map(dto, entity); await Context.SaveChangesAsync(cancellationToken); } /// /// Updates ALL entities that match the predicate (bulk operation). /// Returns count of updated records. /// Auto-saves changes. /// public async Task UpdateAsync( Expression> predicate, TDto dto, CancellationToken cancellationToken = default) { var entities = await DbSet.Where(predicate).ToListAsync(cancellationToken); entities.ForEach(entity => Mapper.Map(dto, entity)); await Context.SaveChangesAsync(cancellationToken); return entities.Count; } // --- DELETE --- /// /// Deletes a SINGLE entity that matches the predicate. /// Throws NotFoundException if 0 or 2+ records match. /// Auto-saves changes. /// public async Task DeleteSingleAsync( Expression> predicate, CancellationToken cancellationToken = default) { var entity = await DbSet.SingleOrDefaultAsync(predicate, cancellationToken) ?? throw new NotFoundException($"No {typeof(TEntity).Name} found matching the predicate."); DbSet.Remove(entity); await Context.SaveChangesAsync(cancellationToken); } /// /// Deletes ALL entities that match the predicate (bulk operation). /// Returns count of deleted records. /// Auto-saves changes. /// public async Task DeleteAsync( Expression> predicate, CancellationToken cancellationToken = default) { var entities = await DbSet.Where(predicate).ToListAsync(cancellationToken); DbSet.RemoveRange(entities); await Context.SaveChangesAsync(cancellationToken); return entities.Count; } }