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