From 00ae8e1ba0a3871c2395b7e4e920f502696a8cff Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 13 Aug 2026 09:36:48 +0200 Subject: [PATCH] Add bulk create and upsert methods to repository Added `CreateAsync` for bulk entity creation and `UpsertAsync` and `UpsertSingleAsync` for upsert operations in `IRepository` and `Repository`. Improved error handling in `UpdateSingleAsync` and `DeleteSingleAsync` by throwing `NotFoundException` when no matching entity is found. Refactored `FindAsync` and `UpdateAsync` for better readability and performance. Cleaned up formatting in `DeleteAsync`. --- .../Interfaces/Repositories/IRepository.cs | 7 ++ .../Repositories/Repository.cs | 79 ++++++++++++++++--- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/Repositories/IRepository.cs b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/Repositories/IRepository.cs index 92f8611..ea85f14 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Interfaces/Repositories/IRepository.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Interfaces/Repositories/IRepository.cs @@ -11,6 +11,8 @@ public interface IRepository where TEntity : class // CREATE Task CreateAsync(TDto dto, CancellationToken cancellationToken = default); + Task> CreateAsync(IEnumerable dtos, CancellationToken cancellationToken = default); + // READ Task GetByIdAsync(int id, CancellationToken cancellationToken = default); Task> GetAllAsync(CancellationToken cancellationToken = default); @@ -20,6 +22,11 @@ public interface IRepository where TEntity : class Task CountAsync(Expression>? predicate = null, CancellationToken cancellationToken = default); Task AnyAsync(Expression> predicate, CancellationToken cancellationToken = default); + // UPSERT + Task<(TEntity Entity, bool Created)> UpsertAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default); + + Task<(TEntity Entity, bool Created)> UpsertSingleAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default); + // UPDATE Task UpdateSingleAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default); Task UpdateAsync(Expression> predicate, TDto dto, CancellationToken cancellationToken = default); diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Repositories/Repository.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Repositories/Repository.cs index 2e78d98..6ef4a35 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Repositories/Repository.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Repositories/Repository.cs @@ -25,6 +25,14 @@ public class Repository(MessagingServiceDbContext Context, IMapper Mapp return entity; } + public async Task> CreateAsync(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) @@ -44,13 +52,13 @@ public class Repository(MessagingServiceDbContext Context, IMapper Mapp 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); } @@ -84,6 +92,59 @@ public class Repository(MessagingServiceDbContext Context, IMapper Mapp 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 --- /// @@ -96,7 +157,7 @@ public class Repository(MessagingServiceDbContext Context, IMapper Mapp TDto dto, CancellationToken cancellationToken = default) { - var entity = await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken) + 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); @@ -113,12 +174,7 @@ public class Repository(MessagingServiceDbContext Context, IMapper Mapp CancellationToken cancellationToken = default) { var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken); - - foreach (var entity in entities) - { - Mapper.Map(dto, entity); - } - + entities.ForEach(entity => Mapper.Map(dto, entity)); await Context.SaveChangesAsync(cancellationToken); return entities.Count; } @@ -134,7 +190,7 @@ public class Repository(MessagingServiceDbContext Context, IMapper Mapp Expression> predicate, CancellationToken cancellationToken = default) { - var entity = await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken) + 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); @@ -150,7 +206,6 @@ public class Repository(MessagingServiceDbContext Context, IMapper Mapp CancellationToken cancellationToken = default) { var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken); - _dbSet.RemoveRange(entities); await Context.SaveChangesAsync(cancellationToken); return entities.Count;