Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Orbis25/FoundationKit/llms.txt

Use this file to discover all available pages before exploring further.

BaseRepository<TContext, TModel> is the abstract class that backs the IBaseRepository<TModel> interface with a full EF Core implementation. Inherit from it, pass your DbContext subclass as TContext and your entity as TModel, and you immediately gain a transaction-wrapped CRUD layer with ordered, filterable, paginated queries — no boilerplate required. All virtual methods can be overridden if you need to customise behaviour for a specific entity. Because the class is abstract it can never be instantiated directly; you must create a concrete subclass for each entity type you want to manage. Dependency injection is typically set up by registering the concrete class against its custom interface (or IBaseRepository<TModel> directly) in your service container.

Class Signature

public abstract class BaseRepository<TContext, TModel> : IBaseRepository<TModel>
    where TContext : DbContext
    where TModel   : BaseModel
  • TContext must be a class deriving from Microsoft.EntityFrameworkCore.DbContext.
  • TModel must be a class deriving from FoundationKit.Domain.Models.BaseModel, which provides Id (Guid), CreatedAt, UpdatedAt, CreatedBy, UpdatedBy, and IsDeleted.

Constructor

protected BaseRepository(TContext context)
context
TContext
required
The EF Core DbContext instance. Inject this via the constructor of your concrete repository class. BaseRepository holds it in a private readonly field; derived classes do not have direct access to _context — all persistence goes through the base methods.

Method Implementations

CreateAsync

Adds the entity to the DbSet<TModel> and calls CommitAsync to persist it inside a database transaction.
public virtual async Task<TModel> CreateAsync(TModel model, CancellationToken cancellationToken = default)
{
    _context.Set<TModel>().Add(model);
    await CommitAsync(cancellationToken);
    return model;
}
The returned entity is the same instance that was passed in, now tracked by EF Core and reflecting any database-generated column values. Override this method if you need to set audit fields (e.g., CreatedBy from IHttpContextAccessor) before the insert.

GetAll

Builds an IQueryable<TModel> with optional Where, Include, and OrderBy/OrderByDescending operators applied in that order. No database round-trip occurs until the query is enumerated.
public virtual IQueryable<TModel> GetAll(
    Expression<Func<TModel, bool>>? expression = default,
    bool orderDesc = true,
    Expression<Func<TModel, object>>? ordered = default,
    params Expression<Func<TModel, object>>[] includes)
Ordering precedence:
ConditionApplied ordering
ordered != null and orderDesc == trueOrderByDescending(ordered)
ordered != null and orderDesc == falseOrderBy(ordered)
ordered == null and orderDesc == trueOrderByDescending(x => x.CreatedAt) (default)
ordered == null and orderDesc == falseOrderBy(x => x.CreatedAt)
The default sort by CreatedAt descending means the most recently created records appear first without any caller configuration, which suits most list endpoints.

GetPaginatedListAsync

Calls GetAll to build the filtered, ordered query, then applies Skip/Take based on Paginate.Page and Paginate.Qyt. When Paginate.NoPaginate is true, paging is skipped and all matching rows are returned in the Results collection.
public virtual async Task<PaginationResult<TModel>> GetPaginatedListAsync(
    Paginate paginate,
    Expression<Func<TModel, bool>>? expression = default,
    Expression<Func<TModel, object>>? ordered = default,
    CancellationToken cancellationToken = default,
    params Expression<Func<TModel, object>>[] includes)
Pagination metadata is populated only when NoPaginate is false:
FieldValue
ActualPagepaginate.Page
Qytpaginate.Qyt
PageTotalMath.Ceiling(total / paginate.Qyt)
Totalresults.Count() (before Skip/Take)
ResultsThe paged items (AsNoTracking)
When NoPaginate is true, only Results is populated; all numeric metadata fields are zero. This is useful for export endpoints or dropdowns that need all records but want to reuse the same API shape.

GetListAsync

Delegates to GetAll and materialises the result with ToListAsync. This is the simplest way to retrieve a filtered, ordered, in-memory collection without dealing with IQueryable composition.
public virtual async Task<IEnumerable<TModel>> GetListAsync(
    bool orderDesc = true,
    Expression<Func<TModel, bool>>? expression = null,
    Expression<Func<TModel, object>>? ordered = null,
    CancellationToken cancellationToken = default,
    params Expression<Func<TModel, object>>[] includes)
    => await GetAll(expression, orderDesc, ordered, includes).ToListAsync(cancellationToken);
The parameter order differs from GetAll: orderDesc comes first so that the most common customisation (changing sort direction) does not require naming the other arguments.

GetByIdAsync

Calls GetAll with a CreatedAt descending sort, optionally switches to AsNoTracking, then executes FirstOrDefaultAsync for the given Id.
public virtual async Task<TModel?> GetByIdAsync(
    Guid id,
    bool asNotTraking = false,
    CancellationToken cancellationToken = default,
    params Expression<Func<TModel, object>>[] includes)
id
Guid
required
Primary key of the entity to retrieve.
asNotTraking
bool
default:"false"
When true, calls AsNoTracking() before executing — suitable for read-only lookups that will not be modified.
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
includes
params Expression<Func<TModel, object>>[]
Navigation-property selectors for eager loading.
TModel?
Task
The matched entity, or null when no record with the given Id exists.

GetOneAsync

Returns the first entity in the DbSet that matches the predicate, using EF Core’s FirstOrDefaultAsync.
public Task<TModel?> GetOneAsync(
    Expression<Func<TModel, bool>> expression,
    CancellationToken cancellationToken = default)
    => _context.Set<TModel>().FirstOrDefaultAsync(expression, cancellationToken);
Unlike GetByIdAsync, no includes or ordering are applied — the query goes directly to FirstOrDefaultAsync. Use this for simple single-row lookups by a non-primary-key predicate.

UpdateAsync

Applies DbSet<TModel>.Update(model) and commits. The verifyEntity flag controls whether a pre-fetch guards against overwriting audit fields.
public virtual async Task<TModel?> UpdateAsync(
    TModel model,
    CancellationToken cancellationToken = default,
    bool verifyEntity = true)
When verifyEntity == true:
  1. GetByIdAsync(model.Id, asNotTraking: true) is called to retrieve the current stored entity.
  2. If the entity is not found, null is returned immediately (no update is attempted).
  3. CreatedBy and CreatedAt from the stored entity are copied onto model to prevent accidental audit-field corruption.
  4. Update(model) and CommitAsync are called.
When verifyEntity == false the pre-fetch is skipped entirely — use this when you have already loaded the entity in the same unit of work and can guarantee the audit fields are correct. A two-parameter convenience overload is also available; it delegates to the full overload with verifyEntity: false:
public virtual Task<TModel?> UpdateAsync(TModel model, CancellationToken cancellationToken = default)
    => UpdateAsync(model, cancellationToken, default);

UpdatePartialEntityAsync

Attaches the entity to the change tracker without loading it from the database, then marks only the properties in updateExpression as EntityState.Modified. EF Core generates an UPDATE statement targeting only those columns.
public async Task UpdatePartialEntityAsync(
    TModel entity,
    List<Expression<Func<TModel, object?>>> updateExpression,
    CancellationToken cancellationToken = default)
This method calls SaveChangesAsync directly rather than going through CommitAsync (no explicit transaction wrapper). If you need transactional guarantees, call CommitAndResultAsync or manage the transaction externally. Example — update only the Price column:
var product = new Product { Id = id, Price = 49.99m };
await _repo.UpdatePartialEntityAsync(
    product,
    new List<Expression<Func<Product, object?>>> { p => p.Price },
    cancellationToken);

SoftRemoveAsync

Fetches the entity by Id, sets IsDeleted = true, then calls Update + CommitAsync. The row is retained in the database with IsDeleted flagged so queries can filter it out.
public virtual async Task<bool> SoftRemoveAsync(Guid id, CancellationToken cancellationToken = default)
Returns false immediately (without touching the database) when no entity with the given Id exists.

RemoveAsync

Permanently deletes the entity by fetching it with GetByIdAsync(id, asNotTraking: true) and then calling DbSet<TModel>.Remove followed by CommitAsync.
public virtual async Task<bool> RemoveAsync(Guid id, CancellationToken cancellationToken = default)
Returns false immediately when no entity with the given Id exists. This operation cannot be undone.

ExistAsync

Returns whether at least one entity matching the optional predicate exists. When expression is null, calls AnyAsync() without a predicate — returning true if the table has any rows.
public async Task<bool> ExistAsync(
    Expression<Func<TModel, bool>>? expression = default,
    CancellationToken cancellationToken = default)
expression
Expression<Func<TModel, bool>>?
default:"null"
Optional filter predicate. When null, returns true if the table contains any rows.
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
bool
Task
true if at least one matching row exists; otherwise false.

CountAsync

Chains zero or more Where predicates on the DbSet<TModel> and returns the row count.
public async Task<int> CountAsync(
    CancellationToken cancellationToken = default,
    params Expression<Func<TModel, bool>>[] expression)
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
expression
params Expression<Func<TModel, bool>>[]
Zero or more filter predicates. Each is applied as a successive Where call (logical AND). Passing no expressions counts all rows.
int
Task
The row count satisfying all predicates.

CommitAsync

Wraps SaveChangesAsync in an explicit database transaction. On success the transaction is committed; on any exception the transaction is rolled back and a DbUpdateException is thrown with the base exception’s message.
public async Task CommitAsync(CancellationToken cancellationToken = default)
{
    using var transaction = _context.Database.BeginTransaction();
    try
    {
        await _context.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);
    }
    catch (Exception ex)
    {
        await transaction.RollbackAsync(cancellationToken);
        throw new DbUpdateException(ex.GetBaseException().Message);
    }
}
CommitAsync is public and can be called directly from a concrete repository when you have staged multiple change-tracker mutations (e.g., Add + Update on different entity sets) that must commit atomically. All of the CreateAsync, UpdateAsync, SoftRemoveAsync, and RemoveAsync methods call CommitAsync internally — they each run in their own transaction. If you need multiple operations in a single transaction, stage them manually and call CommitAsync once at the end.

CommitAndResultAsync

Identical in behaviour to CommitAsync but returns a string? instead of throwing on failure. Returns null when the commit succeeds, or the error message string when it fails (transaction is still rolled back).
public async Task<string?> CommitAndResultAsync(CancellationToken cancellationToken = default)
Use this method when you want to surface database errors as API response messages without propagating exceptions through the call stack.
var error = await _repo.CommitAndResultAsync(ct);
if (error is not null)
    return BadRequest(error);
return Ok();

Minimal Implementation Example

// FoundationKit.Domain.Models
public class Order : BaseModel
{
    public string Reference  { get; set; } = string.Empty;
    public decimal Total     { get; set; }
    public string Status     { get; set; } = "Pending";
}
Every CreateAsync, UpdateAsync, SoftRemoveAsync, and RemoveAsync call wraps its SaveChangesAsync in BeginTransaction / CommitAsync / RollbackAsync. If your database provider does not support explicit transactions (e.g., some in-memory providers used in unit tests), switch to CommitAndResultAsync or mock CommitAsync in your test setup.

Build docs developers (and LLMs) love