214 lines
7.5 KiB
C#
214 lines
7.5 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Generic repository implementation with AutoMapper-based CRUD operations.
|
|
/// IMPORTANT: Each operation auto-saves changes - NO explicit SaveChangesAsync needed!
|
|
/// </summary>
|
|
public class Repository<TEntity>(MessagingServiceDbContext Context, IMapper Mapper) : IRepository<TEntity> where TEntity : class
|
|
{
|
|
protected readonly DbSet<TEntity> DbSet = Context.Set<TEntity>();
|
|
|
|
// --- CREATE ---
|
|
|
|
public async Task<TEntity> CreateAsync<TDto>(TDto dto, CancellationToken cancellationToken = default)
|
|
{
|
|
var entity = Mapper.Map<TEntity>(dto);
|
|
await DbSet.AddAsync(entity, cancellationToken);
|
|
await Context.SaveChangesAsync(cancellationToken);
|
|
return entity;
|
|
}
|
|
|
|
public async Task<IEnumerable<TEntity>> CreateRangeAsync<TDto>(IEnumerable<TDto> dtos, CancellationToken cancellationToken = default)
|
|
{
|
|
var entities = Mapper.Map<IEnumerable<TEntity>>(dtos);
|
|
await DbSet.AddRangeAsync(entities, cancellationToken);
|
|
await Context.SaveChangesAsync(cancellationToken);
|
|
return entities;
|
|
}
|
|
|
|
// --- READ ---
|
|
|
|
public async Task<TEntity?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
|
{
|
|
return await DbSet.FindAsync([id], cancellationToken);
|
|
}
|
|
|
|
public async Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return await DbSet.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<IEnumerable<TEntity>> FindAsync(
|
|
Expression<Func<TEntity, bool>> 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<TEntity?> FindFirstAsync(
|
|
Expression<Func<TEntity, bool>> predicate,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await DbSet.FirstOrDefaultAsync(predicate, cancellationToken);
|
|
}
|
|
|
|
public async Task<TEntity?> FindSingleAsync(
|
|
Expression<Func<TEntity, bool>> predicate,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await DbSet.SingleOrDefaultAsync(predicate, cancellationToken);
|
|
}
|
|
|
|
public async Task<int> CountAsync(
|
|
Expression<Func<TEntity, bool>>? predicate = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return predicate == null
|
|
? await DbSet.CountAsync(cancellationToken)
|
|
: await DbSet.CountAsync(predicate, cancellationToken);
|
|
}
|
|
|
|
public async Task<bool> AnyAsync(
|
|
Expression<Func<TEntity, bool>> predicate,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
return await DbSet.AnyAsync(predicate, cancellationToken);
|
|
}
|
|
|
|
// --- UPSERT ---
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public async Task<(TEntity Entity, bool Created)> UpsertAsync<TDto>(
|
|
Expression<Func<TEntity, bool>> predicate,
|
|
TDto dto,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var entity = await DbSet.FirstOrDefaultAsync(predicate, cancellationToken);
|
|
|
|
if (entity is null)
|
|
{
|
|
entity = Mapper.Map<TEntity>(dto);
|
|
await DbSet.AddAsync(entity, cancellationToken);
|
|
await Context.SaveChangesAsync(cancellationToken);
|
|
return (entity, true);
|
|
}
|
|
|
|
Mapper.Map(dto, entity);
|
|
await Context.SaveChangesAsync(cancellationToken);
|
|
return (entity, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public async Task<(TEntity Entity, bool Created)> UpsertSingleAsync<TDto>(
|
|
Expression<Func<TEntity, bool>> predicate,
|
|
TDto dto,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var entity = await DbSet.SingleOrDefaultAsync(predicate, cancellationToken);
|
|
|
|
if (entity is null)
|
|
{
|
|
entity = Mapper.Map<TEntity>(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 ---
|
|
|
|
/// <summary>
|
|
/// Updates a SINGLE entity that matches the predicate.
|
|
/// Throws NotFoundException if 0 or 2+ records match.
|
|
/// Auto-saves changes.
|
|
/// </summary>
|
|
public async Task UpdateSingleAsync<TDto>(
|
|
Expression<Func<TEntity, bool>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates ALL entities that match the predicate (bulk operation).
|
|
/// Returns count of updated records.
|
|
/// Auto-saves changes.
|
|
/// </summary>
|
|
public async Task<int> UpdateAsync<TDto>(
|
|
Expression<Func<TEntity, bool>> 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 ---
|
|
|
|
/// <summary>
|
|
/// Deletes a SINGLE entity that matches the predicate.
|
|
/// Throws NotFoundException if 0 or 2+ records match.
|
|
/// Auto-saves changes.
|
|
/// </summary>
|
|
public async Task DeleteSingleAsync(
|
|
Expression<Func<TEntity, bool>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes ALL entities that match the predicate (bulk operation).
|
|
/// Returns count of deleted records.
|
|
/// Auto-saves changes.
|
|
/// </summary>
|
|
public async Task<int> DeleteAsync(
|
|
Expression<Func<TEntity, bool>> predicate,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var entities = await DbSet.Where(predicate).ToListAsync(cancellationToken);
|
|
DbSet.RemoveRange(entities);
|
|
await Context.SaveChangesAsync(cancellationToken);
|
|
return entities.Count;
|
|
}
|
|
}
|