MongoDB aggregation pipelines are the foundation of acre Access Control’s data query and analytics capabilities. While our high-level API wrapper methods provide convenient access to common operations, aggregates give you direct access to the full power of MongoDB’s query engine—enabling complex queries, real-time analytics, custom reports, and advanced data transformations that would be impossible with simple filters alone.
Aggregates are not just a query tool—they’re your bread and butter for building sophisticated access control solutions:
Performance Note: Aggregates execute on the MongoDB server, not in your application.
| Use Case | Use Aggregate? | Alternative |
|---|---|---|
| Find all people in a folder | ❌ No | Use GetPeopleAsync() |
| Need specific properties of objects | ✅ Yes | Aggregate with $project |
| Count total doors in instance | ✅ Yes | Aggregate with $count |
| Find people with >3 active cards | ✅ Yes | Aggregate with $match on array size |
| Get single person by key | ❌ No | Use GetPersonAsync(key) |
| Find access events in last 30 days for specific reader | ✅ Yes | Aggregate Events collection |
| List all access levels | ❌ No | Use GetAccessLevelsAsync() |
| Generate occupancy report by building/floor | ✅ Yes | Aggregate with $group and $sum |
| Find hardware offline >24 hours | ✅ Yes | Aggregate with date comparison |
acre Access Control uses two primary MongoDB collections for storing all system data.
What it contains: All acre Access Control objects except Events
Collection Name: KeepObjects
Use For: Finding objects, generating reports, analyzing system configuration
What it contains: All EventMessage objects
Collection Name: Events
Typical Size: Millions to billions of documents (high-volume, time-series data)
Use For: Event history, access analytics, compliance reporting, real-time monitoring
An aggregation pipeline is a sequence of stages that process documents step-by-step, like an assembly line. Each stage transforms the documents and passes them to the next stage.
[
{ "$match": { /* filter criteria */ } }, // Stage 1: Filter documents
{ "$project": { /* field selection */ } }, // Stage 2: Select/transform fields
{ "$sort": { /* sort order */ } }, // Stage 3: Order results
{ "$limit": 10 } // Stage 4: Limit results
]
Key Concept: Each stage receives documents from the previous stage and outputs documents to the next stage. This allows you to chain operations for powerful queries.
Documents in Collection
↓
$match (filter to relevant documents)
↓
$unwind (expand arrays)
↓
$group (aggregate/count)
↓
$sort (order results)
↓
$limit (take top N)
↓
Final Results
Purpose: Select documents that meet specific criteria (like SQL WHERE)
When to use: Always use $match as early as possible to reduce the number of documents processed in subsequent stages.
{
"$match": {
"CommonName": "Front Door"
}
}
Returns: All objects with CommonName exactly “Front Door”
{
"$match": {
"$and": [
{ "_t": "Person" },
{ "IsDeleted": { "$ne": true } }
]
}
}
Returns: All Person objects that are not deleted
{
"$match": {
"$or": [
{ "_t": "MercuryReader" },
{ "_t": "EngageReader" },
{ "_t": "WavelynxReader" }
]
}
}
Returns: All reader objects regardless of hardware type
{
"$match": {
"CardAssignments": { "$exists": true, "$ne": [] }
}
}
Returns: Objects that have a non-empty CardAssignments array
{
"$match": {
"OccurredOn.Date": {
"$gte": { "$date": "2026-01-01T00:00:00Z" },
"$lt": { "$date": "2026-02-01T00:00:00Z" }
}
}
}
Returns: All events that occurred in January 2026
{
"$match": {
"ObjectLinks.Relation": "Person",
"ObjectLinks.LinkedObjectId": { "$oid": "5af040aca3ac160df4ba26dc" }
}
}
Returns: All objects with an ObjectLink referencing the specified Person
{
"$match": {
"$text": { "$search": "John Smith" }
}
}
Returns: All objects matching “John Smith” in indexed text fields (case-insensitive)
Purpose: Include, exclude, rename, or compute fields (like SQL SELECT)
When to use: Reduce document size for performance, create calculated fields, reshape data
{
"$project": {
"_id": 0,
"ParentFolderIds": 0,
"InternalData": 0
}
}
Result: All fields except _id, ParentFolderIds, and InternalData
{
"$project": {
"CommonName": 1,
"CardAssignments": 1,
"_id": 0
}
}
Result: Only CommonName and CardAssignments fields
{
"$project": {
"name": "$CommonName",
"id": "$_id",
"folder": "$InFolderKey"
}
}
Result: Fields renamed to more client-friendly names
{
"$project": {
"CommonName": 1,
"CardCount": { "$size": { "$ifNull": ["$CardAssignments", []] } },
"HasCards": { "$gt": [{ "$size": { "$ifNull": ["$CardAssignments", []] } }, 0] }
}
}
Result: Original name plus computed CardCount and HasCards boolean
Purpose: Sort documents by one or more fields (like SQL ORDER BY)
Sort Values:
1 = Ascending (A-Z, 0-9, oldest-newest)-1 = Descending (Z-A, 9-0, newest-oldest){
"$sort": {
"CommonName": 1
}
}
Result: Documents sorted alphabetically by CommonName
{
"$sort": {
"Priority": -1,
"OccurredOn.Date": -1
}
}
Result: Events sorted by priority (high to low), then by time (newest first)
[
{
"$project": {
"CommonName": 1,
"CardCount": { "$size": { "$ifNull": ["$CardAssignments", []] } }
}
},
{
"$sort": { "CardCount": -1 }
}
]
Result: People sorted by number of cards (most cards first)
Purpose: Return only the first N documents (like SQL LIMIT)
When to use: Prevent overwhelming results, implement pagination, get “top N” results
{
"$limit": 100
}
Result: Only first 100 documents
[
{ "$match": { "_t": "Person" } },
{ "$sort": { "CommonName": 1 } },
{ "$limit": 10 }
]
Result: First 10 people alphabetically
Purpose: Skip the first N documents (like SQL OFFSET)
When to use: Implement pagination with $limit
{
"$skip": 50
}
Result: Skip first 50 documents, return rest
[
{ "$match": { "_t": "Person" } },
{ "$sort": { "CommonName": 1 } },
{ "$skip": 100 }, // Page 6: skip first 100 (pages 1-5)
{ "$limit": 20 } // Show 20 per page
]
Result: Page 6 of results (records 101-120)
Best Practice: Always use $sort before $skip/$limit to ensure consistent pagination
Purpose: Group documents by key and compute aggregates (like SQL GROUP BY)
When to use: Count, sum, average, find min/max, collect arrays
{
"$group": {
"_id": null,
"totalCount": { "$sum": 1 }
}
}
Result: Single document with total count
{
"$group": {
"_id": "$_t",
"count": { "$sum": 1 }
}
}
Result: One document per object type with count
Example Output:
[
{ "_id": "Person", "count": 1523 },
{ "_id": "MercuryReader", "count": 245 },
{ "_id": "AccessLevel", "count": 67 }
]
{
"$group": {
"_id": "$EventTypeId",
"totalEvents": { "$sum": 1 },
"totalPriority": { "$sum": "$Priority" },
"avgPriority": { "$avg": "$Priority" }
}
}
Result: Event statistics by type
{
"$group": {
"_id": "$InFolderKey",
"peopleNames": { "$push": "$CommonName" },
"peopleCount": { "$sum": 1 }
}
}
Result: For each folder, an array of all person names and the count
{
"$group": {
"_id": null,
"earliestEvent": { "$min": "$OccurredOn.Date" },
"latestEvent": { "$max": "$OccurredOn.Date" },
"highestPriority": { "$max": "$Priority" }
}
}
Result: Date range and max priority across all events
Purpose: Create a document for each array element (denormalize arrays)
When to use: Query array contents, join on array elements, count array items
{
"$unwind": "$CardAssignments"
}
Before:
{
"_id": "person1",
"CommonName": "John Smith",
"CardAssignments": [
{ "DisplayCardNumber": "12345", "EncodedCardNumber": 54321 },
{ "DisplayCardNumber": "67890", "EncodedCardNumber": 09876 }
]
}
After:
[
{
"_id": "person1",
"CommonName": "John Smith",
"CardAssignments": { "DisplayCardNumber": "12345", "EncodedCardNumber": 54321 }
},
{
"_id": "person1",
"CommonName": "John Smith",
"CardAssignments": { "DisplayCardNumber": "67890", "EncodedCardNumber": 09876 }
}
]
{
"$unwind": {
"path": "$ObjectLinks",
"preserveNullAndEmptyArrays": true
}
}
Result: Documents without ObjectLinks are still included (not dropped)
{
"$unwind": {
"path": "$CardAssignments",
"includeArrayIndex": "cardIndex"
}
}
Result: Each unwound document includes cardIndex field (0, 1, 2, …)
Purpose: Combine data from multiple collections (like SQL JOIN)
When to use: Enrich objects with related data, resolve references
{
"$keepLookup": {
"localField": "ObjectLinks.LinkedObjectId",
"foreignField": "_id",
"as": "linkedObjects"
}
}
Result: Each document includes linkedObjects array with full objects from KeepObjects collection
| Parameter | Required | Default | Description |
|---|---|---|---|
localField |
✅ Yes | N/A | Field in current document to match |
foreignField |
❌ No | "_id" |
Field in KeepObjects collection to match against |
as |
✅ Yes | N/A | Output array field name for matched documents |
preserveNull |
❌ No | false |
Include documents with no matches (like LEFT JOIN) |
unwind |
❌ No | true |
Automatically unwind the result array |
project |
❌ No | null |
Project specific fields from matched documents |
{
"$keepLookup": {
"localField": "ObjectLinks.LinkedObjectId",
"as": "linkedObjects",
"preserveNull": true
}
}
Result: Documents without matches are still included with empty linkedObjects array
{
"$keepLookup": {
"localField": "ObjectLinks.LinkedObjectId",
"as": "linkedObjects",
"unwind": false
}
}
Result: linkedObjects remains an array (not automatically unwound to individual documents)
{
"$keepLookup": {
"localField": "ObjectLinks.LinkedObjectId",
"as": "linkedObjects",
"project": {
"CommonName": 1,
"_t": 1,
"Email": 1
}
}
}
Result: Matched documents only include CommonName, _t, and Email fields
[
{
"$match": {
"ObjectLinks.Relation": "Person"
}
},
{
"$unwind": "$ObjectLinks"
},
{
"$match": {
"ObjectLinks.Relation": "Person"
}
},
{
"$keepLookup": {
"as": "Person",
"localField": "ObjectLinks.LinkedObjectId",
"foreignField": "_id",
"project": {
"CommonName": 1,
"CardAssignments": {
"$filter": {
"input": "$CardAssignments",
"as": "ca",
"cond": {
"$and": [
{
"$eq": [
"$$ca.IsDisabled",
false
]
},
{
"$gte": [
"$$ca.ExpiresOn",
"$$NOW"
]
}
]
}
}
}
}
}
},
{
"$limit": 100
}
]
Result: Events enriched with person’s card details
Note: $keepLookup always queries the KeepObjects collection. For performance, use $match before $keepLookup to reduce the number of documents being enriched and include a $project in the keepLookup to reduce data transfer.
Result: Each document includes ObjectLinks array with full object “pointers”
Note: $keepLookup has performance implications on large datasets. Use $match before $keepLookup to reduce document count.
Purpose: Return the count of documents (simpler than $group)
{
"$count": "totalDocuments"
}
Result:
{ "totalDocuments": 1523 }
Purpose: Run multiple aggregation pipelines on the same data
When to use: Get multiple statistics in one query (counts, averages, samples)
ALWAYS include a $match step before a $facet in the pipeline
{
"$facet": {
"totalCount": [{ "$count": "count" }],
"topTen": [{ "$sort": { "CommonName": 1 } }, { "$limit": 10 }],
"statistics": [
{
"$group": {
"_id": null,
"avgCards": { "$avg": { "$size": { "$ifNull": ["$CardAssignments", []] } } }
}
}
]
}
}
Result: Single document with totalCount, topTen, and statistics fields
| Operator | Description | Example |
|---|---|---|
$eq |
Equal to | { "Priority": { "$eq": 200 } } |
$ne |
Not equal to | { "IsDeleted": { "$ne": true } } |
$gt |
Greater than | { "Priority": { "$gt": 50 } } |
$gte |
Greater than or equal | { "Priority": { "$gte": 100 } } |
$lt |
Less than | { "Priority": { "$lt": 25 } } |
$lte |
Less than or equal | { "Priority": { "$lte": 10 } } |
$in |
In array | { "_t": { "$in": ["Person", "Attendee"] } } |
$nin |
Not in array | { "Status": { "$nin": ["Offline", "Unknown"] } } |
| Operator | Description | Example |
|---|---|---|
$and |
All conditions must be true | { "$and": [{ "a": 1 }, { "b": 2 }] } |
$or |
At least one condition must be true | { "$or": [{ "a": 1 }, { "b": 2 }] } |
$not |
Inverts condition | { "field": { "$not": { "$eq": 5 } } } |
$nor |
No conditions are true | { "$nor": [{ "a": 1 }, { "b": 2 }] } |
| Operator | Description | Example |
|---|---|---|
$exists |
Field exists | { "CardAssignments": { "$exists": true } } |
$type |
Field type check | { "_id": { "$type": "objectId" } } |
| Operator | Description | Example |
|---|---|---|
$size |
Array size | { "$size": "$CardAssignments" } |
$in |
Value in array | { "Tags": { "$in": ["vip"] } } |
$all |
All values in array | { "Tags": { "$all": ["Employee", "Active"] } } |
Use Case: Locate a cardholder by their encoded card number
[
{
"$match": {
"$and": [ // $and is redundant here
{ "_t": "Person" },
{ "CardAssignments.EncodedCardNumber": 123456 }
]
}
},
{
"$project": {
"_id": 0,
"CommonName": 1,
"CardAssignments": 1
}
}
]
C# Wrapper Example:
var pipeline = new BsonDocument[]
{
new BsonDocument("$match", new BsonDocument
{
{ "$and", new BsonArray
{
new BsonDocument("_t", "Person"),
new BsonDocument("CardAssignments.EncodedCardNumber", 123456)
}
}
})
};
var results = await client.AggregateAsync("KeepObjects", pipeline, instanceKey);
cURL Example:
curl -X POST 'https://api.us.acresecurity.cloud/api/f/FOLDER_ID/search?page=0&pageSize=50' \
-H "Authorization: Bearer TOKEN" \
-H 'Content-Type: application/json' \
-d '"{$and: [ { _t: \"Person\" }, {\"CardAssignments.EncodedCardNumber\": 123456} ] }"'
Use Case: Generate inventory report of all hardware
[
{
"$match": {
"_t": {
"$in": [
"MercuryReader", "MercuryController", "MercuryDownstream",
"EngageReader", "EngageSite",
"WavelynxReader", "WavelynxGateway",
"BoschPanel", "BoschArea"
]
}
}
},
{
"$group": {
"_id": {
"$last": "$_t"
},
"count": { "$sum": 1 },
"devices": { "$push": "$CommonName" }
}
},
{
"$sort": { "count": -1 }
}
]
Result:
[
{
"_id": "MercuryReader",
"count": 245,
"devices": ["Front Door", "Back Door", "Side Entrance", "..."]
},
{
"_id": "MercuryController",
"count": 45,
"devices": ["Building A LP1500", "Building B LP4502", "..."]
},
{
"_id": "EngageReader",
"count": 12,
"devices": ["Elevator Car 1", "Elevator Car 2", "..."]
}
]
Use Case: Generate access report for a specific door
[
{
"$match": {
"EventTypeId": { "$in": [{"$oid": "61cb71500000000000000000"}, {"$oid": "62ab71500000000000000000"} ] },
"ObjectLinks": {
"$elemMatch": {
"LinkedObjectId": { "$oid": "5b2134bca3ac160dd82dd987" },
"Relation": "Reader"
}
},
"OccurredOn.Date": {
"$gte": { "$date": "2025-12-22T00:00:00Z" },
"$lt": { "$date": "2026-01-21T23:59:59Z" }
}
}
},
{
"$sort": { "OccurredOn.Date": -1 }
},
{
"$limit": 100
},
{
"$project": {
"EventTypeId": 1,
"OccurredOn": 1,
"MessageLong": 1,
"Priority": 1,
"ObjectLinks": 1
}
}
]
Use Case: Identify cardholders with excessive card assignments
[
{
"$match": {
"_t": "Person"
}
},
{
"$project": {
"CommonName": 1,
"CardAssignments": 1,
"cardCount": { "$size": { "$ifNull": ["$CardAssignments", []] } }
}
},
{
"$match": {
"cardCount": { "$gt": 3 }
}
},
{
"$sort": { "cardCount": -1 }
}
]
Result: People sorted by number of cards (most first), only those with >3 cards
Use Case: Generate daily access statistics for the past week
[
{
"$match": {
"EventTypeId": {"$oid": "61cb71500000000000000000"},
"OccurredOn.Date": {
"$gte": { "$date": "2026-01-14T00:00:00Z" },
"$lt": { "$date": "2026-01-21T23:59:59Z" }
}
}
},
{
"$group": {
"_id": {
"$dateToString": { "format": "%Y-%m-%d", "date": "$OccurredOn.Date" }
},
"totalEvents": { "$sum": 1 }
}
},
{
"$sort": { "_id": 1 }
}
]
Result:
[
{ "_id": "2026-01-14", "totalEvents": 1523 },
{ "_id": "2026-01-15", "totalEvents": 1687 },
{ "_id": "2026-01-16", "totalEvents": 1456 },
{ "_id": "2026-01-17", "totalEvents": 1598 },
{ "_id": "2026-01-18", "totalEvents": 1234 },
{ "_id": "2026-01-19", "totalEvents": 892 },
{ "_id": "2026-01-20", "totalEvents": 1045 }
]
Use Case: Security dashboard showing active alarms
[
{
"$match": {
"$and": [
{ "_t": { "$in": ["MercuryDoor", "AscDoor"] } },
{
"$or": [
{ "DoorState": "ForcedOpen" },
{ "DoorState": "HeldOpen" },
{ "DoorState": "Propped" }
]
}
]
}
},
{
"$project": {
"CommonName": 1,
"DoorState": 1,
"InFolderKey": 1,
"_t": 1
}
},
{
"$sort": { "CommonName": 1 }
}
]
Use Case: Identify hardware requiring maintenance
[
{
"$match": {
"$and": [
{ "_t": { "$in": ["MercuryController", "BoschPanel"] } },
{ "IsOnline": false },
{
"LastPing": {
"$lt": { "$date": "2026-01-20T00:00:00Z" }
}
}
]
}
},
{
"$project": {
"CommonName": 1,
"MacAddress": 1,
"LastPing": 1,
"hoursOffline": {
"$divide": [
{ "$subtract": [{ "$date": "2026-01-21T00:00:00Z" }, "$LastPing"] },
3600000
]
}
}
},
{
"$sort": { "hoursOffline": -1 }
}
]
Result: Controllers sorted by how long they’ve been offline (worst first)
Use Case: Understand which access levels are most used
[
{
"$match": {
"EventTypeId": {"$oid": "61cb71500000000000000000"},
"OccurredOn.Date": {
"$gte": { "$date": "2026-01-01T00:00:00Z" }
}
}
},
{
"$project":{
"$ObjectLinks": 1
}
},
{
"$unwind": "$ObjectLinks"
},
{
"$match": {
"ObjectLinks.Relation": "AccessLevel"
}
},
{
"$group": {
"_id": {
"accessLevelId": "$ObjectLinks.LinkedObjectId",
"accessLevelName": "$ObjectLinks.CommonName"
},
"usageCount": { "$sum": 1 }
}
},
{
"$sort": { "usageCount": -1 }
},
{
"$limit": 20
}
]
Result: Top 20 access levels by usage in January 2026
Use Case: Identify incomplete cardholder records
[
{
"$match": {
"$and": [
{ "_t": "Person" },
{
"$or": [
{ "CardAssignments": { "$exists": false } },
{ "CardAssignments": { "$size": 0 } }
]
}
]
}
},
{
"$project": {
"CommonName": 1,
"GivenName": 1,
"Surname": 1,
"InFolderKey": 1
}
},
{
"$sort": { "CommonName": 1 }
}
]
Use Case: Real-time occupancy tracking
[
{
"$match": {
"$and": [
{ "EventTypeId": {"$oid": "61cb71500000000000000000"} },
{ "Monikers.Nickname": "Entry" },
{
"OccurredOn.Date": {
"$gte": { "$date": "2026-01-21T00:00:00Z" }
}
}
]
}
},
{
"$project": {
"_id": 1,
"ObjectLinks": 1,
"Metadata": 1
}
},
{
"$unwind": "$Metadata"
},
{
"$match": {
"Metadata.Application": "Building"
}
},
{
"$group": {
"_id": "$Metadata.Values.SiteId",
"entryCount": { "$sum": 1 },
"uniquePeople": { "$addToSet": "$ObjectLinks.LinkedObjectId" }
}
},
{
"$project": {
"building": "$_id",
"totalEntries": "$entryCount",
"uniqueVisitors": { "$size": "$uniquePeople" }
}
},
{
"$sort": { "uniqueVisitors": -1 }
}
]
Result:
[
{ "building": "Building A", "totalEntries": 342, "uniqueVisitors": 156 },
{ "building": "Building B", "totalEntries": 289, "uniqueVisitors": 132 },
{ "building": "Building C", "totalEntries": 178, "uniqueVisitors": 87 }
]
Get both results and total count in one query:
[
{
"$match": { "_t": "Person" }
},
{
"$facet": {
"results": [
{ "$sort": { "CommonName": 1 } },
{ "$skip": 40 },
{ "$limit": 20 }
],
"totalCount": [
{ "$count": "count" }
]
}
}
]
Result:
{
"results": [
{ "_id": "...", "CommonName": "Alice Johnson", "..." },
{ "_id": "...", "CommonName": "Bob Smith", "..." },
"... 18 more person objects ..."
],
"totalCount": [{ "count": 1523 }]
}
Project different fields based on object type:
[
{
"$match": {
"_t": { "$in": ["Person", "MercuryReader"] }
}
},
{
"$project": {
"CommonName": 1,
"objectType": "$_t",
"typeSpecificData": {
"$cond": {
"if": { "$eq": ["$_t", "Person"] },
"then": {
"email": "$Email",
"cards": { "$size": { "$ifNull": ["$CardAssignments", []] } }
},
"else": {
"mac": "$MacAddress",
"online": "$IsOnline"
}
}
}
}
}
]
Find people with specific tag combinations:
[
{
"$match": {
"$and": [
{ "_t": "Person" },
{ "Tags": { "$all": ["employee", "active"] } },
{ "Tags": { "$nin": ["terminated", "on-leave"] } }
]
}
}
]
Logic: Person must have both “employee” AND “active” tags, but NOT “terminated” or “on-leave”
Find all events related to a specific person across different event types:
[
{
"$match": {
"$and": [
{ "ObjectLinks.Relation": "Person" },
{ "ObjectLinks.LinkedObjectId": { "$oid": "PERSON_KEY" } },
{
"OccurredOn.Date": {
"$gte": { "$date": "2026-01-01T00:00:00Z" }
}
}
]
}
},
{
"$group": {
"_id": "$EventTypeId",
"count": { "$sum": 1 },
"lastOccurred": { "$max": "$OccurredOn.Date" },
"sampleEvents": { "$push": { "time": "$OccurredOn.Date", "message": "$MessageLong" } }
}
},
{
"$sort": { "count": -1 }
}
]
Result: Summary of all event types for a person with counts and samples
Group events by hour of day:
[
{
"$match": {
"EventTypeId": {"$oid": "61cb71500000000000000000"},
"OccurredOn.Date": {
"$gte": { "$date": "2026-01-21T00:00:00Z" },
"$lt": { "$date": "2026-01-22T00:00:00Z" }
}
}
},
{
"$project": {
"hour": { "$hour": "$OccurredOn.Date" }
}
},
{
"$group": {
"_id": "$hour",
"count": { "$sum": 1 }
}
},
{
"$sort": { "_id": 1 }
}
]
Result: Access counts by hour (0-23) for January 21, 2026
using Feenics.Keep.WebApi.Wrapper;
using MongoDB.Bson;
using System.Threading.Tasks;
public async Task<BsonDocument[]> GetHardwareInventory(Client client, string instanceKey)
{
var pipeline = new BsonDocument[]
{
new BsonDocument("$match", new BsonDocument
{
{ "_t", new BsonDocument("$in", new BsonArray
{
"MercuryReader", "MercuryController",
"EngageReader", "WavelynxReader"
})
}
}),
new BsonDocument("$group", new BsonDocument
{
{ "_id", "$_t" },
{ "count", new BsonDocument("$sum", 1) }
}),
new BsonDocument("$sort", new BsonDocument("count", -1))
};
var results = await client.AggregateAsync("KeepObjects", pipeline, instanceKey);
return results;
}
For queries returning millions of documents, use streaming:
public async Task ProcessLargeEventSet(Client client, string instanceKey)
{
var pipeline = new BsonDocument[]
{
new BsonDocument("$match", new BsonDocument
{
{ "EventTypeId", new BsonDocument
{
{ "$oid", "61cb71500000000000000000" }
}
},
{ "OccurredOn.Date", new BsonDocument
{
{ "$gte", DateTime.UtcNow.AddDays(-90) }
}
}
})
};
await foreach (var eventDoc in client.AggregateStreamAsync("Events", pipeline, instanceKey))
{
// Process each event without loading all into memory
Console.WriteLine($"Event: {eventDoc["MessageLong"]}");
}
}
When to use streaming:
curl -X POST 'https://api.us.acresecurity.cloud/api/f/FOLDER_ID/search?page=0&pageSize=100' \
-H "Authorization: Bearer YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d '"{$and: [ { _t: \"Person\" }, {\"CardAssignments.EncodedCardNumber\": 123456} ] }"'
curl -X POST 'https://api.us.acresecurity.cloud/api/f/FOLDER_ID/aggregate/KeepObjects' \
-H "Authorization: Bearer YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d '[
{ "$match": { "_t": "Person" } },
{ "$project": { "CommonName": 1, "cardCount": { "$size": { "$ifNull": ["$CardAssignments", []] } } } },
{ "$match": { "cardCount": { "$gt": 0 } } },
{ "$sort": { "cardCount": -1 } },
{ "$limit": 10 }
]'
Understanding how acre API model properties map to MongoDB fields is essential for writing effective aggregates.
| MongoDB Field | MongoDB Type | API Model Property | Model Type | Description |
|---|---|---|---|---|
_id |
ObjectId | Key |
string | Unique identifier for the object |
_t |
string[] | N/A | N/A | Object type discriminator (e.g., “Person”, “MercuryReader”) |
CommonName |
string | CommonName |
string | User-defined name for the object |
InFolderId |
ObjectId | InFolderKey |
string | Parent folder/instance key |
InstanceScopeId |
ObjectId | N/A | N/A | Top-level instance in hierarchy (Enterprise scenarios) |
ResourceInstanceId |
ObjectId | N/A | N/A | Immediate instance containing the object |
ParentFolderIds |
ObjectId[] | N/A | N/A | Full path of folder IDs from root to object |
| N/A | N/A | Href |
string | Generated by API from ParentFolderIds |
| N/A | N/A | InFolderHref |
string | Generated by API from ParentFolderIds |
IsDeleted |
bool | N/A | N/A | Deprecated - Deleted objects are removed, not flagged |
Metadata |
object[] | Metadata |
MetadataItem[] | Custom key-value pairs |
Monikers |
object[] | Monikers |
MonikerItem[] | Type/classification tags |
Notes |
object[] | Notes |
NoteInfo[] | User comments and annotations |
ObjectLinks |
object[] | ObjectLinks |
ObjectLinkItem[] | References to related objects |
Tags |
string[] | Tags |
string[] | User-defined tags |
IsGlobal |
bool | IsGlobal |
bool | Exposes object to entire instance scope (primarily for AccessLevel) |
ConcurrencyVersion |
long | ConcurrencyVersion |
long? | Version number for optimistic concurrency control |
| N/A | N/A | Links |
List<Link> | Generated by API for HATEOAS navigation |
Metadata allows custom key-value pairs to be attached to any object. Each MetadataItem has:
| Property | Type | Description |
|---|---|---|
Application |
string | Namespace/application identifier (e.g., “HRSystem”, “BadgePrinting”) |
Values |
string | JSON or plain text data (flexible format) |
ShouldPublishUpdateEvents |
bool? | If true, updates trigger ObjectModified events (default: false) |
Example in MongoDB:
"Metadata": [
{
"Application": "HRSystem",
"Values": "{\"EmployeeID\":\"E12345\",\"Department\":\"Engineering\"}",
"ShouldPublishUpdateEvents": false
}
]
Monikers provide unique names/IDs for objects within specific namespaces, allowing third-party systems to maintain their own identifiers.
| Property | Type | Description |
|---|---|---|
Namespace |
string | Application or system name (e.g., “ActiveDirectory”, “PayrollSystem”) |
Nickname |
string | Unique identifier within that namespace |
Example in MongoDB:
"Monikers": [
{
"Namespace": "ActiveDirectory",
"Nickname": "john.smith@company.com"
},
{
"Namespace": "PayrollSystem",
"Nickname": "EMP-2024-001"
}
]
Notes are timestamped, user-attributed comments attached to objects.
| Property | Type | Description |
|---|---|---|
Key |
string | Unique identifier for the note |
CreatedOn |
DateTime | When the note was created (UTC) |
User |
ObjectLinkItem | The user who created the note |
NoteText |
string | Plain text content of the note |
Example in MongoDB:
"Notes": [
{
"Key": "507f1f77bcf86cd799439011",
"CreatedOn": "2026-01-15T14:30:00Z",
"User": {
"LinkedObjectKey": "507f191e810c19729de860ea",
"CommonName": "Jane Admin",
"Relation": "User"
},
"NoteText": "Card replaced due to damage. Old card returned to security."
}
]
ObjectLinks represent relationships between objects (e.g., Person → AccessLevel, Event → Reader).
| Property | Type | Description |
|---|---|---|
Href |
string | Generated API path to the linked object |
LinkedObjectKey |
string | Key (ObjectId) of the related object |
CommonName |
string | Name of the related object (for display) |
Relation |
string | Type of relationship (e.g., “AccessLevel”, “Person”, “Reader”) |
MetaDataBson |
byte[] | Optional BSON-encoded metadata about the relationship |
AddAsTag |
bool? | If true, adds the relationship as a tag |
Common Relation Types:
"Person" - Link to a person"AccessLevel" - Access permission assignment"Card" - Card assignment"Reader" - Hardware location"Door" - Physical entry point"Schedule" - Time-based rule"User" - System user referenceExample in MongoDB:
"ObjectLinks": [
{
"LinkedObjectKey": "507f191e810c19729de860ea",
"CommonName": "Building A - Full Access",
"Relation": "AccessLevel",
"AddAsTag": false
},
{
"LinkedObjectKey": "507f1f77bcf86cd799439012",
"CommonName": "Front Door Reader",
"Relation": "Reader"
}
]
| Object Type | _t Value(s) | Collection | Use Case |
|---|---|---|---|
| Person | ["Person"] |
KeepObjects | Cardholders, employees, visitors |
| Access Level | ["AccessLevel"] |
KeepObjects | Access permissions configuration |
| Schedule | ["Schedule"] |
KeepObjects | Time-based access rules |
| Reader | ["MercuryReader"], ["EngageReader"], ["WavelynxReader"] |
KeepObjects | Physical access points |
| Controller | ["MercuryController"], ["BoschPanel"] |
KeepObjects | Hardware controllers |
| Door | ["MercuryDoor"], ["AscDoor"] |
KeepObjects | Controlled entry points |
| Input/Output | ["MercuryInput"], ["MercuryOutput"] |
KeepObjects | Sensors and relays |
| Event | ["EventMessage"] |
Events | All system events |
| Badge Type | ["BadgeType"] |
KeepObjects | Credential templates |
| Card Format | ["CardFormat"] |
KeepObjects | Encoding specifications |
| MongoDB Field | Type | Description | Example |
|---|---|---|---|
EventTypeId |
string | Event classification | “AccessGranted”, “AccessDenied” |
OccurredOn |
DateTime | When event happened (UTC) | “2026-01-21T18:46:58Z” |
PublishedOn |
DateTime | When published to MQTT | “2026-01-21T18:46:59Z” |
Priority |
int | Event importance (0-1000) | 200 (normal), 800 (alarm) |
MessageShort |
string | Brief description | “Access Granted” |
MessageLong |
string | Detailed description | “Card Read Granted: John Smith at Front Door” |
EventDataBsonBase64 |
string | Event-specific data (Base64 BSON) | Encoded binary data |
ObjectLinks |
object[] | Related objects | [{ Relation: “Person”, LinkedObjectId: … }] |
✅ DO:
[
{ "$match": { "_t": "Person" } }, // Filter first
{ "$unwind": "$CardAssignments" },
{ "$group": { ... } }
]
❌ DON’T:
[
{ "$unwind": "$CardAssignments" }, // Expensive operation on all docs
{ "$match": { "_t": "Person" } }, // Filter too late
{ "$group": { ... } }
]
✅ DO:
[
{ "$match": { "_t": "Person" } },
{ "$project": { "CommonName": 1, "CardAssignments": 1 } }, // Only needed fields
{ "$unwind": "$CardAssignments" }
]
Common Indexed fields (faster queries):
_id_tInstanceScopeIdObjectLinks.LinkedObjectIdMonikersTagsEventTypeId (Events collection)OccurredOn.Date (Events collection)Always use $limit unless you truly need all results:
[
{ "$match": { ... } },
{ "$sort": { ... } },
{ "$limit": 1000 } // Prevent runaway queries
]
Instead of running multiple queries:
Always add InstanceScopeId and additional $match parameters before your $facet
{
"$match": {
"InstanceScopeId": {
"$oid": "697451d00000000000000000"
}
}
},
{
"$facet": {
"peopleCount": [{ "$match": { "_t": "Person" } }, { "$count": "count" }],
"readerCount": [{ "$match": { "_t": "MercuryReader" } }, { "$count": "count" }],
"eventCount": [{ "$count": "count" }]
}
}
For security and data integrity, certain MongoDB operators are not allowed:
$out - Cannot write to collections$merge - Cannot merge into collections$currentOp - Cannot access system operations$indexStats - Cannot access index statistics$planCacheStats - Cannot access query plans$collStats - Cannot access collection statistics$lookup - use the alternative $keepLookupAll aggregates are automatically scoped to your authenticated instance:
InFolderId and ResourceInstanceId are enforced server-sideInstanceScopeId in your search queries and $match to leverage indexes$limit and indexed fields to prevent timeoutsCause: Using a blocked operator (e.g., $out, $merge)
Solution: Remove the blocked stage or use allowed alternatives
Cause: Query too complex or missing indexes
Solution:
$match early to reduce document count$limit to cap results$project to reduce document sizeCause: Field name contains special characters or starts with $
Solution: Use dot notation for nested fields: "CardAssignments.EncodedCardNumber"
Causes:
_t value doesn’t match (check exact type name)Debug:
[
{ "$match": { "_t": "Person" } },
{ "$limit": 1 }
]
Start simple and add complexity incrementally.
MongoDB aggregation pipelines are the engine that powers acre Access Control’s flexibility and analytics capabilities. By mastering aggregates, you unlock:
✅ Start with $match - Filter early to reduce processed documents
✅ Use indexes - Query on _t, _id, InstanceScopeId, EventTypeId, OccurredOn.Date, Monikers
✅ Always $limit - Cap results to prevent timeouts
✅ Project early - Reduce document size before expensive operations
✅ Test incrementally - Build pipelines stage-by-stage
✅ Use $facet - Get multiple aggregations in one query
✅ Stream large results - Use AggregateStreamAsync for >5K documents
Start building powerful queries with aggregates today! 🚀