name: foundatio-repositories description: > Use this skill when querying, counting, patching, or paginating data through Foundatio.Repositories Elasticsearch abstractions. Covers filter expressions, aggregation queries, partial and script patches, and search-after pagination. Apply when working with normal query/count/patch/pagination repository code; reserve raw IElasticClient for migrations or index maintenance that cannot be expressed through repositories.
Foundatio Repositories
Foundatio.Repositories provides a high-level Elasticsearch abstraction. Use repository methods for normal query, count, patch, and pagination work. Reserve raw IElasticClient for migrations or index maintenance that cannot be expressed through repositories.
Documentation: https://repositories.foundatio.dev / https://parsers.foundatio.dev
Repository Hierarchy
IRepository<T> — CRUD, Patch, Remove
└─ ISearchableRepository<T> — FindAsync, CountAsync, aggregations
└─ IRepositoryOwnedByOrganization<T>
└─ IRepositoryOwnedByProject<T>
└─ IRepositoryOwnedByOrganizationAndProject<T>
| Interface | Entity | Index Type |
|---|---|---|
IEventRepository |
PersistentEvent |
DailyIndex (date-partitioned) |
IStackRepository |
Stack |
VersionedIndex (single index) |
IProjectRepository |
Project |
VersionedIndex |
IOrganizationRepository |
Organization |
VersionedIndex |
IUserRepository |
User |
VersionedIndex |
ITokenRepository |
Token |
VersionedIndex |
IMigrationStateRepository |
MigrationState |
VersionedIndex |
Important: .Index(start, end) only routes to correct daily shards for DailyIndex (events). It is a no-op for VersionedIndex.
CountAsync + AggregationsExpression
CountAsync returns a CountResult with .Total (long) and .Aggregations (AggregationsHelper).
AggregationsExpression DSL
| Expression | Meaning |
|---|---|
cardinality:field |
Distinct count |
terms:field |
Terms aggregation |
terms:(field~SIZE) |
Terms with bucket size limit |
terms:(field~SIZE sub_agg) |
Terms with nested aggregation |
terms:(field @include:VALUE) |
Terms with include filter |
date:field |
Date histogram (auto interval) |
date:field~1d |
Date histogram, daily interval |
date:field~1M |
Date histogram, monthly interval |
date:(field sub_agg) |
Date histogram with nested agg |
sum:field~DEFAULT |
Sum with default value |
min:field / max:field |
Min/Max aggregation |
avg:field |
Average aggregation |
-sum:field~1 |
Sort descending by this agg (prefix -) |
Multiple aggregations are space-separated: "cardinality:stack_id terms:type sum:count~1"
Accessing Aggregation Results
Naming convention: {type}_{field} — the aggregation type prefix + underscore + field name.
result.Aggregations.Cardinality("cardinality_stack_id").Value
result.Aggregations.Terms<string>("terms_type").Buckets // .Key, .Total
result.Aggregations.DateHistogram("date_date").Buckets // .Date, .Total
result.Aggregations.Sum("sum_count").Value
result.Aggregations.Min<DateTime>("min_date").Value
result.Aggregations.Max<DateTime>("max_date").Value
result.Aggregations.Average("avg_value").Value
// Nested aggs inside buckets
foreach (var bucket in result.Aggregations.Terms<string>("terms_stack_id").Buckets)
bucket.Aggregations.Cardinality("cardinality_user").Value;
FilterExpression (Lucene-style)
FilterExpression accepts Lucene query syntax parsed by Foundatio Parsers:
.FilterExpression("type:error (status:open OR status:regressed)")
.FilterExpression($"project:{projectId}")
.FilterExpression($"stack:{stackId}")
.FilterExpression($"signature_hash:{signature}")
.FilterExpression("is_deleted:false")
Building OR filters from collections:
string filter = String.Join(" OR ", stackIds.Select(id => $"stack:{id}"));
Query Extension Methods
| Method | Purpose | File |
|---|---|---|
.Organization(id) |
Filter by organization_id | OrganizationQuery.cs |
.Organization(ids) |
Filter by multiple org IDs | OrganizationQuery.cs |
.Project(id) |
Filter by project_id | ProjectQuery.cs |
.Stack(id) / .Stack(ids) |
Filter by stack_id | StackQuery.cs |
.ExcludeStack(id) |
Exclude stack_id | StackQuery.cs |
.AppFilter(sf) |
Apply app-level system filter | AppFilterQuery.cs |
.SystemFilter(query) |
Chain a pre-built query | Foundatio built-in |
.EnforceEventStackFilter() |
Resolve stack filters to event IDs | EventStackFilterQuery.cs |
.DateRange(start, end, field) |
Date range filter | Foundatio built-in |
.Index(start, end) |
Route to daily shards (events only) | Foundatio built-in |
.FieldEquals(expr, value) |
Exact field match | Foundatio built-in |
.SortExpression(sort) |
Sort expression | Foundatio built-in |
Pagination
Use SearchAfterPaging() for deep pagination (never offset-based). NextPageAsync() returns Task<bool> and mutates results in-place.
var results = await _repository.GetAllAsync(o => o.SearchAfterPaging().PageLimit(500));
do
{
foreach (var doc in results.Documents)
{
// process document
}
} while (!cancellationToken.IsCancellationRequested && await results.NextPageAsync());
Key rules:
- Never use
while(true) { ... break; }— usedo/whileorwhile(condition) - Always check
CancellationTokenin the loop condition
PatchAllAsync / PatchAsync
Use PartialPatch for field-level updates, ScriptPatch for Painless scripts. Pass o => o.ImmediateConsistency() when write-then-read consistency is needed (tests). Use o => o.Notifications(false) to suppress change notifications.
await _tokenRepository.PatchAllAsync(
q => q.Organization(orgId).FieldEquals(t => t.IsSuspended, false),
new PartialPatch(new { is_suspended = true }),
o => o.ImmediateConsistency());
Anti-Patterns
Avoid these in normal repository/query work:
- Use
_elasticClient.SearchAsync<T>(...)— useCountAsyncorFindAsync - Use
_elasticClient.MultiGetAsync(...)— useGetByIdsAsync - Use
_elasticClient.DeleteByQueryAsync<T>(...)— useRemoveAllAsync - Use
_elasticClient.UpdateByQueryAsync<T>(...)— usePatchAllAsync - Use
_elasticClient.Indices.RefreshAsync(...)— useo => o.ImmediateConsistency() - Use
while(true) { ... break; }for pagination — usedo/whileorwhile(condition)