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;
}
}