Validation Pattern
Overview
Validation is the most common use of PreCreate, PreUpdate, and PromoteActionCommand programs. The pattern checks attribute values against business rules and either blocks the operation by returning an error result (or throwing an exception), or allows it to proceed.
When to Use
- Enforce mandatory fields that cannot be configured declaratively (e.g., fields required only under certain conditions).
- Cross-attribute or cross-instance validations (e.g., start date must be before end date; related instance must be in a specific state).
- Uniqueness checks that go beyond platform-level name uniqueness.
- State-transition guards in
PromoteActionCommand(e.g., cannot approve unless all checklist items are complete).
Applicable Program Types
| Program Type | Interface | Execution |
|---|---|---|
| PreCreate Trigger | IPreCreateTriggerProgramAsync | Synchronous (in transaction) |
| PreUpdate Trigger | IPreUpdateTriggerProgramAsync | Synchronous (in transaction) |
| Promote Action Command | IPromoteActionCommandProgramAsync | Synchronous (in transaction) |
Pattern Structure
- Read attribute values from the incoming
Instanceusing typed accessor methods (GetTextAttributeValue,GetDateAttributeValue, etc.). - Apply business rule checks.
- Return
ProgramResult { IsSuccess = false, Errors = [...] }(or throw anException) to block the operation. - Return
ProgramResult { IsSuccess = true }to allow the operation.
Note: Throwing an
Exceptionis equivalent to returningIsSuccess = falseand is a common alternative in older code. Prefer returningProgramResultwith meaningfulErrorsfor better user-facing messages.
Code Example
This example validates dates, a required user assignment, and a unique external reference ID — all within a single PreCreate program.
using Prorigo.Protrak.API.Contracts;
using Prorigo.Protrak.API.Contracts.Builders;
using Prorigo.Protrak.API.Contracts.Enum;
using Prorigo.Protrak.API.Services;
using Prorigo.Protrak.Programs;
using System;
using System.Threading.Tasks;
namespace YourNamespace.Programs
{
/// <summary>
/// Type: Pre Create Trigger
/// Configured for: Type Project
/// Trigger: Before project instance creation
/// Summary: Validates mandatory dates, owner assignment, and unique external reference ID.
/// </summary>
public class ValidationOnProjectCreate : IPreCreateTriggerProgramAsync
{
public IInstanceService InstanceService { get; set; }
private readonly string TYPE_PROJECT = "Project";
private readonly string ATTRIBUTE_START_DATE = "ScheduledStartDate";
private readonly string ATTRIBUTE_END_DATE = "ScheduledEndDate";
private readonly string ATTRIBUTE_OWNER = "OwnerUser";
private readonly string ATTRIBUTE_EXTERNAL_ID = "ExternalReferenceId";
public async Task<ProgramResult> RunAsync(Instance instance)
{
// 1. Date range validation
var startDate = instance.GetDateAttributeValue(ATTRIBUTE_START_DATE);
var endDate = instance.GetDateAttributeValue(ATTRIBUTE_END_DATE);
if (startDate == null)
throw new Exception("Please select a Scheduled Start Date.");
if (endDate == null)
throw new Exception("Please select a Scheduled End Date.");
if (startDate > endDate)
throw new Exception("Scheduled End Date must be after Scheduled Start Date.");
// 2. Required user attribute validation
var owner = instance.GetUserAttributeValue(ATTRIBUTE_OWNER);
if (owner == null || owner.Length == 0)
throw new Exception("Please assign an Owner.");
// 3. Uniqueness check — query for an existing instance with the same external ID
var externalId = instance.GetTextAttributeValue(ATTRIBUTE_EXTERNAL_ID);
if (!string.IsNullOrEmpty(externalId))
{
var existing = await InstanceService.GetInstancesAsync(
InstanceQueryBuilder.ForType(TYPE_PROJECT)
.Where(ATTRIBUTE_EXTERNAL_ID, AttributeFilterOperator.Equals, externalId)
.Take(1)
.Build());
if (existing != null && existing.TotalCount > 0)
throw new Exception($"A project already exists with External Reference ID '{externalId}'.");
}
return new ProgramResult { IsSuccess = true };
}
}
}
Validate on State Transition (PromoteActionCommand)
public class ValidateBeforeApproval : IPromoteActionCommandProgramAsync
{
public async Task<ProgramResult> RunAsync(Instance instance, string fromState, string toState, string actionName)
{
if (toState == "Approved")
{
var comment = instance.GetTextAttributeValue("ApprovalComment");
if (string.IsNullOrWhiteSpace(comment))
{
return new ProgramResult
{
IsSuccess = false,
Errors = new[] { "Approval comment is required before approving." }
};
}
}
return new ProgramResult { IsSuccess = true };
}
}
Key Services
| Service | Method | Purpose |
|---|---|---|
IInstanceService | GetInstanceAsync | Fetch the current persisted value of the instance to compare against the incoming update |
IInstanceService | GetInstancesAsync | Query for existing instances to enforce uniqueness |
IInstanceService | GetRelatedInstancesAsync | Query related instances to validate cross-type business rules |
Common Gotchas
- Not all attributes are present. The incoming
instance.Attributesonly contains attributes submitted by the caller. Use typed accessors (GetDateAttributeValue, etc.) — they returnnullsafely when an attribute is missing, so you only need to null-check the returned value, not an intermediateAttributeobject. instance.Attributesin PreUpdate is not the full instance. It only contains the attributes being updated. UseInstanceService.GetInstanceAsync(instance.Id, attributes)to fetch the current persisted values for comparison.- Do not perform irreversible side effects (e.g., sending emails) inside a Pre-trigger. If the operation is rolled back, the side effect cannot be undone.