Skip to ContentSkip to Content
Repository API

IRepositoryAsync<T>

IRepositoryAsync<T> is the core interface for all data operations in Forge.Repository. It is resolved through IUnitOfWork.GetRepositoryAsync<T>() and provides full async CRUD, bulk operations, predicate queries, criteria-based queries, entity tracking controls, and raw SQL access — all with CancellationToken support throughout.

namespace Forge.Interfaces; public interface IRepositoryAsync<TEntity> where TEntity : class, IBaseModel { // ... all methods described in this page }

Always resolve repositories via IUnitOfWork.GetRepositoryAsync<T>() rather than injecting IRepositoryAsync<T> directly. This ensures all repositories in a request share the same DbContext instance and can be committed together.


Resolving a Repository

public class ProductService(IUnitOfWork uow) { // Resolved once per service instance — same DbContext scope private readonly IRepositoryAsync<Product> _products = uow.GetRepositoryAsync<Product>(); private readonly IRepositoryAsync<Category> _categories = uow.GetRepositoryAsync<Category>(); }

Read Operations

GetAllAsync

Returns all entities of type T in the table. Use with care on large tables — prefer FindAllAsync with a predicate for filtered queries.

Task<IList<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
var allProducts = await _products.GetAllAsync(ct);

GetByIdAsync

Retrieves a single entity by its string Id. Returns null if no entity with that Id exists.

Task<TEntity> GetByIdAsync(string id, CancellationToken cancellationToken = default);
var product = await _products.GetByIdAsync("01HX9Z3BKFGPV6M2K1N8Q7RW5D", ct); if (product is null) return Results.NotFound();

FindAsync

Returns the first entity matching the given predicate, or null if none matches.

Task<TEntity> FindAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
var featured = await _products.FindAsync(p => p.IsFeatured && !p.IsDeleted, ct);

FindAllAsync

Returns all entities matching the predicate. Returns an empty list if none match.

Task<IList<TEntity>> FindAllAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
// All active products in a given category var electronics = await _products.FindAllAsync( p => p.CategoryId == "electronics" && !p.IsDeleted, ct);

CountAsync

Three overloads — count everything, count by predicate, or count by criteria object.

Task<int> CountAsync(CancellationToken cancellationToken = default); Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default); Task<int> CountAsync(ICountCriteriaAsync<TEntity> criteria, CancellationToken cancellationToken = default);
// Total rows var total = await _products.CountAsync(ct); // Filtered count var activeCount = await _products.CountAsync(p => !p.IsDeleted, ct); // Via criteria object (see Criteria Pattern page) var categoryCount = await _products.CountAsync(new ProductsByCategoryCriteria("electronics"), ct);

Write Operations

Write operations stage changes in the EF Core change tracker but do not persist to the database until you call uow.SaveChangesAsync(). Always call save after your writes.

AddAsync

Adds a single entity to the change tracker as Added.

Task AddAsync(TEntity entity, CancellationToken cancellationToken = default);
var product = new Product { Name = "Wireless Keyboard", Price = 79.99m }; await _products.AddAsync(product, ct); await uow.SaveChangesAsync(ct);

AddRangeAsync

Adds a collection of entities in a single operation. More efficient than looping AddAsync for large collections.

Task AddRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);
var batch = Enumerable.Range(1, 100) .Select(i => new Product { Name = $"Product {i}", Price = i * 9.99m }) .ToList(); await _products.AddRangeAsync(batch, ct); await uow.SaveChangesAsync(ct);

UpdateAsync

Marks a single entity as Modified in the change tracker.

Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
var product = await _products.GetByIdAsync(id, ct); product.Price = 89.99m; await _products.UpdateAsync(product, ct); await uow.SaveChangesAsync(ct);

UpdateRangeAsync

Marks a collection of entities as Modified. Use for bulk updates.

Task UpdateRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);
var discounted = (await _products.FindAllAsync(p => p.CategoryId == "clearance", ct)) .Select(p => { p.Price *= 0.8m; return p; }) .ToList(); await _products.UpdateRangeAsync(discounted, ct); await uow.SaveChangesAsync(ct);

RemoveAsync

For IBaseModel entities: physically deletes the row.
For IAuditableBaseModel entities: calls OnDelete() → sets IsDeleted = true → saves as a soft delete.

Task RemoveAsync(TEntity entity, CancellationToken cancellationToken = default);
var product = await _products.GetByIdAsync(id, ct); await _products.RemoveAsync(product, ct); await uow.SaveChangesAsync(ct); // If Product implements IAuditableBaseModel or inherit from AuditableBaseModel, the row is NOT deleted — IsDeleted = true

RemoveRangeAsync

Removes or soft-deletes a collection of entities.

Task RemoveRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);
var expired = await _products.FindAllAsync( p => p.ExpiresAt < DateTime.UtcNow, ct); await _products.RemoveRangeAsync(expired, ct); await uow.SaveChangesAsync(ct);

Criteria-Based Queries

These overloads accept criteria objects — see the Criteria Pattern page for full details.

MatchAsync — list

Task<IList<TEntity>> MatchAsync( ICriteriaAsync<TEntity> criteria, CancellationToken cancellationToken = default);
var products = await _products.MatchAsync( new ActiveProductsByCategory("electronics"), ct);

MatchAsync — single entity

Task<TEntity> MatchAsync( ICriteriaSingleAsync<TEntity> criteria, CancellationToken cancellationToken = default);
var featured = await _products.MatchAsync( new FeaturedProductCriteria(), ct);

MatchAsync — foreign entity list

Projects entities into a different output type (DTOs, view models).

Task<IList<TFEntity>> MatchAsync<TFEntity>( ICriteriaForeignEntity<TEntity, TFEntity> criteria, CancellationToken cancellationToken = default) where TFEntity : class;
var summaries = await _products.MatchAsync<ProductSummaryDto>( new ProductSummaryQuery(), ct);

MatchAsync — foreign entity single

Task<TFEntity> MatchAsync<TFEntity>( ICriteriaSingleForeignEntity<TEntity, TFEntity> criteria, CancellationToken cancellationToken = default) where TFEntity : class;
var detail = await _products.MatchAsync<ProductDetailDto>( new ProductDetailQuery(id), ct);

CountAsync — criteria

Task<int> CountAsync( ICountCriteriaAsync<TEntity> criteria, CancellationToken cancellationToken = default);
var count = await _products.CountAsync( new ActiveProductsInCategoryCriteria("electronics"), ct);

Raw SQL

For complex queries that LINQ cannot express efficiently. See the Raw SQL page for full details.

FindAllBySql

Task<IList<TFEntity>> FindAllBySql<TFEntity>( string sql, CancellationToken cancellationToken); Task<IList<TFEntity>> FindAllBySql<TFEntity>( string sql, CancellationToken cancellationToken, params object[] parameters);
// Without parameters var products = await _products.FindAllBySql<Product>( "SELECT * FROM Products WHERE IsDeleted = 0", ct); // With parameters (positional — @p0, @p1, ...) var filtered = await _products.FindAllBySql<Product>( "SELECT * FROM Products WHERE CategoryId = @p0 AND Price < @p1", ct, "electronics", 100m);

ExecuteSqlRaw

Executes a non-query SQL command (INSERT / UPDATE / DELETE / stored procedure) and returns the number of rows affected.

Task<int> ExecuteSqlRaw(string sql, CancellationToken cancellationToken); Task<int> ExecuteSqlRaw(string sql, CancellationToken cancellationToken, params object[] parameters);
var rows = await _products.ExecuteSqlRaw( "UPDATE Products SET IsDeleted = 1 WHERE ExpiresAt < @p0", ct, DateTime.UtcNow);

Entity Tracking

EF Core tracks entity state by default. Forge exposes explicit tracking controls when you need to manage this manually.

Attach

Begins tracking an existing entity without marking it as Added or Modified. Useful when you have a detached entity (e.g. from a DTO) and want to attach it before updating.

void Attach(TEntity entity);
var product = new Product { Id = knownId, Name = "Updated Name", Price = 99m }; _products.Attach(product); // Now modify specific properties and mark as Modified await _products.UpdateAsync(product, ct); await uow.SaveChangesAsync(ct);

Detach

Removes a single entity from the change tracker. Changes made to it after detaching will not be saved.

void Detach(TEntity entity);
var product = await _products.GetByIdAsync(id, ct); // Read the data, then stop tracking it _products.Detach(product);

DetachAll

Clears the entire change tracker for this entity type. Useful before performing a large read-only query to avoid tracking overhead.

void DetachAll();
// After a large batch write, clear tracking before the next read await _products.AddRangeAsync(batch, ct); await uow.SaveChangesAsync(ct); _products.DetachAll(); var refreshed = await _products.GetAllAsync(ct);

Complete Method Reference

MethodReturnDescription
GetAllAsyncTask<IList<T>>All entities
GetByIdAsync(id)Task<T>Single by Id or null
FindAsync(predicate)Task<T>First match or null
FindAllAsync(predicate)Task<IList<T>>All matching predicate
CountAsync()Task<int>Total count
CountAsync(predicate)Task<int>Filtered count
CountAsync(criteria)Task<int>Count via criteria
AddAsync(entity)TaskStage single insert
AddRangeAsync(entities)TaskStage bulk insert
UpdateAsync(entity)TaskStage single update
UpdateRangeAsync(entities)TaskStage bulk update
RemoveAsync(entity)TaskStage delete / soft delete
RemoveRangeAsync(entities)TaskStage bulk delete / soft delete
MatchAsync(ICriteriaAsync)Task<IList<T>>Criteria list query
MatchAsync(ICriteriaSingleAsync)Task<T>Criteria single query
MatchAsync<TF>(ICriteriaForeignEntity)Task<IList<TF>>Criteria projection list
MatchAsync<TF>(ICriteriaSingleForeignEntity)Task<TF>Criteria projection single
FindAllBySql<TF>(sql, ct)Task<IList<TF>>Raw SELECT
FindAllBySql<TF>(sql, ct, params)Task<IList<TF>>Raw SELECT with params
ExecuteSqlRaw(sql, ct)Task<int>Raw command
ExecuteSqlRaw(sql, ct, params)Task<int>Raw command with params
Attach(entity)voidStart tracking entity
Detach(entity)voidStop tracking entity
DetachAll()voidClear all tracking