Skip to main content

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

OperatorUsage
EqualsExact match
NotEqualsExclude exact match
ContainsSubstring or array contains
GreaterThan / LessThanNumeric or date comparison
BetweenRange (requires FirstValue and SecondValue)
TodayDate attribute equals today's date
IsEmpty / IsNotEmptyNull 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

ScenarioRecommended Take
Uniqueness check (just need to know if any exist)Take = 1
Process all matching instancesTake = int.MaxValue
Display lists with paginationTake = 20 (or configured page size)

Performance note:

  • Avoid Take = int.MaxValue for 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 / scenarioWhy builder cannot be usedWorkaround
ParentFiltersNo builder method existsUse new InstanceQuery { ParentFilters = ... }
ActivityQueryNo builder method existsUse new InstanceQuery { ActivityQuery = ... }
Today, IsEmpty, IsNotEmpty operatorsWhere(name, op, value) requires a string value; these operators have no meaningful FirstValueUse verbose AttributeFilterCondition form
Between operatorNeeds SecondValue; the builder has no WhereBetween overloadUse verbose AttributeFilterCondition form
Or-grouped multi-condition expressionsBuilder always uses LogicalOperator.AndUse verbose AttributeFilterExpression array
Pre-built AttributeFilterExpressions (e.g., from reportQuery)Builder constructs conditions from scratch onlyUse verbose form and assign the array directly
GetAllowedOperations, GetAllowedActions, GetOnlyActionableInstances, GetOnlyConnectableInstancesNo builder methods for these flagsUse new InstanceQuery { GetAllowedActions = true, ... }

Note for maintainers: The items in the table above are potential additions to InstanceQueryBuilder — consider adding WithParentFilter(...), WithActivityQuery(...), WhereBetween(...), and a no-value Where(name, op) overload.

Key Services

ServiceMethodPurpose
IInstanceServiceGetInstancesAsync(query)Query instances of a type
IInstanceServiceGetRelatedInstancesAsync(instanceId, query)Query instances related to a specific instance
IInstanceServiceGetInstanceAsync(instanceId, attributes)Fetch a single instance by ID