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 = trueRemoveRangeAsync
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
| Method | Return | Description |
|---|---|---|
GetAllAsync | Task<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) | Task | Stage single insert |
AddRangeAsync(entities) | Task | Stage bulk insert |
UpdateAsync(entity) | Task | Stage single update |
UpdateRangeAsync(entities) | Task | Stage bulk update |
RemoveAsync(entity) | Task | Stage delete / soft delete |
RemoveRangeAsync(entities) | Task | Stage 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) | void | Start tracking entity |
Detach(entity) | void | Stop tracking entity |
DetachAll() | void | Clear all tracking |