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.

MapRepository is the abstract base class to use when your application follows the DTO pattern and you want the repository layer to own all mapping concerns. Unlike BaseRepository, which works exclusively with raw EF Core entity types, MapRepository accepts typed input DTOs for writes and automatically projects entity queries to output DTOs at the database level using AutoMapper’s ProjectTo. This means your controllers and services only ever see DTOs — entity types are confined to the repository and the EF Core DbContext. Choose MapRepository over BaseRepository when:
  • Your API surface exposes DTOs that differ structurally from the database entity (e.g., flattened navigation properties, computed fields, or hidden audit columns).
  • You want SELECT statements to include only the columns your DTO needs, reducing data transfer.
  • You want a single AutoMapper profile to serve as the canonical mapping definition for both reads and writes.

Class Signature

public abstract class MapRepository<TContext, TEntity, TInputModel, TEditModel, TDtoModel>
    : IMapRepository<TEntity, TInputModel, TEditModel, TDtoModel>
    where TContext    : DbContext
    where TEntity     : BaseModel
    where TInputModel : BaseInput
    where TDtoModel   : BaseOutput
    where TEditModel  : BaseEdit
Type ParameterConstraintRole
TContextDbContextYour application’s EF Core context
TEntityBaseModelThe database entity class
TInputModelBaseInputDTO for create operations
TEditModelBaseEditDTO for update operations
TDtoModelBaseOutputRead-side DTO returned by all queries

Constructor

protected MapRepository(TContext context, IMapper mapper)
context
TContext
required
The EF Core DbContext instance. Stored privately; subclasses interact with the database exclusively through the base class methods.
mapper
IMapper
required
An AutoMapper IMapper instance. Used for TInputModel → TEntity, TEditModel → TEntity, and TEntity → TDtoModel mappings, as well as the reverse TDtoModel → TEntity mapping needed by SoftRemove and Remove.

Method Implementations

GetAll — Query-Level DTO Projection

GetAll is the foundation of every read method. It calls ProjectTo<TDtoModel>(_mapper.ConfigurationProvider) on the raw entity DbSet, converting the LINQ expression tree into a SQL SELECT that only fetches the columns your DTO maps to. No intermediate entity objects are materialised in memory.
public virtual IQueryable<TDtoModel> GetAll(
    Expression<Func<TDtoModel, bool>>? expression = default,
    bool orderDesc = true,
    Expression<Func<TDtoModel, object>>? ordered = default,
    params Expression<Func<TDtoModel, object>>[] includes)
{
    var results = _context.Set<TEntity>()
        .ProjectTo<TDtoModel>(_mapper.ConfigurationProvider)
        .AsQueryable();

    if (expression != null) results = results.Where(expression);
    foreach (var include in includes) results = results.Include(include);

    // Ordering: explicit selector → fallback to CreatedAt
    if (ordered != null && orderDesc)       results = results.OrderByDescending(ordered);
    else if (!orderDesc && ordered != null) results = results.OrderBy(ordered);
    else if (orderDesc)                     results = results.OrderByDescending(x => x.CreatedAt);
    else                                    results = results.OrderBy(x => x.CreatedAt);

    return results;
}
Because filtering and ordering are applied after ProjectTo, expressions are written against TDtoModel properties, not entity properties. Ensure your AutoMapper profile maps every property your filter or sort expressions reference.

GetPaginatedList

Calls GetAll to build the filtered, ordered query, then applies Skip/Take based on Paginate.Page and Paginate.Qyt.
public virtual async Task<PaginationResult<TDtoModel>> GetPaginatedList(
    Paginate paginate,
    Expression<Func<TDtoModel, bool>>? expression = default,
    Expression<Func<TDtoModel, object>>? ordered = default,
    CancellationToken cancellationToken = default,
    params Expression<Func<TDtoModel, object>>[] includes)
paginate
Paginate
required
Pagination settings: Page, Qyt (items per page), OrderByDesc, and NoPaginate.
expression
Expression<Func<TDtoModel, bool>>?
default:"null"
Optional filter predicate applied before Skip/Take.
ordered
Expression<Func<TDtoModel, object>>?
default:"null"
Optional DTO property selector for ordering. Falls back to CreatedAt when null.
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
includes
params Expression<Func<TDtoModel, object>>[]
Navigation-property selectors for eager loading.
PaginationResult<TDtoModel>
Task
Contains ActualPage, Qyt, PageTotal, Total, and Results (IReadOnlyCollection<TDtoModel>).

GetList

Delegates to GetAll and materialises the result with ToListAsync.
public virtual async Task<IEnumerable<TDtoModel>> GetList(
    Expression<Func<TDtoModel, bool>>? expression = default,
    bool orderDesc = true,
    Expression<Func<TDtoModel, object>>? ordered = default,
    CancellationToken cancellationToken = default,
    params Expression<Func<TDtoModel, object>>[] includes)
    => await GetAll(expression, orderDesc, ordered, includes).ToListAsync(cancellationToken);
expression
Expression<Func<TDtoModel, bool>>?
default:"null"
Optional filter predicate. Pass null to retrieve all rows.
orderDesc
bool
default:"true"
true for descending order; false for ascending.
ordered
Expression<Func<TDtoModel, object>>?
default:"null"
Optional sort-property selector.
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
includes
params Expression<Func<TDtoModel, object>>[]
Navigation properties for eager loading.
IEnumerable<TDtoModel>
Task
A fully materialised, in-memory collection of projected DTOs.

GetById

Projects the entity DbSet to TDtoModel via ProjectTo, then executes FirstOrDefaultAsync for the given Id.
public virtual async Task<TDtoModel?> GetById(
    Guid id,
    bool asNotTraking = false,
    CancellationToken cancellationToken = default,
    params Expression<Func<TDtoModel, object>>[] includes)
id
Guid
required
Primary key of the record to fetch.
asNotTraking
bool
default:"false"
When true, the query is executed with AsNoTracking().
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
includes
params Expression<Func<TDtoModel, object>>[]
Navigation properties to include on the projected DTO query.
TDtoModel?
Task
The projected DTO, or null if no record with the given Id exists.

GetOneAsync

Returns the first projected TDtoModel matching the predicate, using FirstOrDefaultAsync on the ProjectTo query.
public Task<TDtoModel?> GetOneAsync(
    Expression<Func<TDtoModel, bool>> expression,
    CancellationToken cancellationToken = default)
    => _context.Set<TEntity>()
        .ProjectTo<TDtoModel>(_mapper.ConfigurationProvider)
        .FirstOrDefaultAsync(expression, cancellationToken);
expression
Expression<Func<TDtoModel, bool>>
required
Filter predicate applied to the projected DTO query.
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
TDtoModel?
Task
The first matching DTO, or null.

Create — Input DTO to Entity to Output DTO

public virtual async Task<TDtoModel?> Create(
    TInputModel model, CancellationToken cancellationToken = default)
{
    var entity = _mapper.Map<TInputModel, TEntity>(model);
    _context.Set<TEntity>().Add(entity);
    await CommitAsync(cancellationToken);
    return _mapper.Map<TEntity, TDtoModel>(entity);
}
  1. Maps TInputModelTEntity via _mapper.Map.
  2. Adds the entity to the DbSet and commits within a transaction.
  3. Maps the persisted entity (now with its database-assigned Id, CreatedAt, etc.) back to TDtoModel for the response.

Update — Audit-Preserving Entity Update

Two overloads are available. The full overload exposes the verifyEntity flag; the convenience overload omits it and delegates to the full one with verifyEntity: false.
// Full overload
public virtual async Task<TDtoModel?> Update(
    TEditModel model,
    CancellationToken cancellationToken = default,
    bool verifyEntity = true)

// Convenience overload
public Task<TDtoModel?> Update(TEditModel model, CancellationToken cancellationToken = default)
    => Update(model, cancellationToken, default);
When verifyEntity == true:
  1. Calls GetById(model.Id, asNotTraking: true) to fetch the current DTO.
  2. Copies CreatedBy and CreatedAt from the existing DTO onto model to prevent accidental overwrite of immutable audit fields.
  3. Maps TEditModelTEntity and calls Update + CommitAsync.
  4. Maps the updated entity back to TDtoModel.
Returns null immediately when verifyEntity is true and no record with the given Id exists.

Exist

Returns whether any projected TDtoModel matches the predicate. When expression is null, returns false (unlike IBaseRepository.ExistAsync, which returns true for any row when the expression is null).
public async Task<bool> Exist(
    Expression<Func<TDtoModel, bool>>? expression = default,
    CancellationToken cancellationToken = default)
expression
Expression<Func<TDtoModel, bool>>?
default:"null"
Filter predicate. When null, the method returns false.
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
bool
Task
true when at least one projected DTO matches the expression; false otherwise.

Count

Chains zero or more Where predicates on the ProjectTo query and returns the row count.
public async Task<int> Count(
    CancellationToken cancellationToken = default,
    params Expression<Func<TDtoModel, bool>>[] expression)
cancellationToken
CancellationToken
default:"default"
Propagates cancellation.
expression
params Expression<Func<TDtoModel, bool>>[]
Zero or more filter predicates (logical AND). Passing none counts all rows.
int
Task
Row count satisfying all predicates.

SoftRemove — DTO-Mapped Logical Delete

public virtual async Task<bool> SoftRemove(Guid id, CancellationToken cancellationToken = default)
{
    var result = await GetById(id, asNotTraking: true);
    if (result == null) return false;

    var entity = _mapper.Map<TDtoModel, TEntity>(result);
    entity.IsDeleted = true;

    _context.Set<TEntity>().Update(entity);
    await CommitAsync(cancellationToken);
    return true;
}
Because MapRepository does not hold entity references after queries (all data is projected to DTOs), SoftRemove must map the DTO back to an entity before it can set IsDeleted = true. This means your AutoMapper profile must include a TDtoModel → TEntity reverse mapping.

Remove

Fetches the record as a DTO, maps it back to the entity, removes the entity, and commits.
public virtual async Task<bool> Remove(Guid id, CancellationToken cancellationToken = default)
Like SoftRemove, this method requires a TDtoModel → TEntity reverse mapping in your AutoMapper profile. Returns false when no entity with the given Id exists.

GetEntities — Raw Entity Access

The extended IMapRepository<T, ...> interface exposes GetEntities, which returns an IQueryable<TEntity> directly from the DbSet. Use this when you need entity-level data not available through the DTO projection.
public IQueryable<TEntity> GetEntities(Expression<Func<TEntity, bool>>? expression = null)
    => _context.Set<TEntity>().AsQueryable();
The expression parameter is accepted by the method signature but is not applied in the current implementation — the returned IQueryable is always unfiltered. Chain a .Where() call on the returned queryable to apply your predicate.

UpdatePartialEntityAsync — Column-Level Partial Update

Uses EF Core’s change-tracker entry API to mark only listed properties as Modified, generating a targeted UPDATE statement without fetching the current row.
public async Task UpdatePartialEntityAsync(
    TEntity entity,
    List<Expression<Func<TEntity, object?>>> updateExpression,
    CancellationToken cancellationToken = default)
{
    var entry = _context.Entry(entity);
    foreach (var property in updateExpression)
        entry.Property(property).IsModified = true;

    await _context.SaveChangesAsync(cancellationToken);
}

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.
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 when you have staged multiple mutations that must commit atomically. All of Create, Update, SoftRemove, and Remove call CommitAsync internally.

CommitAndResultAsync

Identical to CommitAsync but returns null on success or an error message string on failure instead of throwing. The transaction is still rolled back on error.
public async Task<string?> CommitAndResultAsync(CancellationToken cancellationToken = default)
Use this 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();

AutoMapper Profile Setup

Every MapRepository subclass requires three mapping directions to be configured in AutoMapper:
All three mapping profiles — TInputModel → TEntity, TEditModel → TEntity, and TEntity → TDtoModelmust be registered. Missing any one of them will cause a runtime AutoMapperMappingException (or an InvalidOperationException from ProjectTo) the first time the corresponding operation is called. The TDtoModel → TEntity reverse mapping is also required for SoftRemove and Remove.
public class PersonMappingProfile : Profile
{
    public PersonMappingProfile()
    {
        // Create: TInputModel → TEntity
        CreateMap<CreatePersonInput, Person>();

        // Update: TEditModel → TEntity
        CreateMap<EditPersonInput, Person>();

        // Read: TEntity → TDtoModel (used by ProjectTo)
        CreateMap<Person, PersonDto>()
            .ForMember(d => d.FullName, opt => opt.MapFrom(s => $"{s.FirstName} {s.LastName}"));

        // Reverse: TDtoModel → TEntity (required by SoftRemove / Remove)
        CreateMap<PersonDto, Person>();
    }
}
Call builder.Services.AddFoundationKit(Assembly.GetExecutingAssembly()) in Program.cs to automatically discover and register every Profile subclass in your assembly — including PersonMappingProfile above. You do not need to call services.AddAutoMapper(...) separately; AddFoundationKit handles that registration.

Complete Implementation Example

public class Person : BaseModel
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName  { get; set; } = string.Empty;
    public string Email     { get; set; } = string.Empty;
}

Build docs developers (and LLMs) love