Query and Filter Pattern
Overview
Protrak provides two main query types for fetching instances from the database in programs:
InstanceQuery— queries instances of a specific type, with optional attribute filters, state filters, parent (relation) filters, sorting, and pagination.RelatedInstanceQuery— queries instances that are related to a specific instance through a named relation type.
Both support AttributeFilterExpressions for multi-condition filtering and return PagedData<Instance> or PagedData<RelatedInstance>.
When to Use
- Fetch a list of instances matching business criteria (e.g., all agreements in review states, all tasks scheduled for today).
- Check if an instance with a given attribute value already exists (uniqueness check).
- Retrieve child instances linked to a parent via a specific relation type.
- Filter instances by state, attribute value, or relation membership.
Applicable Program Types
All program types can use queries. They are most common in:
- PreCreate / PreUpdate (uniqueness checks)
- PostCreate / PromoteActionCommand (find related instances to operate on)
- Scheduler (batch processing of instances meeting a time-based condition)
InstanceQuery
Basic structure
using Prorigo.Protrak.API.Contracts.Builders;
// ✅ Builder syntax (preferred)
var query = InstanceQueryBuilder.ForType("Contract")
.Select("Status", "Created") // Attributes to return
.InStates("Draft", "Review") // Optional: only instances in these states
.SortBy("Created") // Optional: sort attribute
.TakeAll() // Take = int.MaxValue, Skip = 0
.Build();
var result = await InstanceService.GetInstancesAsync(query);
foreach (var instance in result.SafeItems()) // SafeItems() handles null PagedData/Items safely
{
// ...
}
AttributeFilterExpressions
Use .Where() on the builder for single-condition attribute filters. Chain multiple .Where() calls to add AND conditions.
using Prorigo.Protrak.API.Contracts.Builders;
using Prorigo.Protrak.API.Contracts.Enum;
// ✅ Builder syntax (preferred) — single condition
var query = InstanceQueryBuilder.ForType("Task")
.Where("ProjectId", AttributeFilterOperator.Equals, "PRJ-001")
.Select("TaskAssignee", "TaskScheduledStartDate")
.Take(50)
.Build();
For multi-condition, Between (needs SecondValue), Or grouping, or value-less operators (Today, IsEmpty, IsNotEmpty), use the verbose form directly:
// Verbose form — required for multi-condition / Between / Or / Today / IsEmpty
using Prorigo.Protrak.API.Contracts.Filters;
using Prorigo.Protrak.API.Contracts.Enum;
var query = new InstanceQuery
{
InstanceTypeName = "Task",
AttributeFilterExpressions = new[]
{
new AttributeFilterExpression
{
AttributeFilterConditions = new List<AttributeFilterCondition>
{
new AttributeFilterCondition
{
AttributeName = "ProjectId",
Condition = AttributeFilterOperator.Equals,
FirstValue = "PRJ-001",
Operator = LogicalOperator.None
}
},
Operator = LogicalOperator.None
}
},
Attributes = new[] { "TaskAssignee", "TaskScheduledStartDate" },
Skip = 0,
Take = 50
};
Common AttributeFilterOperator values
| Operator | Usage |
|---|---|
Equals | Exact match |
NotEquals | Exclude exact match |
Contains | Substring or array contains |
GreaterThan / LessThan | Numeric or date comparison |
Between | Range (requires FirstValue and SecondValue) |
Today | Date attribute equals today's date |
IsEmpty / IsNotEmpty | Null check |
Filter by state
StateFilter = new[] { "Planned", "In Progress" }
Filter by parent relation (get instances linked to a parent)
// Get all Tasks linked FROM a specific Project via "ProjectToTask" relation
ParentFilters = new[]
{
new RelationFilter
{
RelationTypeName = "ProjectToTask",
TypeName = "Project", // The parent type
InstanceId = projectInstanceId, // The parent ID
RelationDirection = RelationDirection.From // Direction from the parent
}
}
RelatedInstanceQuery
Use this to query instances related to a known instance through a specific relation type.
using Prorigo.Protrak.API.Contracts.Builders;
// ✅ Builder syntax (preferred)
var relatedQuery = RelatedQueryBuilder.Create()
.ForRelation("ProjectToTask", "Task", RelationDirection.To) // "To" = destination instances
.Select("TaskAssignee", "TaskScheduledStartDate") // instance attributes to return
.SelectRelationAttributes("AllocationPercentage", "Role") // optional: relation-level attributes
.InStates("Planned") // Optional state filter
.TakeAll()
.Build();
var related = await InstanceService.GetRelatedInstancesAsync(parentInstanceId, relatedQuery);
foreach (var item in related.SafeItems()) // SafeItems() handles null PagedData/Items safely
{
var assignee = item.GetTextAttributeValue("TaskAssignee"); // same getters as Instance
var allocation = item.RelationAttributes.GetNumericAttributeValue("AllocationPercentage");
// item.RelatedInstanceId = the related instance's Guid
// item.RelationId = the relation's Guid (useful for DeleteRelation)
}
Full Code Example
Three common query patterns: filter by date (Today), filter by parent relation + attribute, and filter related instances by state.
using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Builders;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Contracts.Filters;
using Prorigo.Protrak.API.Services;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace YourNamespace.Programs
{
public class QueryExamples
{
public IInstanceService InstanceService { get; set; }
// Example 1: Get all tasks scheduled for today
// ⚠ Builder gap: Today operator has no FirstValue — use verbose form
private async Task<PagedData<Instance>> GetTodayTasksAsync()
{
var query = new InstanceQuery
{
InstanceTypeName = "Task",
AttributeFilterExpressions = new[]
{
new AttributeFilterExpression
{
AttributeFilterConditions = new List<AttributeFilterCondition>
{
new AttributeFilterCondition
{
AttributeName = "StartDate",
Condition = AttributeFilterOperator.Today,
Operator = LogicalOperator.None
}
},
Operator = LogicalOperator.None
}
},
Attributes = new[] { "StartDate", "EndDate", "TaskToResourceRef" },
SortBy = "StartDate",
Skip = 0,
Take = int.MaxValue
};
return await InstanceService.GetInstancesAsync(query);
}
// Example 2: Get resources linked to a work order, filtered by a boolean attribute
// ⚠ Builder gap: ParentFilters not supported — use verbose form
private async Task<PagedData<Instance>> GetEquipmentForWorkOrderAsync(Guid workOrderId)
{
var query = new InstanceQuery
{
InstanceTypeName = "Resource",
ParentFilters = new[]
{
new RelationFilter
{
RelationTypeName = "TaskToResource",
TypeName = "Task",
InstanceId = workOrderId,
RelationDirection = RelationDirection.From
}
},
AttributeFilterExpressions = new[]
{
new AttributeFilterExpression
{
AttributeFilterConditions = new List<AttributeFilterCondition>
{
new AttributeFilterCondition
{
AttributeName = "IsEquipment",
Condition = AttributeFilterOperator.Equals,
FirstValue = "true",
Operator = LogicalOperator.None
}
},
Operator = LogicalOperator.None
}
},
Attributes = new[] { "ResourceStatus" },
Skip = 0,
Take = 1
};
return await InstanceService.GetInstancesAsync(query);
}
// Example 3: Get child tasks of a work order filtered by state
private async Task<PagedData<RelatedInstance>> GetPendingTasksForWorkOrderAsync(Guid workOrderId)
{
var query = RelatedQueryBuilder.Create()
.ForRelation("TaskToWorkOrder", "Task", RelationDirection.From)
.InStates("Planned")
.TakeAll()
.Build();
return await InstanceService.GetRelatedInstancesAsync(workOrderId, query);
}
}
}
Pagination Best Practices
| Scenario | Recommended Take |
|---|---|
| Uniqueness check (just need to know if any exist) | Take = 1 |
| Process all matching instances | Take = int.MaxValue |
| Display lists with pagination | Take = 20 (or configured page size) |
Performance note:
- Avoid
Take = int.MaxValuefor types with many thousands of instances. Consider processing in batches if the result set could be very large. - Avoid calling GetInstanceAsync() inside a foreach loop. Instead, call GetInstancesAsync() once before the loop and iterate over the returned collection.
InstanceQueryBuilder — Unsupported Parameters
The following InstanceQuery properties cannot be set via the builder and require the verbose new InstanceQuery { ... } form:
| Unsupported property / scenario | Why builder cannot be used | Workaround |
|---|---|---|
ParentFilters | No builder method exists | Use new InstanceQuery { ParentFilters = ... } |
ActivityQuery | No builder method exists | Use new InstanceQuery { ActivityQuery = ... } |
Today, IsEmpty, IsNotEmpty operators | Where(name, op, value) requires a string value; these operators have no meaningful FirstValue | Use verbose AttributeFilterCondition form |
Between operator | Needs SecondValue; the builder has no WhereBetween overload | Use verbose AttributeFilterCondition form |
Or-grouped multi-condition expressions | Builder always uses LogicalOperator.And | Use verbose AttributeFilterExpression array |
Pre-built AttributeFilterExpressions (e.g., from reportQuery) | Builder constructs conditions from scratch only | Use verbose form and assign the array directly |
GetAllowedOperations, GetAllowedActions, GetOnlyActionableInstances, GetOnlyConnectableInstances | No builder methods for these flags | Use new InstanceQuery { GetAllowedActions = true, ... } |
Note for maintainers: The items in the table above are potential additions to
InstanceQueryBuilder— consider addingWithParentFilter(...),WithActivityQuery(...),WhereBetween(...), and a no-valueWhere(name, op)overload.
Key Services
| Service | Method | Purpose |
|---|---|---|
IInstanceService | GetInstancesAsync(query) | Query instances of a type |
IInstanceService | GetRelatedInstancesAsync(instanceId, query) | Query instances related to a specific instance |
IInstanceService | GetInstanceAsync(instanceId, attributes) | Fetch a single instance by ID |