using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using PowderCoating.Infrastructure.Data;
namespace PowderCoating.Infrastructure.Repositories;
///
/// Generic repository that provides standard CRUD, search, and pagination operations for any
/// entity that inherits from .
///
/// The repository operates on top of , which already applies
/// two global query filters to every query: soft-delete (IsDeleted == false) and
/// multi-tenancy (CompanyId == CurrentCompanyId). Pass ignoreQueryFilters: true
/// to any read method to bypass both filters simultaneously — use this sparingly and
/// only in SuperAdmin contexts or when generating unique sequential numbers (to avoid gaps).
///
///
/// All write methods (Add, Update, Delete, SoftDelete) stage changes in the EF Core change
/// tracker only. Call (or CompleteAsync)
/// after one or more write operations to flush the batch to the database in a single round-trip.
///
///
/// An entity type that inherits .
public class Repository : IRepository where T : BaseEntity
{
/// The shared DbContext instance; all repositories within the same share this context so they participate in the same change-tracker and transaction.
protected readonly ApplicationDbContext _context;
/// The EF Core DbSet for ; obtained once in the constructor to avoid repeated Set<T>() dictionary lookups.
protected readonly DbSet _dbSet;
///
/// Initialises the repository by resolving the from the shared context.
///
/// The scoped DbContext provided by .
public Repository(ApplicationDbContext context)
{
_context = context;
_dbSet = context.Set();
}
///
/// Retrieves a single entity by its primary key, with optional eager loading.
/// Returns null if no matching row is found (after applying query filters).
///
/// The primary key value to look up.
///
/// When true, bypasses both the soft-delete and multi-tenancy global query filters.
/// Use for SuperAdmin lookups of deleted or cross-tenant records.
///
/// Optional navigation-property selectors for eager loading (avoids N+1 queries).
public virtual async Task GetByIdAsync(int id, bool ignoreQueryFilters = false, params Expression>[] includes)
{
IQueryable query = _dbSet;
if (ignoreQueryFilters)
{
query = query.IgnoreQueryFilters();
}
foreach (var include in includes)
{
query = query.Include(include);
}
return await query.FirstOrDefaultAsync(e => e.Id == id);
}
///
/// Retrieves all entities of type , with optional eager loading.
/// Global query filters (soft-delete + tenant isolation) are applied unless
/// is true.
///
/// Avoid calling this without a filter on large tables — prefer or
/// to limit the result set.
///
///
/// When true, bypasses soft-delete and multi-tenancy filters.
/// Optional navigation-property selectors for eager loading.
public virtual async Task> GetAllAsync(bool ignoreQueryFilters = false, params Expression>[] includes)
{
IQueryable query = _dbSet;
if (ignoreQueryFilters)
{
query = query.IgnoreQueryFilters();
}
foreach (var include in includes)
{
query = query.Include(include);
}
return await query.ToListAsync();
}
///
/// Returns all entities that satisfy , with optional eager loading.
/// The predicate is composed with global query filters (unless bypassed), so the final SQL
/// WHERE clause is the conjunction of both conditions — no tenant data can leak through a
/// permissive predicate.
///
/// A LINQ expression used to filter results.
/// When true, bypasses soft-delete and multi-tenancy filters.
/// Optional navigation-property selectors for eager loading.
public virtual async Task> FindAsync(Expression> predicate, bool ignoreQueryFilters = false, params Expression>[] includes)
{
IQueryable query = _dbSet;
if (ignoreQueryFilters)
query = query.IgnoreQueryFilters();
foreach (var include in includes)
query = query.Include(include);
return await query.Where(predicate).ToListAsync();
}
///
/// Returns the first entity that satisfies , or null if none match.
/// Useful when exactly one result is expected but membership in the result set is uncertain
/// (e.g., looking up a record by a unique business key such as email or job number).
///
/// A LINQ expression used to filter results.
/// When true, bypasses soft-delete and multi-tenancy filters.
/// Optional navigation-property selectors for eager loading.
public virtual async Task FirstOrDefaultAsync(Expression> predicate, bool ignoreQueryFilters = false, params Expression>[] includes)
{
IQueryable query = _dbSet;
if (ignoreQueryFilters)
query = query.IgnoreQueryFilters();
foreach (var include in includes)
query = query.Include(include);
return await query.FirstOrDefaultAsync(predicate);
}
///
/// Returns true if at least one entity satisfies .
/// More efficient than when only existence is needed, because
/// EF Core translates this to SELECT TOP 1 rather than a full aggregation.
///
/// A LINQ expression used to test membership.
/// When true, bypasses soft-delete and multi-tenancy filters.
public virtual async Task AnyAsync(Expression> predicate, bool ignoreQueryFilters = false)
{
IQueryable query = _dbSet;
if (ignoreQueryFilters)
{
query = query.IgnoreQueryFilters();
}
return await query.AnyAsync(predicate);
}
///
/// Counts entities, optionally filtered by .
/// Used by dashboard stat cards and pagination metadata.
/// When is null, counts all visible rows for the current tenant.
///
/// An optional LINQ expression to narrow the count. Pass null for a total count.
/// When true, bypasses soft-delete and multi-tenancy filters to count across all tenants or include deleted rows.
public virtual async Task CountAsync(Expression>? predicate = null, bool ignoreQueryFilters = false)
{
IQueryable query = _dbSet;
if (ignoreQueryFilters)
{
query = query.IgnoreQueryFilters();
}
return predicate == null
? await query.CountAsync()
: await query.CountAsync(predicate);
}
///
/// Stages a new entity for insertion.
/// The entity is not yet written to the database — call
/// to commit. will auto-stamp
/// CreatedAt, CreatedBy, and CompanyId (if 0) before the INSERT.
///
/// The new entity to insert.
/// The same entity reference (useful for chaining or to read the auto-generated ID after save).
public virtual async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
return entity;
}
///
/// Stages a collection of new entities for bulk insertion.
/// More efficient than calling in a loop because EF Core batches the
/// INSERTs into a single round-trip.
///
/// The entities to insert.
/// The same collection reference passed in.
public virtual async Task> AddRangeAsync(IEnumerable entities)
{
await _dbSet.AddRangeAsync(entities);
return entities;
}
///
/// Marks an entity as modified so that all its properties will be included in the next UPDATE.
///
/// Returns a completed synchronously because EF Core's
/// DbSet.Update is synchronous — the async signature is preserved for interface
/// consistency and to allow overrides that perform async validation.
///
///
/// The entity to update. Must be tracked or will be attached in the Modified state.
public virtual Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
return Task.CompletedTask;
}
///
/// Marks a collection of entities as modified for bulk update.
/// EF Core batches the UPDATEs into a single round-trip on save.
///
/// The entities to update.
public virtual Task UpdateRangeAsync(IEnumerable entities)
{
_dbSet.UpdateRange(entities);
return Task.CompletedTask;
}
///
/// Physically removes an entity from the database.
///
/// Use sparingly — most deletions should use instead so that
/// audit history is preserved and the entity remains visible to SuperAdmin queries.
/// Physical delete is appropriate for draft records, temporary data, or entities whose
/// referential integrity constraints prohibit soft delete.
///
///
/// The entity to physically remove.
public virtual async Task DeleteAsync(T entity)
{
_dbSet.Remove(entity);
await Task.CompletedTask;
}
///
/// Retrieves the entity by and then physically removes it.
/// If no entity is found (already deleted or not in this tenant), the operation is a no-op.
///
/// The primary key of the entity to delete.
public virtual async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
await DeleteAsync(entity);
}
}
///
/// Physically removes a collection of entities in a single SQL DELETE statement.
/// EF Core translates RemoveRange into a batched DELETE for efficiency.
///
/// The entities to physically remove.
public virtual async Task DeleteRangeAsync(IEnumerable entities)
{
_dbSet.RemoveRange(entities);
await Task.CompletedTask;
}
///
/// Soft-deletes an entity by setting IsDeleted = true and DeletedAt = UtcNow.
/// The row remains in the database and is visible to SuperAdmin queries with
/// ignoreQueryFilters: true, preserving full audit history.
/// The entity is then staged as Modified — call to commit.
///
/// The entity to soft-delete.
public virtual async Task SoftDeleteAsync(T entity)
{
entity.IsDeleted = true;
entity.DeletedAt = DateTime.UtcNow;
await UpdateAsync(entity);
}
///
/// Retrieves the entity by and soft-deletes it.
/// If no entity is found (already deleted or not in this tenant), the operation is a no-op.
///
/// The primary key of the entity to soft-delete.
public virtual async Task SoftDeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
await SoftDeleteAsync(entity);
}
}
///
/// Returns a page of results with the total count for pagination metadata.
///
/// Design note: the total count is computed before ordering and slicing so
/// that callers always receive the accurate count for the current filter, regardless of page
/// size. The ordering is applied after counting to minimise SQL complexity.
///
///
/// 1-based page index.
/// Number of items per page.
/// Optional predicate to narrow the result set; applied before count and paging.
/// Optional ordering function; applied after counting but before slicing.
/// Optional navigation-property selectors for eager loading.
/// A tuple of (Items, TotalCount) where TotalCount is the total matching rows before paging.
public virtual async Task<(IEnumerable Items, int TotalCount)> GetPagedAsync(
int pageNumber,
int pageSize,
Expression>? filter = null,
Func, IOrderedQueryable>? orderBy = null,
params Expression>[] includes)
{
IQueryable query = _dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var include in includes)
{
query = query.Include(include);
}
var totalCount = await query.CountAsync();
if (orderBy != null)
{
query = orderBy(query);
}
var items = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
}